개발 서적/카프카 핵심 가이드

Kafka : 신뢰성 있는 데이터 전달

H_JU_0527 2025. 1. 14. 17:59
💡 해당 글은 '카프카 핵심 가이드 2nd Edition'을 읽고 정리한 글입니다.

 

1️⃣ 신뢰성 보장

카프카에서 보장하는 것은 아래와 같다.

  • 파티션 안의 메세지들 간에 순서
  • 클라이언트가 쓴 메세지는 모든 인-싱크 레플리카의 파티션에 쓰여진 뒤에야 커밋된 것으로 간주한다.
  • 최소 1개의 작동 가능한 레플리카가 남아 있는 한 유실되지 않는다.
  • 컨슈머는 커밋된 메세지만 읽을 수 있다.

 

2️⃣ 복제

카프카의 복제 메커니즘은 파티션별로 다수의 레플리카를 유지한다는 점에서 카프카 신뢰성 보장의 핵심이다.

  • 각 파티션은 다수의 레플리카를 가질 수 있으며, 그 중 하나가 리더가 된다.
  • 모든 이벤트들은 리더 레플리카에 읽어지고 쓰여진다.
  • 다른 레플리카들은 단순히 리더와 동기화를 맞추면서 최신 이벤트를 제시간에 복사해, 리더와 상태를 유지한다.
  • 만약 리더가 작동 불능이 되면, 인-싱크 레플리카 중 하나가 새 리더가 된다.

 

인-싱크 레플리카의 조건은 다음과 같다. (여기서 말하는 -초는 모두 설정 가능한다.)

  • 주키퍼와 최근 6초 사이에 하트비트를 주고 받았는지
  • 최근 10초사이 리더로부터 메세지를 읽어왔는지
  • 최근 10초사이에 랙이 없었는지

 

만약 레플리카와 주키퍼 사이의 연결이 끊어지거나, 새 메세지를 읽어오지 못한다면 아웃-오브싱크 가 된다.

  • 상대적으로 흔한 원인 중 하나는 최대 요청 크기가 큰 탓에 JVM 힙도 크게 사용하면서 가비지 수집 시간이 오래걸려 순간적으로 주키퍼와 연결이 끊어지는 경우이다.

 

인-싱크 레플리카 수가 줄어들면 파티션의 실질적인 복제 팩터가 줄어들면서, 중단 시간이 길어지거나 데이터 유실 위험이 증가한다.

 

3️⃣ 브로커 설정

복제 팩터

토픽 단위 설정은 `replication.factor`에,  자동으로 생성되는 토픽들에 적용되는 브로커 단위 설정은 `default.replication.factor`설정에 잡아준다.

  • 이미 존재하는 토픽일지라도 우리는 카프카의 레플리카 재할당 툴을 사용해서 레플리카를 추가하거나 삭제할 수 있다.
  • 즉, 자연히 복제 팩터 역시 변경이 가능한다.

 

복제 팩터가 N이면 N-1개의 브로커가 중단되더라도 토픽의 데이터를 읽거나 쓸 수 있다.

  • 반대로 복제 팩터가 N이라는 것은 N개의 복사본을 저장해야 한다는 것을 의미하므로 N배의 디스크 공간이 필요하다는 뜻이 된다.
  • 결과적으로 가용성과 하드웨어 사용량 사이의 트레이드 오프가 존재한다.

 

그래서 트레이드 오프는 아래와 같다.

가용성 레플리카 수가 많을 수록, 가용성 증가
지속성 복사본이 많을 수록, 데이터 지속성 증가
처리량 레플리카가 많을 수록, 처리량 증가
종단 지연 레플리카가 많을 수록, 종단 지연 증가
비용 레플리카가 많을 수록, 비용 증가

 

레플리카의 위치 역시 매우 중요하다.

  • 랙 단위 사고를 방지하기 위해 브로커들을 서로 다른 랙에 배치한 뒤, `broker.rack` 설정 매개변수로 랙 이름을 잡아준다.
  • 서로 다른 랙에 분산되어 저장되도록 함으로써, 브로커 크래시가 났을때를 대비하여 가용성을 높인다.

 

언클린 리더 선출

기존에는 인-싱크 레플리카 중에서 하나의 리더가 선출된다. 커밋된 데이터에 아무런 유실이 없음을 보장한다는 점에서 이러한 리더 선출을 "클린"이라고 한다.

 

그런데 만약 작동 불능에 빠진 리더 외에 인-싱크 레플리카가 없다면 어떻게 될까? 이때 두가지 결정을 할 수 있다.

  1. 예전 리더가 복구 될때까지 해당 파티션을 오프라인 상태로 유지한다.
    • 이 방법은 하드웨어적인 문제인 경우 몇시간이 걸릴 수 도 있다.
  2. `unclean.leader.election.enable` 설정을 true로 하여 언클리 리더 선출을 한다.
    • 리더의 동기화를 따라가지 못하는 레플리카를 리덜로 선출하는 것이기 때문에 동기화 문제를 일으킨다.
    • 일부 파티션은 중복된 커밋 메세지를 받거나, 일부 파티션에서는 메세지 유실이 발생한다.
    • 즉 메세지 일관성 깨짐의 위험성이 있다.
💡 운영자 입장에서는 이러한 상황이 닥쳤을 때 파티션을 사용 가능한 상태로 만들기 위해 데이터 유실을 감수하기로 하고 이 값을 true로 바꾼 뒤 클러스터를 시작하는 것도 언제든 가능하다. 이 경우 클러스터가 복구된 뒤 설정값을 false로 되돌려주는 것을 잊지말자.

 

최소 인-싱크 레플리카

커밋된 데이터를 2개 이상의 레플리카에 쓰고자 한다면, 인-싱크 레플리카의 최소값을 더 높게 잡아줄 필요가 있다.

  • 토픽에 레플리카가 3개 있고, min.insync.replicas를 2로 잡아 줬다고 가정해보자.
  • 만약 3개 중 2개의 레플리카가 작동 불능에 빠질 경우, 브로커는 더 이상 쓰기 요청을 받을 수 없을 것이다. (`NotEnoughReplicasException`)
  • 즉, 하나만 남을 경우 해당 레플리카는 사실상 읽기 전용이 된다.

 

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

아웃-오브-싱크 레플리카는 전반적인 신뢰성을 낮추므로 가능한 한 피할 필요가 있다.

 

zookeeper.session.timeout.ms

  • `zookeeper.session.timeout.ms`는 카프카 브로커가 주키퍼로 하트비트 전송을 멈출 수 있는 최대 시간이다. (기본 : 18초)
  • 일반적으로 이 값은 가비지 수집 혹은 네트워크 상황과 같은 무작위적인 변동에 영향을 받지 않을 만큼 높게, 작동을 멈춘 브로커가 적시에 탐지될 수 있을 만큼 낮게 할 필요가 있다.

replica.lag.time.max.ms

  • 만약 레플리카가 `replica.lag.time.max.ms`에 설정된 값보다 더 오랫동안 리더로부터 읽어오지 못한다면 아웃-오브-싱크가 된다.
  • 기본값은 30초이며, 이것은 컨슈머의 최대 지연에도 영향을 준다는 것을 명심하자.

 

디스크에 저장하기

카프카는 세그먼트를 교체할 때와 재시작 직전에만 메세지를 디스크로 플러시하며, 그 외의 경우에는 리눅스의 페이지 캐시 기능에 의존한다.

 

하지만 브로커가 디스크에 더 자주 메세지를 저장하도록 설정하는 것이 가능하다.

  • `flush.messages` : 디스크에 저장되지 않은 최대 메세지 수
  • `flush.ms`: 디스크에 메세지를 저장하는 주기

 

4️⃣ 신뢰성 있는 시스템에서 프로듀서 사용하기

응답 보내기

ack = 0

  • 프로듀서가 네트워크로 메세지르 전송한 시점에 응답
  • 지연은 낮지만, 메세지가 모든 인-싱크 레플리카에 복제되기를 기다려야 하기 때문에 종단 지연은 개선되지 않는다.

ack = 1

  • 리더가 파티션 데이터 파일에 쓴 직 후 응답 또는 에러를 전달
  • 메세지 복제하는 속도보다 더 빨리 리더에 쓸 수 있기 때문에, 불완전 복제 파티션이 발생할 수 있다.

ack = all(-1)

  • 모든-인-싱크 레플리카가 메세지를 받을 때까지 대기한 후 응답을 전달
  • 프로듀서는 메세지가 완전히 커밋될 때까지 계속해서 메세지를 재전송할 수 있다.
  • 지연이 가장 길어지는 옵션이지만, 가장 안전하다.

 

프로듀서 재시도 설정하기

프로듀서가 브로커에 메세지를 전송하면 성공 혹은 에러코드를 전달받는데, 에러코드는 두 부류로 나뉜다.

  • 재전송으로 해결 가능한 에러 코드 : `LEADER_NOT_AVAILABLE` (재전송시 리더가 선출되어 성공할 것이다.)
  • 해결가능하지 않은 에러 코드 : `INVALID_CONFIG` (설정 문제)

기본적으로 메세지가 유실되지 않은 것을 목표로 하기 때문에, 재전송 횟수는 최대한으로 설정하는 것이 좋다.

  • 재시도 수를 기본 설정값(MAX_INT)으로 내버려 둘 수 있다.
  • 메세지 전송을 포기할 때까지 대기할 수 있는 시간을 지정하는 `delivery.timeout.ms`를 최대로 설정한다.

전송 실패한 메세지를 재시도하는 것은 두 메세지가 모두 브로커에 중복되어 쓰여지는 위험이 있다.

  • `enable.idempotence = true`로 설정함으로써 중복으로 쓰인 메세지는 건너뛸 수 있도록 하자.
💡 위에서 언급한 예외 이외에도 개발자 입장에서 다른 종류의 에러를 여러가지 방식으로 처리할 수 있다. 이는 아키텍처 와 요구사항에 따라 대처법이 달라질 것이다. 만약 메세지 재전송이 에러 핸들러가 하는 일의 전부라면, 프로듀서의 재전송 기능을 사용하는 편이 더 낫다는 점을 유념하자.

 

5️⃣ 신뢰성 있는 시스템에서 컨슈머 사용하기

컨슈머가 해야 할 일은 어느 메세지까지 읽었고 어디까지는 읽지 않았은지를 추적하는 것이다. 이것은 메세지를 읽는 도중에 누락되지 않게 하기 위해 필수적이다.

 

신뢰성 있는 처리를 위해 중요한 컨슈머 설정

group.id

  • 만약 같은 그룹 ID를 갖는 두 개의 컨슈머가 같은 토픽을 구독할 경우, 각각의 컨슈머에는 해당 토픽 전체 파티션의 서로 다른 부분집합이 할당되어 각각 다른 부분의 메세지만 읽게 된다.
  • 만약 위와 같은 경에서 각 컨슈머가 파티션의 모든 메세지를 읽기 원한다면, 고유한 그룹 아이디를 지정하여 설정해주자.

auto.offset.reset

  • 커밋된 오프셋이 없을 때나 컨슈머가 브로커에 없는 오프셋을 요청할 때 컨슈머가 어떦게 해야할지 결정한다.
  • `earliest` : 파티션의 맨 앞에서부터 읽기 시작
    • 이는 유실은 최소화할 수 있지만, 많은 메세지가 중복될 수 있다.
  • `lastest` : 가장 최근 메세지부터 읽기 시작
    • 중복은 최소화할 수 있지만, 유실의 가능성이 존재한다.

enable.auto.commit

  • 자동으로 오프셋을 커밋하는 설정으로, 우리가 처리하지 않은 오프셋을 실수로 커밋하는 사태가 발생하지 않도록 보장해준다.
  • 하지만 자동 오프셋 커밋 기능은 메세지 중복 처리를 우리가 제어할 수 없다.
  • 그래서 멱등 소비자 패턴과 같은 외부 저장소를 활용하여 중복 방지를 해줘야 한다.

auto.commit.interval.ms

  • 자동 커밋을 활성할 경우, 이 설정을 사용해서 커밋되는 주기를 설정할 수 있다.
  • 5초마다 커밋하는 것이 기본 값이며, 더 자주 커밋할수록 오버헤드 역시 늘어나지만 컨슈머가 정지했을 때 발생할 수 있는 중복의 수는 줄어든다.

 

컨슈머에서 명시적으로 오프셋 커밋하기

만약 커밋을 직접 수행하기로 했다면 아래 몇가지 주의 사항을 살펴보자.

 

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

  • 폴링 루프에서 먼저 처리하고 루프 사이의 상태는 저장하지 않는 방식
  • 만약 스레드가 2개 이상 있다면 중간 상태를 저장하지 않기 때문에 동기화는 더 복잡해진다.

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

  • 커밋 작업은 상당한 오버헤드를 수반한다. 그래서 커밋 주기는 성능과 중복 발생의 요구 조건 사이의 균형을 맞춰야 한다.

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

  • 폴링 루프 중간에서 커밋할 때 흔히 하는 실수는 마지막으로 읽어 온 메세지의 오프셋을 커밋하는 것이다.
  • 처리 완료한 메세지를 커밋하는 것을 기억하자.

4. 리밸런스

  • 리밴러스 발생시 할당된 파티션을 해제하는데. 이때 해제 되기 전에 오프셋을 커밋하고 새로운 파티션이 할당되었을때 어플리케이션이 보유하고 있던 상태를 삭제해주는 작업을 넣어야 한다.

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

  • poll()을 호출하고 레코드를 처리한 뒤, 일부 레코드는 처리가 완료되지 않아서 나중에 처리되어야 할 수도 있다.
  • 재시도 가능한 에러가 발생했을 경우, 마지막으로 처리에 성공한 레코드의 오프셋을 커밋하는 것이다.
    • 나중에 처리해야 할 레코드들을 버퍼에 저장하고, 컨슈머의 pause() 메서드를 호출해 추가적인 pol() 호출을 막은 뒤 , 레코드 처리를 계속한다.
  • 다른 방법으로는 별도의 토픽에 쓴 뒤 계속 진행하는 것이다.
    • 별도의 컨슈머 그룹을 사용해서 재시도 토픽에 저장된 레코드들을 처리할 수 있다.
    • 혹은 주 토픽과 재시도 토픽을 모두 구독하는 컨슈머를 하나 둬서 재시도 사이에는 재시도 토픽 구독을 잠시 멈추도록 할수도 있다. ( dead letter queue )

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

  • 만약 상황에 따라 프로세스가 재시작된 후 , 특정 상태값을 요구한다면 컨슈머의 상태를 저장해야 할 수도 있다.
  • 이때 사용할 수 있는 방법은, 마지막으로 누적된 값(상태)을 애플리케이션이 오프셋을 커밋할때 `result` 토픽에 쓰는 것이다.

 

"신뢰성 있는 카프카 설계하기"와 관련된 영상