Redis Cluster를 구성한 후 연결하는 과정에 대해서 정리하였습니다.
먼저 저는 쿠버네티스를 이용하여 Redis Cluster 환경을 구성하였습니다.
(helm으로도 가능하지만 로컬에서는 설정이 잘 안되어 부득이 하게 직접 구성하는 방식으로 진행하였습니다. helm으로 가능하면 helm으로 구성하셔도 될 것 같습니다.)
---
apiVersion: v1
kind: Service
metadata:
name: redis-cluster-0
spec:
type: NodePort
selector:
statefulset.kubernetes.io/pod-name: redis-cluster-0
ports:
- port: 6379
targetPort: 6379
nodePort: 30070
---
# ...다른 서비스도 같이 설정
---
apiVersion: v1
kind: ConfigMap
metadata:
name: redis-config
data:
redis.conf: |
port 6379
cluster-enabled yes
cluster-config-file /data/nodes.conf
cluster-node-timeout 5000
appendonly yes
protected-mode no
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis-cluster
spec:
serviceName: redis-cluster-headless
replicas: 6
selector:
matchLabels:
app: redis-cluster
template:
metadata:
labels:
app: redis-cluster
spec:
containers:
- name: redis
image: redis:7.2
command: ["redis-server", "/etc/redis/redis.conf"]
ports:
- containerPort: 6379
- containerPort: 16379
volumeMounts:
- name: data
mountPath: /data
- name: redis-config
mountPath: /etc/redis
readOnly: true
volumes:
- name: data
emptyDir: {}
- name: redis-config
configMap:
name: redis-config
apiVersion: v1
kind: ConfigMap
metadata:
name: redis-config
data:
redis.conf: |
port 6379
cluster-enabled yes
cluster-config-file /data/nodes.conf
cluster-node-timeout 5000
appendonly yes
protected-mode no
port 6379 : 레디스에서 사용하는 기본 포트설정
cluster-enabled yes : cluster 활성화 (이 설정을 해주지 않으면 클러스터 설정 불가능)
cluster-config-file /data/nodes.conf : cluster 정보 설정 위치(레디스를 재시작해도 클러스터 정보를 이용해서 구성이 유지될 수 있습니다.)
cluster-node-timeout 5000 : cluster heatbeat timeout 설정
appendonly yes : 영속성 설정 옵션(안 해주셔도 괜찮습니다)
protected-mode no : bind 설정과 패스워드 유무를 통해 접근 가능 여부를 판단하게 되는데 여기서는 꺼주었습니다.
저의 경우에는 minikube를 docker로 사용중이기에 부득이하게 minikube service --all을 통해 저의 로컬 환경에서도 접근이 가능하도록 구성하였습니다.
@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "spring.data.redis.cluster")
public class RedisConfProperty {
public List<String> nodes;
public String password;
public int timeout;
}
spring:
data:
redis:
cluster:
nodes:
- "localhost:57528"
- "localhost:57530"
- "localhost:57532"
- "localhost:57534"
- "localhost:57536"
- "localhost:57538"
password: requirepassword
timeout: 100
yaml에서 설정한 값을 가져와서 객체에 저장하는 코드입니다.
초기 비밀번호 값과 타임아웃등을 설정해주었고 nodes에는 현재 쿠버네티스에서 만든 Redis Node들의 host와 port를 적어주었습니다.
@RequiredArgsConstructor
@Configuration
public class RedisConfig {
private final RedisConfProperty redisConfProperty;
@Bean
public LettuceConnectionFactory lettuceConnectionFactory() {
RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration();
List<String> nodes = redisConfProperty.getNodes();
List<RedisNode> list = nodes.stream().map(node -> {
String[] trim = node.split(":");
return new RedisNode(trim[0], Integer.parseInt(trim[1]));
}).toList();
ClusterTopologyRefreshOptions clusterTopologyRefreshOptions = ClusterTopologyRefreshOptions
.builder()
.dynamicRefreshSources(true)
.enableAllAdaptiveRefreshTriggers()
.enablePeriodicRefresh(Duration.ofSeconds(30))
.build();
ClusterClientOptions clusterClientOptions = ClusterClientOptions
.builder()
.pingBeforeActivateConnection(true)
.autoReconnect(true)
.topologyRefreshOptions(clusterTopologyRefreshOptions)
.maxRedirects(3).build();
MappingSocketAddressResolver resolver = MappingSocketAddressResolver.create(
DnsResolvers.UNRESOLVED,
hostAndPort -> switch (hostAndPort.getHostText()) {
case "10.244.59.185" -> HostAndPort.of("localhost", 57528); // redis-cluster-0
case "10.244.205.201" -> HostAndPort.of("localhost", 57530); // redis-cluster-1
case "10.244.151.53" -> HostAndPort.of("localhost", 57532); // redis-cluster-2
case "10.244.59.131" -> HostAndPort.of("localhost", 57534); // redis-cluster-3
case "10.244.205.200" -> HostAndPort.of("localhost", 57536); // redis-cluster-4
case "10.244.151.9" -> HostAndPort.of("localhost", 57538); // redis-cluster-5
default -> hostAndPort;
}
);
ClientResources clientResources = ClientResources.builder()
.socketAddressResolver(resolver)
.build();
redisClusterConfiguration.setClusterNodes(list);
redisClusterConfiguration.setPassword(redisConfProperty.getPassword());
LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
.commandTimeout(Duration.ofSeconds(
redisConfProperty.getTimeout()))
.clientOptions(clusterClientOptions)
.clientResources(clientResources)
.build();
return new LettuceConnectionFactory(redisClusterConfiguration, clientConfig);
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(lettuceConnectionFactory());
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new StringRedisSerializer());
return template;
}
}
ClusterTopologyRefreshOptions clusterTopologyRefreshOptions = ClusterTopologyRefreshOptions
.builder()
.dynamicRefreshSources(true)
.enableAllAdaptiveRefreshTriggers()
.enablePeriodicRefresh(Duration.ofSeconds(30))
.build();
cluster의 구성 환경에 대한 설정값입니다.
cluster 연결시 slot에 대한 정보들을 가져오게 되는데 동적으로 갱신하면서 해당 정보들을 최신화해주는 코드라고 생각하시면 됩니다.
.enableAllAdaptiveRefreshTriggers() : 오래된 정보는 자동 갱신
enablePeriodicRefresh(Duration.ofSeconds(30)) : 30초마다 갱신
ClusterClientOptions clusterClientOptions = ClusterClientOptions.builder()
.pingBeforeActivateConnection(true)
.autoReconnect(true)
.topologyRefreshOptions(clusterTopologyRefreshOptions)
.maxRedirects(3)
.build();
cluster의 환경에 대한 옵션이지만 토폴로지보다는 연결에 관한 설정입니다.
pingBeforeActivateConnection(true): 커넥션 얻을 때마다 PING 으로 연결 확인
autoReconnect(true): 장애 복구 후 자동 재연결
topologyRefreshOptions(...): 위에서 만든 topology refresh 옵션
maxRedirects(3): MOVED/ASK redirect 를 최대 3번 허용
사실 이 글을 쓰게 된 이유입니다.
현재 쿠버네티스에서 Redis를 사용하고 있는 만큼 쿠버네티스 내부 ip를 별도로 갖고 있습니다.
또한 Cluster node 끼리도 서로 내부 IP로 통신을 하고 있는 상황입니다.(설정 값을 통해 변경이 가능합니다.)
연결 자체는 이미 쿠버네티스의 서비스를 노드포트로 열어놓았기에 문제가 없지만 Redirect시에 문제가 됩니다.
localhost:12345로 Redis 연결그래서 위와 같은 문제를 해결하기 위해서는 스프링에게 10.x.x.x라는 주소로 가게 된다면 localhost로 갈 수 있게 host와 port 정보를 매핑해줘야 합니다.
MappingSocketAddressResolver resolver = MappingSocketAddressResolver.create(
DnsResolvers.UNRESOLVED,
hostAndPort -> switch (hostAndPort.getHostText()) {
case "10.244.59.185" -> HostAndPort.of("localhost", 57528); // redis-cluster-0
case "10.244.205.201" -> HostAndPort.of("localhost", 57530); // redis-cluster-1
case "10.244.151.53" -> HostAndPort.of("localhost", 57532); // redis-cluster-2
case "10.244.59.131" -> HostAndPort.of("localhost", 57534); // redis-cluster-3
case "10.244.205.200" -> HostAndPort.of("localhost", 57536); // redis-cluster-4
case "10.244.151.9" -> HostAndPort.of("localhost", 57538); // redis-cluster-5
default -> hostAndPort;
}
);
ClientResources clientResources = ClientResources.builder()
.socketAddressResolver(resolver)
.build();
위와 같이 설정을 해주게 된다면 내부 ip로 Redirect를 하게 되더라도 요청이 제대로 갈 수 있게 됩니다.
지금까지 설정한 값들을 모두 모아서 LettuceConfigFactory를 만들어주면 됩니다.
redisClusterConfiguration.setClusterNodes(list);
redisClusterConfiguration.setPassword(redisConfProperty.getPassword());
LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
.commandTimeout(Duration.ofSeconds(redisConfProperty.getTimeout()))
.clientOptions(clusterClientOptions).clientResources(clientResources)
.build();
return new LettuceConnectionFactory(redisClusterConfiguration, clientConfig);
cloud에서 제공하는 Redis관련 Pass를 사용하신다면 토폴로지 설정값만 넣어주셔도 사용이 가능합니다.