Abstraction
memcached는 커넥션을 항상 유지하는 서버이며 명시적으로 서버에서 끊는 기능이 없다. 요청 성능을 높이기 위해 각각의 요청마다 TCP 커넥션을 맺지 않고 커넥션을 유지하는, 커넥션을 풀링하는 클라이언트 클래스를 Singleton으로 구현해 본다.
Connection Pooling
Java 클라이언트 라이브러리로 spymemcached 를 이용한다. 이를 이용한 커넥션 풀링 클래스는 Java 프로세스에 단 한 개만 존재하는 Singleton 으로 구현하며, instance 요청시 이미 존재하는 객체를 재활용해 리턴한다.
최대 커넥션은 5개로 제한한다. 수십대의 서버에 배포할 것이므로 굳이 더 높게 잡을 필요는 없다. 일반적으로 memcached의 TPS는 동시 커넥션이 64개 이상인 경우 큰 차이가 없다.
key를 해싱하는 알고리즘은 CRC32로 정했으며 좀 더 높은 성능을 내기 위해 BinaryConnection 으로 연결한다. 매 요청이 올때마다 최대 5개의 커넥션 중 하나를 랜덤하게 돌려준다. 클래스의 구현은 Using Memcached with Java 아티클을 참고했다.
import net.spy.memcached.AddrUtil;
import net.spy.memcached.BinaryConnectionFactory;
import net.spy.memcached.HashAlgorithm;
import net.spy.memcached.MemcachedClient;
public class MemcachedConnectionPool {
private static final int MAX_CONNECTION = 5;
private static MemcachedConnectionPool instance = null;
private static MemcachedClient[] m = null;
private MemcachedConnectionPool() {
m = new MemcachedClient[MAX_CONNECTION];
for (int i = 0; i < MAX_CONNECTION; i++) {
try {
MemcachedClient c = new MemcachedClient(
new BinaryConnectionFactory(
BinaryConnectionFactory.DEFAULT_OP_QUEUE_LEN,
BinaryConnectionFactory.DEFAULT_READ_BUFFER_SIZE,
HashAlgorithm.CRC32_HASH),
AddrUtil.getAddresses("server-1:11211 server-2:11211"));
m[i] = c;
} catch (IOException e) {
e.printStackTrace();
} catch (RuntimeException e) {
e.printStackTrace();
}
}
}
public static synchronized MemcachedConnectionPool getInstance() {
if (instance == null)
instance = new MemcachedConnectionPool();
return instance;
}
public Object get(String key) {
try {
return getConn().get(key);
} catch (RuntimeException e) {
e.printStackTrace();
return null;
}
}
public void set(String key, int ttl, final Object value) {
try {
getConn().set(key, ttl, value);
} catch (RuntimeException e) {
e.printStackTrace();
}
}
public void delete(String key) {
try {
getConn().delete(key);
} catch (RuntimeException e) {
e.printStackTrace();
}
}
public MemcachedClient getConn() {
return m[(int) (Math.random() * MAX_CONNECTION)];
}
}
Client Utility
커넥션 풀링외에 필요한 또 한가지 기능이 있다. memcached 는 단순 key/value 형태이므로 이를 구분하기 위한 Bucket의 개념이 필요하다. 이를 prefix로 구분하기로 하고 해당 prefix로 생성한 클래스를 이용해 커넥션 풀링에 접근한다.
이미지로 도식화하면 아래와 같다.

Client Utility 클래스를 prefix로 생성해 key를 전달하면 Utility 클래스는 Connection Pooling 클래스에 prefix+key 값을 함께 전달해 set/get 을 수행한다. 각각의 key 앞에 prefix를 이용해 Bucket 처럼 구분했다. 코드는 아래와 같다.
public class MemcachedClientUtil {
private static final String DEFAULT_PREFIX = "DEFAULT";
private static final int DEFAULT_CACHE_TIME = 60 * 60;
private String prefix = "";
public MemcachedClientUtil(String prefix) {
this.prefix = (StringUtils.isEmpty(prefix)) ?
DEFAULT_PREFIX : prefix;
}
public MemcachedClientUtil() {
this.prefix = DEFAULT_PREFIX;
}
public Object get(String key) {
return MemcachedConnectionPool.getInstance().get(this.prefix + "_" + key);
}
public String getString(String key) {
Object o = this.get(key);
return o instanceof String ? o.toString() : null;
}
public void set(String key, Object value) {
this.set(key, DEFAULT_CACHE_TIME, value);
}
public void set(String key, int ttl, Object value) {
MemcachedConnectionPool.getInstance().set(this.prefix + "_" + key, ttl, value);
}
public void delete(String key) {
MemcachedConnectionPool.getInstance().delete(this.prefix + "_" + key);
}
}
기타
이제 이를 이용해 각각의 모듈에서 호출해 사용한다. 아래와 같은 형태가 된다.
아무리 많은 요청을 날려도 커넥션을 맺거나 끊지 않는다. 항상 일정한 커넥션을 유지한 상태로 커넥션을 풀링한다. 실제 운영중인 서버의 통계 그래프에서도 이를 확인할 수 있다.

전체 아이템 갯수는 계속 변하지만 커넥션 수는 155개(31대 서버 적용 중)로 일정함을 확인할 수 있다.


