개발 일기

Redis Clustering 를 통해 FailOver 구현하기 (도커 컴포즈 환경) 본문

Tech/배워서 남주기

Redis Clustering 를 통해 FailOver 구현하기 (도커 컴포즈 환경)

flow123 2023. 1. 25. 20:06

상황

프레시 카트는  Redis 를 사용하고 있고, 용도는 아래와 같습니다

 

- 다중 서버 환경에서 로그인 유저의 세션을 보관할 수 있는 공통 저장소 

- 추후 메인 페이지에서 보여지는 전체 상품 데이터를 캐싱하기 위해.

 

서버는 언제나 fail 될 수 있고, 서버 문제로 애플리케이션이 동작하지 않는다고 가정해봅시다. 

서버가 동작하지 않는 시간 만큼 비즈니스 중단/고객의 불편이라는 손실로 이어집니다. 

특히 프레시 카트의 메인 기능인 주문은 로그인을 해야 가능하기 때문에, 늘 로그인을 가능하게 하는 것이 중요합니다. 

Redis Clustering 을 선택한 이유 

레디스의 fail over 방식으로는 replication, clustering, sentinel이 있습니다. 이에 대해서는 별도 포스팅에서 자세히 설명 드리겠습니다. replication 은 서버 fail 시, 개발자가 수동으로 개입해야 하기 때문에 보통 sentinel vs clustering 을 비교합니다. 그러면 둘 중 어떻게 결정해야 할까요? 아래 스택 오버 플로우 글에도 저와 비슷한 질문이 올라와 있었습니다. 

 

요약해보면, 두 기술을 택하는 지표는 데이터가 지속 증가하는가 입니다. 

 

유저의 로그인 데이터는 계속 증가하는 성격의 데이터는 아닙니다. 한 번 로그인 되면, 세션이 일정 시간 동안 유지되기 때문입니다. 그렇다면 동시접속자만 생각하면 되기 때문에, 동시접속자 수에 따라서 데이터 상승에 한계가 있습니다. 따라서 세션의 경우 Sentinel 이 적합해보였습니다. 반면 상품은 이커머스의 특성 상 시간이 지날수록 계속 증가하는 성격의 데이터 입니다. (서비스가 커져서 판매자가 많아질 수록 그렇겠죠?) 처음부터 scal-eout에 대한 고려가 필요했습니다. 확장성을 고려할 때 Clustering이 적합해 보였습니다.

 

캐시와 세션 용도에 따라 분리하는 것, 즉 둘 다 구현하는 것이 용도의 분리/ 성능 측면에서 깔끔해보였습니다. 그럼에도 아직 데이터가 많이 쌓이지 않았기 때문에 저장소 분리는 불필요하다는 생각이 들었습니다. 상품 전체 정보(캐시)/로그인 정보(세션)을 동일한 레디스 저장소에 둠으로써, 당장의 불필요한 리소스를 줄이고 추후 필요에 따라 분리하기로 했습니다. 

 


구성 

Clustering 적용을 위해 아래 참고자료의 블로그들을 주로 참고했습니다. 레디스 학습 초기 단계이기 때문에, 해당 코드가 최선인지는 확신하지 않습니다. 개선할 수 있는 부분이 있거나, 제가 잘못 이해한 부분이 있을 수 있습니다. 피드백이 있다면 공유해주시면 정말 감사하겠습니다.

 

1. Application 코드

 

application.yml 에 명시된 포트를 매핑해줍니다.

spring:
  redis:
    host: 127.0.0.1
    port: 7001
    nodes:
      - 127.0.0.1:7001
      - 127.0.0.1:7002
      - 127.0.0.1:7003
      - 127.0.0.1:7004
      - 127.0.0.1:7005
      - 127.0.0.1:7006

RedisProperties 는 application.yml 에 명시된 포트를 매핑해줍니다.

레디스 클러스터는 최소 3개의 마스터 노드가 필요하고, 각 노드는 하나의 replica 노드를 백업/ 복제 용으로 갖습니다.

따라서 nodes 를 리스트로 만들어줍니다. 

 

파일의 스프링 어노테이션을 간단히 설명하겠습니다. 

애플리케이션과 레디스 클러스터 노드를 연결해주려면,  properties, yml 같은 외부 파일의 변수를 주입시켜야 합니다. 

@이렇게 외부 변수를 주입할 때, @VALUE 방식을 쓰기도 하고 저도 standalone (레디스 한대) 으로 구성할 때는, @value 방식을 썼습니다. 하지만 테코블 아티클을 읽고 나서, 해당 방식은 타입 안정성을 보장하지 않는다는 것을 알게 되었습니다.

 

configurationProperties 주입 방식은 불변 보장이 되지 않습니다. 따라서, 스프링 2.3 이후 생성자 주입 방식으로 불변성을 가지고 Properties 만들게 하는 방식이 추가되었습니다. ConstructorBinding 어노테이션을 이용하면 final 필드에 대해 값을 주입합니다. 

@ConfigurationProperties는 properties, yml 같은 외부 파일의 변수를 주입시키고 싶을 때 사용합니다.해당 방식은 prefix = "spring.redis" 와 같이, application.yml 파일의 경로를 지정해줍니다

 

@Configuration
@EnableConfigurationProperties(value = {RedisProperties.class})
public class RedisPropertiesConfig {

  @Getter
  @RequiredArgsConstructor
  @ConstructorBinding
  @ConfigurationProperties(prefix = "spring.redis")
  public static class RedisProperties {

    private final List<String> nodes;
    private final String host;
    private final int port;
  }
}

RedisProperties 를 지역 변수로 가져와서, Connection Factory에 설정해줍니다. 

Replica에서 Read를 하는 이유는 쓰기 작업이 Master에서만 가능하기 때문입니다.

현재 master - replica 구조로 이중화가 되어 있지만, master에 읽기/쓰기 작업이 쏠린다면, 성능에 도움이 되지 않을 것입니다.

로그인/캐시 모두 읽기 작업이 많기 때문에, Master로 부하가 쏠리는 것을 분산하기 위해서, 읽기는 Replica 로 했습니다. lettuce 드라이버는 ReadFrom 설정으로 어디에서 읽어올지 선택이 가능합니다.

@Configuration
@EnableRedisRepositories
@RequiredArgsConstructor
public class RedisConfig {

  private final RedisProperties properties;

  @Bean
  public RedisConnectionFactory redisConnectionFactory() {
    LettuceClientConfiguration clientConfiguration = LettuceClientConfiguration.builder()
        .readFrom(ReadFrom.REPLICA_PREFERRED)
        .build();
    RedisClusterConfiguration redisClusterConfig = new RedisClusterConfiguration(properties.getNodes());
    return new LettuceConnectionFactory(redisClusterConfig, clientConfiguration);
  }
}

2. conf 파일을 구성합니다. conf 는 configuration 의 약자입니다. 

 

포트만 다르고 모두 동일합니다. 

 

port 7001

#cluster 사용 여부
cluster-enabled yes

#cluster 설정 파일 이름
cluster-config-file node.conf

#timeout 시간 지정 (ms)
cluster-node-timeout 5000

#failover된 redis node 재실행 시 이전 데이터를 다시 로드해올 수 있음
appendonly yes

 

3. docker-compose.yml 수정

 

공식 문서의 Redis Cluster and Docker 섹션을 봅시다.

 

To make Docker compatible with Redis Cluster, you need to use Docker's host networking mode . 

 

레디스 공식 문서에도 나와 있듯, 도커 상에서 실행하는 레디스 컨테이너들이 레디스 클러스터에 호환하도록 하기 위해서는 “host” 네트워크 모드를 사용하는 것을 필요로 합니다

 

도커는 포트 매핑이라는 기술을 사용합니다. Docker yml파일을 보면 ports: -”6379:6379” 이런식으로 두 포트를 나란히 매핑하고 있습니다. 이는 도커 호스트 포트 / 컨테이너 내부 포트 입니다. 즉 6379번을 쓰는 REDIS 컨테이너 내부 포트를, 도커 호스트 (제 파일의 경우 127.0.0.1)의 6379번에 매핑해줘. 라는 뜻입니다.

 

하지만 도커 문서를 보면, window나 mac 환경에서는 지원하지 않습니다. host 모드를 켤 수 없으니, 본 예제에서는 다른 블로그의 예시를 참고해서, 노드 하나에(임의로 마스터 노드 1 선택) 모든 레디스 노드에 대한 포트가 한번에 매핑시키는 방법을 썼습니다. 그리고 해당 노드를 각 노드가 네트워크로 공유 하도록 구성했습니다.

 

  • redis master 1에서 모든 포트를 지정해주고, 다른 컨테이너 노드들이 이 노드를 바라보도록 하겠습니다.

따라서, master2, master3, replica 1 - 3 모두 network_mode 가 “"service:redis-master-1" 입니다.

network_mode 옵션을 host가 아닌  service:node1에 연결해줍니다.(위에도 말씀 드렸듯 linux 환경만 지원되기 때문입니다. linux 시면 host로 쓰셔도 됩니다). 

 

  • 마지막에는 redis-cluster-entry 에서 클러스터를 생성해줍니다.

클러스터 모드를 활성화 하는 작업을 진행하는 컨테이너 입니다. 해당 컨테이너는 redis-cli --cluster create 명령어를 실행해서, 클러스터를 시작합니다. 레디스 노드가 준비되어야 클러스터로 묶어주는 구성이 가능합니다.

따라서 6개 컨테이너에 의존(depends-on)하고 있고, 컨테이너들이 실행된 후에야 cluster-entry가 작동 가능합니다. 그래서 마지막에 배치되어 있기도 합니다.

 

* --cluster-replicas n: 마스터 당 슬레이브 노드를 몇 개 만들지를 정한다.

여기서는 슬레이브 노드가 1개이고, 전체 노드가 6개 주어졌으니, 마스터 3개 당 슬레이브 3개로 구성하게 됩니다. (반대로 말하면, replica가 1이라면, 최소 6노드 이상이어야 한다는 것)

 

 

docker - compose.yml

version: "3.1"
services:
  db:
    image: mysql:8.0.28
    cap_add:
      - SYS_NICE
    container_name: freshcart_db
    restart: always
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: freshcart
      MYSQL_USER: admin
      MYSQL_PASSWORD: password

  redis-master-1:
    container_name: redis-cluster
    image: redis:latest
    command: redis-server /usr/local/etc/redis/redis.conf
    volumes:
      - ./redis/master1.conf:/usr/local/etc/redis/redis.conf
    ports:
      - "7001:7001"
      - "7002:7002"
      - "7003:7003"
      - "7004:7004"
      - "7005:7005"
      - "7006:7006"

  redis-master-2:
    network_mode: "service:redis-master-1"
    container_name: redis-master-2
    image: redis:latest
    command: redis-server /usr/local/etc/redis/redis.conf
    volumes:
      - ./redis/master2.conf:/usr/local/etc/redis/redis.conf

  redis-master-3:
    network_mode: "service:redis-master-1"
    container_name: redis-master-3
    image: redis:latest
    command: redis-server /usr/local/etc/redis/redis.conf
    volumes:
      - ./redis/master3.conf:/usr/local/etc/redis/redis.conf

  redis-replica-1:
    network_mode: "service:redis-master-1"
    container_name: redis-replica-1
    image: redis:latest
    command: redis-server /usr/local/etc/redis/redis.conf
    volumes:
      - ./redis/replica1.conf:/usr/local/etc/redis/redis.conf

  redis-replica-2:
    network_mode: "service:redis-master-1"
    container_name: redis-replica-2
    image: redis:latest
    command: redis-server /usr/local/etc/redis/redis.conf
    volumes:
      - ./redis/replica2.conf:/usr/local/etc/redis/redis.conf

  redis-replica-3:
    network_mode: "service:redis-master-1"
    container_name: redis-replica-3
    image: redis:latest
    command: redis-server /usr/local/etc/redis/redis.conf
    volumes:
      - ./redis/replica3.conf:/usr/local/etc/redis/redis.conf

  redis-cluster-entry:
    network_mode: "service:redis-master-1"
    image: redis:latest
    container_name: redis-cluster-entry
    command: redis-cli --cluster create 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 127.0.0.1:7006 --cluster-replicas 1 --cluster-yes
    depends_on:
      - redis-master-1
      - redis-master-2
      - redis-master-3
      - redis-replica-1
      - redis-replica-2
      - redis-replica-3

 

 


테스트 

 

변경된 설정을 적용해봅시다. 

 

1. docker compose up -d로 실행

2. 이제 해당 클러스터가 잘 작동하는지, redis-cli 를 통해 확인해봅니다. redis-cli를 실행시킬 때 -c -p [포트번호] 를 통해 cluster 옵션과 port 옵션을 꼭 주어야 합니다.

 

참고로, Redis Cluster는 hashslot 이라는 기능을 통해서, 입력값이 세 슬랏 (마스터 노드 7001 - 7003에 위치한다) 에 고루 분배되도록 합니다. 

hashslot을 거쳐서 각 a (7003), b (7001), c(7002)의 키가 다른 노드에 저장 됩니다.

7001번 포트에서 실행 되더라도, 7003번에 저장될 수 있습니다.

set a의 경우, slot이 7003번 포트 (master 3)에 해당하기 때문에, 7003으로 다시 이동합니다. set b는 주소값이 7001에 속하기 때문에, 이때 7001번으로 명령창의 포트가 이동합니다. 

 

이때 7002번 포트에서 get a를 시도 하더라도 실제 a의 값이 존재하는 7003으로 이동해서 꺼내줍니다.

 

3. 노드 하나를 멈춘다면 어떻게 될까요?

 

레디스 공식 문서에는 replica가 새 master가 되고, master로서의 역할을 할 것이라고 말합니다.

실제로 그렇게 동작하는지 확인해 봅시다.

Node B1 replicates B, and B fails, the  cluster will promote node B1 as the new master and will continue to operate correctly.

redis-master-2 (7002) 컨테이너를 stop 합니다.

이때 master-2의 Replica인 Replica-2의 로그를 봅시다.

 

1:S 04 Feb 2023 15:22:06.841 # Connection with master lost.
1:S 04 Feb 2023 15:22:06.841 * Caching the disconnected master state.
1:S 04 Feb 2023 15:22:06.841 * Reconnecting to MASTER 127.0.0.1:7002
1:S 04 Feb 2023 15:22:06.841 * MASTER <-> REPLICA sync started
1:S 04 Feb 2023 15:22:06.841 # Error condition on socket for SYNC: Connection refused

Master와의 연결이 끊겼기 때문에, Connection Refused가 10번 뜨고, 연결이 끊긴 master 의 상태를 caching 하고 있다는 로그가 뜹니다. 이 와중에 master 7002 와의 연결도 계속 시도합니다.

 

1:S 04 Feb 2023 15:22:15.508 * FAIL message received from 07189bd800caee3a8cfb878997f8de6eb6e49b28 about 6d6041f098f0579e4678a4dff773615f1c36f04a
1:S 04 Feb 2023 15:22:15.508 # Cluster state changed: fail
1:S 04 Feb 2023 15:22:15.542 # Start of election delayed for 510 milliseconds (rank #0, offset 208).
1:S 04 Feb 2023 15:22:15.843 * Connecting to MASTER 127.0.0.1:7002
1:S 04 Feb 2023 15:22:15.867 * MASTER <-> REPLICA sync started
1:S 04 Feb 2023 15:22:15.868 # Error condition on socket for SYNC: Connection refused
1:S 04 Feb 2023 15:22:16.068 # Starting a failover election for epoch 7.
1:S 04 Feb 2023 15:22:16.076 # Failover election won: I'm the new master.
1:S 04 Feb 2023 15:22:16.076 # configEpoch set to 7 after successful failover
1:M 04 Feb 2023 15:22:16.076 * Discarding previously cached master state.
1:M 04 Feb 2023 15:22:16.076 # Setting secondary replication ID to 21b8de2e0fb16808ace6451677447a147d530b15, valid up to offset: 209. New replication ID is 0ec1a899485a7048ab171f1677fda76c2a4587bc
1:M 04 Feb 2023 15:22:16.077 # Cluster state changed: ok

Master2노드에 대한 fail 메시지를 받은 뒤, Cluster의 상태가 fail로 바뀌고, failover election 이 일어납니다. Master - Replica 사이에 sync가 일어나고. 스스로가 마스터가 되었다고 선언하는 모습을 볼 수 있습니다. 그리고 cluster는 다시 ok 상태가 됩니다.

 

아까 키 c 는 - 7002 포트 (redis-master-2)에 속해있었습니다. 첫 번째 set c fresh 뒤에, 임의로 7002를 종료했다) get c 를 하면 서버가 커넥션을 종료했다고 뜹니다.

다시 redis-cli에 접속합니다.  그리고 다시 get c를 하면,

7002포트의 replica인 7005 포트가 데이터를 백업하였음을 알 수 있습니다. 

c에 새로 쓰기 작업도 가능한데, master 로 promote 되었기 때문입니다. 

Redis Clustering의 장점은 분산 저장이 되어서, scale-out 시 유리하다는 것입니다.

마지막으로, cluster nodes 명령어를 통해, node의 상태를 확인해봅시다. 

여기서 connected 앞의 수로 클러스터가 어떻게 사용되고 있는지를 볼 수 있습니다.

해당 노드에 연결된 client 를 말한다. 여기서 client 는 유저일 수도 있고, application 일수도 있습니다. 

 

hashslot이 어떻게 동작하는 지 몰라서, 특정 slot(7005 노드)에 쏠리고 있긴 하지만, 3개 마스터 노드에 모두 잘 저장되고 있음을 확인할 수 있습니다. 현재는 로그인 수가 적어서, 눈에 띄지 않지만, 로그인 데이터가 쌓임에 따라 계쏙 모니터링을 하려고 합니다. (특정 노드에 clients가 몰린다면, 노드를 추가하거나, 별도 설정을 해야할 것입니다). monitor the performance of the Redis cluster over time and make adjustments as needed to maintain good performance and reliability.


참고 자료 

https://redis.io/docs/management/config-file/

https://redis.io/docs/management/scaling/

https://docs.docker.com/network/host/

https://hwasurr.io/redis/cluster/

http://redisgate.kr/redis/cluster/redis-cli-cluster.php

*그 외 Docker-compose 구성과 실험 전반은 코딩하는 주노님 블로그를 참고했습니다.

https://velog.io/@ililil9482/Redis-Cluster-구성

https://stackoverflow.com/questions/53060714/redis-sentinel-standalone-or-cluster-which-is-best-for-session

https://blog.leocat.kr/notes/2021/08/10/redis-read-from-replica

Comments