영속성과 고가용성은 다른 문제다
지난 장에서 다룬 RDB와 AOF는 Redis 데이터를 디스크에 남겨 재시작 후 복원하기 위한 장치다. 하지만 영속성이 있다고 해서 서비스가 계속 살아 있는 것은 아니다. Redis 서버 한 대가 내려가면, 디스크에 파일이 남아 있더라도 그 서버가 다시 뜰 때까지 애플리케이션은 Redis에 접근할 수 없다.
고가용성(High Availability)은 이 시간을 줄이는 설계다. 핵심은 단순하다.
데이터를 복사해 둔 Redis를 준비한다.
현재 쓰기 담당 서버가 장애 나면 복사본 중 하나를 새 쓰기 서버로 승격한다.
애플리케이션이 새 서버를 찾아 다시 연결하게 한다.
Redis에서 이 구조를 만드는 기본 조합이 Replication과 Sentinel이다. Replication은 데이터를 복제하고, Sentinel은 장애를 감지해 자동 장애 조치(failover)를 수행한다.1
AI 생성 이미지
Primary-Replica 구조 이해하기
Redis 복제는 보통 하나의 Primary와 하나 이상의 Replica로 구성한다. 예전 문서나 명령어에서는 master/slave라는 표현이 남아 있을 수 있지만, 이 책에서는 Primary/Replica라는 용어를 사용한다.
Primary는 쓰기를 받는 서버다. SET, HSET, ZADD, XADD 같은 변경 명령은 Primary로 보내야 한다. Replica는 Primary의 데이터를 따라가는 서버다. 기본적으로 Replica는 읽기 전용으로 동작하며, Primary에서 발생한 쓰기 명령을 전달받아 자신의 메모리에 반영한다.
가장 단순한 구성은 다음과 같다.
Application
|
v
Redis Primary ---> Redis Replica 1
---> Redis Replica 2Replica를 붙이는 설정은 간단하다.
replicaof 10.0.0.10 6379또는 실행 중인 Redis에서 다음 명령으로 설정할 수도 있다.
REPLICAOF 10.0.0.10 6379복제 상태는 INFO replication으로 확인한다.
INFO replicationPrimary에서는 연결된 Replica 수와 복제 offset을 볼 수 있고, Replica에서는 자신이 어떤 Primary를 따라가는지 확인할 수 있다.
읽기 분산은 가능하지만 정합성은 약해진다
Replica가 있으면 읽기 요청 일부를 Replica로 보낼 수 있다. 예를 들어 상품 상세 캐시나 공개 게시글 조회처럼 약간 오래된 값을 잠깐 보여줘도 되는 기능은 Replica 읽기로 부하를 나눌 수 있다.
하지만 Redis 복제는 기본적으로 비동기(asynchronous)다. 클라이언트가 Primary에 쓰기를 보냈을 때, Primary는 Replica가 모두 반영할 때까지 기다리지 않고 응답할 수 있다.2
다음 상황을 보자.
사용자가 닉네임을 변경한다.
애플리케이션이 Primary에
HSET user:1001 name "new-name"을 보낸다.Primary는 성공을 응답한다.
복제 명령이 Replica에 도착하기 전, 다른 요청이 Replica에서
user:1001을 읽는다.
이 요청은 잠깐 이전 닉네임을 볼 수 있다. 이것을 복제 지연(replication lag)이라고 한다. 보통은 매우 짧지만, 네트워크 지연, Replica 부하, 큰 RDB 로딩, 디스크 I/O 문제 등이 있으면 길어질 수 있다.
그래서 읽기 분산을 적용할 때는 기능별로 판단해야 한다.
결제 상태, 재고, 권한처럼 최신성이 중요한 값은 Primary에서 읽는다.
프로필, 카운터, 인기 목록처럼 짧은 지연을 허용할 수 있는 값은 Replica 읽기를 검토한다.
쓰기 직후 바로 읽어야 하는 요청은 같은 흐름 안에서 Primary를 읽도록 한다.
Redis에는 WAIT 명령도 있다. 이전 쓰기가 지정한 수의 Replica에 복제될 때까지 일정 시간 기다리는 명령이다.2
SET order:1001:status paid
WAIT 1 1000위 예시는 앞선 쓰기가 최소 1개 Replica에 복제되기를 최대 1000밀리초 기다린다. 다만 WAIT가 모든 장애 상황에서 강한 영속성을 보장하는 것은 아니다. 지연 시간을 늘리는 선택이기도 하다. 모든 쓰기에 습관적으로 붙이기보다, 손실 비용이 큰 일부 흐름에서만 검토하는 편이 좋다.
Primary 장애와 데이터 손실 가능성
Replica가 있으면 Primary 장애 시 서비스를 이어갈 수 있다. 그러나 "Replica가 있으니 데이터 손실이 없다"는 뜻은 아니다.
비동기 복제에서는 다음 일이 가능하다.
클라이언트가 Primary에 쓰기를 보낸다.
Primary가 성공 응답을 보낸다.
해당 쓰기가 Replica에 복제되기 전에 Primary가 장애 난다.
Sentinel이 Replica를 새 Primary로 승격한다.
새 Primary에는 방금 성공 응답을 받은 데이터가 없다.
애플리케이션 입장에서는 성공한 쓰기가 사라진 것처럼 보인다. 이것이 Redis 고가용성 설계에서 반드시 이해해야 할 지점이다. Sentinel은 서비스 중단 시간을 줄여 주지만, 비동기 복제에서 발생할 수 있는 마지막 일부 데이터 손실을 완전히 없애지는 못한다.
따라서 데이터 성격에 따라 보완책이 필요하다.
캐시는 손실되어도 원본 DB에서 다시 만들 수 있으므로 대부분 괜찮다.
세션은 일부 로그아웃을 허용할지, 더 강한 저장소를 쓸지 결정해야 한다.
큐나 이벤트는 중복 처리와 재처리 전략을 함께 설계해야 한다.
결제, 정산, 주문 원장처럼 사라지면 안 되는 데이터는 Redis만 원본으로 두지 않는 편이 안전하다.
전체 동기화와 부분 재동기화
Replica가 Primary를 따라가려면 먼저 데이터를 맞춰야 한다. Redis 복제 과정은 크게 전체 동기화(full synchronization)와 부분 재동기화(partial resynchronization)로 나눌 수 있다.
Replica가 처음 Primary에 연결하면 자신이 이어받을 수 있는 이전 복제 상태가 없다. 이때는 전체 동기화가 필요하다. 흐름은 대략 다음과 같다.
Replica가 Primary에 복제를 요청한다.
Primary가 현재 데이터의 RDB 스냅샷을 만든다.
Primary가 RDB 파일을 Replica에 보낸다.
Replica는 기존 데이터를 비우고 RDB를 메모리에 적재한다.
그 사이 Primary에서 발생한 쓰기 명령과 이후 새 쓰기 명령을 계속 전달받는다.
전체 동기화는 비용이 크다. Primary는 RDB를 생성해야 하고, Replica는 큰 파일을 받아 메모리에 적재해야 한다. 데이터가 클수록 CPU, 메모리, 네트워크, 디스크 I/O에 부담이 생긴다. 운영 중 Replica를 여러 대 한꺼번에 새로 붙이면 Primary에 큰 부하를 줄 수 있다.
반면 일시적인 네트워크 끊김처럼 Replica가 잠깐 떨어졌다가 다시 붙는 경우에는 전체 데이터를 다시 받을 필요가 없을 수 있다. Redis는 복제 ID와 offset을 기준으로 Replica가 어디까지 받았는지 판단한다. Primary의 replication backlog에 누락된 명령이 아직 남아 있으면, Replica는 빠진 부분만 받아 이어갈 수 있다. 이것이 부분 재동기화다.
부분 재동기화가 실패하면 전체 동기화로 돌아간다. 대표적인 경우는 다음과 같다.
Replica가 너무 오래 끊겨 backlog에 필요한 명령이 사라졌다.
Primary가 재시작되거나 승격되어 복제 이력이 이어지지 않는다.
네트워크 분리 후 구조가 바뀌어 기존 offset으로 이어받을 수 없다.
운영에서는 Replica가 자주 전체 동기화되는지 확인해야 한다. 잦은 full sync는 네트워크 문제, backlog 부족, Redis 재시작, 과도한 부하의 신호일 수 있다.
Sentinel이 필요한 이유
Replication만 있으면 복사본은 생긴다. 하지만 Primary가 죽었을 때 누가 Replica를 승격할지는 아직 해결되지 않았다.
사람이 직접 처리한다면 다음 일을 해야 한다.
Primary 장애를 감지한다.
가장 적절한 Replica를 고른다.
그 Replica에
REPLICAOF NO ONE을 실행해 Primary로 승격한다.나머지 Replica들이 새 Primary를 따라가게 바꾼다.
애플리케이션 설정을 새 Primary 주소로 바꾼다.
장애 상황에서 이 절차를 사람이 매번 정확히 수행하기는 어렵다. Sentinel은 이 과정을 자동화한다. Sentinel은 Redis 서버와 별도로 실행되는 감시 프로세스이며, 여러 Sentinel이 서로 통신하면서 Redis Primary와 Replica 상태를 확인한다.1
Sentinel은 보통 최소 3개 이상 둔다. 중요한 이유는 다수결이다. Sentinel이 한 개뿐이면 그 Sentinel 자체가 장애 지점이 된다. 두 개만 두면 네트워크가 갈라졌을 때 어느 쪽 판단을 믿을지 애매해질 수 있다. 서로 독립적으로 실패할 가능성이 높은 서버나 가용 영역에 분산 배치하는 것이 좋다.1
Sentinel 장애 감지와 자동 승격 흐름
Sentinel 장애 조치는 단계적으로 진행된다.
먼저 각 Sentinel은 Primary와 Replica에 주기적으로 연결해 응답을 확인한다. 특정 Redis 인스턴스가 down-after-milliseconds 동안 응답하지 않으면, 한 Sentinel은 그 인스턴스를 주관적 다운(S_DOWN, Subjectively Down) 상태로 본다. 말 그대로 "내가 보기에는 내려갔다"는 판단이다.
하지만 한 Sentinel의 판단만으로 Primary를 바꾸면 위험하다. 네트워크 문제로 특정 Sentinel만 Primary에 접근하지 못하는 상황일 수 있기 때문이다. 그래서 여러 Sentinel이 의견을 모은다. 설정된 quorum 수 이상이 Primary를 다운으로 판단하면 객관적 다운(O_DOWN, Objectively Down)으로 넘어간다.1
그다음에는 장애 조치를 수행할 Sentinel 리더를 뽑는다. quorum은 장애 감지를 위한 기준이고, 실제 failover를 진행하려면 Sentinel 다수의 승인이 필요하다. 다수와 통신할 수 없는 쪽에서는 failover가 시작되지 않는다. 이 성질은 네트워크 분리 상황에서 두 Primary가 동시에 생기는 split-brain 위험을 줄이는 데 중요하다.1
리더 Sentinel이 정해지면 다음 절차가 이어진다.
승격할 Replica를 선택한다.
선택된 Replica에
REPLICAOF NO ONE을 실행해 새 Primary로 만든다.나머지 Replica들이 새 Primary를 바라보도록 재설정한다.
기존 Primary가 나중에 돌아오면 새 Primary의 Replica로 편입한다.
Sentinel이 새 Primary 주소를 클라이언트에 제공한다.
기본 Sentinel 설정 예시는 다음과 같다.
port 26379
sentinel monitor mymaster 10.0.0.10 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
sentinel parallel-syncs mymaster 1mymaster는 Sentinel이 관리하는 Primary 그룹의 이름이다. 애플리케이션은 나중에 이 이름으로 현재 Primary 주소를 물어본다. 마지막 숫자 2는 quorum이다. 즉 최소 2개의 Sentinel이 Primary 장애에 동의해야 O_DOWN으로 판단한다.
parallel-syncs는 failover 후 몇 개 Replica를 동시에 새 Primary에 붙일지 정한다. 값을 크게 하면 복구는 빨라질 수 있지만, 새 Primary에 복제 부하가 몰릴 수 있다. 운영에서는 빠른 복구와 부하 분산 사이에서 선택해야 한다.
애플리케이션은 Primary 주소를 하드코딩하지 않는다
Sentinel을 구성했는데 애플리케이션 설정에 여전히 10.0.0.10:6379를 Primary로 고정해 두면 자동 장애 조치의 효과가 줄어든다. Primary가 바뀌었는데 애플리케이션이 옛 주소만 바라보기 때문이다.
Sentinel 환경에서 애플리케이션은 다음 정보를 가져야 한다.
Sentinel 주소 목록
Sentinel 서비스 이름, 예:
mymasterRedis 인증 정보
연결 실패 시 재조회와 재연결 정책
Sentinel을 지원하는 Redis 클라이언트 라이브러리는 Sentinel에 현재 Primary 주소를 질의한 뒤 그 주소로 Redis 연결을 만든다. failover가 발생하면 기존 연결은 끊기거나 쓰기 오류를 낼 수 있다. 클라이언트는 Sentinel에 다시 물어 새 Primary 주소를 받고 재연결해야 한다.1
개념적으로는 다음 흐름이다.
Application
|
| 1. mymaster의 현재 Primary는 어디인가?
v
Sentinel 1, Sentinel 2, Sentinel 3
|
| 2. 현재 Primary 주소 반환
v
Redis Primary운영 코드에서는 장애 조치 순간의 오류를 정상적인 상황으로 다뤄야 한다. failover 중에는 몇 초 동안 쓰기가 실패할 수 있다. 이때 무조건 무한 재시도하면 중복 주문, 중복 쿠폰 발급, 중복 메시지 발송 같은 문제가 생긴다.
따라서 다음 원칙을 함께 적용한다.
쓰기 요청은 멱등성 키를 사용해 중복 실행을 막는다.
재시도 횟수와 대기 시간을 제한한다.
읽기와 쓰기 연결을 분리하되, 최신성이 필요한 읽기는 Primary로 보낸다.
Sentinel 주소를 하나만 넣지 말고 여러 개 넣는다.
클라이언트 라이브러리가 Sentinel failover를 어떻게 처리하는지 문서로 확인한다.
운영에서 확인할 것
복제와 Sentinel은 한 번 설정하고 끝나는 기능이 아니다. 장애를 전제로 만든 구조이므로 실제 장애 상황에서 기대대로 움직이는지 확인해야 한다.
운영 점검 항목은 다음과 같다.
INFO replication에서 Replica 연결 상태와 lag를 확인한다.Replica가 반복적으로 full sync를 수행하지 않는지 본다.
Sentinel이 최소 3개 이상 독립된 장애 영역에 배치되어 있는지 확인한다.
quorum 값이 Sentinel 개수와 배치 구조에 맞는지 검토한다.
애플리케이션이 Sentinel 서비스 이름으로 Primary를 찾는지 확인한다.
failover 테스트를 정기적으로 수행한다.
failover 중 발생한 쓰기 실패와 재시도 로그를 수집한다.
승격된 Redis의 영속성 설정이 기존 Primary와 일관된지 확인한다.
특히 테스트가 중요하다. Primary 프로세스를 강제로 중지했을 때 Sentinel이 실제로 승격을 수행하는지, 애플리케이션이 새 Primary로 재연결하는지, 장애 조치 중 사용자 요청이 어떻게 실패하거나 복구되는지 확인해야 한다.
고가용성의 목표는 장애를 없애는 것이 아니다. 장애가 나도 시스템이 예측 가능한 방식으로 반응하게 만드는 것이다. Redis도 마찬가지다. 운영 환경의 Redis는 한 대가 아니라, 복제와 자동 장애 조치, 클라이언트 재연결, 데이터 손실 허용 범위까지 함께 설계해야 한다.
참고
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.