Valkey 장애 복구의 내부 원리: RDB·AOF·Replication·Sentinel·Cluster Failover
Valkey는 데이터를 메모리에 저장하기 때문에 빠른 읽기·쓰기 성능을 제공한다.
하지만 데이터를 메모리에 보관한다는 설명만 들으면 다음과 같은 의문이 생긴다.
Valkey 프로세스가 종료되면 데이터는 모두 사라지는가?
서버가 재부팅되면 메모리 데이터를 어떻게 복구하는가?
Primary가 장애 나면 Replica가 자동으로 전환되는가?
Failover가 완료되면 애플리케이션은 어떻게 새로운 Primary를 찾는가?
복제본이 있다면 데이터 유실은 발생하지 않는가?
Valkey의 장애 복구는 하나의 기능으로 이루어지는 것이 아니다.
다음 세 가지 계층이 서로 다른 역할을 수행한다.
Persistence
→ 프로세스 재시작 후 데이터를 디스크에서 복원
Replication
→ Primary의 데이터를 Replica에 복제
Sentinel 또는 Cluster
→ Primary 장애를 감지하고 Replica를 새로운 Primary로 승격
즉, Persistence는 데이터 복구, Replication은 복제본 유지, Sentinel과 Cluster는 서비스 가용성 복구를 담당한다.
이 글에서는 Valkey 장애 상황을 유형별로 나누고, RDB·AOF·Replication·Sentinel·Cluster가 내부적으로 어떻게 복구를 수행하는지 정리한다.
1. 먼저 장애 유형을 구분해야 한다
Valkey 장애라고 해도 원인은 다양하다.
Valkey 프로세스 비정상 종료
운영체제 재부팅
서버 장비 장애
디스크 장애
Primary 노드 장애
Primary와 Replica 사이 네트워크 단절
Cluster 일부 노드 장애
데이터 오삭제 또는 논리적 장애
장애 유형에 따라 복구 방식도 달라진다.
| 장애 유형 | 주요 복구 수단 |
|---|---|
| 프로세스 종료 | RDB 또는 AOF 로딩 |
| 서버 재부팅 | Persistence 파일 복원 |
| Primary 장애 | Sentinel 또는 Cluster Failover |
| 복제 연결 단절 | Partial 또는 Full Resynchronization |
| 디스크 파일 손상 | 백업 복원, AOF 점검 도구 |
| 데이터 오삭제 | 별도 백업에서 복원 |
| Cluster 노드 장애 | Replica 승격 및 Slot 소유권 변경 |
여기서 중요한 점은 고가용성과 데이터 영속성은 서로 다른 문제라는 것이다.
Replica가 있다고 해서 반드시 모든 데이터가 보존되는 것은 아니며, AOF가 있다고 해서 서비스가 자동으로 다른 서버로 전환되는 것도 아니다.
2. Valkey 데이터는 기본적으로 메모리에 존재한다
Valkey의 실제 데이터 조회와 변경은 메모리에서 수행된다.
Client
↓
Valkey Command
↓
Memory Dataset 변경
↓
Persistence 및 Replication 처리
예를 들어 다음 명령을 실행하면:
SET user:1001:name "JY"
우선 Valkey 프로세스의 메모리 데이터가 변경된다.
이후 설정에 따라 다음 작업이 추가로 수행된다.
RDB
→ 특정 시점의 전체 데이터 스냅샷 생성
AOF
→ 실행된 쓰기 명령을 로그에 기록
Replication
→ 동일한 변경 명령을 Replica로 전달
따라서 메모리 데이터와 디스크 Persistence 파일, Replica 데이터 사이에는 짧은 시간 차이가 존재할 수 있다.
이 시간 차이가 장애 시 데이터 유실 범위를 결정한다.
3. RDB 기반 복구 원리
RDB는 특정 시점의 전체 데이터셋을 바이너리 파일로 저장하는 방식이다.
기본 파일명은 일반적으로 다음과 같다.
dump.rdb
RDB는 다음과 같이 설정할 수 있다.
save 900 1
save 300 10
save 60 10000
예를 들어:
save 60 10000
은 60초 동안 10,000개 이상의 키 변경이 발생하면 스냅샷을 생성한다는 의미다.
Valkey는 지정된 조건을 만족하면 전체 메모리 데이터셋의 시점 복사본을 RDB 파일로 저장한다.
3.1 BGSAVE 내부 동작
운영 환경에서는 일반적으로 SAVE가 아니라 BGSAVE 방식이 사용된다.
BGSAVE
SAVE는 메인 프로세스가 직접 디스크 저장을 수행하기 때문에 저장이 완료될 때까지 명령 처리가 중단될 수 있다.
반면 BGSAVE는 자식 프로세스를 생성해 저장 작업을 수행한다.
Valkey Parent Process
│
├─ fork()
│
├─ Parent: 클라이언트 명령 계속 처리
│
└─ Child: 메모리 데이터를 RDB 파일로 저장
운영체제의 fork()가 실행되면 부모와 자식 프로세스는 논리적으로 같은 메모리 내용을 가진다.
하지만 전체 메모리를 즉시 복사하는 것이 아니라 Copy-on-Write 방식으로 메모리 페이지를 공유한다.
fork 직후
Parent ─┐
├─ 동일한 메모리 페이지 공유
Child ─┘
이후 부모 프로세스가 특정 메모리 페이지를 변경하면 해당 페이지만 복사된다.
쓰기 발생
Parent → 변경된 새 페이지
Child → fork 당시의 기존 페이지
따라서 자식 프로세스는 스냅샷 시작 시점의 데이터를 기준으로 RDB를 작성할 수 있고, 부모 프로세스는 계속 새로운 요청을 처리할 수 있다.
다만 쓰기 트래픽이 많은 상태에서 BGSAVE가 실행되면 변경되는 메모리 페이지가 많아져 Copy-on-Write 메모리 사용량이 증가할 수 있다.
기존 Dataset Memory
+ 변경된 Copy-on-Write Page
+ Valkey 자체 메모리
따라서 RDB 저장 중 메모리 사용량과 Fork 지연을 모니터링해야 한다.
3.2 RDB 파일 생성 과정
RDB 저장은 대략 다음 순서로 진행된다.
1. Parent가 fork() 실행
2. Child가 메모리 데이터 순회
3. Child가 임시 RDB 파일 생성
4. 데이터 직렬화 및 디스크 기록
5. 기록 완료 후 기존 RDB 파일과 교체
6. Child 프로세스 종료
임시 파일을 먼저 생성하는 이유는 저장 도중 장애가 발생했을 때 기존 정상 RDB 파일이 손상되는 것을 방지하기 위해서다.
저장 성공
→ 새 RDB 파일로 원자적 교체
저장 실패
→ 기존 RDB 파일 유지
3.3 서버 재시작 시 RDB 복구
Valkey 프로세스가 다시 시작되면 디스크에 있는 RDB 파일을 읽어 메모리 데이터 구조를 다시 생성한다.
Valkey 시작
↓
dump.rdb 읽기
↓
RDB 바이너리 파싱
↓
Key, Value, TTL 복원
↓
Memory Dataset 재구성
↓
클라이언트 요청 수신
RDB는 전체 데이터셋이 하나의 압축된 바이너리 파일로 저장되기 때문에 일반적으로 복원 속도가 빠른 편이다.
하지만 마지막 RDB 생성 이후에 변경된 데이터는 복원할 수 없다.
10:00 RDB 생성
10:01 데이터 변경
10:02 데이터 변경
10:03 서버 장애
재시작 시 10:00 시점의 데이터만 복원되므로 10:00 이후 변경분은 유실될 수 있다.
따라서 RDB 단독 사용은 일정 범위의 데이터 유실을 허용할 수 있는 캐시, 세션, 재생성 가능한 데이터에 적합하다.
4. AOF 기반 복구 원리
AOF는 Append Only File의 약자로, Valkey가 처리한 쓰기 명령을 로그 형태로 기록한다.
예를 들어 다음 명령이 실행되었다면:
SET account:100:status ACTIVE
INCR account:100:login-count
EXPIRE account:100:session 3600
AOF에는 이 변경을 재현할 수 있는 명령이 기록된다.
SET account:100:status ACTIVE
INCR account:100:login-count
EXPIRE account:100:session 3600
서버가 재시작되면 AOF 명령을 처음부터 순서대로 재실행해 메모리 상태를 복원한다.
Valkey 시작
↓
AOF 파일 읽기
↓
쓰기 명령 순차 Replay
↓
Memory Dataset 재구성
AOF는 Valkey 프로토콜과 동일한 형태로 쓰기 명령을 기록하고, 시작 시 해당 명령을 재생하여 데이터셋을 복원한다.
4.1 AOF 설정
AOF는 다음과 같이 활성화할 수 있다.
appendonly yes
appendfilename "appendonly.aof"
Valkey가 운영체제의 디스크에 데이터를 동기화하는 정책은 appendfsync로 설정한다.
appendfsync always
appendfsync everysec
appendfsync no
always
매 쓰기 명령마다 fsync를 수행한다.
Client Write
→ AOF Buffer
→ write()
→ fsync()
→ 응답
데이터 유실 가능성은 가장 작지만 디스크 동기화 비용 때문에 쓰기 성능이 저하될 수 있다.
everysec
일반적으로 가장 많이 사용하는 절충안이다.
Client Write
→ AOF Buffer
→ write()
→ 백그라운드에서 약 1초 단위 fsync
성능과 내구성의 균형이 좋지만 장애 시 최근 약 1초 범위의 데이터가 유실될 수 있다. Valkey 공식 문서도 everysec 정책에서 일반적으로 최대 약 1초의 쓰기 유실 가능성을 설명한다.
no
Valkey가 직접 fsync 시점을 제어하지 않고 운영체제에 맡긴다.
Valkey write()
→ OS Page Cache
→ 운영체제가 적절한 시점에 Disk Flush
성능은 좋을 수 있지만 장애 시 유실 범위가 커질 수 있다.
4.2 write와 fsync의 차이
애플리케이션에서 파일에 write()했다고 해서 데이터가 실제 디스크 플래터나 SSD에 즉시 기록된 것은 아니다.
Valkey Process
↓ write()
OS Page Cache
↓ fsync()
Storage Device
write()는 데이터를 운영체제의 페이지 캐시에 복사하는 수준에서 완료될 수 있다.
운영체제가 비정상 종료되거나 전원이 차단되면 페이지 캐시에만 존재하던 데이터는 유실될 수 있다.
fsync()는 운영체제에 해당 데이터를 실제 저장 장치까지 동기화하도록 요청한다.
따라서 AOF 내구성은 단순히 AOF 활성화 여부가 아니라 appendfsync 정책에 따라 달라진다.
4.3 AOF Rewrite가 필요한 이유
AOF는 모든 쓰기 명령을 계속 추가하기 때문에 시간이 지날수록 파일 크기가 증가한다.
예를 들어:
SET counter 1
SET counter 2
SET counter 3
SET counter 4
SET counter 5
최종 상태는 다음 하나뿐이다.
counter = 5
하지만 기존 AOF에는 이전 변경 이력이 모두 남아 있을 수 있다.
AOF Rewrite는 현재 데이터셋을 만들기 위해 필요한 최소 명령만으로 새로운 AOF를 생성한다.
기존 AOF
SET counter 1
SET counter 2
SET counter 3
SET counter 4
SET counter 5
Rewrite AOF
SET counter 5
4.4 Multi-Part AOF Rewrite 내부 동작
Valkey의 AOF Rewrite는 일반적인 로그 파일을 단순히 수정하는 방식이 아니다.
Valkey는 새로운 Base AOF와 Incremental AOF를 조합하는 방식을 사용한다.
AOF Manifest
├─ Base AOF
├─ Incremental AOF 1
└─ Incremental AOF 2
Rewrite 과정은 다음과 같다.
1. Parent가 fork()
2. Child가 현재 데이터셋으로 새로운 Base AOF 작성
3. Parent는 새로운 Incremental AOF에 이후 변경 기록
4. Child의 Base AOF 작성 완료
5. Base AOF와 Incremental AOF를 Manifest에 반영
6. 기존 불필요한 AOF 파일 정리
Valkey는 AOF Rewrite 중에도 기존 AOF 기록을 유지하며, 자식 프로세스가 새로운 Base 파일을 생성하는 동안 부모는 새 Incremental 파일에 변경분을 기록한다. Rewrite 실패 시 기존 파일과 Incremental 파일을 이용할 수 있도록 설계되어 있다.
4.5 AOF 손상 시 복구
프로세스 강제 종료나 디스크 문제로 AOF 마지막 부분이 불완전하게 기록될 수 있다.
정상 명령
정상 명령
정상 명령
불완전하게 기록된 마지막 명령
Valkey는 시작 시 AOF의 유효성을 확인한다.
파일 끝부분의 일부 명령만 불완전한 경우 설정에 따라 잘못된 마지막 부분을 제거하고 복구할 수 있다.
관리 도구로 다음 명령도 사용할 수 있다.
valkey-check-aof --fix appendonly.aof
다만 손상 지점 이후의 데이터는 제거될 수 있으므로 실행 전 원본 파일을 별도 백업하는 것이 안전하다.
cp appendonly.aof appendonly.aof.backup
valkey-check-aof --fix appendonly.aof
AOF 손상이 파일 앞부분에서 발생했다면 손상 이후의 많은 로그를 사용할 수 없으므로 데이터 유실 범위가 커질 수 있다.
5. RDB와 AOF를 함께 사용하는 이유
운영 환경에서는 RDB와 AOF를 함께 활성화하는 구성을 고려할 수 있다.
save 900 1
save 300 10
save 60 10000
appendonly yes
appendfsync everysec
각 방식의 역할은 다음과 같다.
| 항목 | RDB | AOF |
|---|---|---|
| 저장 방식 | 전체 스냅샷 | 쓰기 명령 로그 |
| 복구 속도 | 상대적으로 빠름 | 명령 재생량에 따라 느릴 수 있음 |
| 데이터 유실 범위 | 마지막 스냅샷 이후 | fsync 정책에 따라 결정 |
| 파일 크기 | 상대적으로 작음 | Rewrite 전까지 커질 수 있음 |
| 백업 용도 | 적합 | 가능하지만 관리 필요 |
| 쓰기 부하 | 스냅샷 시 Fork·I/O | 지속적 Append·fsync |
RDB는 빠른 재시작과 백업에 유리하고, AOF는 더 작은 데이터 유실 범위를 제공한다.
Valkey 공식 문서 역시 AOF만 사용하는 것보다 주기적인 RDB 스냅샷을 함께 유지하는 것이 백업, 빠른 복원, AOF 엔진 문제 대응 측면에서 유용하다고 설명한다.
6. 복제는 어떻게 동작하는가
Valkey Replication은 Primary의 데이터 변경을 Replica에 복제하는 구조다.
Client
↓ Write
Primary
↓ Replication Stream
Replica 1
Replica 2
Primary와 Replica 연결이 정상이라면 Primary는 다음 변경을 Replica에 명령 스트림으로 전달한다.
클라이언트 쓰기
키 만료
키 Eviction
데이터를 변경하는 기타 명령
Valkey 복제는 기본적으로 비동기 방식이다. Primary는 일반적으로 Replica가 실제 변경을 반영할 때까지 기다리지 않고 클라이언트에 성공을 응답할 수 있다.
6.1 Replication ID와 Offset
각 Primary는 다음 정보를 유지한다.
Replication ID
Replication Offset
Replication ID는 해당 데이터셋 변경 이력의 계보를 식별한다.
Replication Offset은 Primary가 생성한 복제 스트림의 바이트 위치를 나타낸다.
Primary
Replication ID: abc123
Current Offset: 1,500,000
Replica도 자신이 어디까지 처리했는지 기록한다.
Replica
Replication ID: abc123
Processed Offset: 1,490,000
이 경우 Replica는 Primary보다 10,000바이트 뒤처져 있는 상태다.
Valkey Primary는 데이터 변경을 복제 스트림으로 생성할 때마다 Offset을 증가시키며, Replica는 자신이 처리한 위치를 추적한다.
7. Replica 연결 복구: Partial Resynchronization
Primary와 Replica 사이 네트워크가 잠시 끊겼다고 가정해 보자.
Primary Offset: 1000
Replica Offset: 900
네트워크 단절
Primary Offset: 1300
Replica Offset: 900
Replica가 다시 연결되면 자신이 알고 있는 Replication ID와 Offset을 Primary에 전달한다.
Replica
→ 나는 replication ID abc123의 offset 900까지 가지고 있다.
Primary가 901부터 1300까지의 복제 데이터를 Backlog에 보관하고 있다면 누락된 부분만 전송한다.
Primary
→ Offset 901~1300 전달
Replica
→ 누락 데이터 적용
→ 다시 실시간 복제 상태 전환
이것이 Partial Resynchronization이다.
전체 Dataset 재전송: 불필요
누락된 Replication Stream만 전송
네트워크가 짧게 단절된 경우 전체 RDB 복사 없이 빠르게 복구할 수 있다.
7.1 Replication Backlog
Partial Resynchronization을 위해 Primary는 최근 복제 명령을 메모리의 원형 버퍼에 보관한다.
Replication Backlog
[old data ... recent data ... newest data]
Replica의 Offset이 Backlog 범위 안에 있으면 부분 동기화가 가능하다.
Backlog 범위: 500~1500
Replica Offset: 900
→ Partial Sync 가능
하지만 Replica가 너무 오래 단절되어 필요한 Offset이 Backlog에서 사라졌다면 부분 동기화가 불가능하다.
Backlog 범위: 1000~2000
Replica Offset: 900
→ Offset 901~999 없음
→ Full Resynchronization 필요
따라서 복제 Backlog 크기는 네트워크 단절 시간과 쓰기 처리량을 고려해 설정해야 한다.
대략적으로는 다음과 같이 판단할 수 있다.
필요 Backlog 크기
≈ 초당 복제 데이터량 × 허용 단절 시간
예를 들어 초당 5MB의 쓰기가 발생하고 60초 단절까지 Partial Sync로 복구하려면:
5MB × 60초 = 약 300MB
여기에 트래픽 변동과 여유분을 추가해야 한다.
8. Full Resynchronization 내부 원리
Partial Sync가 불가능하면 Primary는 Replica에 전체 데이터셋을 다시 전송한다.
1. Replica가 Primary에 연결
2. Partial Sync 불가능 판단
3. Primary가 RDB 스냅샷 생성
4. RDB를 Replica에 전송
5. Replica가 기존 데이터 제거
6. Replica가 RDB 로딩
7. RDB 생성·전송 중 발생한 후속 명령 적용
8. 실시간 복제 전환
개념적으로는 다음과 같다.
Primary Memory
↓ Snapshot
RDB Stream
↓ Network
Replica
↓ Load
Replica Memory Dataset
Full Sync 중에도 Primary에는 새로운 쓰기가 계속 들어올 수 있다.
따라서 Primary는 RDB 생성 이후 발생한 변경 명령을 별도로 버퍼링했다가 RDB 전송이 끝난 후 Replica에 전달한다.
RDB 기준 시점
↓
RDB 전송 중 발생한 추가 쓰기
↓
후속 Replication Stream 전송
Full Resynchronization은 CPU, 네트워크, 디스크 및 Copy-on-Write 메모리 사용량을 증가시킬 수 있다.
Replica가 여러 대 동시에 Full Sync를 수행하면 Primary 부하가 급증할 수 있으므로 운영 시 주의해야 한다.
9. 복제본이 있어도 데이터가 유실될 수 있는 이유
Valkey 복제는 기본적으로 비동기다.
다음 상황을 생각해 보자.
1. Client가 Primary에 SET 요청
2. Primary Memory에 반영
3. Primary가 Client에 성공 응답
4. Replica로 복제되기 전 Primary 장애
5. Replica가 새로운 Primary로 승격
새로운 Primary에는 해당 쓰기가 존재하지 않는다.
Client 관점
→ 성공했다고 응답받음
새 Primary 관점
→ 해당 데이터 없음
즉, 복제본이 있다고 해서 Zero Data Loss가 자동 보장되는 것은 아니다.
Valkey 공식 문서는 비동기 복제 특성상 Failover 시 승인된 쓰기가 유실될 수 있으며, WAIT 명령도 유실 가능성을 낮출 뿐 강한 일관성 시스템으로 바꾸지는 않는다고 설명한다.
9.1 WAIT 명령
중요한 쓰기 이후 일정 수 이상의 Replica가 복제했는지 기다릴 수 있다.
SET payment:1001:status COMPLETED
WAIT 1 1000
의미는 다음과 같다.
최소 1개 Replica가 복제를 확인할 때까지
최대 1,000ms 대기
하지만 WAIT도 완전한 합의 프로토콜은 아니다.
Replica가 쓰기를 확인
→ 아직 디스크 fsync 전일 수 있음
→ Primary와 Replica가 동시에 장애 날 수 있음
따라서 중요 데이터의 최종 정합성은 관계형 데이터베이스나 별도 영속 저장소에서 보장하고, Valkey는 캐시·락·빠른 상태 조회 용도로 사용하는 것이 일반적으로 안전하다.
10. Sentinel 기반 자동 장애 복구
Sentinel은 Valkey Cluster를 사용하지 않는 Primary-Replica 구조에서 고가용성을 제공한다.
Sentinel의 주요 역할은 다음과 같다.
Monitoring
Notification
Automatic Failover
Service Discovery
Sentinel은 Primary와 Replica 상태를 지속적으로 확인하고, Primary 장애가 확정되면 Replica 하나를 새로운 Primary로 승격한다. 또한 클라이언트가 현재 Primary 주소를 조회할 수 있는 설정 제공자 역할도 한다.
일반적인 구성은 다음과 같다.
Sentinel 1 ─┐
Sentinel 2 ─┼─ Primary 감시
Sentinel 3 ─┘
Primary
├─ Replica 1
└─ Replica 2
Sentinel도 장애 판단에 참여하므로 일반적으로 홀수 개, 최소 3개 구성을 사용한다.
11. Sentinel 장애 감지 내부 원리
Sentinel은 주기적으로 Valkey 노드에 PING을 전송한다.
Sentinel
↓ PING
Primary
↑ PONG
일정 시간 동안 정상 응답을 받지 못하면 해당 Sentinel은 Primary를 주관적으로 장애 상태라고 판단한다.
이를 SDOWN, Subjectively Down이라고 한다.
Sentinel 1 관점
Primary 응답 없음
→ SDOWN
하지만 하나의 Sentinel 판단만으로 즉시 Failover하지는 않는다.
다른 Sentinel들에게 Primary 상태를 확인한다.
Sentinel 1 → Primary가 장애라고 보는가?
Sentinel 2 → Yes
Sentinel 3 → Yes
설정된 Quorum 이상의 Sentinel이 장애에 동의하면 객관적 장애 상태로 판단한다.
이를 ODOWN, Objectively Down이라고 한다.
다수 Sentinel의 동의
→ ODOWN
→ Failover 후보
예를 들어 다음 설정이 있다.
sentinel monitor mymaster 10.0.0.10 6379 2
sentinel down-after-milliseconds mymaster 5000
의미는 다음과 같다.
Primary 이름: mymaster
Primary 주소: 10.0.0.10:6379
Quorum: 2
장애 판단 대기 시간: 5초
Sentinel 공식 문서의 예제에서도 down-after-milliseconds 동안 PING 응답이 없으면 장애 판단이 시작되고, Quorum을 기반으로 Failover가 수행된다.
12. Sentinel Leader 선출
여러 Sentinel이 동시에 Failover를 실행하면 서로 다른 Replica를 Primary로 승격할 수 있다.
이를 방지하기 위해 Sentinel 중 하나가 해당 Failover 작업의 Leader로 선출된다.
Sentinel 1 ─┐
Sentinel 2 ─┼─ 투표
Sentinel 3 ─┘
↓
Sentinel 2가 Failover Leader
Leader Sentinel만 Replica 승격과 복제 토폴로지 변경을 주도한다.
Failover는 대략 다음 순서로 진행된다.
1. Primary ODOWN 판단
2. Sentinel Leader 선출
3. 승격할 Replica 선택
4. Replica를 새로운 Primary로 승격
5. 다른 Replica들이 새로운 Primary를 바라보도록 재설정
6. 기존 Primary가 복구되면 Replica로 전환
7. 클라이언트에 새로운 Primary 정보 제공
13. 승격할 Replica는 어떻게 선택하는가
Sentinel은 아무 Replica나 무작위로 선택하지 않는다.
대략적으로 다음 요소를 고려한다.
Replica 우선순위
Primary와의 연결 상태
최근 통신 상태
Replication Offset
복제 지연 정도
Replication Offset이 크다는 것은 기존 Primary의 변경을 더 많이 반영했다는 의미다.
Replica A Offset: 150000
Replica B Offset: 149000
Replica C Offset: 120000
다른 조건이 유사하다면 더 최신 상태인 Replica가 유리하다.
하지만 비동기 복제이므로 가장 최신 Replica를 선택하더라도 Primary 장애 직전의 모든 데이터가 존재한다고 보장할 수는 없다.
14. Sentinel Failover 이후 애플리케이션 연결
서버 측에서 Replica가 Primary로 승격되더라도 애플리케이션이 계속 이전 Primary 주소로 연결하면 서비스를 사용할 수 없다.
따라서 클라이언트는 Sentinel을 지원해야 한다.
Application
↓ Sentinel 조회
현재 Primary 주소 요청
↓
Sentinel
↓
10.0.0.12:6379 반환
↓
Application이 새 Primary 연결
Sentinel 지원 클라이언트는 보통 다음 순서로 동작한다.
1. 설정된 Sentinel 목록 중 하나에 연결
2. SENTINEL get-master-addr-by-name 실행
3. 현재 Primary 주소 획득
4. Primary 연결
5. 연결 오류 발생 시 Sentinel에 다시 조회
6. 주소가 변경되었으면 기존 Pool 폐기
7. 새로운 Primary로 Connection Pool 재생성
Sentinel 클라이언트 명세는 Primary 주소가 변경되면 기존 Pool의 연결을 종료하고 새 Primary로 다시 연결하도록 설명한다.
Spring Boot에서는 개념적으로 다음과 같이 구성할 수 있다.
spring:
data:
redis:
sentinel:
master: mymaster
nodes:
- 10.0.0.21:26379
- 10.0.0.22:26379
- 10.0.0.23:26379
timeout: 2s
connect-timeout: 1s
애플리케이션에 Primary IP를 직접 고정하면 Sentinel Failover의 장점을 활용할 수 없다.
# Failover 대응에 부적합한 구성
spring:
data:
redis:
host: 10.0.0.10
15. Failover 중 애플리케이션에서는 무엇이 발생하는가
Failover가 즉시 완료되는 것은 아니다.
Primary 장애
→ 장애 감지 대기
→ Sentinel 간 합의
→ Leader 선출
→ Replica 승격
→ 클라이언트 재연결
이 과정에서 수초 이상의 연결 오류가 발생할 수 있다.
애플리케이션에서는 다음 예외가 나타날 수 있다.
Connection refused
Connection reset
Command timeout
READONLY
Connection closed
따라서 애플리케이션은 Failover 시간 동안의 실패를 처리해야 한다.
Valkey 명령 실패
↓
짧은 Backoff
↓
새 Primary 탐색 및 재연결
↓
멱등성이 보장되는 명령만 제한적으로 재시도
모든 명령을 무조건 재시도하면 안 된다.
예를 들어 클라이언트가 응답을 받기 전에 연결이 끊겼다고 가정해 보자.
Client → INCR 실행 요청
Primary → 실제 INCR 처리
Primary → 응답 전 장애
Client → Timeout 발생
Client → INCR 재시도
실제 값은 두 번 증가할 수 있다.
원래 의도: +1
실제 결과: +2
따라서 재시도 대상 명령의 멱등성을 확인해야 한다.
| 명령 | 단순 재시도 안전성 |
|---|---|
GET |
비교적 안전 |
동일 값 SET |
비교적 안전 |
DEL |
일반적으로 멱등 |
INCR |
중복 실행 위험 |
LPUSH |
중복 삽입 위험 |
| 재고 차감 Lua | 업무 멱등성 필요 |
| 분산 락 획득 | 소유자 토큰 검증 필요 |
16. Valkey Cluster 장애 복구
Valkey Cluster는 데이터를 여러 Primary 노드에 Hash Slot 단위로 분산한다.
전체 Key Space는 16,384개의 Hash Slot으로 나뉜다.
Primary A → Slot 0~5460
Primary B → Slot 5461~10922
Primary C → Slot 10923~16383
각 Primary는 일반적으로 하나 이상의 Replica를 가진다.
Primary A ─ Replica A1
Primary B ─ Replica B1
Primary C ─ Replica C1
Cluster는 데이터 분산과 Failover를 함께 제공한다.
17. Cluster 노드 장애 감지
Cluster 노드들은 Cluster Bus를 통해 서로 상태를 교환한다.
Node A ↔ Node B
Node A ↔ Node C
Node B ↔ Node C
각 노드는 다른 노드에 PING, PONG 메시지를 보내고 Gossip 정보를 교환한다.
특정 노드가 응답하지 않으면 먼저 개별 노드가 장애 의심 상태로 표시한다.
PFAIL
→ Possible Failure
다수의 Primary 노드가 해당 노드의 장애에 동의하면 최종 장애 상태가 된다.
FAIL
→ Confirmed Failure
Replica는 자신이 복제하던 Primary가 FAIL 상태가 되면 새로운 Primary가 되기 위한 선거를 시작할 수 있다.
Primary A 장애
↓
Replica A1이 Failover Election 요청
↓
다른 Primary들의 투표
↓
과반수 획득
↓
Replica A1 → 새로운 Primary
18. Cluster Failover와 Epoch
Cluster에서는 여러 노드의 구성 변경 순서를 일관되게 판단하기 위해 Epoch 개념을 사용한다.
쉽게 말하면 Cluster 구성 변경의 논리적 버전 번호다.
Configuration Epoch 100
→ Primary A가 Slot 0~5460 소유
Configuration Epoch 101
→ Replica A1이 새로운 Primary로 승격
→ Slot 0~5460 소유
더 높은 Epoch의 구성 정보가 더 최신 상태로 인정된다.
이를 통해 장애 복구 후 다른 노드들이 어떤 노드가 해당 Slot의 현재 Primary인지 판단할 수 있다.
19. Cluster Client의 MOVED 처리
Cluster Client는 Key의 Hash Slot을 계산해 해당 Primary로 요청을 보낸다.
Key
↓ CRC16
Hash Slot 계산
↓
Slot 담당 Primary로 요청
Failover 직후 클라이언트가 이전 Primary로 요청을 보내면 MOVED 응답을 받을 수 있다.
MOVED 1200 10.0.0.15:6379
이는 다음을 의미한다.
Slot 1200은 이제 10.0.0.15:6379에서 처리한다.
Cluster-aware Client는 이 응답을 받으면 Slot Map을 갱신하고 새로운 노드로 요청을 보낸다.
기존 Slot Map
Slot 1200 → Node A
MOVED 수신
갱신된 Slot Map
Slot 1200 → Node A1
Valkey Cluster는 노드가 요청을 Proxy하지 않고 MOVED 또는 ASK 리다이렉션을 반환하며, 클라이언트가 Slot-Node 매핑을 캐시하면 성능을 높일 수 있다.
20. Cluster가 모든 장애에서 가용한 것은 아니다
Cluster는 일부 노드 장애를 자동 복구할 수 있지만, 다음 경우에는 서비스를 계속할 수 없을 수 있다.
Primary와 해당 Replica가 동시에 장애
다수의 Primary가 동시에 장애
Primary 과반수와 통신 불가능
모든 Slot을 정상적으로 제공할 수 없는 상태
예를 들어:
Primary A + Replica A1 동시 장애
→ A가 담당한 Slot 복구 불가
cluster-require-full-coverage yes인 기본적인 구성에서는 일부 Slot을 제공할 수 없으면 Cluster 전체가 요청을 거부할 수 있다.
반대로 no로 설정하면 정상 Slot에 대해서는 요청을 계속 받을 수 있지만 일부 Key는 접근할 수 없다.
Valkey Cluster는 일부 장애와 Partition 상황에서 가용성을 제공하지만, 다수 Primary 장애 같은 더 큰 장애에서는 Cluster가 사용할 수 없는 상태가 될 수 있다.
21. Cluster도 데이터 유실 가능성이 있다
Cluster 역시 Primary-Replica 간 비동기 복제를 사용한다.
따라서 다음 상황에서 쓰기 유실이 가능하다.
1. Client가 기존 Primary에 쓰기
2. Primary가 성공 응답
3. Replica 복제 전 Primary 장애
4. Replica가 새로운 Primary로 승격
또한 Network Partition에서 소수 측 Primary에 쓰기가 발생하고, 다수 측에서 Replica가 새로운 Primary로 승격되면 소수 측에서 처리된 쓰기가 최종적으로 사라질 수 있다.
Valkey Cluster는 비동기 복제를 사용하며, Partition과 Failover의 특정 시간 구간에서 승인된 쓰기가 유실될 수 있음을 공식 명세에 명시한다.
22. 분산 락은 Failover 시 더 주의해야 한다
Valkey를 분산 락 저장소로 사용할 때 Primary 장애는 특히 중요하다.
다음 상황을 가정해 보자.
1. 요청 A가 Primary에서 락 획득
2. 락 정보가 Replica로 복제되기 전 Primary 장애
3. Replica가 새로운 Primary로 승격
4. 새 Primary에는 락 키가 없음
5. 요청 B가 동일한 락 획득
결과적으로 A와 B가 동시에 임계 구역을 실행할 수 있다.
요청 A → 기존 락 소유자로 인식
요청 B → 새 Primary에서 락 획득 성공
따라서 중요한 업무에서 Valkey 분산 락만으로 데이터 정합성을 보장하면 안 된다.
다음 방어 계층을 함께 적용해야 한다.
Valkey 분산 락
+ DB Unique Constraint
+ 조건부 UPDATE
+ 낙관적 락
+ 멱등 키
+ Fencing Token
예를 들어 Fencing Token을 사용하면 락 획득 순서마다 증가하는 번호를 발급할 수 있다.
요청 A → Token 100
요청 B → Token 101
최종 저장소는 더 오래된 Token의 쓰기를 거절한다.
UPDATE resource
SET value = :value,
fencing_token = :token
WHERE resource_id = :resourceId
AND fencing_token < :token;
요청 A가 뒤늦게 실행되더라도:
현재 Token: 101
A의 Token: 100
100 < 101
→ UPDATE 0건
→ 오래된 요청 차단
23. 데이터 오삭제는 자동 Failover로 복구되지 않는다
운영자가 실수로 다음 명령을 실행했다고 가정해 보자.
DEL important:key
또는:
FLUSHALL
이 명령은 정상적인 쓰기 명령이므로 Replica에도 그대로 복제된다.
Primary 데이터 삭제
↓ Replication
Replica 데이터도 삭제
AOF에도 삭제 명령이 기록될 수 있다.
DEL important:key
따라서 Replication과 AOF는 논리적 오삭제를 자동으로 보호하지 못한다.
이 경우 필요한 것은 별도의 백업이다.
주기적 RDB 백업
원격 Storage 복사
백업 파일 보존 정책
복원 훈련
백업은 Valkey 서버가 사용하는 동일 디스크에만 보관해서는 안 된다.
Valkey 서버 디스크 장애
→ 운영 데이터 손실
→ 같은 디스크의 백업도 손실
별도 디스크나 Object Storage에 복제해야 한다.
24. 실제 백업 복구 절차
데이터 오삭제나 파일 손상 시 일반적인 복구 절차는 다음과 같다.
1. 장애 시점 확인
2. 쓰기 트래픽 차단 또는 격리
3. 현재 데이터 및 Persistence 파일 별도 보존
4. 복구 대상 시점의 RDB 또는 AOF 선택
5. 별도 Valkey 인스턴스에서 먼저 복원
6. 데이터 검증
7. 운영 인스턴스 전환
8. 애플리케이션 재연결
9. 정합성 검증
운영 서버 파일에 바로 덮어쓰는 것보다 별도 인스턴스에서 복원 검증하는 것이 안전하다.
Backup File
↓
Recovery Valkey Instance
↓
Key Count 검증
업무 데이터 검증
샘플 조회
Checksum 비교
↓
운영 전환
24.1 RDB 복원 예시
systemctl stop valkey
cp /backup/2026-07-29/dump.rdb /var/lib/valkey/dump.rdb
chown valkey:valkey /var/lib/valkey/dump.rdb
systemctl start valkey
시작 후 확인한다.
valkey-cli INFO persistence
valkey-cli INFO keyspace
valkey-cli DBSIZE
24.2 AOF 복원 예시
먼저 원본을 보존한다.
cp -r /var/lib/valkey/appendonlydir \
/var/lib/valkey/appendonlydir.backup
AOF 파일 상태를 확인한다.
valkey-check-aof appendonly.aof
복구가 필요하다면 복사본에서 먼저 수행한다.
valkey-check-aof --fix appendonly.aof
그 후 별도 인스턴스에서 로딩 결과를 검증한다.
25. 애플리케이션 장애 전파를 막는 방법
Valkey 장애가 발생했을 때 애플리케이션 요청 스레드가 무한정 대기하면 장애가 전체 서비스로 전파될 수 있다.
Valkey 응답 지연
↓
Connection Pool 대기
↓
HTTP 요청 스레드 점유
↓
Thread Pool 고갈
↓
전체 API 장애
따라서 다음 타임아웃을 명확하게 설정해야 한다.
Connection Timeout
Command Timeout
Pool Max Wait
Retry Count
Retry Backoff
예시:
spring:
data:
redis:
connect-timeout: 1s
timeout: 2s
lettuce:
pool:
max-active: 16
max-idle: 8
min-idle: 2
max-wait: 500ms
설정값은 예시일 뿐이며 실제 명령 지연, TPS, Pool 사용률과 Failover 시간을 측정해 결정해야 한다.
26. 캐시 장애와 락 장애는 실패 정책이 달라야 한다
Valkey 사용 목적에 따라 장애 대응 정책을 구분해야 한다.
캐시 조회 장애
원본 DB 조회로 우회할 수 있다.
Valkey GET 실패
→ DB 조회
→ 응답
이를 Fail-Open 방식으로 볼 수 있다.
다만 Valkey 장애 시 모든 요청이 DB로 몰리는 Cache Stampede에 주의해야 한다.
Valkey 장애
→ 전체 요청 DB 유입
→ DB Connection Pool 고갈
→ DB 장애
따라서 Circuit Breaker, Rate Limiter, Local Cache 등을 함께 고려해야 한다.
분산 락 장애
락을 획득하지 못했는데 업무를 그대로 실행하면 중복 처리가 발생할 수 있다.
Valkey Lock 실패
→ 락 없이 업무 실행
→ 동시 처리 발생
정합성이 중요한 작업은 Fail-Closed가 더 안전하다.
락 저장소 장애
→ 요청 실패 또는 재처리 큐 적재
→ 락 없이 실행하지 않음
세션 장애
세션을 Valkey에만 저장했다면 사용자의 재로그인이 필요할 수 있다.
메시지 처리 상태 장애
멱등 처리 상태를 Valkey에만 저장했다면 장애 후 중복 소비 가능성이 있으므로 DB Unique Constraint 등 최종 방어가 필요하다.
27. 스케줄러 중복 실행 방지와 장애 복구
Valkey 분산 락으로 스케줄러 중복 실행을 막고 있다고 가정해 보자.
App 1 → 스케줄러 락 획득
App 2 → 락 실패, 실행 안 함
App 3 → 락 실패, 실행 안 함
App 1이 배치 중간에 종료되면 finally의 락 해제가 실행되지 않는다.
락 획득
→ 10만 건 중 4만 건 처리
→ App 1 장애
→ 락 해제 실패
이때 TTL이 있어야 일정 시간 후 다른 인스턴스가 재실행할 수 있다.
락 TTL 만료
→ App 2가 락 획득
→ 배치 재실행
하지만 TTL은 단지 락을 해제할 뿐, 어디까지 처리했는지는 알지 못한다.
따라서 다음 구조가 필요하다.
분산 락
→ 중복 스케줄 실행 방지
Chunk Commit
→ 롤백 범위 축소
Checkpoint
→ 마지막 성공 위치 기록
멱등 처리
→ 재실행 중 중복 방지
예를 들어:
Batch Job ID: DAILY-FLIGHT-20260729
Last Processed ID: 45000
Status: FAILED
재실행 시:
SELECT *
FROM flight_data
WHERE id > 45000
ORDER BY id
FETCH FIRST 1000 ROWS ONLY;
분산 락은 실행 주체를 하나로 제한하고, Checkpoint와 멱등성은 중간 장애 후 안전한 재실행을 보장한다.
28. 모니터링해야 할 지표
Persistence
rdb_last_save_time
rdb_last_bgsave_status
rdb_bgsave_in_progress
rdb_last_bgsave_time_sec
aof_enabled
aof_rewrite_in_progress
aof_last_rewrite_time_sec
aof_last_bgrewrite_status
aof_delayed_fsync
Replication
role
connected_slaves
master_link_status
master_last_io_seconds_ago
master_sync_in_progress
master_repl_offset
slave_repl_offset
repl_backlog_size
Primary와 Replica Offset 차이를 이용해 복제 지연을 판단할 수 있다.
Replication Lag
≈ Primary Offset - Replica Offset
다만 Offset 차이는 바이트 기준이므로 업무 지연 시간과 함께 해석해야 한다.
Sentinel
sdown
odown
failover-start
selected-slave
promoted-slave
switch-master
Cluster
cluster_state
cluster_slots_ok
cluster_slots_pfail
cluster_slots_fail
cluster_known_nodes
cluster_size
애플리케이션
Valkey command latency P95/P99
Connection failure count
Command timeout count
Retry count
Pool wait time
Pool active connections
Failover recovery time
Cache fallback count
Distributed lock failure count
29. 장애 복구 테스트 시나리오
운영 전에 실제 장애 테스트를 수행해야 한다.
프로세스 종료
kill -9 <valkey-pid>
확인 항목:
자동 재시작 여부
RDB/AOF 데이터 복원 여부
애플리케이션 재연결 시간
최근 데이터 유실 범위
Primary 노드 종료
systemctl stop valkey
확인 항목:
Sentinel 장애 감지 시간
Replica 승격 여부
새 Primary 주소
애플리케이션 Connection Pool 재생성
Failover 중 오류 요청 수
네트워크 단절
Primary ↔ Replica 통신 차단
Sentinel ↔ Primary 통신 차단
Cluster 노드 간 통신 차단
확인 항목:
Partial Sync 여부
Full Sync 발생 여부
Split-Brain 가능성
쓰기 유실 여부
디스크 Full
RDB 저장 실패
AOF Append 실패
AOF Rewrite 실패
확인 항목:
쓰기 명령 허용 여부
Persistence 오류 알림
디스크 확보 후 정상화
데이터 오삭제
별도 복구 환경에서 백업 파일을 로딩하고 원하는 시점의 데이터 복원이 가능한지 검증한다.
30. 운영 구성 예시
중요도가 중간 수준인 캐시·분산 락 환경이라면 다음 구성을 고려할 수 있다.
Primary 1대
Replica 2대
Sentinel 3대
AOF everysec
주기적 RDB
외부 Storage 백업
Application
↓
Sentinel-aware Client
↓
Current Primary
↓ async replication
Replica 1 / Replica 2
Cluster가 필요한 경우:
Primary 3대
Replica 3대
총 6개 노드
Primary A ─ Replica A1
Primary B ─ Replica B1
Primary C ─ Replica C1
단, Cluster는 단순히 고가용성만을 위해 도입하기보다 데이터 용량과 처리량을 여러 Shard로 분산해야 할 때 선택하는 것이 적절하다.
단일 노드 용량으로 충분하다면 Sentinel 기반 구성이 운영과 장애 분석 측면에서 더 단순할 수 있다.
31. 복구 목표를 수치로 정의해야 한다
장애 복구 설계에서는 RPO와 RTO를 먼저 정의해야 한다.
RPO
Recovery Point Objective는 장애 시 허용 가능한 데이터 유실 범위다.
RPO 0초
→ 데이터 유실을 허용하지 않음
RPO 1초
→ 최대 약 1초 데이터 유실 허용
RPO 5분
→ 최대 5분 데이터 유실 허용
예를 들어 AOF everysec는 일반적으로 약 1초 수준의 RPO를 목표로 할 수 있지만, 하드웨어·운영체제·동시 장애 시나리오까지 포함한 절대적인 보장은 아니다.
RTO
Recovery Time Objective는 장애 발생 후 서비스를 복구해야 하는 목표 시간이다.
Primary 장애 감지: 5초
Sentinel 합의: 2초
Replica 승격: 2초
Application 재연결: 3초
예상 RTO: 약 12초
실제 RTO는 설정값만으로 계산하지 말고 장애 훈련에서 측정해야 한다.
32. 실무 체크리스트
Persistence
- RDB 또는 AOF 활성화 여부를 결정한다.
- AOF
appendfsync정책을 명확히 한다. - RDB와 AOF를 동일 디스크에만 의존하지 않는다.
- 백업을 외부 Storage에 보관한다.
- 정기적으로 복원 테스트를 수행한다.
BGSAVE, AOF Rewrite 실패를 모니터링한다.- 디스크 여유 공간을 감시한다.
Replication
- Replica를 최소 1대 이상 구성한다.
- 중요 환경에서는 2대 이상을 고려한다.
- Replication Lag을 모니터링한다.
- Backlog 크기를 쓰기량과 단절 시간 기준으로 설정한다.
- Full Sync 발생 빈도를 확인한다.
- 비동기 복제의 데이터 유실 가능성을 인정한다.
Sentinel
- Sentinel을 홀수 개로 구성한다.
- 일반적으로 최소 3개를 사용한다.
- Sentinel을 동일 장애 영역에 모두 배치하지 않는다.
- Quorum과 장애 감지 시간을 조정한다.
- 클라이언트가 Sentinel을 지원하는지 확인한다.
- Primary 주소를 애플리케이션에 고정하지 않는다.
Cluster
- 각 Primary에 Replica를 배치한다.
- Primary와 Replica를 다른 장애 영역에 배치한다.
- Cluster-aware Client를 사용한다.
MOVED,ASK리다이렉션을 처리할 수 있어야 한다.- Slot Coverage 정책을 확인한다.
- 다수 Primary 장애 시 가용성 한계를 이해한다.
애플리케이션
- Connection Timeout을 제한한다.
- Command Timeout을 제한한다.
- Pool 대기 시간을 무제한으로 두지 않는다.
- 재시도 명령의 멱등성을 검토한다.
- 캐시와 분산 락의 실패 정책을 분리한다.
- 락 장애 시 무조건 Fail-Open하지 않는다.
- 최종 정합성은 DB Constraint와 멱등 처리로 보호한다.
33. 결론
Valkey의 장애 복구는 다음 계층으로 이해해야 한다.
RDB
→ 특정 시점의 전체 데이터 스냅샷 복구
AOF
→ 쓰기 명령을 재생하여 최신 상태 복구
Replication
→ Primary 데이터를 Replica에 지속적으로 복제
Sentinel
→ 비 Cluster 환경에서 Primary 장애 감지와 자동 승격
Cluster
→ Shard별 Primary 장애 감지와 Replica 승격
하지만 어떤 구성을 사용하더라도 다음 사실은 변하지 않는다.
비동기 복제에서는 쓰기 유실 가능성이 존재한다.
Failover 중에는 일시적인 연결 오류가 발생할 수 있다.
Replication은 데이터 오삭제를 막지 못한다.
Persistence 파일도 별도 백업 없이는 완전한 복구 수단이 아니다.
분산 락은 Failover 상황에서 중복 소유자가 발생할 수 있다.
따라서 실무에서는 다음과 같이 계층적으로 대응해야 한다.
1. RDB와 AOF로 프로세스 재시작 복구
2. Replica로 서버 장애 대비
3. Sentinel 또는 Cluster로 자동 Failover
4. 외부 백업으로 오삭제와 디스크 장애 대비
5. 애플리케이션 Timeout과 재연결로 장애 전파 방지
6. DB Constraint와 멱등 처리로 최종 정합성 보장
7. 정기적인 장애 복구 훈련으로 실제 RPO·RTO 검증
Valkey의 장애 복구를 단순히 “Replica가 있으므로 안전하다”라고 이해해서는 안 된다.
어떤 장애에서 자동 복구되는지, 어느 시점의 데이터까지 복구되는지, Failover 과정에서 어떤 요청이 실패하거나 중복될 수 있는지를 정확히 이해해야 운영 가능한 구조를 설계할 수 있다.
'나의 주니어 개발 일기 > Redis' 카테고리의 다른 글
| Valkey로 구현하는 분산 락: 원리부터 성능·커넥션·장애 대응까지 (0) | 2026.07.29 |
|---|---|
| Redis 자주 사용하는 명령어 (0) | 2024.02.19 |
