Spring Redis Cluster 연결

greenTea·2025년 3월 29일

Redis Cluster를 구성한 후 연결하는 과정에 대해서 정리하였습니다.

1. 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

Redis 설정파일

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을 통해 저의 로컬 환경에서도 접근이 가능하도록 구성하였습니다.

Spring

Redis 설정 정보 구성

@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를 적어주었습니다.

Spring Configuration

@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 = ClusterTopologyRefreshOptions
			.builder()
			.dynamicRefreshSources(true)
			.enableAllAdaptiveRefreshTriggers()
			.enablePeriodicRefresh(Duration.ofSeconds(30))
			.build();

cluster의 구성 환경에 대한 설정값입니다.
cluster 연결시 slot에 대한 정보들을 가져오게 되는데 동적으로 갱신하면서 해당 정보들을 최신화해주는 코드라고 생각하시면 됩니다.
.enableAllAdaptiveRefreshTriggers() : 오래된 정보는 자동 갱신
enablePeriodicRefresh(Duration.ofSeconds(30)) : 30초마다 갱신

ClusterClientOptions

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번 허용

MappingSocketAddressResolver

사실 이 글을 쓰게 된 이유입니다.

현재 쿠버네티스에서 Redis를 사용하고 있는 만큼 쿠버네티스 내부 ip를 별도로 갖고 있습니다.
또한 Cluster node 끼리도 서로 내부 IP로 통신을 하고 있는 상황입니다.(설정 값을 통해 변경이 가능합니다.)

연결 자체는 이미 쿠버네티스의 서비스를 노드포트로 열어놓았기에 문제가 없지만 Redirect시에 문제가 됩니다.

  1. localhost:12345로 Redis 연결
  2. 연결시 Cluster 내부 정보를 Redis가 스프링에게 전달
  3. Redis가 전달한 정보의 경우 내부IP(10.x.x.x)로 연결된 정보들을 전달
  4. 스프링에서는 위에서 전달받은 정보를 토대로 환경을 구성하려 하지만 접근이 불가능(내부 파드 접근이 불가능해서 서비스를 만든 것인데 내부ip로 접근을 시도하기에 문제)
  5. 연결 불가능

그래서 위와 같은 문제를 해결하기 위해서는 스프링에게 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를 하게 되더라도 요청이 제대로 갈 수 있게 됩니다.

LettuceClientConfiguration

지금까지 설정한 값들을 모두 모아서 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를 사용하신다면 토폴로지 설정값만 넣어주셔도 사용이 가능합니다.

profile
greenTea입니다.

0개의 댓글