07. 신뢰성 있는 데이터 전달

Last updated - 2025년 07월 03일 Edit Source

시스템에 있어 신뢰성은 매우 중요한 속성 중 하나이다. 웹사이트의 클릭 추적, 신용카드 결제 등 카프카를 통한 신뢰성 있는 데이터 전달에 많은 활용 사례가 있다. 최고의 신뢰성 / 처리속도와 단순성을 유연하게 처리하는 카프카를 알아보자



# 신뢰성 보장


보장 (guarantee) : 서로 다른 상황에서도 시스템이 지킬 것이라 보장되는 행동

  • e.g. 관계형 데이터베이스
    • ACID를 통해 신뢰성 보장
    • 어떤 DB가 ACID를 준수한다고 하면, 트랜잭션 처리 관련하여 어떠한 행동을 보장한다

카프카가 보장하는 것

  • 파티션 안의 메세지들 간에 순서 보장 (메세지 A 다음 B가 쓰여졌다면, B의 오프셋 > A의 오프셋 보장)
  • 클라이언트가 쓴 메세지는 모든 인-싱크 레플리카의 파티션에 쓰여진 뒤에야 커밋된 것으로 간주
    • 프로듀서는 아래의 옵션 선택 가능
      • 메세지가 완전히 커밋된 다음 응답이 올지
      • 파티션 리더에게 쓰여진 다음 응답이 올지
      • 네트워크로 전송된 다음 바로 응답이 올지
  • 커밋된 메세지들은 최소 1개의 작동 가능한 레플리카가 남아있으면 유실되지 않음
  • 컨슈머는 커밋된 메세지만 읽을 수 있음

카프카는 트레이드오프를 고려하여 어느 정도의 신뢰성이 필요한지 결정할 수 있도록 개발됨

  • 신뢰성과 일관성 우선
  • 가용성
  • 높은 처리량 (high throughput)
  • 낮은 지연 (low latency)
  • 하드웨어 비용



# 복제


카프카의 신뢰성 보장 핵심

  • 파티션별로 다수의 레플리카를 유지하는 특성
  • 복제 메커니즘

# 메커니즘


복제 메커니즘 : 하나의 메세지를 여러 개의 레플리카에 씀으로써 카프카는 크래시가 나더라도 메세지의 지속성을 유지

6장 리마인드

  • 토픽 > 파티션 > 메세지
  • 하나의 파티션은 하나의 디스크에 저장
  • 파티션에 저장된 이벤트(메세지)들의 순서 보장
  • 파티션은 온라인 상태(사용 가능 🟢) / 오프라인 상태(사용 가능❌)
  • 각 파티션은 다수의 레플리카 가질 수 있고, 그중 하나가 리더가 됨
  • 모든 이벤트들은 리더 레플리카에 write (대체로)
  • 팔로워 레플리카들은 리더와 동기화 맞추면서 최신 이벤트를 재시간에 복사해오면 됨
  • 리더가 작동 불능 상태가 되면, 인-싱크 레플리카 중 하나가 새로운 리더로 선출

인-싱크 레플리카(ISR) 상태로 간주되는 기준

  • 파티션의 리더 레플리카
  • 아래의 조건을 만족하는 팔로워 레플리카
    • 주키퍼와의 활성 세션 존재 (최근 6초 내에 주키퍼로 하트비트 전송한적 있는지)
    • 최근 10초 내에 리더로부터 메세지 읽어온적 있는지
    • 최근 10초 내에 리더로부터 읽어온 메세지들이 가장 최근 메세지인지

최근 10초 내에 랙이 없었던 적이 한 번은 있어야한다의 의미

  • 랙 (lag) : 팔로워가 리더로부터 데이터를 얼마나 지연되게 받고 있는지에 대한 지표
  • e.g. 리더에 쓰여진 최신 메세지의 offset이 105, 팔로워가 읽어온 메세지의 offset이 102이면, lag은 3
  • 즉, 팔로워 레플리카가최근 10초 동안 리더 레플리카의 업데이트 내역을 따라가지 못한다는 의미
  • 랙이 있다면 이미 뒤쳐졌다는 의미이니까

아웃-오브-싱크 레플리카

  • 인-싱크 레플리카가 아니면 아웃-오브-싱크 레플리카
    • 레플리카와 주키퍼 사이의 연결이 끊어짐
    • 새 메세지 읽어오는 것을 주단
    • 최근 10초 동안의 업데이트 내역을 따라가지 못함
  • 카프카 예전 버전에서는 인 - 아웃 싱크 상태를 빠르게 교차하는 것이 매우 흔한 일이었음
    • 최대 요청 크기가 커서 JVM 힙을 많이 잡고 있는 경우, 튜닝을 안해주면 브로커의 GC 시간이 오래 걸리면서 순간적으로 주키퍼와의 연결이 끊어질 수 있었음
    • 큰 메세지를 사용할 경우 튜닝이 필요

결국은 트레이드오프 !

  • 동기화가 살짝 늦은 인-싱크 레플리카
    • 프로듀서와 컨슈머를 느리게 만들 수 있음
    • 메세지가 커밋되기 전, 모든 인-싱크 레플리카가 해당 메세지를 받을 때까지 프로듀서와 컨슈머가 기다려야함
  • 아웃-오브-싱크 레플리카
    • 성능에 영향 없음
      • 더이상 이 레플리카가 메세지를 받을 때까지 기다릴 필요 없으니까
  • 인-싱크 레플리카의 수 ↓ + 아웃-오브-싱크 레플리카의 수 ↑
    • 파티션의 실질적인 복제 팩터가 줄어들면서 중단 시간 길어짐
    • 데이터 유실 위험성 높아짐



# 브로커 설정

메세지 저장 신뢰성 관련된 브로커의 설정 매개변수 3개 존재

  • 브로커 단위로 제어 가능 (시스템 내부 모든 토픽 단위로 제어 가능)
  • 특정 토픽 단위로 제어 가능

즉, 신뢰성이 필요한 토픽, 필요하지 않은 토픽을 같은 카프카 클러스터에 저장할 수 있음



# 복제 팩터

  • replication.factor : 토픽 단위 설정
  • default.replication.factor : 자동으로 생성되는 토픽들에 적용되는 브로커 단위 설정

복제 팩터 (Replication Factor) : 토픽의 파티션의 복제본을 몇 개를 생성할 지

  • e.g. 복제 팩터가 3이다 → 각 브로커가 3대의 서로 다른 브로커에 3개 복제된다는 것
  • 복제 팩터가 N이면 N - 1개의 브로커가 중단되더라도 토픽의 데이터를 읽거나 쓰기 가능 🟢
  • 복제 팩터가 클수록
    • 가용성과 신뢰성 ↑
    • 장애가 발생할 가능성 ↓
    • 필요한 브로커 수 ↑
    • 필요한 디스크 공간 ↑
      • e.g. N개의 브로커가 필요하다 → N개의 복사본 저장해야한다 → N배의 디스크 공간 필요하다
  • 가용성 - 하드웨어 사용량 사이 트레이드오프 존재

Q. 토픽에 몇 개의 레플리카가 적절할까? 아래의 핵심 사항을 고려

가용성 (availability)

  • 레플리카가 1개인 파티션 → 브로커를 재시작하기만 해도 작동 불능
  • 레플리카 수 많을수록 → 가용성 ↑

지속성 (durability)

  • 각 레플리카는 파티션 내부의 모든 데이터(메세지)에 대한 복사본
  • 레플리카가 1개이고 디스크가 사용 불가능한 상태 → 해당 파티션의 모든 데이터 유실됨

처리량 (throughput)

  • 클러스터의 크기와 용량 산정시 고려 필요
  • 레플리카 수 많을수록 → 브로커 간 트래픽 ↑
  • 복제 트래픽 : 데이터 복제 과정에서 발생하는 네트워크 트래픽
    • e.g. 특정 파티션에 10Mbps 속도로 쓰는 상태
      • 레플리카 1개 → 복제 트래픽 X
      • 레플리카 2개 → 복제 트래픽 10Mbps
      • 레플리카 3개 → 복제 트래픽 20Mbps

종단 지연 (end-to-end delay, OWD : one-way delay)

  • 네트워크의 출발지에서 목적지까지 도달하는데 걸리는 시간
    • RTT(round-trip time)는 왕복 시간을 의미
    • RTT와는 다르게 단방향으로 측정
  • 쓰여진 메세지를 컨슈머가 읽으려면 모든 인-싱크 레플리카에 복제되어야 함
  • 레플리카 수 많을수록 → 하나가 느려져서 컨슈머까지 함께 느려질 가능성 ↑
  • 사실, 복제 팩터와 무관하게 특정 브로커 느려지면 다 느려짐
    • 특정 브로커 느려짐 → 해당 브로커 사용하는 모든 클라이언트 느려짐

비용

  • 레플리카 수 많을수록 → 저장소, 네트워크에 들어가는 비용 ↑
  • 중요하지 않은 데이터는 복제 팩터 3 미만으로 잡자
    • 복제 팩터를 2로 잡아서 저장 비용을 합리적으로 줄이기

레플리카의 위치 또한 중요 !

  • 카프카는 같은 파티션의 레플리카들을 언제나 서로 다른 브로커에 저장하는데, 이러한 레플리카의 위치는 안전하지 않을 수 있음
  • e.g. 같은 파티션의 모든 레플리카들이 같은 랙에 설치되어 있는 브로커들에 저장됨
    • 랙 스위치 오작동 → 복제 팩터와 상관없이 해당 파티션 사용 ❌
  • 랙 단위 사고 방지 위해 브로커를 서로 다른 랙에 배치하고 broker.rack 매개변수에 랙 이름 잡아서 방지하자
    • 랙 이름 설정하면 서로 다른 랙에 분산되어 저장되도록 하여 가용성 ↑



# 언클린 리더 선출

  • 브로커 단위에서만 제어 가능 (실제로는 클러스터 단위)
  • unclean.leaderelection.enable
  • default : false

클린 리더 선출

  • 파티션의 리더가 더이상 작동 불능하여 인-싱크 레플리카 중 하나가 새로운 리더가 되는 경우
  • 커밋된 데이터에 아무런 유실이 없음을 보장
    • 커밋된 데이터의 정의가 모든 인-싱크 레플리카에 존재하는 데이터니까

언클린 리더 선출

  • 작동 불능한 리더 외에 인-싱크 레플리카가 없는 경우
  • (1) 아웃-오브-싱크 레플리카를 리더로 선출
  • (2) 원래 리더가 복구될 때까지 대기

언클린 리더 선출 시나리오 (1)

e.g. 파티션에 3개의 레플리카 존재, 팔로워 2개가 작동 불능 (e.g. 브로커가 크래시 난 경우)

  • 프로듀서는 리더에 쓰기 작업을 계속 진행할 것
  • 모든 메세지는 커밋되고 응답이 감 (리더가 유일한 ISR 이니까)
  • 이후, 브로커 크래시로 리더 또한 작동 불능이 됨
  • → 아웃-오브-싱크 레플리카 중 하나가 먼저 시작되면, 해당 파티션에서 유일하게 사용 가능한 레플리카가 아웃-오브-싱크 레플리카가 됨

언클린 리더 선출 시나리오 (2)

e.g. 파티션에 3개의 레플리카 존재, 팔로워 2개의 복제 작업이 뒤쳐짐 (e.g. 네트워크 문제)

  • 복제 작업은 계속 되지만, 뒤쳐져서 인-싱크 상태는 아님
  • 리더 레플리카만 유일한 ISR이라 계속 메세지 받음
  • 이후, 브로커 크래시로 리더 또한 작동 불능이 됨
  • → 이러한 경우 리더가 될 수 있는 레플리카는 아웃-오브-싱크 레플리카밖에 없음

언클린 리더를 선출하기 위한 결정

(1) unclean.leader.election.enable=false

  • 아웃-오브-싱크 레플리카가 새로운 리더가 될 수 있도록 허용 ❌
  • 예전 리더가 복구될 때까지 대기
  • 해당 파티션은 오프라인 상태

(2) unclean.leader.election.enable=true

  • 아웃-오브-싱크 레플리카가 새로운 리더가 될 수 있도록 허용 🟢
  • 새로운 리더가 동기화를 못한 사이에 예전 리더에 쓰여졌던 모든 메세지들이 유실됨
  • 컨슈머 입장에서의 일관성 깨짐

일관성 깨지는 이유

e.g. 레플리카 1(리더), 2(팔로워), 3(팔로워)가 있고, 2와 3이 작동 불능 상태라고 하자

  • 작동 불능 상태 이후 1이 오프셋 100~200에 해당하는 메세지를 작성함
  • 그러면 2와 3은 0~99만 있고 100~200은 없음
  • 이후 1이 작동 불능되고 2가 새로운 리더가 되었다고 하자
  • 2는 오프셋 100~200에 해당하는 새로운 메세지 작성함
  • 오프셋 100~200에 해당하는 메세지가 다르군요 ~



# 최소 인-싱크 레플리카

  • min.insync.replicas : 토픽 단위, 브로커 단위 모두 설정 가능
  • default : 1

데이터가 커밋되었다 → 모든 인-싱크 레플리카에 쓰여졌다

  • 그런데, 이때 모든단 1개의 레플리카를 의미할 수도 있음
  • 이 단 1개의 레플리카가 작동 불능에 빠짐 → 데이터 유실
  • 그래서 최소 인-싱크 레플리카 수를 충족하는 파티션에만 메세지 쓰기가 가능하도록 위와 같은 옵션이 생김

e.g. 토픽에 레플리카가 3개 있고, min.insync.replicas = 2

  • 프로듀서들은 3개의 레플리카 중 최소 2개가 인-싱크 상태인 파티션에만 쓰기 가능
  • 2개의 레플리카가 작동 불능에 빠지면 브로커는 더이상 쓰기 요청 받을 수 없음
    • 이때 데이터를 전송하려고 시도한 프로듀서는 NotEnoughReplicasException 예외 받음
    • 이때 존재하는 데이터에 한해서 컨슈머는 계속 읽기 작업 가능
    • 즉, 사실상 살아남은 1개의 레플리카는 읽기 전용이 되어버림
  • 읽기 전용 상태에서 회복하려면, 최소 인-싱크 레플리카 수를 충족할 때까지 대기해야함
    • 아웃-오브-싱크 레플리카가 브로커 재시작하고 복구 후에 리더 레플리카 상태 따라잡을 때까지



# 레플리카 인-싱크 상태 유지하기

  • 브로커 단위에서만 제어 가능 (실제로는 클러스터 단위)
  • zookeeper.session.timeout.ms
  • default : 1800ms (18s) (2.5.0 버전부터, 이전 버전까진 6초였음)
  • replica.lag.time.max.ms
  • default : 3000ms (30s) (2.5.0 버전부터, 이전 버전까진 10초였음)

아웃-오브-싱크 레플리카 상태가 될 수 있는 2가지 이유

  • 주키퍼와의 연결 끊어짐
  • 리더 업데이트 내역을 따라가는데 실패하여 복제 랙 발생

zookeeper.session.timeout.ms

  • 카프카 브로커 → 주키퍼으로 하트비트 전송을 멈출 수 있는 최대 시간
  • 이 간격 안에만 하트비트를 보내면 주키퍼는 브로커가 죽었다고 판단 ❌ → 클러스터에서도 제외하지 않음
  • 클라우드 환경은 네트워크 지연 변동폭이 커서, 이러한 안정성 증대를 위해 최신 버전부터 default 값 증가
  • 값 설정 가이드
    • GC나 네트워크 상황 같이 무작위적인 변동에 영향 받지 않을 만큼 높게
    • 실제로 작동이 멈춘 브로커가 적시에 탐지될 수 있을 만큼 충분히 낮게

replica.lag.time.max.ms

  • 이 값보다 더 오랫동안 리더로부터 데이터를 읽어오지 못함 + 리더에 쓰여진 최신 메세지를 따라잡지 못함
    • → 아웃-오브-싱크 레플리카
  • 클러스터의 회복 탄력성 증대 + 불필요한 변동 피하기 위해 최신 버전부터 default 값 증가
  • 컨슈머의 최대 지연에도 영향을 줌
    • 해당 설정값 넘기기 전에는 인-싱크 레플리카라는 의미
    • 메세지가 모든 레플리카에 도착하여 커밋된 데이터가 되어야 컨슈머가 읽을 수 있음
    • default가 30s니까 컨슈머가 메세지 읽으려면 최대 30초까지 기다릴 수도 있다는 의미



# 디스크에 저장

  • flush.messages : 디스크에 저장되지 않은 최대 메세지 수를 조절
  • flush.ms : 얼마나 자주 디스크에 메세지를 저장하는지를 조절

메세지를 디스크로 플러시하는 경우

  • 카프카가 직접 메세지를 디스크로 플러시
    • 세그먼트를 교체할 때 (default : 1GB)
    • 재시작 직전
  • 그외에는 리눅스의 페이지 캐시 기능에 의존
    • 페이지 캐시 공간이 다 찼을 경우에만 메세지를 플러시

Q. 위처럼 하는 이유?

A. 보통 서로 다른 랙이나 가용 영역에 레플리카가 위치하게 되는데, 리더의 디스크에 메세지를 쓰는 것보다 더 안전하다는 판단이 있어서



# 신뢰성 있는 시스템에서 프로듀서

  • 브로커에 사용 가능한 가장 높은 신뢰성 설정 적용한 경우
  • 프로듀서 역시 신뢰성 있도록 설정하지 않으면 시스템 전체적으로 봤을 때 여전히 데이터 유실될 수 있음

# 신뢰성 없는 시스템 예시

  • 프로듀서를 신뢰성 있게 설정하지 못하여, 시스템 전체적으로 영향을 끼칠 수 있는 문제를 살펴보자

첫번째 시나리오

(1) 토픽별로 3개의 레플리카를 가지도록 브로커 설정, 언클린 리더 선출 기능 false

  • 카프카 클러스터에 커밋된 메세지 유실되지 않음

(2) 프로듀서가 메세지를 보내는 과정에서 acks=1 설정으로 보냄

(3) 프로듀서에서 메세지 전송하여 리더에 쓰여짐

  • 인-싱크 레플리카에 반영되지는 않은 상태

(4) 리더가 프로듀서에게 성공 응답 보낸 직후, 크래시가 나서 데이터가 레플리카로 복제 ❌

(5) 다른 레플리카들은 여전히 인-싱크 상태니까 클린 리더 선출로, 이 중 하나가 리더로 됨

  • (4)에서 크래시 난 리더가 아웃-오브-싱크 상태 될 때까지는 시간이 좀 걸려서 아직 인-싱크인거

(6) 메세지가 레플리카에 반영되지 않은 만큼 메세지 유실

(7) 메세지를 쓰고 있는 애플리케이션 입장에서는 성공적으로 썼다고 착각

  • 어떤 컨슈머도 해당 메세지를 못봄
  • 레플리카 입장에서는 본 적 없는 메세지라서 커밋된 것도 아님
  • → 따라서, 시스템의 일관성은 유지됨
  • → 그러나 프로듀서의 입장에서 보면 메세지는 유실된 것

두번째 시나리오

(1) 토픽별로 3개의 레플리카를 가지도록 브로커 설정, 언클린 리더 선출 기능 false

  • 카프카 클러스터에 커밋된 메세지 유실되지 않음

(2) 프로듀서가 메세지를 보내는 과정에서 acks=all 설정으로 보냄

(3) 카프카에 메세지를 쓰려고 하는데, 쓰고 있는 파티션의 리더 브로커가 크래시나고 새 리더는 아직 선출중이라고 가정

(4) 카프카는 “Leader not Available” 응답 보냄

(5) 프로듀서가 올바르게 에러 처리 안함 + 성공할 때까지 재시도 안함 → 메세지 유실될 수 있음

  • 브로커는 메세지를 받지도 않았으니까 브로커의 신뢰성과 관련 ❌
  • 컨슈머 역시 메세지 받지도 않았으니까 일관성 문제 ❌

결론

  • 신뢰성 요구 조건에 맞게 프로듀서에 올바른 acks 설정하기
  • 설정과 코드 모두에서 에러를 올바르게 처리하기



# 프로듀서 응답 보내기

프로듀서가 선택 가능한 세 가지 응답 모드


acks=0

  • 프로듀서가 네트워크로 메세지를 전송한 시점에서 메세지가 성공적으로 쓰여진 것으로 간주
  • 에러 발생하는 경우
    • 전송하는 객체가 직렬화 될 수 없음
    • 네트워크 카드 오작동
  • 에러 발생하지 않는 경우
    • 파티션 오프라인
    • 리더 선출이 진행중
    • 전체 카프카 클러스터가 작동 불능
  • 특징
    • 지연 낮음
      • 지연이 낮아서 이 설정으로 벤치마크 돌릴 때 많음
    • 종단 지연이 개선되지는 않음
      • 컨슈머는 메세지가 모든 인-싱크 레플리카로 복제되지 않는 한 해당 메세지를 볼 수 없으니까

acks=1

  • 리더가 메세지를 받아서 파티션 데이터 파일에 쓴 직후 응답 또는 에러 보냄 (디스크에 반영될 필요 ❌)
  • 데이터 유실될 수 있는 경우
    • 일부 메세지가 리더에 성공적으로 쓰여져서 클라이언트로 응답이 간 상태
    • 팔로워로 복제 완료되기 전에 리더가 정지하거나 크래시 날 경우 데이터가 유실될 수 있음
  • 불완전 복제 파티션 (under-replicated partition, URP)
    • 메세지를 복제하는 속도 < 리더에 메세지를 쓰는 속도 가 가능해서 발생하는 문제
    • 리더 입장에서는 팔로워로 복제 완료 이전에 프로듀서에게 응답 먼저 하기 때문

acks=all

  • 리더가 모든 인-싱크 레플리카가 메세지를 받아갈 때까지 대기 후 응답 또는 에러 보냄
  • 브로커의 min.insync.replicas 설정과 함께, 응답이 오기 전까지 얼마나 많은 레플리카에 메세지가 복제될 것인지를 조절할 수 있게 해줌
  • 특징
    • 가장 안전한 옵션
    • 프로듀서는 메세지가 완전히 커밋될 때까지 계속해서 메세지 재전송
    • 프로듀서 지연이 가장 길어지는 옵션
    • 프로듀서는 모든 인-싱크 레플리카가 메세지 받을 때까지 대기 → 이후 해당 메세지 배치에 완료 표시하고 작업 짆애



# 프로듀서 재시도 설정

프로듀서의 에러 처리

  • 프로듀서가 자동으로 처리해주는 에러
  • 프로듀서 라이브러리 사용하는 개발자들이 처리해야 하는 에러

프로듀서는 재시도 가능한 에러(retriable error)를 자동으로 처리 가능

  • 프로듀서가 브로커에 메세지를 전송하면 브로커는 성공 혹은 에러 코드 리턴
  • LEADER_NOT_AVAILABLE 예외 리턴
    • 프로듀서는 전송 재시도 가능 🟢
    • 새로운 브로커가 리더로 선출된 상황이라 두 번쨰 시도는 성공할 것
  • INVALID_CONFIG 예외 리턴
    • 프로듀서는 전송 재시도 가능 ❌
    • 재전송한다고 설정이 바뀌지 않으니까

메세지가 유실되지 않는 것이 목표

  • 재시도 가능 에러 발생 시, 프로듀서가 계속해서 메세지 전송을 재시도하도록 설정
  • 3장에서 권장한 것처럼 재시도 수를 기본 설정값(MAX_INT, 사실상 무한)으로 두기
  • 3장에서 권장한 것처럼 메세지 전송 포기까지 대기할 수 있는 시간 delivery.timeout.ms 설정값을 최대로 잡기

전송 실패한 메세지를 재시도하는 것은 메세지가 중복될 위험 내포

  • 실패했다고 생각했지만 실제로 실패가 아닌 메세지
  • 재전송된 메세지
  • 위의 두 메세지가 모두 브로커에 성공적으로 쓰여지는 경우 있을 수 있음

즉, 각 메세지가 최소 한 번 저장되도록 보장할 수 있지만, 정확히 한 번은 보장할 수 없음

  • enable.idempotence=true 설정으로 프로듀서가 추가적인 정보를 레코드에 포함할 수 있도록 하고 이를 활용하여 브로커가 재시도로 인한 중복된 메세지를 건너뛸 수 있도록 설정 가능



# 추가적인 에러 처리

프로듀서가 재시도 불가능한 에러에 대해 개발자의 수동 처리 필요

  • e.g.
    • 메세지 크기와 관련되었거나 인가 관련 에러와 같이 재시도 불가능한 브로커 에러
    • 메세지 → 브로커 전송 이전 발생한 에러 (e.g. 객체 직렬화 에러)
    • 프로듀서가 모든 재전송 시도를 소진했거나, 재시도 과정에서 프로듀서가 사용하는 가용 메모리가 메세지로 가득 차서 발생하는 에러
    • 타임아웃

수동 처리는 애플리케이션의 조건에 따라 적절한 방식 선택

  • 잘못된 메세지는 폐기해야 하는가?
  • 에러를 로깅해야 하는가?
  • 원본 시스템으로부터 메세지를 읽는 것을 중단해야 하는가?
  • 원본 시스템에 백프레셔를 적용하여 잠시 메세지 전송을 멈추도록 해야하는가?
  • 이 메세지를 로컬 디스크의 디렉토리에 저장해야 하는가?

에러 핸들러로 제어하는게 메세지 재전송이 전부에요

  • 그럼 그냥 프로듀서의 재전송 기능 쓰는게 나아요



# 신뢰성 있는 시스템에서 컨슈머

  • 컨슈머는 일관성이 보장되는 데이터만 읽음 (커밋된 데이터, 즉, 모든 인-싱크 레플리카에 쓰여진 이후에만 읽으니까)
  • 컨슈머가 해야할 일은 어느 메세지까지 읽었고 어디까지 읽지 않았는지를 추적하는 것
    • 이것 때문에 컨슈머는 메세지의 오프셋을 커밋
    • 이유 : 해당 컨슈머나 다른 컨슈머가 재시작 이후에도 어디부터 작업을 계속할 지 알 수 있어서
  • 컨슈머가 메세지를 누락할 수 있는 경우
    • 읽기는 했지만, 아직 처리는 완료되지 않은 이벤트들의 오프셋을 커민하는 경우
    • 다른 컨슈머가 작업 이어받았을 때, 이 메세지들은 건너뛰고 영원히 처리 ❌
    • 컨슈머가 언제 어떻게 커밋되는지 신경써야함

커밋된 메세지 vs 커밋된 오프셋

  • 커밋된 메세지 : 모든 인-싱크 레플리카에 쓰여져서 컨슈머가 읽을 수 있는 메세지
  • 커밋된 오프셋 : 컨슈머가 특정 파티션 어느 오프셋까지의 모든 메세지를 받아서 처리 완료했는지 알리기 위해 카프카에 보낸 오프셋



# 컨슈머 설정

신뢰성을 갖는 컨슈머를 설정하기 위한 컨슈머 속성

  • group.id
  • auto.offset.reset
  • enable.auto.commit
  • auto.commit.interval.ms

group.id

  • 컨슈머가 구독한 토픽의 모든 메세지를 읽어야한다면 고유한 그룹 ID 필요
  • e.g. 2개의 컨슈머가 같은 그룹 ID 가지고 같은 토픽 구독하는 경우
    • 해당 토픽 전체 파티션의 서로 다른 부분을 각각 읽음
    • 즉, 컨슈머그룹 전체적으로 모든 메세지를 읽고 있는 셈
    • 2개의 컨슈머가 반띵해서 읽고 있는 것
    • 컨슈머 1개가 구독한 토픽 다 읽으려면 고유한 그룹id 가져야함

auto.offset.reset

  • 커밋된 오프셋이 없을 때, 컨슈머가 브로커에 없는 오프셋을 요청할 때 컨슈머가 어떻게 해야할지 결정
    • earliest : 유효한 오프셋이 없는 한, 컨슈머는 파티션의 맨 앞에서부터 읽기 시작
      • 컨슈머는 많은 메세지들을 중복 처리하게 될 수 있음
      • 데이터 유실은 최소화 할 수 있음
    • latest : 컨슈머는 파티션의 끝에서부터 읽기 시작
      • 중복 처리 최소화
      • 컨슈머가 일부 데이터 누락할 것이 거의 확실

enable.auto.commit

  • true : 일정한 시간에 맞춰 컨슈머가 알아서 오프셋 커밋하게 함
  • faule : 코드에서 직접 오프셋 커밋할지
  • 자동 오프셋 커밋 기능 장점
    • 애플리케이션에서 컨슈머 사용할 때 걱정거리 줄어듬
    • 우리가 처리하지 않은 오프셋을 실수로 커밋하는 사태가 발생하지 않도록 보장
  • 자동 오프셋 커밋 기능 단점
    • 메세지 중복 처리를 개발자가 제어할 수 없음
    • 자동 커밋되지 않은 상태에서 컨슈머 멈추면, 컨슈머 재시작 이후 메세지 중복처리 피할 수 없음

auto.commit.interval.ms

  • 오프셋 자동 커밋 시, 커밋되는 주기 설정 (default : 5s)
  • 더 자주 커밋할수록
    • 오버헤드 ↑
    • 컨슈머 정지 시 발생할 수 있는 중복의 수 ↓

추가 내용) 컨슈머의 빈번한 리밸런싱

  • 신뢰성과 직접적으로 연관된 것은 ❌
  • 그러나, 컨슈머가 리밸런스 수행을 위해 너무 자주 멈추면 신뢰성 있다고 하기에는 어려울 것
  • 불필요한 리밸런싱과 리밸런싱 발생 시 멈춤을 최소화 하는 방법 고려하자 (4장에서 설명함)
    • 불필요한 리밸런싱 최소화 : e.g. 정적 그룹 멤버쉽
    • 리밸런싱 발생 시 멈춤 최소화 : e.g. 협력적 리밸런스



# 명시적으로 오프셋 커밋

오프셋을 자동 커밋하지 않고, 직접 수행했을 때 고려해야하는 부분들


(1) 메세지 처리 먼저, 오프셋 커밋은 나중에

  • 폴링 루프에서 모든 처리를 하고 루프 사이의 상태는 저장하지 않으면 괜찮음
    • 자동 오프셋 커밋 설정 키거나
    • 폴링 루프 끝에서 오프셋 커밋하거나
    • 루프 안에서 일정 주기로 오프셋 커밋
  • 이러면 오버헤드와 중복 처리 회피 사이의 요구 조건 균형 맞추면 됨
  • 스레드가 2개 이상 있거나 stateful 처리가 필요한 경우는 컨슈머 객체 스레드가 안전하지 않아서 복잡해짐

(2) 커밋 빈도는 성능과 크래시 발생시 중복 개수 사이의 트레이드 오프다

  • 폴링 루프에서 모든 처리를 하고 루프 사이의 상태는 저장하지 않는 경우 아래 옵션 선택 가능
    • 루프 안에서 여러 번 커밋하거나
    • 루프가 몇 번 지나갈 때마다 커밋하거나
  • 커밋 작업 → 높은 오버헤드
    • 프로듀서의 acks=all 설정과 함께 쓰기 작업 수행하는 것과 비슷
    • 특정 컨슈머 그룹의 모든 오프셋 커밋이 동일한 브로커로 간다는 점이 다르긴 함
  • 커밋 주기 → 성능과 중복 발생의 요구 조건 사이에서 균형 맞추기
    • 메세지 읽어올 때마다 커밋하는건 매우 드물게 메세지 들어오는 토픽에나 쓸 수 있음

(3) 정확한 시점에 정확한 오프셋을 커밋하자

  • 폴링 루프 중간에서 커밋할 때 흔히 하는 실수
    • 원래는, 마지막으로 처리된 메세지의 오프셋 가져와야함
    • 근데 마지막으로 읽어온 메세지의 오프셋을 커밋하는 경우 많음
    • 읽기는 했지만 처리되지 않은 메세지의 오프셋을 커밋하지 말라는 말
    • → 컨슈머 입장에서 메세지가 누락될 수 있음

(4) 리밸런스

  • 컨슈머 리밸런스가 발생할 것이라는 것을 기억해야 함
  • 할당된 파티션 해제 되기 전에 오프셋을 커밋
  • 새로운 파티션이 할당되었을 때 애플리케이션이 보유하고 있던 상태를 삭제해주는 작업
  • 이러한 작업들이 포함됨

(5) 컨슈머는 재시도를 해야 할 수도 있다

  • poll() 호출하고 레코드 처리 → 처리가 완료되지 않은 일부 레코드는 나중에 처리되어야 할 수 있음
  • 전통적인 발행/구독 메세지 전달 시스템 → 응답을 보냄
  • 카프카 컨슈머 → 오프셋을 커밋할 뿐, 각각의 메세지에 응답을 보내지 않음
  • 방식
    • 동기 방식 재시도
      • 재시도 가능한 에러가 발생했을 경우 → 마지막으로 처리에 성공한 레코드의 오프셋을 커밋
      • 나중에 처리해야 할 레코드들을 버퍼에 저장하고, 컨슈머의 pause() 메서드를 호출해 추가적인 poll() 호출이 데이터를 리턴하지 않도록 한 뒤, 레코드 처리를 계속 진행
    • 비동기 방식 재시도
      • dead-letter-queue 방식
      • 별도의 재시도 토픽에 메시지를 발행한 뒤 기존 메시지 처리는 계속 진행

(6) 컨슈머가 상태를 유지해야 할 수도 있다

  • poll() 메서드 호출 간 상태를 유지해야 할 수도 있음
  • e.g. 이동평균을 계산하는 경우
    • 카프카에서 새 메시지를 폴링해 올 때마다 평균값을 업데이트 해줘야 함
    • 컨슈머가 재시작되면 단순히 마지막 오프셋에서부터 읽기 작업 재개하는 것으로는 안되고, 이동평균값을 복구시켜야 함
  • e.g. 이동평균값 복구하는 방법
    • 마지막으로 누적된 값을 애플리케이션이 오프셋을 커밋할 때 results 토픽에 쓰는 것
    • 스레드가 시작될 때, 작업 중단 시점 + 마지막으로 누적된 값 가져올 수 있음



# 시스템 신뢰성 검증

# 설정 검증

  • 브로커와 클라이언트 (프로듀서, 컨슈머) 설정 검증하는 것은 쉬움
  • 아래의 이유로 설정 검증을 권장
    • 우리가 선택한 구성이 요구 조건을 충족시킬 수 있는지 확인하는데 도움됨
    • 시스템의 예상 작동 추론하기 좋음

검증용 프로듀서 (org.apache.kafka.tools.VerifiableProducer), 검증용 컨슈머 (org.apache.kafka.tools.VerifiableConsumer) 활용하여 CLI든 테스팅 프레임워크든 활용하기 좋음


어떤 테스트를 할 것인지에 대한 고려

  • 리더 선출
    • 리더 정지시키면 어떻게 되나?
    • 프로듀서/컨슈머가 정상 작동 재개하는데까지 얼마나 걸리나?
  • 컨트롤러 선출
    • 컨트롤러 재시작 후, 시스템 재개까지 얼마나 걸리나?
  • 롤링 재시작
    • 메세지 유실 없이 브로커들을 하나씩 재시작시킬 수 있나?
  • 언클린 리더 선출 테스트
    • 한 파티션의 모든 레플리카들을 하나씩 중단시킨 후, 다음 아웃-오브-싱크 상태가 된 브로커를 시작시키면 어떻게 되나?
    • 작업을 재개하려면 어떻게 해야할까?



# 애플리케이션 검증

애플리케이션 로직이 카프카의 클라이언트 라이브러리와 상호작용 하는 부분에 대한 테스트

  • 아래와와 같은 곳들을 확인
    • 커스텀 에러 처리 코드
    • 오프셋 커밋
    • 리밸런스 리스너

다양한 장애 상황에 대한 테스트도 해보기

  • 클라이언트가 브로커 중 하나와 연결 끊어짐
  • 클러이언트와 브로커 사이의 긴 지연
  • 디스크 꽉 참
  • 디스크 멈춤
  • 리더 선출
  • 브로커 롤링 재시작
  • 컨슈머 롤링 재시작
  • 프로듀서 롤링 재시작

장애 주입을 위한 카프카의 자체적인 프레임워크

  • 트록도르 (Trogdor) 테스트 프레임워크



# 프로덕션 환경 모니터링

  • 클러스터 상태 모니터링
  • 클라이언트와 전체 데이터의 흐름 모니터링

카프카의 자바 클라이언트들은 클라이언트 쪽 상태/이벤트를 모니터링 할 수 있게 해주는 JMX 지표 포함

  • 각각 신뢰성에서 가장 중요한 지표를 알아보자
  • 프로듀서
    • 레코드별 에러율 (error-rate)
    • 재시도율 (retry-rate)
    • 이벤트 전송 도중 발생하는 에러 로그 모니터링
      • WARN 레벨
        • “Got error produce response with correlation id 5689 on topic-partition ~, retrying ~”
      • ERROR 레벨
        • 재시도 불가능한 에러
        • 재시도 횟수가 고갈된 재시도 가능 에러
        • 타임아웃으로 메세지 전송 완전히 실패했음
  • 컨슈머
    • 컨슈머 랙 (consumer lag)
      • 컨슈머가 브로커 내 파티션에 커밋된 가장 최신 메세지에서 얼마나 뒤떨어져 있는지
      • 이상적인 상황
        • 랙은 0, 컨슈머는 계속해서 최신 메세지만 읽음
      • 현실
        • poll() 호출이 여러 메세지를 리턴 → 컨슈머는 다음 번 메세지를 읽어오기 전까지 리턴된 메세지를 처리하는데 시간 씀 → 랙은 계속해서 올랐다 내렸다 반복
        • 중요한건 컨슈머가 계속 뒤쳐지는게 아니라, 계속 따라붙는 것
      • 링크드인이 개발한 버로우 사용하면 컨슈머 랙 확인하는 작업 수월함

데이터의 흐름을 모니터링 하는 것 → 모든 쓰여진 데이터가 적절한 시기에 읽혀진다

  • 모든 메세지는 이벤트가 생성된 시점인 타임스탬프를 포함함
  • 타임스탬프는 애플리케이션이나 브로커에 의해 재정의될 수 있음

모든 쓰여진 메세지가 적절한 시간 내에 읽히도록 하기 위한 방법

  • 애플리케이션
    • 자신이 쓴 이벤트의 수를 메세지 형태로 작성 (보통, 초당 이벤트 수 형태)
  • 컨슈머
    • 단위 시간당 읽은 이벤트 수, 이벤트가 쓰여진 시점과 읽힌 시점 사이의 랙을 기록
  • 프로듀서 - 컨슈머 양쪽에서 들어온 초당 이벤트 수를 서로 맞춘 뒤, 이벤트가 쓰여진 시점과 읽힌 시점 사이의 간격이 적절함을 확인할 수 있는 시스템 필요
  • 이런 종단 모니터링 시스템은 컨플루언트 컨트롤 센터의 일부인 제품을 쓰자

카프카 브로커가 클라이언트로 보내는 에러 응답률을 보여주는 지푯값 수집하기

  • kafka.server:type=BrokerTopicMetrics,name=FailedProduceRequestsPerSec
  • kafka.server:type=BrokerTopicMetrics,name=FailedFetchRequestsPerSec
  • 때때로 일정 수준의 에러 응답이 예상되는 경우
    • e.g. 유지관리 작업을 위해 브로커를 꺼서 다른 브로커가 리더로 선출될 경우
      • 프로듀서가 NOT_LEADER_FOR_PARTITION 에러 받는 것은 예상된 작동

Comment