요약

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 등)으로 재기동 순서와 배치를 근본적으로 해결하는 방향도 함께 검토했지만, 그것은 이 글의 범위를 넘으므로 따로 정리해 두었다. 당장의 쏠림 문제는 스크립트 하나로 충분히 제어되었다.

참고