학습 자료
카프카의 ‘정확히 한 번'(exactly-one) 구조는 멱등성을 보장하는 프로듀서와 트랜잭션의 원자성으로 이뤄진다.
프로듀서는 어떻게 멱등성을 보장하는가
- enable.idempotence 를
true
으로 설정하면 프로듀서가 전송하는 모든 메세지에는 프로듀서의 아이디와 시퀸스 넘버가 붙는다.
- 리더 브로커에서는 각 파티션마다 가장 최근에 기록된 메세지 5개를 추적하는 데에 [프로듀서 아이디 + 시퀸스 넘버] 조합을 사용한다. (5개 미만으로 추적을 제한하고 싶다면 max.in.flights.requests.per.connection 을 5 미만으로 설정한다.)
- 리더 브로커는 추적한 메세지 중에 이미 파티션에 기록된 중복 메세지가 있다면 메세지 기록을 거부한다.
- 거부에 의한 에러는 프로듀서의 record-error-rate, 브로커의 ErrorPerSec 지표에 반영된다.
- 만약 리더브로커에서 예상하는 시퀸스 넘버보다 더 큰 숫자를 받게 될 경우 (예. 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(...)
‘정확히 한 번’ 을 위반하는 상황
애플리케이션의 일반적인 스트림 처리는 다음과 같다.
A
토픽의a
파티션에서 메세지 읽기
- 읽은 메세지를 처리하기
- 처리한 결과를
B
토픽의b
파티션에 쓰기
__consumer_offsets
토픽에 1번에 대한 커밋 하기
이 과정에서 ‘정확히 한 번’ 원칙을 위반하는 상황들이 있다.
리밸런싱에 의한 중복 처리
만약 3번까지 마치고 4번을 하기 전에 애플리케이션이 크래시 된다면 아래 상황이 발생할 수 있다.
- 컨수머가
B
토픽의b
파티션에 처리 결과 기록 완료
__consumer_offsets
토픽에 커밋하기 전에 컨수머 크래시
- heartbeat 를
A
토픽의a
파티션의 리더 브로커에게 전송하지 못함
A
토픽a
파티션 리밸런싱 발생
A
토픽a
파티션을 할당받은 다른 컨수머가 1~4번 과정 진행 →B
토픽의b
번 파티션에 쓰기 중복 발생
좀비 프로듀서에 의한 중복 처리
만약 1번까지 마치고 2번을 하기 전에 애플리케이션이 크래시 된다면 아래 상황이 발생할 수 있다.
c1
컨수머가A
토픽의a
파티션에서 메세지 읽기 완료
- 읽은 메세지를 처리하기 전에
c1
컨수머 크래시
- heartbeat 를
A
토픽의a
파티션의 리더 브로커에게 전송하지 못함
A
토픽a
파티션 리밸런싱
A
토픽a
파티션을 할당받은c2
컨수머가 1~4번 과정 진행 (B
토픽의b
파티션에 쓰기)
-
c1
컨수머가 복구되고 나서 2~4번 과정 진행 →B
토픽의b
파티션에 쓰기 중복 발생
→
c1
컨수머는 본인이 죽은 줄 모른 채 메세지를 처리했기 때문에 ‘좀비’라고 불린다.transactional producer
- 위 상황들은
A
토픽의 메세지를 읽고B
토픽에 쓰는 것에 대한 커밋을 원자적으로 처리하지 않기 때문에 발생한다.
- 원자적인 동작을 지원하기 위해 카프카는 atomic multipartition write 기능을 도입했다. (
__consumer_offset
토픽에 커밋 메세지를 쓰는 것도 결국에 파티션에 메세지를 쓰는 것과 같다는 사실에 착안)
- atomic multipartition write 를 수행하려면 트랜잭셔널 프로듀서(transactional producer)를 사용해야 한다.
- 트랜잭셔널 프로듀서는 일반적인 프로듀서와 다르게 초기화 될 때
initTransactions()
를 호출하고transactional.id
를 할당 받는다.
transactional.id
transactional.id
는 재시작 이후의 프로듀서를 식별하기 위한 용도로, 프로듀서 프로세스가 죽었다 살아나도 재발급하지 않고 유지된다.카프카 브로커는
transactional.id
와 producer.id
의 연결 정보를 가지고 있는데, 만약 브로커가 이미 가지고 있는 transactional.id
의 프로듀서가 죽었다 살아나는 과정에서 initTransactions()
를 호출하면 transactional.id
를 새로 발급하지 않고 기존의 값을 할당해준다.zombie fencing
- 좀비 프로듀서에 의한 메세지 중복 기록을 방지하는 방법으로, 에포크(epoch)를 사용한다.
- 카프카 브로커는 프로듀서가
initTransactions()
를 호출하면transactional.id
에 해당하는 에포크 값을 증가시킨다.
- 에포크 값이 낮은 프로듀서는 좀비로 간주하고 해당 프로듀서가 메세지를 전송하거나 트랜잭션을 커밋하려고 하면 브로커는
FenceError
에러를 발생시키며 거부한다.