[Kafka Study] exactly-one semantics

Created
Oct 23, 2023
Created by
Tags
Kafka
Property
 
 
학습 자료
 
 

 
 
카프카의 ‘정확히 한 번'(exactly-one) 구조는 멱등성을 보장하는 프로듀서와 트랜잭션의 원자성으로 이뤄진다.
 
 

프로듀서는 어떻게 멱등성을 보장하는가

 
  • enable.idempotencetrue 으로 설정하면 프로듀서가 전송하는 모든 메세지에는 프로듀서의 아이디와 시퀸스 넘버가 붙는다.
  • 리더 브로커에서는 각 파티션마다 가장 최근에 기록된 메세지 5개를 추적하는 데에 [프로듀서 아이디 + 시퀸스 넘버] 조합을 사용한다. (5개 미만으로 추적을 제한하고 싶다면 max.in.flights.requests.per.connection 을 5 미만으로 설정한다.)
  • 리더 브로커는 추적한 메세지 중에 이미 파티션에 기록된 중복 메세지가 있다면 메세지 기록을 거부한다.
  • 만약 리더브로커에서 예상하는 시퀸스 넘버보다 더 큰 숫자를 받게 될 경우 (예. 2번 다음에 4번이 올 경우) 리더 브로커는 out of order sequence number 에러를 발생시킨다.
  • out of order sequence number 예외가 발생했다는 것은 중간에 메세지 유실이 있었다는 뜻이므로, 프로듀서가 신뢰성을 보장하기 위한 설정이 되어있는지 (예. acks = all 으로 되어있는지) 또는 카프카 클러스터에서 unclean leader election 이 발생했는지 살펴볼 필요가 있다.
 
 

프로듀서 멱등성의 한계

 

프로듀서가 재시작하는 경우

  • 트랜잭션 기능을 사용하지 않는다면 프로듀서는 초기화 될 때마다 브로커로부터 프로듀서 아이디를 새로 발급받는다.
  • 기존 프로듀서 프로세스에 문제가 생겨서 프로듀서를 새로 띄우고 이미 전송된 메세지를 재전송한다면 [프로듀서 아이디 + 시퀸스 넘버] 조합으로 메세지의 중복을 추적하는 리더브로커는 재전송된 메세지를 새 메세지로 간주해서 파티션에 동일한 메세지를 또 기록할 수 있다.
 

애플리케이션에서 메세지를 직접 재전송 하는 경우

  • producer.send() 를 두 번 호출하면 프로듀서는 동일한 메세지가 재전송 됨을 알지 못하고 같은 메세지를 두 번 보낸다. (각 메세지에 시퀸스 넘버가 새로 붙을 듯.)
  • 이건 프로듀서가 재시도 로직 외에 중복으로 메세지가 전송되는 로직에는 관여할 수 없음을 의미하므로 메세지 전송 과정에 예외가 발생할 것을 감안해야 한다면 아래처럼 하지 않고 프로듀서의 retries 설정을 통해 재시도 매커니즘을 태우는 게 좋다.
try: produer.send(...) except BlahException: produer.send(...)
 
 

‘정확히 한 번’ 을 위반하는 상황

애플리케이션의 일반적인 스트림 처리는 다음과 같다.
  1. A 토픽의 a 파티션에서 메세지 읽기
  1. 읽은 메세지를 처리하기
  1. 처리한 결과를 B 토픽의 b 파티션에 쓰기
  1. __consumer_offsets 토픽에 1번에 대한 커밋 하기
 
이 과정에서 ‘정확히 한 번’ 원칙을 위반하는 상황들이 있다.
 

리밸런싱에 의한 중복 처리

만약 3번까지 마치고 4번을 하기 전에 애플리케이션이 크래시 된다면 아래 상황이 발생할 수 있다.
  1. 컨수머가 B 토픽의 b 파티션에 처리 결과 기록 완료
  1. __consumer_offsets 토픽에 커밋하기 전에 컨수머 크래시
  1. heartbeat 를 A 토픽의 a 파티션의 리더 브로커에게 전송하지 못함
  1. A 토픽 a 파티션 리밸런싱 발생
  1. A 토픽 a 파티션을 할당받은 다른 컨수머가 1~4번 과정 진행 → B토픽의 b번 파티션에 쓰기 중복 발생
 

좀비 프로듀서에 의한 중복 처리

만약 1번까지 마치고 2번을 하기 전에 애플리케이션이 크래시 된다면 아래 상황이 발생할 수 있다.
  1. c1 컨수머가 A 토픽의 a 파티션에서 메세지 읽기 완료
  1. 읽은 메세지를 처리하기 전에 c1 컨수머 크래시
  1. heartbeat 를 A 토픽의 a 파티션의 리더 브로커에게 전송하지 못함
  1. A 토픽 a 파티션 리밸런싱
  1. A 토픽 a 파티션을 할당받은 c2 컨수머가 1~4번 과정 진행 (B토픽의 b 파티션에 쓰기)
  1. c1 컨수머가 복구되고 나서 2~4번 과정 진행 → B 토픽의 b 파티션에 쓰기 중복 발생
c1 컨수머는 본인이 죽은 줄 모른 채 메세지를 처리했기 때문에 ‘좀비’라고 불린다.
 
 

transactional producer

 
  • 위 상황들은 A 토픽의 메세지를 읽고 B 토픽에 쓰는 것에 대한 커밋을 원자적으로 처리하지 않기 때문에 발생한다.
  • 원자적인 동작을 지원하기 위해 카프카는 atomic multipartition write 기능을 도입했다. (__consumer_offset 토픽에 커밋 메세지를 쓰는 것도 결국에 파티션에 메세지를 쓰는 것과 같다는 사실에 착안)
  • atomic multipartition write 를 수행하려면 트랜잭셔널 프로듀서(transactional producer)를 사용해야 한다.
  • 트랜잭셔널 프로듀서는 일반적인 프로듀서와 다르게 초기화 될 때
      1. initTransactions() 를 호출하고
      1. transactional.id 를 할당 받는다.
 

transactional.id

    transactional.id 는 재시작 이후의 프로듀서를 식별하기 위한 용도로, 프로듀서 프로세스가 죽었다 살아나도 재발급하지 않고 유지된다.
    카프카 브로커는 transactional.idproducer.id 의 연결 정보를 가지고 있는데, 만약 브로커가 이미 가지고 있는 transactional.id의 프로듀서가 죽었다 살아나는 과정에서 initTransactions() 를 호출하면 transactional.id 를 새로 발급하지 않고 기존의 값을 할당해준다.
     

    zombie fencing

    • 좀비 프로듀서에 의한 메세지 중복 기록을 방지하는 방법으로, 에포크(epoch)를 사용한다.
    • 카프카 브로커는 프로듀서가 initTransactions() 를 호출하면 transactional.id 에 해당하는 에포크 값을 증가시킨다.
    • 에포크 값이 낮은 프로듀서는 좀비로 간주하고 해당 프로듀서가 메세지를 전송하거나 트랜잭션을 커밋하려고 하면 브로커는 FenceError 에러를 발생시키며 거부한다.