카프카에서 정확히 한 번 메시지를 전송하는 방법과 활용 사례, 그리고 한계에 대해 알아본다.
정확히 한 번 이란?
으로 이루어진다.
동일한 작업을 여러번 실행해도 한 번 실행한 것과 같은 서비스를 멱등적이라고 한다.
데이터베이스에서는 다음과 같이 설명된다.
// 멱등적 X
UPDATE t Set x=x+1 where y=5
// 멱등적 O
UPDATE t Set x=18 where y=5
카프카에선 메시지 전송을 재시도함으로써 동일한 작업을 여러번 수행하는 결과가 나올 수 있다.
멱등적 프로듀서 기능을 켜면 메시지는 고유한 프로듀서 ID
와 시퀀스 ID
를 가지게 된다.
따라서 메시지마다 고유한 식별자가 추가됨
각 브로커느 할당된 모든 파티션들에 쓰여진 마지막 5개 메시지를 이 고유 식별자로 추적한다. (max.in.flights.requests.per.connection
: 추적 메시지 개수)
따라서 이런 고유 식별자를 가지고 중복처리를 수행한다.
+) 예상보다 높은 시퀀스 넘버의 메시지를 받게된다면 out of order sequence number
에러를 발생시킨다. (2번 메시지가 와야하는데 23번 메시지가 온 상황)
프로듀서에 장애가 발생할 경우, 보통 새 프로듀서를 생성해서 장애가 난 프로듀서를 대체한다.(쿠버네티스와 같은 장애 복구 프레임워크 사용)
멱등적 프로듀서기능이 켜있을 경우 브로커로부터 프로듀서 ID를 생성받는다. (꺼져있을 경우 새로운 ID발급) 따라서 프로듀서ID와 시퀀스 넘버를 가지고 메시지 중복체크를 진행한다.
브로커에 장애가 발생할 경우 파티션에 대해 새 리더를 선출하게 된다.
리더의 경우 새 메시지가 쓰여질 때 마다 인-메모리 프로듀서 상태에 최근 N개의 시퀀스 넘버를 가지고 있다.
팔로워 레플리카는 리더로부터 새로운 메시지를 받을 때 마다 자체적인 인-메모리 버퍼를 업데이트한다. 즉, 팔로워가 리더가 되어도 이 버퍼를 활용해 중복처리를 진행한다.
+) 예전 리더가 다시 돌아와서 인-메모리 버퍼가 없어도, 상태에 대한 스냅샷을 파일형태로 저장해서 복구작업을 진행한다.
카프카의 멱등적 프로듀서는 프로듀서 내부의 중복 로직만을 방지한다. 따라서 producer.send()
로 똑같은 메시지를 2번 보내면 메시지 중복이 발생하게 된다.
이러한 예외처리는 어플리케이션이 하는 것 보단 카프카 프로듀서의 멱등적 기능을 활용하는 것이 낫다.
프로듀서 설정에 enable.idempotence=true
를 추가한다. 만약 acks=all
이면 성능에는 큰 차이가 없다.
멱등적 프로듀서 기능의 특징으로써는
트랜잭션 기능은 카프카 스트림즈를 사용해서 개발된 어플리케이션에 정확성을 보장하기 위해 도입되었다.
주로 금융 어플리케이션에서의 복잡한 스트림 처리 어플리케이션에는 트랜잭션이 활용된다.
보통 플로우는 다음과 같다.
이 과정에서 다음과 같은 에러가 발생할 수 있다.
어플리케이션은 특정 처리를 하고, 오프셋을 커밋해야 한다. 하지만, 특정 처리를 하고 어플리케이션이 종료된다면? 하트비트가 끊어지면서 리밸런스가 발생하고, 다른 컨슈머가 새로운 파티션을 할당받아 중복된 처리를 진행한다.
위의 상황을 겪었다가 갑자기 어플리케이션이 다시 작동한다면?(오프셋을 또 커밋해버린다면) 다른 컨슈머가 해당 파티션이 할당받은 상태를 알아차릴 때 까지 이 작업을 계속한다면 메시지 중복이 발생할 수 있다.
읽기, 처리, 쓰기 작업이 원자적으로 이루어져야 한다.
카프타 트랜잭션은
원자적 다수 파티션 쓰기
기능을 도입했다.
이는 메시지 커밋을 다수 파티션에 원자적으로 쓰는 것을 의미한다. (커밋 메시지에 consumer-offsets
를 같이 넣음)
이를 활용하기 위해서는 트랜잭션적 프로듀서
를 활용해야 한다. 트랜잭션적 프로듀서와 보통 프로듀서의 차이점이란 transactional.id
설정이 잡혀있어 내부적으로 프로듀서의 아이디와 비교하여 메시지 처리를 진행하게 된다.
프로듀서 뿐만 아니라 컨슈머 격리 수준을 조절해야 한다.(
isolation.level
설정 값)
read_committed
read_uncommitted
(default)즉 read_comitted
모드로 작동중인 컨슈머는 read_uncommitted
로 작동중인 컨슈머보다 더 뒤에 있는 메시지를 할당 받는다.
원자적 쓰기 기능, 멱등적 프로듀서를 활용해도 해결할 수 없는 것이 몇 가지 있다.
어플리케이션 처리 중간에 이메일을 보내는 작업이 포함되어 있다고 하자. 멱등적 프로듀서 기능을 활용한다고 이메일이 한 번만 보내질 것이라 장담할 수 없다. (이는 카프카 레코드에만 적용되는 기능이기에)
따라서 스트림 처리의 외부효과는 정확히 한 번을 제공할 수 없다.
위와 비슷한 맥락이다. 외부 데이터베이스에는 결과를 쓰고 카프카에는 오프셋을 커밋하는 이 두작업을 트랜잭션을 활용해 구현할 수는 없다.(데이터베이스의 트랜잭션 보장에 달렸다.)
이러한 작업을 수행하기 위해서는
아웃 박스 패턴
에 대해 공부해보자.
이러한 종류의 종단 보장은 제공하지 않는다.
이는 카프카 트랜잭션이 아니라 미러메이커를 호라용해야 한다.
메시지를 쓰고 나서 커밋하기 전 다른 어플리케이션이 응답하기를 기다리는 패턴은 지양해야한다. 결과적으로 데드락이 발생하기 때문이다.
read_committed
모드를 활용하면 중단된 트랜잭션에 대한 메시지를 모두가 컨슘하지 못하고 데드락이 발생한다.
가장 권장되는 방법은 카프카 스트림즈에서 exactly-once
를 활성화 하는 것이다. 이렇게 하면 트랜잭션 기능을 직접적으로 사용할 일은 없지만, 알아서 보장을 해준다.
카프카 스트림즈란??
토픽(topic)에 있는 데이터를 낮은 지연과 빠른 속도로 처리
토픽에 적재된 데이터를 실시간으로 변환하여 다른 토픽에 적재하는 라이브러리
하지만, 카프카 스트림즈를 활용하지 않고 정확히 한번
을 보장하고자 한다면 트랜잭션 API를 직접 사용해야한다.
트랜잭션은 어쩔 수 없이 약간의 오버헤드를 발생시킨다.
프로듀서를 생성해서 사용하는 동안 트랜잭션 ID 등록 요청은 단 한 번 발생한다. 트랜잭션의 일부로써 파티션들을 등록하는 추가적인 호출은 각 트랜잭션에 있어서 파티션 별로 최대 한 번씩만 이루어진다.
read_committed
모드로 읽기에 약간 지연이 생긴다.