🚀 요약

SUMMARY

서버를 재기동하면 Redis 클러스터의 마스터가 특정 한 노드로 몰려 HA가 무의미해지는 문제가 있었다. Sentinel 도입, 애플리케이션 스케줄러, 쉘 스크립트(cron) 세 가지를 놓고 관리 포인트·라이브러리 종속성·예외 케이스 관점에서 따져본 뒤, 운영 부담이 가장 작은 cron 기반 재분배 스크립트로 정리했다.

여러 고객사에 온프레미스로 납품하는 솔루션의 데이터스토어를 맡고 있었다. Redis는 도커(Docker) 컨테이너로 클러스터(마스터 3 + 레플리카 3)를 구성해 서버 3대에 나눠 얹는 형태였다. 각 서버가 마스터 하나, 레플리카 하나를 들고 있는 그림이 정상이다. 문제는 이 그림이 재기동 한 번에 쉽게 무너진다는 거였다.

1. 마스터 쏠림 현상

서버가 재부팅되거나 도커 데몬이 재시작되면 컨테이너들이 순서 없이 우르르 같이 뜬다. depends_ondocker compose up에만 먹히고 재기동 상황에선 순서를 보장하지 못한다. 그렇게 클러스터가 다시 조립되는 과정에서 마스터가 특정 한 서버로 몰리는 일이 생겼다.

마스터 3개가 전부 한 서버에 올라가 있으면, 그 서버 하나 죽는 순간 마스터 3개가 동시에 날아간다. 서버를 3대로 나눈 이유가 통째로 사라지는 셈이다. 레플리카가 승격되며 복구되긴 하지만, 애초에 분산해둔 의미가 없다. 재기동할 때마다 사람이 붙어서 “지금 마스터가 어디어디 떠 있지?” 확인하고 손으로 옮기는 것도 우습고.

그래서 마스터가 서버마다 하나씩 흩어지도록 자동으로 되돌리는 장치가 필요했다. 방법을 세 갈래로 놓고 봤다.

2. 재분배가 해야 할 일

어떤 방법을 쓰든 재분배 로직이 할 일은 같았다.

  • 전체 노드에 ping을 날려 살아있는지 확인한다.
  • 서버 수·서버당 노드 수를 근거로 마스터가 몇 개여야 하는지 계산하고, 실제 마스터 수가 맞는지 본다.
  • 마스터가 서버별로 고르게 흩어져 있는지, 한 곳에 몰렸는지 확인한다.
  • 몰렸으면 다른 서버에 있는 레플리카에 수동 페일오버를 걸어 마스터로 승격시킨다. 그러면 마스터 자리가 그 서버로 옮겨간다.
  • 다시 확인하고 끝낸다.

핵심은 네 번째 줄이다. Redis 클러스터에서 특정 레플리카를 마스터로 올리는 건 그 레플리카에 cluster failover를 거는 것으로 된다. 원하는 서버의 레플리카를 골라 승격시키면 마스터 배치가 자연스럽게 흩어진다.

# 지정한 레플리카를 마스터로 승격시켜 마스터 배치를 흩뜨린다.
# 비밀번호(-a)는 평문 대신 환경변수로 주입한다.
docker exec -it redis-7001-0 \
  redis-cli -c -a "$REDIS_PASS" -h 10.10.30.11 -p 7001 cluster failover

레플리카가 특정 마스터 한쪽에만 붙어버리는 경우도 같은 방식으로 손볼 수 있다. 레플리카를 소프트 리셋한 뒤 원하는 마스터에 다시 붙이는 식이다.

# 레플리카를 리셋하고, 원하는 마스터 노드에 다시 복제 관계로 붙인다.
redis-cli -c -a "$REDIS_PASS" -h 10.10.30.12 -p 7002 cluster reset soft
redis-cli -c -a "$REDIS_PASS" -h 10.10.30.12 -p 7002 cluster meet 10.10.30.11 7002
redis-cli -c -a "$REDIS_PASS" -h 10.10.30.12 -p 7002 cluster replicate <NODE-ID>

이때 슬롯(0~16383)이 제대로 할당돼 있는지, 고아 노드가 남지 않았는지도 같이 봐야 한다. 이 확인·조작을 어디에 태우느냐가 세 방법의 갈림길이었다.

3. Sentinel 도입

가장 먼저 떠오른 건 Redis의 표준 HA 도구인 Sentinel이었다. 마스터가 죽으면 레플리카를 자동 승격시켜주는 물건이다.

그런데 우리 문제는 “마스터가 죽는” 게 아니라 “재기동 후 마스터 배치가 한쪽으로 쏠리는” 것이었다. Sentinel이 직접 풀어주는 문제가 아니다. 게다가 재기동 시나리오에서는 순서가 꼬인다. 마스터보다 레플리카가 먼저 다운됐다가 복구되면 그 레플리카가 마스터로 전환되지 않는 식의 예외 케이스가 나오는데, 이런 경우들을 Sentinel 로직에 다 태워 다루기가 어려워 보였다. Sentinel 프로세스도 노드마다 띄워 관리해야 하니 운영 대상이 하나 더 는다.

풀려는 문제와 결이 안 맞고 예외만 늘어날 것 같아서 보류했다.

4. API 스케줄러 확장

두 번째는 백엔드 애플리케이션의 스케줄러에 재분배 기능을 넣는 안이었다. 앱이 이미 Redis에 붙어 있으니 거기서 같이 처리하면 깔끔해 보였다.

막상 들여다보니 걸리는 게 여럿이었다. 현재 솔루션은 Redisson 라이브러리로 Redis에 붙는데, Redisson에는 cluster failover 같은 클러스터 관리 명령이 없다. 그래서 이 기능만을 위해 Lettuce를 따로 붙여야 했다.

  • Redisson을 그대로 둔 채 Lettuce를 추가하면 충돌이 없는지부터 확인해야 한다.
  • config에 Redis 설정을 둘로 만들면 복잡성이 는다. 기존 설정을 재사용하려면 서버 수, 서버당 노드 수 같은 정보가 부족하거나, 로직에서 문자열을 파싱해 뽑아 써야 하는 등 불완전한 구석이 생긴다.
  • API 인스턴스가 여러 개 뜨는데, 동시에 재분배를 시도하면 중복 동작이 문제다. 동시성 방지(분산 락 같은)를 또 얹어야 한다.

앱에 이걸 넣는 순간 애플리케이션이 인프라 배치까지 책임지게 되고, 라이브러리 종속성과 설정 복잡도만 늘어난다. 얻는 것보다 짊어질 게 많다고 봤다.

5. 쉘 스크립트(cron)

세 번째는 호스트에서 cron으로 주기적으로 redis-cli를 때려 재분배하는 안이다. 소박하지만 이게 제일 나아 보였다.

  • 애플리케이션·라이브러리와 완전히 분리된다. redis-cli만 있으면 되고, Redisson이든 Lettuce든 앱 사정과 무관하다.
  • apps 폴더 안의 .env를 직접 읽을 수 있어 서버 정보·Redis 노드 정보를 파악하기 쉽다. 덕분에 마스터 재분배뿐 아니라, 레플리카가 특정 마스터에 몰리는 현상에 대한 대응까지 같은 스크립트로 처리할 수 있다.
  • 예외 케이스는 스크립트 안 조건문으로 직접 다루면 된다. Sentinel처럼 정해진 로직에 끼워 맞출 필요가 없다.

단점도 솔직히 있다. 여러 사이트에 배포하다 보면 관리 포인트가 하나 더 는다는 점이다. cron이 도는지, 사이트마다 스크립트 버전이 갈리지는 않는지 챙겨야 한다. 이 부분이 마음에 완전히 드는 건 아니었다(사이트가 늘수록 부담이 커지는 구조라서). 그래도 종속성을 안 늘리고 앱 배포와 무관하게 고칠 수 있다는 점에서, 셋 중 운영 부담이 가장 작았다.

6. 비교

세 방법을 같은 잣대로 놓고 보면 이렇게 갈렸다.

방법관리 포인트종속성예외 케이스 대응
Sentinel노드마다 Sentinel 프로세스 추가Redis 자체순서 꼬임 등 로직에 태우기 어려움
API 스케줄러앱에 흡수(겉보기엔 적음)Lettuce 추가 도입·충돌 위험앱 로직으로 가능하나 설정·동시성 복잡
쉘 스크립트(cron)사이트마다 스크립트+cron 관리redis-cli조건문으로 유연하게 처리

풀려는 문제(배치 쏠림)와 결이 맞고, 종속성을 안 늘리고, 예외를 직접 다룰 수 있다는 세 가지를 놓고 보면 cron 쪽으로 기울 수밖에 없었다. 관리 포인트가 는다는 단점은 남지만, 나머지 둘이 지우는 부담에 비하면 감당할 만했다.

7. 지금 돌아가는 것

지금은 사내 운영 서버 한 대에서 cron으로 마스터 노드 재분배 스크립트가 돌고 있다. 주기적으로 마스터 분포를 확인하고, 쏠려 있으면 조용히 흩뜨려 놓는다. 재기동 뒤에 사람이 붙어 “마스터 어디 떴나” 들여다볼 일은 없어졌다.

컨테이너 오케스트레이션(Docker Swarm 등)으로 재기동 순서와 배치를 근본적으로 잡는 방향도 같이 검토했지만, 그건 이 글의 범위를 넘어 따로 정리해뒀다. 당장의 쏠림은 스크립트 한 장으로 충분히 눌러졌다.

🔗 참고