프로젝트/redis

Redis Cluster + Spring Boot + Lettuce client 이론과 세팅

Junuuu 2024. 8. 5. 00:00
728x90

Redis Sentinel이란?

https://meetup.nhncloud.com/posts/226

 

Redis Cluster를 알아보기 전에 간략하게 Redis Sentinel에 대해서도 알아보겠습니다.

만약 Redis Cluster를 활용하지 않는다면 대안으로 자동으로 failover를 지원하는 Redis Sentinel을 활용해 볼 수 있습니다.

마스터가 더 이상 사용할 수 없다는 사실에 여러 Sentinel이 합의하여 Slave 중 하나를 Master로 승격시키고 기존의 Master는 Slave로 강등시킵니다. 

단순히 하나의 Sentinel에서 네트워크 문제로 Master와 연결되지 않을 수 있으므로 특정 quorum 이상에서 다운되었다고 인지한 경우 장애 조치를 진행합니다.

 

Redis Cluster란 무엇일까?

https://meetup.nhncloud.com/posts/226

Redis Cluster는 N개의 마스터 노드를 활용하여 데이터셋을 여러 노드에 자동으로 분산하며, 일부 노드가 다운되어도 계속 활용가능하기 때문에 확장성과 고가용성의 특징을 가지고 있습니다.

 

Redis Cluster를 활용하기 위해 최소 세 개의 마스터 노드가 필요하며 Gossip Protocol을 이용하여 서로 통신합니다.

 

Gossip Protocol은 각 노드가 주기적으로 다른 노드에게 상태 정보를 브로드캐스팅하여 네트워크 전체의 일관된 상태를 유지하는 방식으로 동작합니다.

이를 통해 Redis Cluster는 자동화된 노드 발견, 실패 감지 및 재구성을 수행합니다.

Redis Cluster Bus라고 불리는 TCP bus와 binary protocol을 활용하여 다른 모든 노드가 제대로 동작하는지 ping 패킷을 보내고 특정 조건을 알리기 위해서 Cluster bus에 pub/sub message로 정보를 전파합니다.

 

Redis Cluster는 모든 노드가 TCP로 연결되어 있는 pull mesh입니다.

위와 같은 네트워크 구성을 Redis Topology라고 부릅니다.

Topology는 위상수학이라는 뜻이지만, 네트워크에서 토폴로지는 컴퓨터 네트워크의 구성을 설명하는 데 쓰이는 용어이다. 

 

Redis Topology는 실시간으로 재구성될 수 있습니다.

Redis Cluster가 실행되는 동안 노드를 추가 및 제거하는 기능을 지원합니다.

클러스터가 새 노드를 추가하면 빈 노드가 클러스터에 추가되고 일부 해시 슬롯 세트가 기존 노드에서 새 노드로 이동합니다.

Redis Cluster와 Sharding

Redis Cluster는 기본적으로 Sharding을 지원합니다.

Redis Cluster는 16384개의 슬롯으로 분할되어 최대 16384개의 마스터 노드를 가질 수 있습니다. (단 권장되는 최대 노드 크기는 약 1000개 정도입니다.)

 

Redis의 Hash Slot의 알고리즘은 아래와 같습니다.

HASH_SLOT = CRC16(key) mod 16384

 

 

Redis Cluster를 활용하는 Client는 Cluster의 상태를 보유할 필요 없이 모든 노드에 자유롭게 요청을 경우 필요한 경우  MOVED와 ASK를 활용하여 다른 노드로 Redirection이 이루어집니다.

(즉, Client에서는 샤딩을 고려하지 않고 요청해도 Redis Cluster가 알아서 처리합니다)

 

예를 들어 특정 노드가 가진 해시 슬롯으로 키를 처리할 수 없을 때 MOVED를 응답합니다.

GET x
-MOVED 3999 127.0.0.1:6381

키의 해시 슬롯(3999)와 쿼리를 처리할 수 있는 인스턴스의 엔드포인트:포트가 반환됩니다.

잘못된 연결에 대해 직접 이동하여 데이터를 찾아와서 전달하지 않으며 그저 올바른 주소만 전달합니다.

 

ASK의 경우 MOVED와 유사하지만 마이그레이션도중 발생할 수 있습니다.

 

MOVED는 해시 슬롯이 다른 노드에서 영구적으로 제공되며 다음 쿼리는 지정된 노드에 대해 시도되어야 할 때 제공되는 반면에 ASK는 다음 쿼리만 지정된 노드로 전송하는 것을 의미합니다.

 

해시 슬롯에 대한 쿼리가 아직 A에 있는 키에 대한 쿼리일 수 있으므로 클라이언트가 항상 A를 시도한 다음 필요한 경우 B로 시도하기를 원하는 상황에서 활용됩니다.

 

 

Replication의 주의사항 - 비동기 복제지연의 문제점

https://redis.io/docs/latest/operate/rs/databases/durability-ha/consistency/

Redis Cluster는 노드 간 비동기 복제를 활용하기 때문에 마스터의 쓰기가 손실될 수 있는 경우가 항상 존재합니다.

 

https://redis.io/docs/latest/operate/rs/databases/durability-ha/consistency/

다만 WAIT 명령어를 활용하여 명시적으로 복제를 비동기가 아닌 동기적으로 요청하도록 변경할 수 있습니다.

WAIT 명령어를 활용하면 쓰기에 대한 손실 가능성이 크게 줄어들지만 Redis Cluster가 String Consistency를 구현하지 않으므로 더 복잡한 장애 시나리오에서는 쓰기를 수신하지 못한 복제본이 마스터로 선출된 가능성이 존재합니다.

 

WAIT numreplicas timeout

WAIT 명령은 timeout에 지정한 값에 도달하면 지정된 복제본 수(numreplicas)에 아직 도달하지 못했더라도 명령이 반환됩니다. 

이때 timeout이 0인 경우 영원히 차단합니다.

그러면 WAIT (replicas의 수) 0 을 활용하면 모든 복제본이 sync 될 때까지 영원히 기다리지 않을까요?

클라이언트가 OK 응답을 받았다면 쓰기 유실은 발생하지 않겠지만 중간에 타임아웃이 발생하게 되면 이 쓰기가 어떤 Slave까지 반영되었는지는 알 수 없게 됩니다.

추가적으로 하나의 Slave 노드라도 문제가 생기면 바로 위험해질 수 있는 구조가 됩니다.

 

Lettuce란?

Lettuace는 Netty와 Reactor를 기반으로 구성되어 있습니다.

Thread-safe하며 확장 가능한 구조를 가진 Redis Client입니다.

동기식, 비동기식, 반응형 API를 제공하면 Redis와 상호 작용할 수 있습니다.

 

Spring Boot Redis AutoCongifuration

@AutoConfiguration
@ConditionalOnClass(RedisOperations.class) // RedisOperation 클래스가 클래스패스에 있는 조건이면 RedisAutoConfiguration이 동작
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate") // RedisTemplate redisTemplate 스프링 빈이 없는 경우
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class) // RedisConnectionFactory 스프링 빈이 하나만 존재하는 경우
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        // 생략
        return template;
    }

    @Bean
    @ConditionalOnMissingBean // StringRedisTemplate stringRedisTemplate 스프링 빈이 없는 경우
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)  // RedisConnectionFactory 스프링 빈이 하나만 존재하는 경우
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        return new StringRedisTemplate(redisConnectionFactory);
    }
}

 

스프링 AutoConfiguration에 의해 개발자는 RedisConnectionFactory 스프링 빈을 하나만 설정하면 됩니다.

AutoConfiguration에 의해서 RedisTemplate redisTemplate, RedisTemplate stringRedisTemplate 스프링 빈이 자동 생성됩니다.

 

RedisConnectionFactory는 Redis와 애플리케이션 사이에 Connection 객체를 생성하는 기능을 제공합니다. LettuceConnectionFactory는 RedisConnectionFactory 인터페이스의 구현 클래스들 중 하나이며, LettuceConnection 객체를 생성하는 팩토리 클래스입니다.

 

그러면 LettuceConnectionFactory를 어떻게 구성하면 좋을지 알아보겠습니다.

 

RedisConnectionFactory 설정 - LettuceConnectionFactory

    @Bean(name = "redisConnectionFactory")
    public RedisConnectionFactory redisConnectionFactory() {

        //----------------- (1) Socket Option
        SocketOptions socketOptions = SocketOptions.builder()
                                                   .connectTimeout(Duration.ofMillis(100L))
                                                   .keepAlive(true)
                                                   .build();
                                                   
        // redis connect을 위한 timeout 100ms
        // redis connect를 tcp keepAlivce 옵션 활용, 그러나 java 11 이상 활용해야하며 기본값은 false

        //----------------- (2) Cluster topology refresh 옵션
        ClusterTopologyRefreshOptions clusterTopologyRefreshOptions = ClusterTopologyRefreshOptions
                .builder()
                .dynamicRefreshSources(true)
                .enableAllAdaptiveRefreshTriggers()
                .enablePeriodicRefresh(Duration.ofSeconds(30))
                .build();

        // dynamicRefreshSources
        // enableAllAdaptiveRefreshTriggers - Redis 클러스터에서 발생하는 모든 이벤트(MOVE, ACK)등에 대해서 topology 갱신
        // enablePeriodicRefresh - 30초 마다 주기적으로 토폴로지를 갱신하는
		

        //----------------- (3) Cluster client 옵션
        ClusterClientOptions clusterClientOptions = ClusterClientOptions
                .builder()
                .pingBeforeActivateConnection(true) // connection 연결을 위해 ping 명령어로 검증 - 기본값 true
                .autoReconnect(true) // 자동 재접속 연결 - 기본값 true
                .socketOptions(socketOptions) // (1) socket 옵션 설정
                .topologyRefreshOptions(clusterTopologyRefreshOptions) // (2) topology 옵션 설정
                .maxRedirects(3) // moved, ask 등의 redirect 허용 횟수, 노드 개수와 동일하게 구성하면 명령어 실패 확률 낮아짐
                .build();

        //----------------- (4) Lettuce Client 옵션
        final LettuceClientConfiguration clientConfig = LettuceClientConfiguration
                .builder()
                .commandTimeout(Duration.ofMillis(150L)) // 명령어 타임아웃 시간 150ms
                .clientOptions(clusterClientOptions)
                .build();

        RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration(redisClusterProperties.getNodes());
        clusterConfig.setMaxRedirects(3);
        clusterConfig.setPassword("password");

        LettuceConnectionFactory factory = new LettuceConnectionFactory(clusterConfig, clientConfig);
        //----------------- (5) LettuceConnectionFactory 옵션
        factory.setValidateConnection(false); // redis connection을 가져올때 검증 여부 - 기본값 false

        return factory;
    }

 

 

 

참고자료

https://redis.io/docs/latest/operate/oss_and_stack/reference/cluster-spec/

https://meetup.nhncloud.com/posts/226

https://lettuce.io/core/release/reference/#masterreplica.standalone-masterreplica

https://docs.google.com/presentation/d/1FtEFBCubpcqMJ6C7YV55KAxjhZ5znYn6A3f1c341Lcg/edit#slide=id.p

https://stackoverflow.com/questions/57349100/redis-synchronous-replication-failure-scenarios#comment126035039_57352422

https://meetup.nhncloud.com/posts/379