프로젝트 STG 환경 Redis 구성을 Cluster 모드로 업그레이드를 진행하였습니다.
ECS 컨테이너에서는 접속에 문제가 없지만 Bastion Host를 통한 Local 접속 시 아래와 같이 오류가 발생하였습니다.
[전제조건] Redis는 Private Subnet 네트워크 구성
2024-03-25 15:00:22.591 WARN [NewEverTimeServer,,] 3782 --- [ioEventLoop-6-3] i.l.core.cluster.RedisClusterClient : connection timed out: test-redis-0001-001.test-cluster-redis.******.apn2.cache.amazonaws.com/10.***.***.129:6379
2024-03-25 15:00:32.633 WARN [NewEverTimeServer,,] 3782 --- [ioEventLoop-6-4] i.l.core.cluster.RedisClusterClient : connection timed out: test-redis-0001-002.test-cluster-redis.******.apn2.cache.amazonaws.com/10.***.***.47:6379
org.springframework.data.redis.RedisConnectionFailureException: Redis connection failed; nested exception is io.lettuce.core.RedisConnectionException: Unable to connect to [rediss://127.0.0.1:56379?verifyPeer=NONE, rediss://127.0.0.1:56380?verifyPeer=NONE]
원인을 찾아보니 Cluster모드의 Redis에서 응답하는 nodes 정보가 Private Subnet IP로 확인하였습니다.
즉 Lettuce 라이브러리에 Cluster Nodes 정보를 Mapping 해주지 않으면 Redis에서 응답받은 nodes 정보가 반영되어
접속 오류가 발생하는 것입니다.
Lettuce 라이브러리에서도 특별한 가이드라인은 찾지 못하였습니다.
hinweis@MacBook-Pro ~ % redis-cli -h 127.0.0.1 -p 56379
127.0.0.1:56379> ping
PONG
127.0.0.1:56379> cluster slots
1) 1) (integer) 0
2) (integer) 16383
3) 1) "test-redis-0001-001.test-cluster-redis.******.apn2.cache.amazonaws.com"
2) (integer) 6379
3) ""
4) 1) "ip"
2) "10.***.***.129"
4) 1) "test-redis-0001-002.test-cluster-redis.******..apn2.cache.amazonaws.com"
2) (integer) 6379
3) "9221821ec8a571b31cfe80a60de87efda4526f4c"
4) 1) "ip"
2) "10.***.***.47"
문제 해결을 위해서 아래와 같이 작업을 진행하였습니다.
AWS Redis 서비스를 사용하기 위해서는 Lettuce 2.6.6 이상 권장합니다. (공식문서 참고)
1. Application.yml 설정
- cluster nodes 정보 추가
redis:
# host: 127.0.0.1
# port: 56379
cluster:
nodes: 127.0.0.1:56379, 127.0.0.1:56380
max-redirects: 2
2. RedisConfig 구성 변경
- Redis 모드 (Standalone, Cluster)에 따라 설정 분기 로직 추가
- 아래는 이해하기 편하게 테스트 코드 기준으로 명시 (추후 구조화 적용)
@Configuration
@RequiredArgsConstructor
public class RedisConfig {
@Value("${spring.redis.host:localhost}")
private String redisHost;
@Value("${spring.redis.port:6379}")
private int redisPort;
@Value("${spring.redis.cluster.nodes:127.0.0.1:6379}")
private List<String> clusterNodes;
@Value("${spring.redis.cluster.max-redirects:0}")
private int maxRedirects;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
if (maxRedirects > 0) {
List<String> nodes;
if (clusterNodes.size() == INTEGER_ONE) {
nodes = Collections.singletonList(HostAndPort.of(redisHost, redisPort).toString());
}
else {
nodes = clusterNodes;
}
// cluster nodes를 원하는 값으로 적용
RedisClusterConfiguration clusterConfiguration = new RedisClusterConfiguration(nodes);
clusterConfiguration.setMaxRedirects(maxRedirects);
// topology 자동 업데이트 옵션 추가
ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
.enableAllAdaptiveRefreshTriggers()
.enablePeriodicRefresh()
.dynamicRefreshSources(false)
.build();
// topology 옵션 및 timeout 세팅
ClusterClientOptions clientOptions = ClusterClientOptions.builder()
.autoReconnect(true)
.topologyRefreshOptions(topologyRefreshOptions)
.validateClusterNodeMembership(false)
.build();
LettuceClientConfiguration lettuceClientConfiguration = useSsl ?
LettuceClientConfiguration.builder()
.clientOptions(clientOptions)
.readFrom(ReadFrom.REPLICA_PREFERRED)
.useSsl()
.disablePeerVerification()
.build()
:LettuceClientConfiguration.builder()
.clientOptions(clientOptions)
.readFrom(ReadFrom.REPLICA_PREFERRED)
.build();
return new LettuceConnectionFactory(clusterConfiguration, lettuceClientConfiguration);
} else {
RedisStandaloneConfiguration redisConfiguration = new RedisStandaloneConfiguration(redisHost, redisPort);
LettuceClientConfiguration lettuceClientConfiguration = useSsl ?
LettuceClientConfiguration.builder()
.useSsl()
.disablePeerVerification()
.build()
: LettuceClientConfiguration.builder().build();
return new LettuceConnectionFactory(redisConfiguration, lettuceClientConfiguration);
}
}
}
읽어주셔서 감사합니다.
레퍼런스
반응형
최근댓글