Kafka Shallow Dive - Replication과 고가용성

Published:

“acks=all이면 안전하다”는 설명을 여러 번 봤지만, 정확히 언제/왜 안전한지는 Replication과 ISR을 알아야 말이 됨. 이번 주는 내구성 파트.

Replication ?

파티션을 여러 브로커에 복사해서 한 대가 죽어도 데이터가 살아남게 하는 것.

orders-0 (replication.factor = 3)

Broker 1 [Leader]    ← Producer/Consumer 요청은 여기로만
   │
   ├── fetch ──▶ Broker 2 [Follower]
   └── fetch ──▶ Broker 3 [Follower]
  • 파티션마다 Leader 1개 + Follower N-1개
  • 쓰기/읽기는 Leader만 담당 (읽기는 3.x부터 follower read도 가능하지만 기본은 leader)
  • Follower는 leader에서 fetch해서 똑같이 기록

Replication은 파티션 단위로 걸린다. Topic의 RF=3이면 모든 파티션이 3copy.

ISR (In-Sync Replicas)

“leader를 충분히 따라잡은” replica 집합. 리더 포함.

Replicas: [1, 2, 3]
ISR:      [1, 2, 3]       ← 다 따라붙은 상태

뒤처진 follower는 ISR에서 제외(shrink) 됨. 기준:

  • replica.lag.time.max.ms (기본 30초) — 이 시간 내 fetch 못 따라잡으면 제외
  • 복구되면 다시 ISR에 합류(expand)

ISR이 중요한 이유: acks=all의 “all”은 replicas 전체가 아니라 ISR 전체다.

acks=all + min.insync.replicas 조합

이 둘을 세트로 보지 않으면 함정이 있음.

시나리오: RF=3, acks=all 만 설정.

정상: ISR=[1,2,3] → leader가 3대 모두에 복제됐다고 확인 후 ack
장애: Broker 2,3 다운 → ISR=[1] → leader 혼자서 "다 됐다" 하고 ack

  이 상태에서 Broker 1까지 죽으면? → 유실

acks=all만으로는 부족. 최소 몇 개 replica가 살아 있어야 write를 받을지 명시해야 함.

# topic-level
min.insync.replicas=2
# producer
acks=all

RF=3, min.insync.replicas=2 조합이면:

  • 정상: 3대 모두 쓰여야 ack
  • 1대 죽어도 쓰기 가능 (ISR=2)
  • 2대 죽으면 쓰기 거부 (NotEnoughReplicasException) → 가용성은 떨어지지만 유실은 안 됨

정석 레시피

목적RFmin.insync.replicasacks
고가용성 + 내구성32all
개발/테스트111
내구성 극한53all

현업 기본은 RF=3, min.ISR=2, acks=all. 1대 장애는 허용, 2대 동시 장애 시엔 쓰기 거부로 유실 방지.

High Watermark

Consumer가 읽을 수 있는 한계선.

Partition orders-0

offsets:  0 1 2 3 4 5 6 7 8 9 10
          └─── replicated ───┘
                             ▲
                       High watermark (=7)

Consumer는 offset 7까지만 읽을 수 있음 (8,9,10은 아직 follower 복제 전)
  • 모든 ISR에 복제된 지점까지가 HW
  • Leader가 죽고 새 leader로 승격되면 HW 이후 데이터는 truncate될 수도 있음
  • acks=all일 때 producer ack 시점이 HW 이상 진행됐음을 보장

Unclean Leader Election

극단적 장애 시 — ISR이 전부 죽고 out-of-sync replica만 남았을 때 — 그걸 leader로 승격할 것이냐.

unclean.leader.election.enable=false   # 기본값 (2.0+)
  • false — leader 없음, 쓰기/읽기 불가. 데이터는 안전, 가용성 포기
  • true — out-of-sync replica를 leader로. 가용성 회복, 뒤처졌던 부분은 영구 유실

대부분 false가 맞음. “데이터 날아갔는데 동작 중”보다 “잠깐 멈춤”이 낫다. 메트릭 같은 유실 허용 topic만 개별로 true.

Replication은 어떻게 작동하나

Follower가 leader에게 주기적으로 FetchRequest 보냄. HTTP 풀링과 비슷.

Follower 2 → Fetch(offset=100) → Leader
Leader     → Records[100..150] → Follower 2
Follower 2 → append to local log, update LEO
Follower 2 → Fetch(offset=150) → Leader
  • 풀 기반이라 leader 부담이 덜함
  • fetch 간격/크기는 replica.fetch.* 설정으로 조정
  • follower의 LEO가 leader의 HW를 따라잡으면 ISR

Leader 죽으면?

  1. Controller가 감지
  2. ISR 중에서 새 leader 선출 (보통 가장 오래 ISR에 있던 replica)
  3. 메타데이터 갱신 → producer/consumer는 새 leader로 재접속
  4. KRaft에선 이 과정이 수백 ms 수준

Rack Awareness

한 AZ(가용 영역) 전체가 죽어도 살아남으려면 replica를 물리적으로 분산해야 함.

broker.rack=us-east-1a   # 각 브로커별로 설정

Kafka가 replica 배치 시 같은 rack에 몰리지 않게 분산. AZ 한 개 날아가도 다른 AZ에서 서비스 계속됨.

체크리스트

내구성을 진지하게 볼 topic이라면:

  • replication.factor >= 3
  • min.insync.replicas = RF - 1 (보통 2)
  • producer acks=all
  • producer enable.idempotence=true
  • unclean.leader.election.enable=false
  • rack awareness 설정 (클라우드 멀티 AZ)
  • ISR shrink 발생을 모니터링 (IsrShrinksPerSec)

ISR 변동이 잦을 때

IsrShrinksPerSec가 계속 터지면:

  • 네트워크/디스크 병목
  • GC pause가 길어서 heartbeat 놓침
  • Follower가 leader의 쓰기 속도를 못 따라감 → leader 쪽에 max.bytes 너무 크거나, follower fetch 설정이 작거나
  • replica.lag.time.max.ms를 살짝 늘려서 튐 완화 (근본 대책은 아님)

정리

RF만 크게 해둔다고 안전한 게 아니라, min.insync.replicas + acks=all + unclean.leader.election=false 의 3종 세트가 맞아야 진짜 유실 방지. ISR이라는 “동기 집합” 개념을 중심에 놓고 보면 설정들이 다 연결된다. 예전에 그냥 acks=all만 넣고 안심했던 게 반쪽짜리였다.

다음 주는 한 단계 더 — Exactly-once와 Transactions.