안녕하세요? 웹지니입니다.

오늘은 .NET on OpenSource 시리즈의 두 번째 포스트로 Memcached라는 이름의 분산 메모리 캐싱 시스템을 Ubuntu 리눅스에 설치하고 이를 .NET Application에서 활용하는 방법에 대해 소개해 볼까 합니다. 사실 이번 시리즈를 시작하게 된 계기가 있는데요. 다른 .NET 개발자 분들은 어떨지 모르겠지만 저 개인적으로는 많은 부분을 Microsoft에 의존하고 있었던 것 같아요. 그 사실을 Memcached라는 녀석을 알게 되면서 깨닫게 되었습니다.

현재 Microsoft는 Velocity라는 이름으로 Memcached와 유사한 분산 캐싱 시스템을 구현 중에 있어요. Velocity의 가장 버전은 이 글을 쓰는 시점에서는 CTP3이며 조만간 CTP4가 출시될 예정입니다. 사실 CTP 버전이면 실무에 도입하기에는 다소 무리가 따르죠. 반면 Memcached는 이미 LiveJournal.com을 비롯하여 이미 많은 곳에서 사용되고 있으며 충분히 검증된 시스템입니다.

이런 경우 당연히 후자인 Memcached를 선택하는 것이 타당하겠지요. 문제는 .NET 환경에서 사용이 가능한지 여부입니다. 다행히 Memcached 그 자체는 객체를 메모리 캐시에 넣고 빼는 매우 기본적인 기능에만 충실합니다. 대신 Client API를 위한 프로토콜을 정의하고 Memcached 서버와 클라이언트 사이의 통신은 Client API가 책임지도록 하고 있어요. 따라서 잘 만들어진 .NET용 Client API가 있다면 .NET 환경에서도 얼마든지 Memcached 서버를 활용할 수 있는 셈이지요.

또한 Win32버전의 Memcached 서버도 존재합니다. 그러나 안타깝게도 원래 Memcached를 개발하고 배포하는 Danga Interactive가 제공하는 것은 아니며 Linux용 Memcached가 현재 1.2.8 버전인 것에 비해 Win32용은 1.2.1 버전이 제공되고 있습니다. 해서 저는 최종적으로 Linux 환경에서 Memcached 1.2.8을 설치하고 .NET 환경을 위한 Client API를 사용하여 .NET Application에 분산 캐시를 도입하기로 마음 먹었답니다.

1. Installing libevent on Ubuntu

여러분이 이미 Ubuntu 리눅스가 설치된 VM이나 혹은 물리적 머신을 가지고 있다는 가정하에 Ubuntu 리눅스에 Memcached 서버를 설치해 보도록 하겠습니다. Memcached 서버를 설치하려면 그 전에 libevent라는 라이브러리를 먼저 설치해야 합니다. 우선 libevent 라이브러리의 홈페이지를 방문해 볼까요?

img1

그림에서 보듯이 좌측의 Download 메뉴를 보면 libevent-1.4.11이 현재 가장 최신의 안정 버전임을 알 수 있습니다. 그러면 리눅스 환경에서 이 파일을 다운로드하여 설치를 해야겠지요. 링크를 마우스 오른쪽 버튼으로 클릭해 보면 해당 링크가 가리키는 주소를 알 수 있습니다. 이 주소는 리눅스에서 파일을 다운로드할 때 필요합니다. 리눅스의 터미널을 열고 다음과 같이 명령을 날려봅니다.

  1: $ wget http://www.monkey.org/~provos/libevent-1.4.1-stable.tar.gz\
  2: $ gunzip libevent-1.4.11-stable.tar.gz
  3: $ tar xvf libevent-1.4.11-stable.tar
  4: $ cd libevent-1.4.11-stable/
  5: $ ./configure
  6: $ make
  7: $ sudo make install

1번 라인의 커맨드는 libevent 홈페이지로부터 libevent-1.4.11-stable.tar.gz 파일을 다운로드하는 명령입니다. 다운로드가 완료되면 2번 라인과 같이 gunzip 명령으로 gz 파일의 압축을 해제한 후 다시 3번 라인과 같이 tar 명령을 이용하여 libevent-1.4.11-stable.tar 파일의 압축을 해제합니다.

압축이 해제되면 4번 라인과 같이 압축이 해제된 디렉터리로 이동한 후 5번, 6번, 7번 라인의 명령을 차례대로 실행합니다. 이 세 가지 명령은 리눅스에서 뭔가를 빌드해서 설치하기 위한 기본 단계입니다. 별 다른 오류 없이 설치가 완료되었다면 whereis 명령으로 libevent 라이브러리가 제대로 설치되었는지 확인할 수 있습니다.

  1: $ whereis libevent
  2: libevent: /usr/local/lib/libevent.a /usr/local/lib/libevent.so /usr/local/lib/libevent.la

저의 경우에는 /usr/local/lib 디렉터리에 libevent가 설치된 것을 확인할 수 있었습니다. 그러면 이제 Memcached를 설치해 보겠습니다.

2. Installing Memcached on Ubuntu

Memcached는 이미 리눅스 배포판에 포함되어 있습니다. 이 포스트에서 사용하고 있는 Ubuntu 9.0.4의 경우에도 이미 포함되어 있으며 아래의 커맨드를 통해 확인할 수 있습니다.

  1: apt-cache search memcached 혹은
  2: apt-cache pkgnames | grep memcached

둘 중 하나의 커맨드를 실행해 보면 Memcached와 관련된 패키지들이 주르륵 나타날 것입니다. 그러나 가장 최신 버전의 모듈이 아닌 관계로 웹을 통해 Memcached 1.2.8 버전을 다운로드해서 설치해 보겠습니다. 리눅스의 터미널에서 다음의 커맨드를 차례대로 실행합니다.

  1: $ wget http://memcached.googlecode.com/files/memcached-1.2.8.tar.gz
  2: $ gunzip memcached-1.2.8.tar.gz
  3: $ tar xvf memcached-1.2.8.tar
  4: $ cd memcached-1.2.8/
  5: $ sudo mkdir -p /opt/memcached/
  6: $ ./configure --prefix=/opt/memcached/
  7: $ make
  8: $ sudo make install
  9: $ whereis memcached
 10: /opt/memcached/bin/memcached

우선 1번 라인과 같이 Memcached 1.2.8 버전을 다운로드한 후 2번, 3번 라인과 같이 파일의 압축을 해제합니다. 그런 후 5번 라인과 같이 /opt/memcached/ 라는 이름의 디렉터리를 생성하는데 이는 제가 Memcached를 설치하려고 하는 디렉터리입니다. 이 디렉터리는 6번 라인과 같이 설치 환경을 설정할 때 --prefix 매개 변수를 이용할 수 있습니다. 그런 후 7번, 8번 라인과 같이 make, make install을 차례로 실행하여 Memcached를 설치한 후 9번 라인과 같이 설치를 확인하면 10번 라인처럼 /opt/memcached/bin/디렉터리에 Memcached가 설치됩니다.

3. Configuring and Running Memcached Service

이제 Memcached의 설치를 마쳤으므로 간단한 설정을 거쳐 서비스를 실행해 보겠습니다. 우선 Memcached가 설치된 /opt/memcached/bin/ 디렉터리로 이동하여 Memcached를 실행해 봅니다.

  1: $ cd /opt/memcached/bin/
  2: $ memcached

그러면 아마도 Memcached가 설치되지 않았다는 메시지를 보게 될 것입니다. 아니, 방금 설치했는데 이게 무슨 소리??? 그래서 Memcached가 제대로 설치됐는지를 다음과 같이 확인해 봅니다.

  1: $ ldd /opt/memcached/bin/memcached
  2:   linux-gate.so.1 => (oxb7fa0000)
  3:   libevent-1.4.so.2 => not found
  4:   libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7e2e000)
  5:   /lib/ld-linux.so.2 (0xb7fa1000)

ldd 명령을 통해 Memcached가 필요로 하는 Library Depenency를 살펴보니 3번 라인과 같이 libevent 라이브러리를 찾을 수 없다는 문장이 보이네요. 아하 이제보니 앞서 설치한 libevent 라이브러리를 Memcached가 알아보지 못했나봅니다. 해서 /etc/ld.so.conf 파일을 편집하여 libevent 라이브러리가 설치된 폴더를 지정해 주어야 합니다. vi 에디터를 이용해서 ld.so.conf 파일에 다음과 같이 문장을 추가합니다.

  1: include /usr/local/lib/libevent/

그런 후 다음과 같이 ldconfig 명령을 실행하여 방금 수정한 설정 내용이 적용되도록 합니다.

  1: $ sudo ldconfig /etc/ld.so.conf
  2: $ ldd /opt/memcached/bin/memcached
  3:   linux-gate.so.1=> (0xb8034000)
  4:   libevent-1.4.so.2 => /user/local/lib/libevent-1.4.so.2 (0xb800d000)
  5:   libc.so.6 => /lib/tls/i686/libc.so.6 (0x7eaa000)
  6:   libnsl.so.1 => /lib/tls/i686/cmov/libnsl.so.1 (0xb7e90000)
  7:   librt.so.1 => /lib/tls/i686/cmov/librt.so.1 (0xb7e87000)
  8:   libresolv.so.2 => /lib/tls/i686/cmov/libresolv.so.2 (0xb7e71000)
  9:   /lib/ld-linux.so.2 (0xb8035000)
 10:   libpthread.so.0 => /lib/tls/i686/cmov/libpthread.so.0 (0xb7e58000)

1번 라인과 같이 ldconfig 명령을 실행한 후 2번의 ldd 명령을 다시 날려보면 이번에는 모든 라이브러리들이 제대로 로드되어 있음을 확인할 수 있습니다. 이제 다음과 같이 Memcached를 실행해 봅니다.

  1: $ sudo /opt/memcached/bin/memcached -d -m 2048 -p 11211

위의 명령은 /opt/memcached/bin/ 디렉터리의 memcached 파일을 실행합니다. 이  때 –m 옵션에 의해 메모리는 2GB를 사용하며 포트 번호는 –p 11211에 의해 11211번 포트를 사용하게 됩니다. 참고로 이 포트 번호는 Memcached의 기본 포트 번호입니다. 이 커맨드 역시 /etc/rc.local 파일에 기록해 두면 리눅스가 재시작할 때 자동으로 Memcached 서비스를 시작하도록 구성할 수 있습니다.

  1: /opt/memcached/bin/memcached -d -m 2048 -p 11211 2 > &1 > /dev/null

4. Using Memcached on Microsoft .NET

이제 Memcached 서비스를 설치하고 서비스를 시작하는데 성공했다면 .NET 환경에서 Memcached 서비스를 활용하는 방법에 대해 살펴보겠습니다. 먼저 Memcached 서비스를 사용하기 위한 .NET용 Client API를 구해야 합니다. 이미 CodePlex를 통해 몇 가지 API가 제공되고 있으며 그 중 제가 보기에 enyim.com Memcached Client 프로젝트가 가장 괜찮아 보였습니다.

img2

오른쪽의 Download Now 링크를 클릭하여 enyim.com_memcached_1.2.0.2.zip 파일을 다운로드 한 후 압축을 풀어보면 Enyim.Caching.dll 파일을 발견할 수 있습니다. 그러면 이 어셈블리를 이용하여 Memcached 서비스를 이용하는 간단한 ASP.NET 웹 애플리케이션을 구현해 보겠습니다.

4.1 Create new classic ASP.NET Web Application project

Visual Studio 2008을 실행하고 새로운 ASP.NET Web Application 프로젝트를 선택한 후 아래 그림과 같이 새 프로젝트를 생성합니다.

img3

프로젝트가 생성되면 솔루션 탐색기를 통해 앞서 다운로드 한 Enyim.Caching.dll 파일을 프로젝트로 참조합니다.

img4

그런 후 Web.config 파일을 열고 앞서 다운로드한 파일의 압축이 해제된 폴더에 저장된 Sample.config 파일을 참고하여 Memcached 서비스에 대한 설정을 추가해야 합니다. 이 코드는 다음과 같습니다.

  1: <configuration>
  2:   <configSections>
  3:     <sectionGroup name="enyim.com">
  4:       <section name="memcached" type="Enyim.Caching.Configuration.MemcachedClientSection, Enyim.Caching" />
  5:     </sectionGroup>
  6:   </configSections>
  7:   <enyim.com>
  8:     <memcached enabled="true">
  9:       <!-- keyTransformer="" -->
 10:       <servers>
 11:         <add address="192.168.160.11" port="11211" />
 12:       </servers>
 13:       <socketPool minPoolSize="10" maxPoolSize="100" connectionTimeout="00:10:00" deadTimeout="00:02:00" />
 14:     </memcached>
 15:   </enyim.com>
 16: </configuration>
 17: 

여기서 가장 중요한 설정은 바로 8번 라인부터 14번 라인까지의 설정입니다. 8번 라인에서는 Memcached를 사용하도록 enabled 특성에 true를 지정하였으며 11번 라인에서는 Memcached가 서비스되는 서버의 IP와 포트 번호를 지정합니다. <servers> 요소에는 여러 개의 <add> 요소를 지정할 수 있으므로 여러 개의 Memcached 서버를 지정하여 일종의 클러스터링 캐시를 구성할 수도 있습니다.

이제 Memcached 캐시를 이용하는 간단한 예제를 구현해 보겠습니다. 우선 다음과 같이 Account라는 이름의 클래스를 구현합니다.

  1: public class Account
  2: {
  3:   public string UserName { get; set; }
  4:   public string DisplayName { get; set; }
  5:   public int Age { get; set; }
  6: 
  7:   public override string ToString()
  8:   {
  9:     return String.Format(
 10:       "{0}, {1}",
 11:       this.DisplayName,
 12:       this.Age
 13:     );
 14:   }
 15: }

Account 클래스를 추가했으면 Default.aspx.cs 파일에 다음과 같이 코드를 추가합니다.

  1: protected void Page_Load(object sender, EventArgs e)
  2:     {
  3:       MemcachedClient client = new MemcachedClient();
  4:       List<Account> accounts = client.Get<List<Account>>("myAccounts");
  5:       
  6:       if (accounts == null)
  7:       {
  8:         accounts = CreateSampleData();
  9:         client.Store(StoreMode.Set, "myAccounts", accounts);
 10:         Response.Write("Cached");
 11:         Response.Write("<br>");
 12:       }
 13: 
 14:       foreach (Account account in accounts)
 15:       {
 16:         Response.Write(account.ToString());
 17:         Response.Write("<br>");
 18:       }
 19:     }
 20: 
 21:     private List<Account> CreateSampleData()
 22:     {
 23:       List<Account> accounts = new List<Account>();
 24:       accounts.Add(new Account()
 25:       {
 26:         UserName = "webgenie",
 27:         DisplayName = "웹지니",
 28:         Age = 100
 29:       });
 30: 
 31:       accounts.Add(new Account()
 32:       {
 33:         UserName = "zmeun",
 34:         DisplayName = "천호민",
 35:         Age = 80
 36:       });
 37: 
 38:       accounts.Add(new Account()
 39:       {
 40:         UserName = "sleepy",
 41:         DisplayName = "꽃미남",
 42:         Age = 70
 43:       });
 44: 
 45:       return accounts;
 46:     }

우선 3번 라인의 코드를 보면 MemcachedClient 클래스의 인스턴스를 생성합니다. 이 클래스가 바로 Memcached 서버에 액세스하기 위한 Entry Point 역할을 담당하는 클래스입니다. 4번 라인과 같이 MemcachedClient.Get 메서드를 이용하면 Memcached 서버의 캐시로부터 지정된 키로 저장된 객체를 얻어올 수 있습니다. 만일 객체를 찾을 수 없다면 null이 리턴되므로 이 경우에는 6번 라인의 if 구문을 이용해 새로운 객체를 생성하고 이를 MemcachedClient.Store 메서드를 호출하여 Memcached 서버의 캐시에 객체를 저장합니다.

이제 이 예제 애플리케이션을 실행해 보면 아래 그림과 같이 10번 라인에서 객체를 캐시에 추가할 때 출력한 Cached!라는 문자열이 출력된 채로 데이터가 보여지는 것을 볼 수 있습니다.

img5

F5 키를 눌러 페이지를 새로 고치면 아래 그림과 같이 캐시로부터 데이터를 가져와 보여주게 됩니다.

img6

5. Why need Memcached?

자, 이제 Memcached 서버가 올바르게 동작하는 것을 확인했습니다. 그런데 이 녀석을 어디에 어떻게 써먹을 수 있을까요? ASP.NET도 이미 훌륭한 캐싱 기능을 제공하고 있는데 말이지요.

아시다시피 ASP.NET의 캐싱 기능은 웹 서버의 메모리를 활용합니다. 따라서 너무 많이 사용할 경우 결국은 웹 서버의 메모리 부하로 이어질 수 있다는 단점이 있지요. 그러나 물리적 서버 수에 여유가 있다면 이처럼 Memcached 서버를 구축함으로써 많은 데이터를 캐싱할 수 있어 전반적인 서비스의 성능 향상을 꾀할 수 있습니다.

특히 Memcached Providers 프로젝트 사이트를 보면 이 포스트에서 사용했던 Enyim Memcached Client API를 바탕으로 ASP.NET의 Cache API와 Session 객체를 대체한 Provider 객체가 구현되어 제공되고 있습니다. 따라서 현재 ASP.NET의 Cache나 Session을 사용하고 있다면 이를 Memcached 서버로 옮김으로써 웹 서버의 부하를 줄일 수 있겠지요?

뿐만 아니라 여러 개의 Memcached 서버를 구축하고 Web.config 파일에 서버들의 IP를 추가해주면 캐싱하는 객체들이 여러 Memcached 서버에 분산되어 저장될 뿐 아니라 Memcached 서버에 복제 기능이 추가된 repcached라는 녀석을 이용하면 분산 캐시 클러스터를 구성할 수도 있습니다.

물론 이들 기능들은 Velocity에서도 구현이 되고 있습니다만 분산 캐시가 필요한데 Velocity를 마냥 기다릴 수만 없다면 Memcached가 현재로서는 가장 탁월한 선택이 아닐까 생각됩니다.

요즘들어 느끼는 거지만 세상 참 좋아졌어요 ^^

즐거운 하루 되세요~

[UPDATED: 2009-06-26 13:39]

조금 전 저는 이 포스트와 동일한 방법으로 구성한 Memcached 서버에 간단한 테스트를 진행해 보았습니다. 제가 실행했던 테스트는 미국에 위치한 우리 회사의 DB로부터 100개의 레코드를 가져와 이를 List<T> 타입의 Entity 객체로 변환한 후 GridView 컨트롤에 바인딩 했을 때와 변환된 Entity 객체를 Memcached 캐시에 추가한 후 캐시로부터 가져왔을 때의 성능 비교였습니다. 먼저 그 결과를 보여드리자면 다음과 같습니다.

Try from Database (밀리초) from Cache (밀리초)
1 3862 8
2 3123 7
3 2987 7
4 3136 8
5 3124 7

위의 표에서 알 수 있듯이 성능의 차이가 상당함을 볼 수 있습니다. 물론 DB는 미국에 있고 Memcached 서버는 현재 회사 네트워크 내에 있기 때문에 더 큰 성능 상의 차이가 발생했겠습니다. 해서 회사 내의 네트워크에 존재하는 다른 DB에 대해 동일한 테스트를 수행해 보았습니다. 그 결과는 아래 표와 같습니다.

Try from Database (밀리초) from Cache (밀리초)
1 2176 8
2 2172 7
3 2174 7
4 2182 8
5 2171 7

로컬 DB와의 테스트에서도 상당한 성능 차이가 있군요. 물론 이런 이유로 캐싱을 사용하는 것이겠지만 이 정도라면 Memcached 웹사이트에 적혀있던 "Very Fast"라는 말이 무색하지 않네요. 도움이 되셨기를 바랍니다. ^^

Posted by 웹지니 트랙백 1 : 댓글 0