[Kafka DR ④] 페일오버 & 페일백 런북 — 실제로 전환하기
MirrorMaker 2 기반 Kafka DR 환경에서 재해 발생 시 DR 클러스터로 전환(페일오버)하고, 복구 후 원래 클러스터로 되돌리는(페일백) 전 과정을 순서대로 정리한 운영 런북입니다. 토픽 이름 변경 함정, 오프셋 변환, 중복 재처리 화해까지 다룹니다.
새벽 3시, 1차 데이터센터가 통째로 사라졌습니다. 모니터링은 빨간색으로 도배됐고, 온콜 폰이 울립니다. 이때 여러분이 펼쳐야 할 것은 영웅적인 즉흥 대응이 아니라 이미 쓰여 있고 손에 익은 런북입니다. 지금까지 ①복제 아키텍처, ②MirrorMaker 2 구성, ③오프셋 변환을 다뤘다면, 이번 4편은 그 모든 준비를 실제 버튼을 누르는 절차로 바꿉니다. 페일오버는 "할 수 있다"가 아니라 "순서대로 한다"의 문제입니다.
이 글에서 배우는 것
- 페일오버를 시작하기 전에 반드시 충족돼야 하는 전제 조건
- 재해 선언부터 클라이언트 재지정까지, 순서가 보장된 페일오버 절차
- 복구 후 원래 클러스터로 안전하게 되돌리는 페일백 절차
DefaultReplicationPolicy의 토픽 이름 변경(prefix) 함정과IdentityReplicationPolicy로 그림이 어떻게 바뀌는지- 중복·재처리 화해 문제와 멱등 컨슈머가 왜 협상 불가능한 요구사항인지
- 온콜 엔지니어가 새벽에 그대로 따라 할 수 있는 체크리스트와 RTO 기대치
1. 전제 조건 — 런북을 펼치기 전에
페일오버 런북은 진공 상태에서 작동하지 않습니다. 아래 전제가 사전에 충족돼 있어야 새벽 3시의 절차가 흔들리지 않습니다. 하나라도 빠지면 페일오버는 "전환"이 아니라 "도박"이 됩니다.
1.1 MM2 복제가 건강한 상태인가
DR 클러스터는 MirrorMaker 2가 1차 → DR로 데이터를 실시간 복제하고 있어야 의미가 있습니다. 페일오버 직전이 아니라 평상시에 복제 지연(lag)이 통제되고 있어야 합니다.
# MM2 소스 커넥터의 복제 지연 확인 (JMX 메트릭)
# kafka.connect.mirror:type=MirrorSourceConnector,...
# replication-latency-ms : 레코드가 source에 기록된 뒤 target에 복제될 때까지의 시간
# record-age-ms : target에 막 도착한 레코드의 나이
# 컨슈머 그룹 기준으로 본 end-to-end lag (DR 측에서)
kafka-consumer-groups.sh --bootstrap-server dr-broker:9092 \
--describe --group __replication-health-probe| 신호 | 건강함 | 경고 | 위험 |
|---|---|---|---|
replication-latency-ms (p99) | < 수 초 | 수십 초 | 분 단위 이상 지속 |
| MM2 커넥터 상태 | RUNNING | RUNNING(태스크 일부 FAILED) | FAILED |
| DR 토픽 최신 오프셋 갱신 | 지속적으로 증가 | 정체 | 멈춤 |
복제가 이미 밀려 있는 상태에서 1차가 죽으면, 그 밀린 만큼이 그대로 데이터 손실(RPO) 이 됩니다. 페일오버 절차가 아니라 평상시 모니터링이 RPO를 결정한다는 점을 기억하세요.
1.2 오프셋 변환이 동작하는가 (3편 복습)
페일오버의 핵심은 "컨슈머가 DR 클러스터의 올바른 위치에서 재개"하는 것입니다. MM2는 source와 target의 오프셋이 절대 같지 않기 때문에(파티션마다 시작 오프셋·압축·리텐션이 다름), 오프셋을 변환해야 합니다.
sync.group.offsets.enabled = true로 MM2가__consumer_offsets를 주기적으로 변환·체크포인트하도록 설정돼 있어야 합니다.- 또는 페일오버 시점에
RemoteClusterUtils.translateOffsets()로 source 오프셋을 target 오프셋으로 변환합니다.
# MM2 connect-mirror-maker.properties (3편에서 설정한 값 — 여기서 재확인)
primary->dr.emit.checkpoints.enabled = true
primary->dr.sync.group.offsets.enabled = true
primary->dr.sync.group.offsets.interval.seconds = 10
primary->dr.refresh.groups.interval.seconds = 10이 설정이 없으면 페일오버 후 컨슈머는 auto.offset.reset 정책에 따라 처음부터(earliest) 또는 끝부터(latest) 읽게 됩니다. 전자는 대량 재처리, 후자는 데이터 누락입니다. 둘 다 재앙입니다.
1.3 클라이언트가 간접 지정 계층을 쓰는가
이것이 가장 자주 놓치는 전제입니다. 클라이언트의 bootstrap.servers가 하드코딩된 1차 브로커 주소라면, 페일오버는 전체 애플리케이션 재배포를 의미합니다. 새벽 3시에 할 일이 아니죠.
대신 재지정 가능한 간접 계층을 통해 부트스트랩 주소를 주입해야 합니다.
| 방식 | 전환 방법 | 전파 지연 | 주의점 |
|---|---|---|---|
| DNS CNAME | 레코드 변경 | TTL에 좌우 (TTL을 짧게, 예: 30s) | 클라이언트 DNS 캐싱, JVM networkaddress.cache.ttl |
| Config Service | 키 값 변경 + 클라이언트 리스타트/리프레시 | 즉시~수 초 | 클라이언트가 변경을 감지·재연결하도록 구현돼야 함 |
| L4/L7 로드밸런서 | 백엔드 풀 전환 | 즉시 | 헬스체크 오탐 주의 |
핵심은 "클라이언트 코드를 건드리지 않고 부트스트랩 주소만 바꿀 수 있어야 한다" 입니다.
1.4 컨슈머가 멱등(idempotent)한가
MM2 기반 페일오버는 본질적으로 at-least-once입니다. 오프셋 변환은 근사치이고, 복제 경계에서 일부 메시지는 반드시 두 번 처리될 수 있습니다. 따라서 컨슈머는 같은 메시지를 두 번 받아도 결과가 한 번 처리한 것과 같아야 합니다.
// 멱등 처리: 메시지 키 + 시퀀스로 dedup
String dedupKey = record.key() + ":" + record.headers().lastHeader("event-id");
if (processedStore.putIfAbsent(dedupKey, true) == null) {
process(record); // 최초 1회만 실제 처리
}
// 이미 본 키면 조용히 스킵 (재처리 안전)멱등성은 페일오버를 위한 선택이 아니라 요구사항입니다. 멱등하지 않은 컨슈머로 페일오버하면 중복 주문, 이중 청구, 중복 알림이 발생합니다.
전제 조건 게이트: 위 네 가지(① MM2 건강, ② 오프셋 변환, ③ 간접 계층, ④ 멱등 컨슈머)가 모두 충족되지 않았다면, 지금 이 런북을 따르기 전에 그것부터 고치세요. 런북은 준비를 대체하지 못합니다.
2. 페일오버 절차 (순서 보장)
페일오버는 순서가 곧 정확성입니다. 단계를 건너뛰거나 순서를 바꾸면 데이터 손실이나 중복이 늘어납니다. 아래 절차를 그대로 따르세요.
2.1 (a) 재해 감지 및 선언
먼저 "이것이 정말 재해인가"를 판단합니다. 일시적 네트워크 글리치에 페일오버를 발동하면 split-brain과 불필요한 데이터 화해 비용을 치릅니다.
| 판단 기준 | 페일오버 대상 | 대기 |
|---|---|---|
| 1차 클러스터 전체 도달 불가 (DC 다운) | O | |
| 다수 브로커 동시 손실 + ISR 붕괴 | O | |
| 단일 브로커 장애 | O (Kafka 자체 복제로 처리) | |
| 일시적 네트워크 파티션 (수십 초) | O (먼저 회복 대기) |
재해 선언은 사람이 명시적으로 합니다. 자동 페일오버는 split-brain 위험이 크므로, 사람의 "선언" 게이트를 둡니다. 선언 즉시 인시던트 채널에 시각과 결정자를 기록하세요.
2.2 (b) 1차로 향하는 프로듀서 중단 (가능하다면)
1차가 부분적으로 도달 가능하다면(예: 일부 브로커 생존, 네트워크 일부 회복), 먼저 1차로 보내는 프로듀서를 멈춥니다. 그래야 페일오버 시점 이후 1차에만 쓰이고 DR엔 복제 안 된 메시지가 늘어나는 것을 막습니다.
# 프로듀서를 멈추는 방법은 환경마다 다름:
# - 간접 계층(config service)에서 "produce: paused" 플래그
# - 또는 프로듀서 앱을 일시 스케일다운 (k8s: replicas=0)
kubectl scale deployment order-producer --replicas=0 -n prod1차가 완전히 도달 불가라면 이 단계는 건너뛰고, 진행 중이던 RPO를 수용합니다. 즉, 1차에만 쓰이고 DR로 복제되지 못한 메시지는 손실로 받아들입니다. 이 손실량을 인시던트 로그에 추정치로 남기세요.
2.3 (c) 복제 따라잡기 확인 (또는 RPO 수용)
1차가 부분적으로라도 살아 있고 MM2가 동작 중이라면, DR이 1차의 마지막 오프셋까지 최대한 따라잡도록 잠시 기다립니다.
# DR 측에서 본 복제 진행 — primary.<topic> 의 최신 오프셋이
# 더 이상 증가하지 않을 때까지(= 따라잡음) 관찰
watch -n2 'kafka-run-class.sh kafka.tools.GetOffsetShell \
--broker-list dr-broker:9092 \
--topic primary.orders --time -1'- MM2 lag ≈ 0 이 되면 데이터 손실 없이 전환 가능 — 이상적.
- 1차가 완전히 죽어 더 따라잡을 수 없다면, 현재 RPO를 수용하고 다음 단계로 진행합니다. 무한정 기다리면 RTO만 늘어납니다. "얼마나 기다릴지"의 상한(예: 60초)을 런북에 미리 정해두세요.
2.4 (d) 프로듀서를 DR로 재지정
이제 프로듀서의 부트스트랩 주소를 DR로 바꿉니다. 여기서 토픽 이름 함정을 주의하세요(상세는 4장).
DefaultReplicationPolicy를 쓰면 DR 클러스터에는 복제본이 primary.orders 라는 이름으로 존재합니다. 하지만 페일오버 후 프로듀서는 DR의 원본 토픽인 orders(prefix 없음)에 써야 합니다. primary.orders는 MM2가 관리하는 복제 토픽이지, 새 쓰기를 받을 곳이 아닙니다.
# 간접 계층(config service) 값 변경
- bootstrap.servers: primary-broker:9092
+ bootstrap.servers: dr-broker:9092
# 토픽 이름은 그대로 'orders' — DR에서 새 쓰기는 prefix 없는 원본 토픽으로
topic: orders# 프로듀서를 다시 가동
kubectl scale deployment order-producer --replicas=4 -n prod2.5 (e) 컨슈머를 DR로 재지정 — 변환된 오프셋에서 재개
컨슈머는 변환된 오프셋에서 재개해야 합니다. 방법은 두 가지입니다.
방법 1 — sync.group.offsets.enabled로 자동 동기화된 오프셋 사용
MM2가 평상시에 __consumer_offsets를 DR로 변환·동기화해 뒀다면, 컨슈머는 DR 클러스터에서 같은 그룹 ID로 그냥 다시 붙기만 하면 됩니다. DR의 __consumer_offsets에 이미 변환된 위치가 있습니다.
# 컨슈머: 그룹 ID 동일 유지, 부트스트랩만 DR로
group.id = order-processor
bootstrap.servers = dr-broker:9092
# auto.offset.reset 은 "동기화된 오프셋이 없을 때만" 발동되므로
# 정상 동기화 상태에서는 변환된 위치에서 재개됨
auto.offset.reset = latest방법 2 — RemoteClusterUtils로 명시적 변환
자동 동기화를 켜지 않았거나, 정확한 변환 위치를 검증하고 싶다면 페일오버 시점에 직접 변환합니다.
// source(primary) 오프셋 → target(dr) 오프셋 변환
Map<TopicPartition, OffsetAndMetadata> translated =
RemoteClusterUtils.translateOffsets(
mm2Props,
"primary", // source 클러스터 alias
"order-processor", // consumer group
Duration.ofSeconds(20));
// 변환된 오프셋을 DR 컨슈머 그룹에 커밋한 뒤 재개
try (Admin admin = Admin.create(drProps)) {
admin.alterConsumerGroupOffsets("order-processor", translated).all().get();
}주의: 변환된 오프셋은 "이 위치 이후는 확실히 안 봤다"를 보장할 뿐, "이 위치까지 정확히 봤다"를 보장하지 않습니다. 그래서 일부 재처리가 생기고, 그래서 1.4의 멱등성이 필요합니다.
2.6 (f) 검증
전환했다고 끝이 아닙니다. 검증 없는 페일오버는 절반만 끝난 것입니다.
# 1) 컨슈머가 진행 중인가 — lag이 줄고 있는가
kafka-consumer-groups.sh --bootstrap-server dr-broker:9092 \
--describe --group order-processor
# CURRENT-OFFSET 가 증가, LAG 가 감소해야 정상
# 2) 대량 재처리가 일어나고 있지 않은가
# (LAG 가 토픽 전체 크기로 튀었다면 오프셋 변환 실패 → earliest 재처리 의심)검증 체크:
- 컨슈머 그룹의
CURRENT-OFFSET이 증가하고LAG이 수렴한다. -
LAG이 토픽 전체 크기로 폭증하지 않았다(= earliest 재처리 아님). - 프로듀서가 DR의 원본 토픽(
orders)에 정상 쓰기 중이다. - 비즈니스 검증 통과: 핵심 지표(주문 수, 결제 성공률 등)가 정상 범위.
- 다운스트림(DB, 검색 색인 등)에 데이터가 흐른다.
여기까지 통과하면 페일오버 완료입니다. 인시던트 채널에 "DR 활성, 페일오버 완료, RPO 추정 N건/초"를 기록하세요.
3. 페일백 절차 — 원래 클러스터로 되돌리기
1차 데이터센터가 복구됐습니다. 하지만 서두르지 마세요. 페일백은 페일오버보다 까다롭습니다. 페일오버는 "한쪽이 죽어서" 하는 것이지만, 페일백은 "양쪽 다 살아 있는데 의도적으로 옮기는" 것이라 split-brain과 중복 화해를 더 신중히 다뤄야 합니다.
3.1 역방향 복제 설정 (DR → Primary)
페일오버 동안 새 데이터는 모두 DR에 쌓였습니다. 1차는 이 데이터를 모릅니다. 따라서 DR → 1차 방향의 복제를 먼저 켜서 1차가 따라잡게 합니다.
# MM2: 역방향 흐름 활성화 (active-passive를 잠시 양방향으로)
clusters = primary, dr
# 기존 정방향은 페일오버 중이므로 중단/일시정지하고, 역방향을 켠다
dr->primary.enabled = true
dr->primary.emit.checkpoints.enabled = true
dr->primary.sync.group.offsets.enabled = true
# 토픽 화이트리스트는 원본 토픽만 — 'primary.' 접두 토픽을 되돌려 복제하지 않도록 제외
dr->primary.topics = orders, payments, .*
dr->primary.topics.exclude = .*\.internal, primary\..*, .*\.replica순환 복제 주의: 정방향(primary→dr)과 역방향(dr→primary)을 동시에 켜면,
DefaultReplicationPolicy의 prefix 덕에 무한 루프는 막히지만(예:primary.orders는 다시 복제되지 않음), 설정을 잘못하면 토픽이 핑퐁될 수 있습니다. 정방향을 확실히 멈춘 뒤 역방향을 켜는 것이 안전합니다.
3.2 1차가 따라잡기를 기다림
# 1차 측에서 본 역복제 진행 — dr.<topic> 최신 오프셋이 DR 원본을 따라잡을 때까지
watch -n2 'kafka-run-class.sh kafka.tools.GetOffsetShell \
--broker-list primary-broker:9092 \
--topic dr.orders --time -1'역방향 lag이 충분히 작아질 때까지 기다립니다. 이건 RTO에 쫓기지 않는 계획된 작업이므로, lag ≈ 0이 될 때까지 충분히 기다릴 수 있습니다.
3.3 컷오버 윈도우 잡기
페일백은 계획된 유지보수 윈도우에 수행합니다. 트래픽이 적은 시간대를 골라, 짧은 쓰기 정지(quiesce)를 허용받습니다.
3.4 DR 프로듀서 중단 → 클라이언트를 1차로 재지정
# 1) DR로 향하는 프로듀서를 멈춰 쓰기를 정지 (quiesce)
kubectl scale deployment order-producer --replicas=0 -n prod
# 2) 역복제가 마지막 메시지까지 1차로 흘렀는지 최종 확인 (lag == 0)
# 3) 간접 계층을 다시 1차로
# config service: bootstrap.servers = primary-broker:9092
# 또는 DNS CNAME: kafka.internal → primary
# 4) 컨슈머도 1차로 — 역방향 sync.group.offsets 로 변환된 오프셋에서 재개
# 5) 프로듀서 재가동 (이제 1차의 원본 토픽 'orders'로)
kubectl scale deployment order-producer --replicas=4 -n prod순서가 페일오버와 거울 대칭입니다: 프로듀서 중단 → 마지막 복제 확인 → 컨슈머/프로듀서 재지정 → 재가동.
3.5 화해(reconciliation) — 중복·재처리 문제
페일백의 가장 어려운 부분입니다. 페일오버 경계와 페일백 경계, 두 번의 경계를 지나면서 일부 메시지는 양쪽에서 처리됐을 수 있습니다.
왜 멱등/dedup 컨슈머가 중요한가: 두 경계 모두에서 오프셋 변환은 근사치이므로, "정확히 한 번"은 보장되지 않습니다. 멱등 컨슈머(1.4)는 이 중복을 자동으로 흡수합니다. 비멱등 컨슈머라면 페일백 후 다음을 수동으로 해야 합니다.
| 화해 항목 | 방법 |
|---|---|
| 중복 처리 탐지 | 멱등 키(이벤트 ID) 기준으로 다운스트림에서 중복 행 검출 |
| 이중 부수효과 정정 | 이중 청구·이중 알림을 보상 트랜잭션으로 취소 |
| 데이터 일관성 검증 | 페일오버 직전 ~ 페일백 직후 구간의 집계값을 source-of-truth와 대조 |
| 오프셋 갭 점검 | 변환 경계에서 컨슈머 그룹 오프셋이 역행/도약하지 않았는지 |
화해 비용이 곧 페일백의 진짜 비용입니다. 멱등성에 투자하면 이 비용이 0에 수렴합니다.
4. 토픽 이름 변경 함정 — DefaultReplicationPolicy vs IdentityReplicationPolicy
이 한 가지가 active-passive 페일오버/페일백에서 가장 많은 사고를 냅니다. 별도 장으로 떼어 다룹니다.
4.1 DefaultReplicationPolicy의 prefix
MM2의 기본 정책은 복제 토픽 앞에 source 클러스터 별칭을 prefix로 붙입니다. 1차의 orders는 DR에서 primary.orders가 됩니다. 이 prefix는 순환 복제를 막고 어느 클러스터에서 온 데이터인지 추적하기 위한 장치입니다.
함정은 명확합니다. 페일오버 후 프로듀서가 어디에 써야 하는가?
| 토픽 | 정체 | 페일오버 후 새 쓰기 |
|---|---|---|
primary.orders (DR) | MM2가 관리하는 1차의 복제본 | ❌ 여기 쓰면 안 됨 |
orders (DR) | DR의 원본 토픽 | ✅ 여기 써야 함 |
그런데 컨슈머는 페일오버 직후 과거 데이터를 primary.orders에서 읽어야 할 수도 있습니다(페일오버 전 1차 데이터). 즉 같은 논리적 스트림이 DR에서 두 토픽(primary.orders + orders)으로 쪼개지는 혼란이 생깁니다. 이걸 깔끔하게 처리하려면 컨슈머가 두 토픽을 모두 구독하거나, 정규식 구독(.*orders)으로 통합해야 합니다.
페일백에서는 이 혼란이 거울처럼 반복됩니다: DR의 orders가 1차에서 dr.orders가 되고, 1차의 원본 orders와 다시 갈라집니다.
4.2 IdentityReplicationPolicy로 그림이 어떻게 바뀌나
IdentityReplicationPolicy(구 LegacyReplicationPolicy)는 prefix를 붙이지 않습니다. 1차의 orders는 DR에서도 그냥 orders입니다.
# prefix 없는 동일 이름 복제
replication.policy.class = org.apache.kafka.connect.mirror.IdentityReplicationPolicy| 항목 | DefaultReplicationPolicy | IdentityReplicationPolicy |
|---|---|---|
| DR의 토픽 이름 | primary.orders | orders |
| 페일오버 후 클라이언트 변경 | 토픽 이름도 신경 써야(prefix 제거) | 토픽 이름 그대로 — 부트스트랩만 변경 |
| 순환 복제 방지 | prefix로 자연 차단 | 위험 — 양방향 시 별도 방지 필요 |
| 출처 추적 | 토픽 이름으로 명확 | 불명확(헤더 등 별도 수단) |
| 적합한 시나리오 | 양방향/팬인(aggregation) | 단방향 active-passive DR |
active-passive DR의 단순한 페일오버/페일백에서는 IdentityReplicationPolicy가 운영을 크게 단순화합니다. 토픽 이름이 양쪽에서 같으므로, 클라이언트는 부트스트랩 주소만 바꾸면 되고 토픽 이름 분기 로직이 사라집니다. 다만 양방향 복제(페일백 시 역방향)를 켤 때 순환 복제를 막을 책임이 운영자에게 넘어옵니다. topics 화이트리스트/exclude와 source.cluster.alias 헤더 기반 필터로 차단해야 합니다.
선택 가이드: 단방향 DR이 주목적이고 토픽 이름 함정을 피하고 싶다면
IdentityReplicationPolicy. 여러 클러스터를 한곳으로 모으거나(aggregation) 출처 추적이 중요하면DefaultReplicationPolicy. 하나를 골랐으면 전 클러스터·전 단계에서 일관되게 쓰세요 — 섞으면 페일백에서 토픽 이름이 어긋납니다.
5. 전체 흐름 다이어그램 — 페일오버 그리고 페일백
6. 온콜 체크리스트 & RTO 기대치
6.1 페일오버 체크리스트 (새벽 3시용)
그대로 따라 하세요. 위에서 아래로, 건너뛰지 않습니다.
- 재해 선언: 1차 전체 도달 불가 확인, 인시던트 채널에 시각·결정자 기록.
- MM2 상태 확인: 복제 lag 스냅샷 캡처(RPO 추정에 사용).
- 1차 프로듀서 중단: 1차가 부분 도달 가능하면 quiesce. 아니면 스킵하고 RPO 수용.
- 복제 따라잡기: lag→0 대기(상한 60초). 1차 완전 다운이면 현재 RPO 수용.
- 프로듀서 재지정: 간접 계층 → DR. 토픽은 prefix 없는 원본(
orders). - 컨슈머 재지정: 간접 계층 → DR. 변환된 오프셋에서 재개(
sync.group.offsets또는RemoteClusterUtils). - 검증: 컨슈머 lag 수렴, 대량 재처리 없음, 프로듀서 정상 쓰기, 비즈니스 지표 정상.
- 공지: "DR 활성, 페일오버 완료, RPO 추정치" 인시던트 채널 기록.
6.2 페일백 체크리스트 (계획된 윈도우용)
- 역방향 복제 설정:
dr->primary활성, 순환 복제 차단 확인. - 1차 따라잡기 대기: 역복제 lag≈0.
- 컷오버 윈도우 진입: 저트래픽 시간대, 관계자 공지.
- DR 프로듀서 중단: quiesce, 마지막 메시지까지 역복제 확인(lag==0).
- 클라이언트 재지정: 간접 계층 → Primary, 컨슈머는 변환된 오프셋에서 재개.
- 프로듀서 재가동: 1차 원본 토픽으로.
- 화해: 중복/재처리 정정(멱등이면 자동 흡수, 아니면 수동).
- 정방향 복제 복원:
primary->dr재활성, active-passive 정상화.
6.3 RTO 기대치
RTO(복구 시간 목표)는 "어디에 투자했는가"의 함수입니다.
| 준비 수준 | 페일오버 RTO(대략) | 결정 요인 |
|---|---|---|
| 간접 계층 없음(하드코딩) | 수십 분~시간 | 전체 앱 재배포 시간 |
| DNS CNAME (TTL 30s) | 수 분 | DNS 전파 + 클라이언트 캐시 만료 |
| Config service (즉시 리프레시) | 1~수 분 | 변경 전파 + 컨슈머 재연결 |
| + 오프셋 자동 동기화 | < 1~2분 | 변환 오프셋이 이미 DR에 존재 |
핵심: RTO는 페일오버 절차의 속도가 아니라 사전 준비(간접 계층·오프셋 동기화)가 결정합니다. RPO는 평상시 복제 lag이 결정하고, RTO는 전환 자동화가 결정합니다. 둘 다 "재해가 나기 전에" 정해집니다.
마치며
페일오버는 영웅담이 아니라 절차입니다. 이번 편에서 다룬 것을 요약하면:
- 전제가 90%다: MM2 건강, 오프셋 변환, 재지정 가능한 간접 계층, 멱등 컨슈머 — 이 넷이 없으면 런북도 소용없습니다.
- 순서가 정확성이다: 재해 선언 → (가능하면)프로듀서 중단 → 복제 확인/RPO 수용 → 프로듀서 재지정 → 컨슈머 재지정(변환 오프셋) → 검증.
- 페일오버는 at-least-once다: 멱등성은 선택이 아니라 요구사항. 변환 오프셋은 근사치이고, 경계에서 재처리가 반드시 생깁니다.
- 페일백이 더 어렵다: 두 번의 경계를 지나며 쌓인 중복을 화해해야 하고, 멱등 컨슈머라야 이 비용이 0에 수렴합니다.
- 토픽 이름 함정:
DefaultReplicationPolicy의 prefix는 페일오버 후 "어디에 쓰고 어디서 읽나"를 헷갈리게 합니다. 단방향 active-passive라면IdentityReplicationPolicy가 운영을 단순화합니다. - RTO/RPO는 사전 결정: RPO는 평상시 lag, RTO는 전환 자동화가 결정합니다. 재해 당일엔 바꿀 수 없습니다.
그리고 가장 중요한 진실 하나. 리허설하지 않은 런북은 반드시 실패합니다. 위 절차가 아무리 정교해도, 실제로 새벽에 처음 해보는 페일오버는 빠진 권한, 캐시된 DNS, 잊힌 토픽 하나에서 무너집니다. 다음 5편에서는 이 런북을 정기적으로 테스트하고 검증하는 방법 — 게임데이, 카오스 주입, 복제·오프셋 변환 정확성 검증, RTO/RPO 측정 — 을 다룹니다. 런북은 쓰는 순간이 아니라 연습하는 순간 살아 있습니다.
참고 자료
- Apache Kafka — Geo-Replication (Cross-Cluster Data Mirroring): https://kafka.apache.org/documentation/#georeplication
- KIP-382: MirrorMaker 2.0: https://cwiki.apache.org/confluence/display/KAFKA/KIP-382%3A+MirrorMaker+2.0
- Confluent — Disaster Recovery for Multi-Datacenter Apache Kafka Deployments: https://docs.confluent.io/platform/current/multi-dc-deployments/index.html
- Apache Kafka —
RemoteClusterUtils/MirrorClient(offset translation)
— Data Dynamics 엔지니어링 팀