Company/이슈

Redis Cluster Connection 유지를 위한 Client Refresh 설정 이슈

nineDeveloper 2025. 2. 16.
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 명령을 실행
    • period: 클러스터 토폴로지 새로고침 기간(주기)
      • ClusterTopologyRefreshOptions.enablePeriodicRefresh(Duration.ofSeconds(60))
        • 설정된 주기마다 레디스 서버에 HELLO, CLIENT SETNAME, CLUSTER NODES, INFO 4개 명령을 실행함
        • 3 마스터, 3 복제 구성에서 마스터1번이 다운되면 새로고침(refresh) 전까지는 마스터1번에 입력되는 명령은 에러가 발생함
        • 다른 마스터에 입력되는 명령과 복제에 조회 명령은 정상적으로 실행됨
        • 새로고침 후에는 복제1번이 새 마스터가 된것을 인지해서 입력 명령이 정상적으로 실행됨
        • 설정하지 않으면 새로고침을 하지 않음
        • 적정 값: 30초 ~ 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

 

728x90

댓글

💲 추천 글