이번에는 ElastiCache Redis를 운영하면서 발생했던 이슈와 해결했던 경험들을 공유해보려고 합니다.

ElastiCache Clustered Redis 다중 키 작업 문제

클러스터로 구성하면서 다중 키를 작업하면 아래와 같은 에러가 발생할 수 있습니다.

CROSSSLOT Keys in request don't hash to the same slot

해당 에러는 다중 키 작업시 키가 동일한 해시 슬롯에 있지 않아서 발생하는 문제입니다. 해결 방법은 2가지가 있습니다.

첫번째 방법은 Redis 클러스터를 지원하는 Redis 클라이언트 라이브러리를 사용하는 것입니다. 라이브러리를 변경하면 구현 소스를 일부 변경해야 할 수 있습니다.

두번째 방법은 키에 “{..}” 패턴을 포함하여 강제로 동일한 해시슬롯에 넣는 방법입니다. 아래의 와 같이 value 값에는 "{CACHE_KEYS}"로 공통 패턴을 주었고, key 값에는 "{AWS_CACHE}"로 공통 패턴을 주어서 나중에 한번에 찾아 지우기 쉽도록 구현했습니다.

@Cacheable(value = "{CACHE_KEYS}.TestService.listForAwsData", key = "{'{AWS_CACHE}', #root.targetClass, #root.methodName, #model.value}")
ResultModel listForAwsData(CommonSearchModel model) {
...
}
Code language: CSS (css)

Redis OOM

캐싱 중 메모리가 꽉 찬 경우에 아래와 같은 에러가 발생합니다.

OOM command not allowed when used memory > 'maxmemory'
Code language: JavaScript (javascript)

캐싱 정책을 살펴보고, 더 이상 캐싱 데이터를 줄일 여지가 없다면 스케일업을 진행합니다. 스케일업 작업 중에도 이미 캐싱된 데이터는 정상적으로 조회됩니다.

Error executing cache operation

가능성은 현저히 적지만, 서비스 중 ElastiCache Redis에 문제가 생길 수 있습니다. 자동 장애조치를 통해서도 해결할 수 없는 장애를 대비해서 아래와 같이 캐시 데이터를 가져올 때 에러가 나더라도 null을 리턴하여 정상적으로 db에서 데이터를 조회하도록 CustomRedisTemplate을 구현합니다.

public class CustomRedisTemplate<K, V> extends RedisTemplate<K, V> {

private static final Logger logger = LoggerFactory.getLogger(CustomRedisTemplate.class);

@Override
public <T> T execute(final RedisCallback<T> action, final boolean exposeConnection, final boolean pipeline) {
try {
return super.execute(action, exposeConnection, pipeline);
} catch(final Throwable t) {
logger.warn(“Error with cache : {}”, t.getMessage());
return null;
}
}

@Override
public <T> T execute(final RedisScript<T> script, final List<K> keys, final Object… args) {
try {
return super.execute(script, keys, args);
} catch(final Throwable t) {
logger.warn(“Error with cache : {}”, t.getMessage());
return null;
}
}

@Override
public <T> T execute(final RedisScript<T> script, final RedisSerializer<?> argsSerializer, final RedisSerializer<T> resultSerializer, final List<K> keys, final Object… args) {
try {
return super.execute(script, argsSerializer, resultSerializer, keys, args);
} catch(final Throwable t) {
logger.warn(“Error with cache : {}”, t.getMessage());
return null;
}
}

@Override
public <T> T execute(final SessionCallback<T> session) {
try {
return super.execute(session);
} catch(final Throwable t) {
logger.warn(“Error with cache : {}”, t.getMessage());
return null;
}
}
}