728x90
📌 Problem
- 2024.06.25 AWS ElastiCache Cluster Scale Down 작업이 있었음
- 예상은 Redis Cluster는 Private Domain 형태로 Connection 설정을 하기 때문에 Node 정보가 바뀌더라도 Connection 에 문제가 없을줄 알았음
- 하지만 예상과 다르게 AWS ElastiCache Cluster Scale Down 작업으로 인해 Cluster Node 정보가 바뀌자 해당 Redis Cluster 에 연결된 Application 들이 일제히 reconnect 에 실패함
🚦 cause
- SpringBoot 의 Lettuce Redis Client 의 Connection 설정에서 Redis Cluster 의 nodes 설정만 했을 경우 Cluster Topology를 새로고침 하지 않고 최초 Connection 을 맺을때의 Node 정보를 DNS 형태로 캐싱해서 가지고 있음
- Scale Up/Down, Redis 버전 업그레이드, Master 장애로 인한 복제 구성 승격 등으로 인해 Node 정보가 변경될 경우 별도의 Refresh 설정을 하지 않으면 변경된 Node 정보를 받아오지 못해 Reconnect를 할 수 없음
- 이에 Redis Client 에서 지원하는 Refresh 설정을 추가하여 Cluster Topology 를 주기적으로 새로고침 하도록해야됨
🔑 Solution
- 테스트는 ElastiCache Redis 5.0.6 버전으로 했으며 Refresh 설정/미설정 상태로 Scale Up/Down 및 Redis 버전 업그레이드(7.1 버전)를 하며 Connection 유지 테스트를 진행
- Redis 버전과 상관없이 Cluster Refresh 미설정시 Reconnect 에 실패함
- Cluster Refresh 설정을 30초로 설정했을 시 Redis Cluster Topology 정보를 30초마다 갱신하여 변경된 Node 정보가 있다면 자동으로 갱신하고 reconnect 가 잘되는 것을 확인함
- 적정 Refresh 주기는 30~60초 정도를 권장함
- 주기를 더 짧게 가져갈 경우 새로 고침을 너무 자주해서 Redis 서버에 부하를 줄 수 있음
API 의 Redis 기존 설정
spring:
profiles:
active: dev
data:
redis:
cluster:
# 마스터 노드 뿐만 아니라 복제 노드까지 모두 나열(마스터가 모두 다운되었을 경우를 대비해서)
nodes: connection-test-redis5.xxxxx4.clustercfg.apn2.cache.amazonaws.com:6379
client-name: omakase-api
client-type: lettuce
timeout: 1s
connect-timeout: 5s
lettuce:
pool:
# Pool을 활성화할지 여부: "commons-pool2"를 사용할 수 있는 경우 자동으로 활성화됨
# Jedis를 사용하면 센티넬 모드에서 풀링이 암시적으로 활성화되며 이 설정은 단일 노드 설정에만 적용됨
enabled: true
# Pool의 최대 "유휴(idle)" 연결 수: 유휴 연결 수에 제한이 없음을 나타내려면 음수 값을 사용함
max-active: 8
# Pool에서 유지 관리할 최소 유휴 연결 수의 목표: 이 설정은 해당 설정과 제거 실행 사이의 시간이 모두 양수인 경우에만 효과가 있음
max-idle: 8
# 주어진 시간에 pool에서 할당할 수 있는 최대 연결 수: 제한이 없으면 음수 값을 사용함
min-idle: 1
# Pool이 소진되었을 때 예외가 발생하기 전에 연결 할당이 차단되어야 하는 최대 시간: 무기한 차단하려면 음수 값을 사용함
max-wait: 5s
# 유휴(idle) 개체 축출기 스레드 실행 사이의 시간: 양수이면 유휴 개체 제거 스레드가 시작되고, 그렇지 않으면 유휴 개체 제거가 수행되지 않음
time-between-eviction-runs: 10m
🚫 1. 기존설정으로 Connection 테스트(실패)
기존설정으로 Application이 Connect 된 상태에서 Redis를 Scale Down 하여 Connection 유지 테스트
2024.06.25 상황과 똑같이 변경된 Node 정보가 갱신되지 않아 기존 Node IP로 reconnect 를 계속 시도
✅ 2. Redis Cluster Refresh 설정 추가후 테스트(성공)
refresh 설정 추가후 Application이 Connect 된 상태에서 ElastiCache를 Scale Down 하여 Connection 유지 테스트
spring:
data:
redis:
lettuce:
cluster:
refresh:
adaptive: on
dynamic-refresh-sources: true
period: 30s
위와 같이 설정하면 30초 마다 Redis Cluster Topology 정보를 30초마다 갱신하여 변경된 Node 정보가 있다면 자동으로 갱신하고 reconnect 해줌
💡 결론
- SpringBoot Redis Client 를 사용하고 있다면 거의 대부분 2.0 버전 이상일 것이므로 Lettuce 를 사용중일 것이다
- Lettuce 의 Cluster Refresh 옵션을 반드시 설정하여 Redis Cluster Topology 정보를 주기적으로 갱신하도록 설정 해주어야만 Topology의 Node 정보가 변경되면 자동으로 Reconnect 처리가 된다
- 테스트에 성공한 Refresh 옵션을 참고하여 각자 관리하고 있는 Application 에 관련 설정을 추가해주자
🚦 주요 옵션 설명
- adaptive: 사용 가능한 모든 새로 고침 트리거를 사용하여 적응형 토폴로지 새로 고침을 사용해야 하는지 여부
- ClusterTopologyRefreshOptions.enableAllAdaptiveRefreshTriggers()
- dynamic-refresh-sources: 클러스터 토폴로지를 얻기 위해 모든 클러스터 노드를 검색하고 쿼리할지 여부
- true: 발견된 모든 노드로부터 topology 정보를 얻어온다
- CLUSTER NODES 명령을 실행해서 얻은 모든 노드들에 CLUSTER NODES 명령을 실행
- false: 처음 설정한 seed 노드로부터로만 topology 정보를 얻어온다
- spring.data.redis.cluster.nodes 에 지정된 노드들에만 CLUSTER NODES 명령을 실행
- true: 발견된 모든 노드로부터 topology 정보를 얻어온다
- period: 클러스터 토폴로지 새로고침 기간(주기)
- ClusterTopologyRefreshOptions.enablePeriodicRefresh(Duration.ofSeconds(60))
- 설정된 주기마다 레디스 서버에 HELLO, CLIENT SETNAME, CLUSTER NODES, INFO 4개 명령을 실행함
- 3 마스터, 3 복제 구성에서 마스터1번이 다운되면 새로고침(refresh) 전까지는 마스터1번에 입력되는 명령은 에러가 발생함
- 다른 마스터에 입력되는 명령과 복제에 조회 명령은 정상적으로 실행됨
- 새로고침 후에는 복제1번이 새 마스터가 된것을 인지해서 입력 명령이 정상적으로 실행됨
- 설정하지 않으면 새로고침을 하지 않음
- 적정 값: 30초 ~ 60초 (일반적인 기준)
- ClusterTopologyRefreshOptions.enablePeriodicRefresh(Duration.ofSeconds(60))
▶︎ Java Configuration 으로 구성할 때의 예시
private fun createRedisClientConfiguration(redisProperties: RedisProperties, clientResources: ClientResources) =
LettuceClientConfiguration.builder()
.commandTimeout(redisProperties.timeout)
.readFrom(ReadFrom.REPLICA_PREFERRED) // 읽기 명령은 replica에서 우선으로 실행
.shutdownTimeout(redisProperties.lettuce.shutdownTimeout)
.clientOptions(getClusterClientOptions(redisProperties))
.clientResources(clientResources)
.build()
private fun getClusterClientOptions(redisProperties: RedisProperties): ClientOptions {
val topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
.dynamicRefreshSources(true) // default: true
.enablePeriodicRefresh(redisProperties.lettuce.cluster.refresh.period)
.enableAllAdaptiveRefreshTriggers()
.adaptiveRefreshTriggersTimeout(Duration.ofSeconds(30)) // default: 30초
.build()
return ClusterClientOptions.builder()
.autoReconnect(true)
.publishOnScheduler(true)
.disconnectedBehavior(ClientOptions.DisconnectedBehavior.DEFAULT)
.socketOptions(SocketOptions.builder().connectTimeout(redisProperties.connectTimeout).keepAlive(true).build())
.topologyRefreshOptions(topologyRefreshOptions)
.timeoutOptions(TimeoutOptions.enabled(redisProperties.timeout))
.build()
}
▶︎ Spring Boot 버전별 Redis Client
- Jedis
- SpringBoot 2.0 이전의 표준 Redis Client
- Jedis 는 요즘 거의 사용되지 않지만 만약 Jedis 를 사용하고 있다면 아래의 내용을 참고해 설정하면 된다
- Lettuce
- SpringBoot 2.0 부터 Netty (비동기 이벤트 기반 고성능 네트워크 프레임워크) 기반의 Redis Client 가 표준이됨
- 비동기 요청으로 처리하기 때문에 고성능이고 Jedis 에 비해 평균적으로 두배의 TPS 가 나옴
- SpringBoot 2.0 부터 Netty (비동기 이벤트 기반 고성능 네트워크 프레임워크) 기반의 Redis Client 가 표준이됨
- 참고
728x90
댓글