Redis 클러스터 적용기

레디스 클러스터 #


Redis Cluster를 사용하게 된 이유 #

프로젝트를 진행중에 모든 서비스에서 특정 데이터에 의존되어있었다.
NFT 마켓플레이스라 NFT 메타데이터(이미지,collection이름, nft이름,chain)에 의존되어있어 처음엔 nft데이터가 있는 nft서버에서 호출하여 취합하는 방식으로 진행하다,
MSA 설계가 이렇게 특정 서버에 의존되어있는게 맞는가에 대해 의문이들었다.
NFT 서버에 부하가 있을것이고, 장애로 이어질 가능성이 있었기에 고민을 많이했던 부분같다.

3가지 방식을 고민했다

  1. 모든 서버에 NFT 데이터를 저장
    • 데이터가 중복되어 효율적이지 못함
    • 걍 별로임
  2. api게이트웨이에서 redis(replication)를 두고 해당 nft key를 redis에 찾아와 취합하기
    • 마스터 장애위험
  3. redis 고가용성 (sentinel,cluster)
    • 각 서버에 redis 연결 코드작성 (관리 어려움)
    • 오버헤드

Redis Cluster #

레디스 아키텍처의 종류는 크게 3가지가있다

  1. Replication
  • master와 replica로 나누어짐 master에 데이터가 들어오면 replica가 복제됨
  1. Sentinel
  • master와 replica 추가로 sentinel 노드가 추가됨 세티널이 마스터와 replica노드를 모니터링하고 관리함
  • 장애상황에서 자동으로 페일오버를 수행함 ( master장애시 slave가 master로 승격)
  1. Cluster
  • 데이터샤딩 기능을 지원함 ( 수평적확장)
  • 최소 3개의 master를 가져야됨 redis 노드들이 통신하여 상태를 공유함(노드 디스커버리)
  • 센티널과 마찬가지로 자동으로 페일오버를 수행함

그 중 3번째 아키텍처에 대해 자세하게 알아보자

클라이언트 노드간 통신 #

cb58e233-fa66-4501-8307-721fd28f8571

redis 클러스터는 프록시 없이 노드 디스커버리를 수행할수있다고 공식문서에 나와있다.
MSA의 서비스 디스커버리는 프록시로 각 서비스들의 위치를 확인하는데 redis 는 프록시 없이 클라이언트가 node의 구성정보를 가져와 위치를 파악하므로 프록시가 필요가 없는걸 알 수 있다.

redis 클러스터는 마스터에 해시슬롯을 가지고있고 이 슬롯에 포함된 실제 키들이 있다 각 키의 슬롯번호를 기준으로 저장위치가 결정된다

redis cluster를 생성하면 각 노드들에 slot을 할당하는 걸 볼수있다. d8612597-a83e-48bb-8536-5305f777aaae

클러스터에 참여된 노드수에 따라 슬롯은 균등하게 분배하고있는데, 이 슬롯을 기반으로 데이터를 해시값을 계산하여 계산결과에 맞는 데이터를 노드에 저장하고있다. 레디스의 클러스트의 목표는 데이터를 샤딩하는것에 의미를 두기에 부하를 균등하게 분산하고있는걸 볼 수 있다.
맵정보(슬롯-노드매핑) 는 클라이언트가 캐시하고있어, 이 맵을 사용하여 슬롯번호가 어느 노드에 있는지 파악할수있다.
만약 맵이 변경되여 잘못된 노드에 도달했다면, 해당 노드는 moved 응답을 보내 클라이언트에게 노드의 정보를 다시 알려준다.

663c089a-a0e7-4b02-b03b-baab1c8e90d7

자세하게 redis 클러스터를 공부하기전에 해당 노드들에게 리다이렉션 되는 줄 알았는데, 클라이언트에게 노드정보를 반환하는 이유가 궁금했다.
클라이언트가 노드의 정보를 알고있으면 이 후 요청에 대해서 바로 해당 노드를 찾아 갈 수 있다는 장점이 있기도하고, 클러스터의 크기가 커져도 리다이렉션 처리에 따른 부하가 분산되기때문에 중간 노드를 거치는 불필요한 작업이 수행하지 않아도된다.

클러스트 접근 동작흐름

6db2a83a-93a0-4cec-8ec1-16248dfac5bd

노드간 통신(클러스터 버스포트) #

노드간 주기적인 핑퐁을 통해 노드의 상태를 계속 모니터링한다 만약 master노드가 응답하지않으면 장애로 판단하고 master에 연결된 slave를 master로 승격시킨다.
또한. 노드가 추가되거나 삭제 되었을때 노드정보 변경을 각 노드들에게 알린다.
클러스터 버스는 바이너리 프로토콜(TCP버스와 Redis 클러스터 버스 ex: 6379 + 1000)을 사용하여 노드간 정보교환에 더 적합하다고 redis 공식문서에서 찾아볼수있다.

client 와 클러스터 연결해보기 #

docker exec -it nft-redis-1-1 redis-cli —cluster create 172.26.0.2:6379 172.26.0.3:6379 172.26.0.4:6379 172.26.0.5:6379 172.26.0.6:6379 172.26.0.7:6379 172.26.0.8:6379 172.26.0.9:6379 172.26.0.10:6379 —cluster-replicas 2

나는 9개의 노드를만들어 마스터3개 각 마스터당 2개의 replica를 갖게 만들었다
클라이언트에서는 노드로 접근하기위해 내부포트와 외부포트를 연결해주고, 클러스터를 생성할때 도커 네트워크로 내부망을 만들어주고 cluster가 각각의 노드의 내부 ip로 클러스터로 초기해줬다.

초기 설계

1814be21-4bc9-4fe5-96be-a751c937970a

초기설계는 이렇게 구성하고 스프링과 컨테이너를 연결하려고 했을때, 노드를 못찾아 timed out 이 발생했다 에러 로그에는 컨테이너에서 설정한 내부 ip로 계속 요청을 시도하고있고있었다 172.24.0.2 로 연결하다 실패하니, 172.24.0.3 .. 172.24.0.4 모든 노드의 내부 ip를 뱉고있는것이였다.
클라이언트에서 redis와 연결할때 localhost:외부포트로도 연결을 해줬음에도 불구하고 클라이언트가 대체 내부ip를 어떻게 아는것이지..? 멘붕 상태에 빠지면서 2일동안 삽질을 했는데, 결국 해결을 못했다

redis-cli에서는 외부접속이 분명히 되는데, 왜 스프링부트와 연결을 하려고하면 노드를 찾을수없는지 열심히 찾던 와중 나와같은 문제를 겪는 블로그를 찾았다
https://velog.io/@ddh963963/docker-redis-cluster-%EC%99%B8%EB%B6%80-%EC%A0%91%EC%86%8D-%ED%95%B4%EA%B2%B0%EA%B8%B0

redis.conf 파일

port 6379
bind 0.0.0.0
protected-mode no
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
appendfilename "appendonly.aof"
cluster-announce-ip 127.0.0.1
cluster-announce-port 6379
cluster-announce-bus-port 16379

cluster-announce-ip 를 분명 설정해줬는데 같은 문제가 발생하는거였다.
처음엔, 외부 ip 주소로 접근하려면 이 설정을 해주는건가 싶었는데 문제는 전체 노드에 대한 같은 conf 파일을 작성해준게 문제였다
각 노드에 대한 port도 외부포트와 announce-ip를 따로 작성해줘야되는거였다.

클라이언트에게 반환할때 자신이 가지고있는 내부ip를 반환하고 그 클라이언트가 내부ip로 연결을 시도하려고하니 timed out이 발생한거였다
cluster-announce-ip 는 자신의 외부 ip를 노출하는거였다 그렇기에 각 노드들마다 conf파일을 다르게 작성해줘야한다
이걸 몰라서 같은 conf파일을 적용하고 도커컨테이너만 50번넘게 밀었다가 변경하고 다시키고 삽질의 연속이였다.

이 문제를 알기전까지는 회사에 친한 데브옵스분이 계셔서 같이 해결해주셨는데 그 분이 올린 코드를 보자

  redis-node-0:
    image: docker.io/bitnami/redis-cluster:7.2
    ports:
      - 6379:6379
    volumes:
      - redis-cluster_data-0:/bitnami/redis/data
    environment:
      - 'REDIS_PASSWORD=bitnami'
      - 'REDIS_NODES=redis-node-0:6379 redis-node-1:6380 redis-node-2:6381 redis-node-3:6382 redis-node-4:6383 redis-node-5:6384 redis-node-6:6385 redis-node-7:6386 redis-node-8:6387 redis-node-9:6388'
      - 'REDIS_CLUSTER_ANNOUNCE_HOSTNAME=localhost’
      - 'REDIS_CLUSTER_ANNOUNCE_PORT=6379'
      - 'REDIS_CLUSTER_PREFERRED_ENDPOINT_TYPE=hostname'

  redis-node-1:
    image: docker.io/bitnami/redis-cluster:7.2
    ports:
      - 6380:6380
    volumes:
      - redis-cluster_data-1:/bitnami/redis/data
    environment:
      - 'REDIS_PASSWORD=bitnami'
      - 'REDIS_NODES=redis-node-0:6379 redis-node-1:6380 redis-node-2:6381 redis-node-3:6382 redis-node-4:6383 redis-node-5:6384 redis-node-6:6385 redis-node-7:6386 redis-node-8:6387 redis-node-9:6388'
      - 'REDIS_PORT_NUMBER=6380'
      - 'REDIS_CLUSTER_ANNOUNCE_HOSTNAME=localhost'
      - 'REDIS_CLUSTER_ANNOUNCE_PORT=6380'
      - 'REDIS_CLUSTER_PREFERRED_ENDPOINT_TYPE=hostname'

도커 내부 네트워크 설정없이, 내부포트와 외부포트를 연결하지않고 호스트 네트워크를 활성화 시키고 각 노드에 환경변수를 설정해주었다. 노드들이 REDIS_CLUSTER_ANNOUNCE_HOSTNAME,REDIS_CLUSTER_ANNOUNCE_PORT 가 다른걸 볼 수 있다 여기에서 다시 블로그를 보니 이제서야 이해가 갔다.

각 노드마다 REDIS_CLUSTER_ANNOUNCE_HOSTNAME 와 REDIS_CLUSTER_ANNOUNCE_PORT 를 적용해주면 클라이언트와 통신완료 399426fc-4efd-4730-b8a4-d96cd6d0357e

master 장애 시 페일오버 테스트 #

그럼 이제 마스터장애시 slave가 master로 승격이 되는지 테스트 해보자

클러스터 정보 e86dce1a-81b4-466b-b845-133699780b00

해당 노드정보 33c4ccf2-7b3d-4899-9d28-8e259759897a

해당 노드의 데이터 94cd12af-7b06-4690-b836-6d1e566c581d

마스터 노드인 6380포트를 중지 시켜보겠다.
6380포트에 replica는 6385포트와,6386포트인데 이 중 하나가 master가 되고 또한 마스터노드에 있던 데이터가 slave에서 조회 시 같게 보이면 성공이다.

3ac123cc-6151-46c4-8187-ef7a98fcc8c4 클러스터 노드들을 확인해보니 6380인 마스터가 fail뜨고, 예상대로 6386포트가 승격되었다

6386 포트에서 데이터를 잘 가져오는것도 확인했다. cab7a0e6-7305-4249-82da-b1f15afd21b2

장애복구 시 6380은 slave가 된 걸 확인할수있다.

72564fbe-41ae-4839-a220-904b10837b5e

느낀점 #

Redis는 이 전 NestJs를 사용한 코노방구미라는 프로젝트에서 다룬적이 있다
그 당시 세션 저장소로 사용했었다 redis는 워낙 설정도 간단해 깊게 공부를 따로 한 적은 없지만 이번기회에 레디스 클러스터를 공부해보고 MSA에 한 걸음 다가간 느낌이다