카프카가 제공하는 신뢰성 보장 항목들은 아래와 같다. 이러한 기본적인 보장들을 신뢰성 있는 시스템 구축을 위해 사용될 수 있지만, 그 자체로는 시스템 전체를 완전히 신뢰성 있게 만들진 못한다. 카프카가 제공하는 설정값들을 조절함으로써 원하는 정도의 신뢰성을 얻도록 설정할 수 있다.
파티션 안의 메시지들 간 순서를 보장한다.
동일한 프로듀서가 동일한 파티션으로 메시지 A → B 순서로 발행했다면, 카프카는 B의 오프셋이 A보다 큰 것을 보장하고 컨슈머는 A를 B보다 먼저 읽게 된다.
클라이언트가 쓴 메시지는 모든 인-싱크 레플리카의 파티션에 쓰여진 뒤에야 '커밋'된 것으로 간주된다.
다만, 프로듀서에게 완료 응답이 가는 시점은 acks 설정을 어떻게 하느냐에 따라 달라진다.
커밋된 메시지들은 최소 1개의 작동 가능한 레플리카가 남아 있는 한 유실되지 않는다.
컨슈머는 커밋된 메시지만 읽을 수 있다.
복제
카프카는 하나의 메시지를 여러 레플리카에 씀으로써 크래시가 나더라도 메시지의 지속성을 유지한다. 카프카 토픽의 각 파티션은 다수의 레플리카를 가질 수 있고, 레플리카들 중 하나가 리더가 된다. 모든 이벤트들은 리더 레플리카에 쓰여지며 (일반적으로) 컨슈머는 리더 레플리카를 통해 메시지를 읽어온다. 팔로워 레플리카들은 단순히 리더와 동기화를 맞추면서 최신 이벤트를 제 시간에 복사해오면 되며, 리더가 장애가 났을 때 인-싱크 레플리카 중 하나가 리더로 선출된다.
레플리카는 파티션의 리더 레플리카이거나 아래 조건을 만족하는 팔로워 레플리카에 대해 인-싱크 상태로 본다.
주키퍼와의 활성 세션이 있다. 최근 X초 사이에 주키퍼로 하트비트를 전송했다.
zookeeper.session.timeout.ms - 2.5.0 버전 기준 기본값 18초
최근 X초 사이 리더로부터 메시지를 읽어왔다.
최근 X초 사이에 리더로부터 읽어 온 메시지가 가장 최신 메시지이며, 최근 X초 사이에 Lag이 발생하지 않았던 적이 한 번은 있어야 한다.
관련 설정값: replica.lag.time.max.mx - 2.5.0 버전 기준 기본값 30초
만약, 위 조건들을 만족하지 못하여 동기화가 풀린 레플리카는 아웃-오브-싱크 상태로 간주되며, 위 조건들을 다시 만족하면 다시 인-싱크 레플리카가 된다.
인-싱크 레플리카 모두에 메시지가 쓰였을 때 메시지 커밋으로 간주하기 때문에, 동기화가 살짝 늦은 인-싱크 레플리카는 프로듀서와 컨슈머 모두를 느리게 만들 수 있다. 레플리카의 동기화가 풀리면 아웃-오브-싱크 레플리카로 간주되어 해당 레플리카가 메시지를 받을 때까지 기다리지 않는다.
참고로, 카프카는 디스크에 저장되지 않은 메시지에 대해서도 응답을 한다. 카프카는 세그먼트를 교체할 때와 재시작 직전에만 메시지를 디스크로 플러시하며, 그 외 상황에서는 리눅스의 페이지 캐시 기능에 의존한다. 만약, 브로커가 디스크에 더 자주 메시지를 저장하도록 하고 싶다면 flush.messages(디스크에 저장되지 않은 최대 메시지 수)와 flush.ms(얼마나 자주 디스크에 메시지 저장할지) 설정을 조절할 수 있다. 단, 이 설정 변경이 카프카 처리량에 영향을 주는지는 확인이 필요하다.
브로커 설정
메시지 저장 신뢰성과 관련된 브로커 설정 매개변수는 3개가 있으며, 브로커 단위에 적용할 수도 있고 특정 토픽 단위로도 제어가 가능하다. 따라서, 같은 카프카 클러스터 내에 신뢰성이 필요한 토픽과 아닌 토픽을 함께 저장할 수 있다.
복제 팩터가 N이면, 각 브로커가 N대의 서로 다른 브로커에 N개 복제된다는 것을 의미한다. 즉, N-1개의 브로커가 중단되더라도 토픽의 데이터를 읽거나 쓸 수 있다. 복제 팩터가 클수록 가용성과 신뢰성은 늘어나도, 장애가 발생할 가능성은 줄어든다. 반대로 복제 팩터가 N이면 최소 N개의 브로커가 필요하고, 복사본도 N개 저장해야 하므로 디스크 공간이 N배로 필요하다 → 가용성과 하드웨어 사용량 사이 트레이드오프
[ 복제 팩터 결정 시 고려 사항 ] - 가용성: 레플리카 수가 많을 수록 가용성 늘어남 - 지속성: 레플리카 수가 많을 수록 데이터 유실 가능성 줄어듬 - 처리량: 레플리카 수가 많을 수록 브로커간 복제 트래픽이 늘어남 - 종단 지연: 레플리카 수가 많을 수록 일반적으로 인-싱크 레플리카 수도 늘어나므로 메시지가 커밋될 때까지 시간이 늘어날 가능성이 높음 - 비용: 레플리카 수가 많을 수록 저장소와 네트워크에 들어가는 비용 증가함 - 레플리카 위치: 같은 가용 영역(AZ)에 모든 레플리카를 두면 가용 영역에 문제가 생겼을 때 해당 파티션을 아예 사용할 수 없으므로 분산 저장 필요
언클린 리더 선출 (클러스터 단위 설정) - unclean.leader.election.enable
기본적으로 파티션의 리더가 불능 상태일 때 인-싱크 레플리카 중 하나가 새 리더가 된다. 인-싱크 레플리카 중 하나가 리더가 되므로, 커밋된 데이터는 유실이 없기 때문에 클린 리더 선출이라고 본다.
이때, 작동 불능에 따진 리더 외 인-싱크 레플리카가 없다면 아래와 같이 동작된다. - 언클린 리더 선출 false(기본값) - 예전 리더가 복구될 때까지 해당 파티션은 오프라인 상태 - 언클린 리더 선출 true - 아웃-오브-싱크 레플리카가 새 리더가 되며, 새 리더가 동기화하지 못한 예전 리더에 쓰인 메시지는 유실되고 컨슈머들 간에 일관성 깨짐 - 예) 기존 리더에 오프셋 100~200 메시지가 쓰였으나, 새 리더에는 해당 메시지가 아직 쓰이지 못했음. 이때, 인-싱크 레플리카는 기존 리더 뿐이었어서, 컨슈머는 기존 리더의 오프셋 100~200 메시지를 읽을 수 있음. 컨슈머 그룹 A는 기존 리더 기준으로 100~200 오프셋을 읽었고, 컨슈머 그룹 B는 속도가 느려서 언클린 리더로 선출된 새 리더에 새로이 쓰인 100~200 오프셋을 읽어 서로 다른 데이터를 읽게될 수 있음
최소 인-싱크 레플리카 (토픽/브로커 단위) - min.insync.replicas
예를 들어, 복제 팩터는 3개를 설정했어도 리더 레플리카 외에는 모두 아웃-오브-싱크 레플리카가 될 수 있다. 이때 리더 레플리카(유일한 인-싱크)에 문제가 생기면 가용성과 일관성 사이 하나를 골라야 하는 상황이 벌어진다.
따라서, 인-싱크 레플리카의 최소값을 1보다 높은 값으로 설정하면 적어도 팔로워 레플리카 하나에 데이터가 쓰일 수 있는 상태일 때 프로듀서로부터 메시지를 받게 된다.
만약, 최소 인-싱크 레플리카 설정에 도달하지 못하는 상황이 오면 브로커는 더 이상 쓰기 요청을 받지 않고, 데이터 전송을 시도하는 프로듀서에 NotEnoughReplicationException을 던진다. (사실상 읽기 전용 레플리카가 됨)
신뢰성 있는 시스템에서 프로듀서 사용하기
사용 가능한 높은 신뢰성 설정을 브로커에 적용하더라도, 프로듀서 역시 신뢰성이 있도록 설정하지 않으면 시스셈 전체로서는 여전히 데이터가 유실될 수 있다. 따라서, 프로듀서를 구성할 때에는 아래 두가지를 신경써야 한다.
신뢰성 요구 조건에 맞는 올바른 acks 설정을 사용한다.
설정과 코드 모두에서 에러를 올바르게 처리한다.
응답(acknowledgement) 모드
acks=0
프로듀서가 네트워크로 메시지를 전송한 시점에 카프카에 메시지가 성공적으로 쓰여진 것으로 간주
단, 컨슈머는 인-싱크 레플리카가 해당 메시지를 모두 받아서 메시지가 커밋된 이후에 받을 수 있기 때문에 종단 지연은 개선되지 않음
acks=1
리더가 메시지를 받아서 파티션 데이터 파일을 쓴 직후 응답 또는 에러를 보낸다
팔로워에 복제가 완료되기 전 리더가 불능이 되면 데이터가 유실될 수 있음
acks=all
리더가 모든 인-싱크 레플리카가 메시지를 받아갈 때까지 기다렸다가 응답하거나 에러를 보냄
브로커의 min.insync.replicas 설정과 함께 이 설정은 응답이 오기 전까지 얼마나 많은 레플리카에 메시지가 복제될 지 조절함
프로듀서는 모든 인-싱크 레플리카가 메시지를 받을 때까지 기다린 후 메시지 배치에 완료 표시를 하고 다음 작업을 진행함
프로듀서 재시도 설정하기
프로듀서의 에러 처리는 크게 2가지로 나뉘며, 하나는 프로듀서가 자동으로 처리해주는 에러이고 나머지 하나는 개발자가 직접 처리해야 하는 에러이다.
기본적으로 프로듀서는 재시도 가능한 에러를 처리해주며, 재시도를 통해 해결 가능한 에러는 프로듀서가 알아서 재시도를 한다.
LEADER_NOT_AVAILABLE과 같은 에러는 일시적인 에러로 판단하여 재시도를 진행하며, INVALID_CONFIG와 같은 에러는 메시지를 재전송한다고 해서 해결되지 않는다고 판단하여 따로 재시도를 진행하지 않는다.
이 외에 개발자가 직접 처리해야 하는 에러는 아래와 같은 것들이 있다.
메시지 크기에 관련되었거나 인가 관련 에러와 같이 재시도가 불가능한 브로커 에러
메시지가 브로커에 전송되기 전에 발생한 에러(예: 직렬화 과정에서 발생한 에러)
프로듀서가 모든 재전송 시도를 소진했거나, 재시도 과정에서 프로듀서가 사용하는 가용 메모리가 메시지로 가득 차서 발생하는 에러
타임아웃
신뢰성 있는 시스템에서 컨슈머 사용하기
컨슈머가 해야 할 일은 어느 메시지까지 읽었고 어디까지는 읽지 않았는지 추적하는 것이다. 이는 메시지를 읽는 도중 누락되지 않도록 하기 위해서는 필수적이다.
파티션으로부터 데이터를 읽어올 때, 컨슈머는 메시지를 배치 단위로 읽고 배치별로 마지막 오프셋을 확인한 뒤 마지막 오프셋 값에서 시작하는 다른 메시지 배치를 요청한다. 특정 카프카 컨슈머가 작동을 정지해서, 해당 컨슈머가 읽던 파티션을 다른 컨슈머가 읽게되면 어디서부터 읽을지 알아야 한다. 이때 활용하는 것이 오프셋이기 때문에 컨슈머가 읽어온 오프셋을 커밋해야 한다.
커밋된 메시지 VS 커밋된 오프셋 이 둘은 완전히 다른 의미를 갖는다. - 커밋된 메시지: 모든 인-싱크 레플리카에 쓰여져 컨슈머가 읽을 수 있는 메시지 - 커밋된 오프셋: 컨슈머가 특정 파티션 어느 오프셋까지의 모든 메시지를 받아 처리를 완료했는지 알리고자 카프카에 보낸 오프셋
컨슈머 설정
원하는 신뢰성 수준을 갖추기 위해 알아야 하는 컨슈머 설정들은 다음과 같다.
설정
설명
group.id
같은 그룹 ID를 갖는 두 개의 컨슈머가 같은 토픽을 구독하면, 이 컨슈머들은 서로 다른 파티션들을 읽는다. 이때, 같은 그룹 기준으로는 모든 파티션을 읽어 메시지 누락은 없다.
auto.offset.reset
컨슈머가 처음 시작되어서 커밋된 오프셋이 없거나, 컨슈머가 브로커에 없는 오프셋을 요청할 때 어디서부터 메시지를 읽어올 지 결정한다. - earliest: 유효한 오프셋이 없으면 파티션 맨 앞부터 읽음(많은 메시지 중복 처리 발생 가능) - latest: 유효한 오프셋이 없으면 파티션 끝에서부터 읽음(중복 처리 최소화하지만, 일부 메시지 누락)
enable.auto.commit
일정 시간에 맞춰 컨슈머가 알아서 오프셋을 커밋할지, 직접 오프셋을 커밋하도록 로직을 구현할지 결정한다. 애플리케이션이 백그라운드에서 처리를 수행하기 위해 다른 스레드에 레코드를 넘기는 등 복잡한 처리가 필요할 경우, 직접 오프셋을 커밋하는 방식을 쓴다.
직접 오프셋을 커밋하기로 했다면 아래 내용을 바탕으로 정확성과 성능 영향도도 파악을 해야한다. - 메시지 선 처리 후 오프셋 커밋 - 커밋 빈도는 성능과 크래스 발생 시 중복 개수 사이 트레이드오프 - 커밋 작업은 상당한 성능 오버헤드를 수반함 - 메시지를 읽어올때마다 커밋하는 방식은 매우 낮은 빈도로 메시지가 들어오는 토픽에나 사용 가능(보통은 배치리스너로 설정하여 여러 메시지를 한 번에 읽어오고 해당 메시지 처리가 모두 성공하면 커밋하는 식) - 정확한 시점에 정확한 오프셋 커밋 필요 - 리밸런스 - 리밸런스가 발생할 경우, 할당된 파티션이 해제되기 전 오프셋을 커밋하여야 함 - 컨슈머는 재시도해야 할 수도 있음 - 컨슈머가 받은 배치 메시지들 중 일부에 대해 에러가 발생했을 때, 별도의 토픽(DLT)에 쓴 뒤 따로 재시도를 하도록 할 수 있음 - 컨슈머가 상태를 유지해야 할 수도 있음
auto.commit.interval.ms
오프셋 자동 커밋을 켤 경우, 이 설정으로 커밋되는 주기를 설정한다.
멱등적 프로듀서
동일한 작업을 여러 번 실행해도 한 번 실행한 것과 결과가 같은 서비스를 멱등적(idempotent)이라고 한다.
멱등적 프로듀서 기능은 프로듀서 설정의 enable.idempotence을 true로 설정하면 사용이 가능하다. 멱등적 프로듀서 기능 활성화 시, 모든 메시지는 고유한 프로듀서 ID(producer ID, PID)와 시퀀스 넘버(sequence ID)를 가지게 된다. 대상 토픽 및 파티션과 이 두 값을 합치면 각 메시지의 고유한 식별자가 된다. 각 브로커는 해당 브로커에 할당된 모든 파티션들에 쓰여진 마지막 5개 메시지들을 추적하기 위해 이 식별자를 사용한다.
파티션별로 추적되어야 하는 시퀀스 넘버의 수를 제한하고 싶다면 프로듀서의 max.in.flights.requrests.per.connection 설정값이 5 이하로 설정되어야 함
브로커는 이전에 받은 메시지를 다시 받으면, 적절한 에러를 발생시킴으로써 중복 메시지를 거부한다. 이 에러는 프로듀서에 로깅도 되고 지표값에도 반영되지만, 예외가 발생하지는 않으므로 사용자에 경보를 보내진 않는다.
프로듀서 클라이언트에서는 record-error-rate 지표값을 통해 에러를 확인할 수 있고, 브로커는 RequestMetrics 유형의 ErrorsPerSec 지표값에 기록된다.
프로듀서 재시작
멱등적 프로듀서 기능이 켜있는 상태로 프로듀서가 재시작하여 초기화되면 카프카 브로커로부터 프로듀서 ID를 받는다. 트랜잭션 기능을 켜지 않으면, 프로듀서를 초기화할때마다 새로운 ID로 설정되어 새로운 프로듀서가 기존 프로듀서가 보낸 메시지를 재전송할 경우 브로커는 메시지 중복이 있음을 알아차릴 수 없다.
브로커 장애
리더 레플리카는 새 메시지가 쓰여질 때마다 인-메모리 프로듀서 상태에 저장된 최근 5개의 시퀀스 넘버를 업데이트한다. 팔로워 레플리카는 리더로부터 새로운 메시지를 복제할 때마다 자체적인 인-메모리 버퍼를 업데이트한다.
만약 브로커 장애가 발생하면, 컨트롤러는 장애가 난 브로커가 리더로 맡고 있던 파티션들에 대해 새 리더를 선출하며, 새로 리더가 된 레플리카는 메모리 안에 최근 5개의 시퀀스 넘버를 가지고 있게 된다. 이로 인해, 이슈 없이 새로 쓰여진 메시지가 중복된 메시지인지 유효성 검증이 가능하다. 이후에 예전 리더가 다시 복구되어서 레플리카로 들어오면, 현재 리더로부터 복제한 레코드를 사용해 프로듀서 상태를 업데이트하면서 최신 상태를 복구한다.
멱등적 프로듀서 한계
멱등적 프로듀서는 프로듀서 내부 로직으로 인한 재시도가 발생할 경우(프로듀서, 네트워크, 브로커 에러로 인해 발생하는) 생기는 중복만 방지가 가능하다. 예를 들어 동일한 메시지를 가지고 producer.send()를 두 번 호출하는 경우 중복 메시지로 판단되지 않아서 중복된 메시지가 발생할 수 있다.
트랜잭션
트랜잭션 기능은 카프카 스트림즈를 사용해 개발된 애플리케이션에 정확성을 보장하기 위해 도입되었다. 스트림 처리 애플리케이션을 위해 특별히 개발되었기 때문에 스트림 처리 애플리케이션의 기본 패턴인 '읽기-처리-쓰기' 패턴에서 사용되도록 개발되었다.
트랜잭션이 해결하는 문제
애플리케이션 크래시로 인한 재처리
원본 클러스터로부터 메시지를 읽은 후 애플리케이션은 두 가지를 해야 한다. 첫 번째로는 결과를 출력 토픽에 써야 하고, 두 번째로는 읽어 온 메시지의 오프셋 커밋을 해야 한다.이 두 작업 중 출력 토픽에 결과 작성만 성공한 뒤 크래시가 발생하면 오프셋이 업데이트가 되지 않아서 중복이 발생한다.
좀비 애플리케이션에 의해 발생하는 재처리
애플리케이션이 카프카로부터 레코드 배치를 읽어온 후 중간에 멈추는 상황이 생겨 하트비트가 끊겨 애플리케이션이 죽은 것으로 간주되어 다른 컨슈머가 이어서 처리를 하고 있다고 가정한다. 이때, 멈췄던 애플리케이션이 다시 작동을 하게 되어서, 이전에 처리하던 데이터를 이어서 처리하면, 이미 다른 컨슈머가 할당받아 작업을 처리했음에도 중복 처리가 될 수 있다.
트랜잭션이 '정확히 한 번' 보장하는 방법
'정확히 한 번' 처리라는 것은 읽기, 처리, 쓰기 작업이 원자적으로 이루어져야 한다는 의미다. 읽어 온 원본 메시지의 오프셋 커밋 및 메시지 쓰기 작업이 모두 일어나거나, 둘 다 일어나지 않거나 해야 한다.
카프카 트랜잭션은 부분적인 성공이 발생하지 않도록 원자적 다수 파티션 쓰기 기능(atomic multipartition write)을 도입했다. 트랜잭션을 시작해서 출력 토픽과 _consumer_offsets 토픽에 메시지를 쓰고 둘 다 성공해서 커밋할 수 있다면, 그 다음부터는 '정확히 한 번' 의미 구조가 알아서 해준다.
트랜잭션적 프로듀서와 여러 파티션에 대한 원자적 쓰기
트랜잭션을 사용한 원자적 다수 파티션 쓰기를 수행하려면 트랜잭션적 프로듀서를 사용해야 한다. 트랜잭션적 프로듀서와 보통 프로듀서간 차이는 transactional.id 설정이 잡혀있고 initTransactions()을 호출해서 초기화했다는 것뿐이다. transactional.id는 브로커가 자동 생성해주는 producer.id와 다르게 프로듀서 설정의 일부이며 재시작을 해도 값이 유지된다.
애플리케이션의 좀비 인스턴스가 중복 프로듀서 생성을 막으려면 좀비 펜싱(zombie fencing) 혹은 애플리케이션의 좀비 인스턴스가 출력 스트림에 결과를 쓰는 것을 방지해야 한다. 일반적인 좀비 펜싱 방법은 에포크(epoch)를 사용하는 것이며, transactional.id에 해당하는 에포크 값을 증가시키는 형태로 동작시킨다.만약 동일한 transactional.id를 가진 프로듀서 중 에포크 값이 낮은 프로듀서가 메시지를 전송한다면 FencedProducer 에러가 발생하면서 거부된다.
버전 2.5까지 펜싱(fencing)을 보장하는 유일한 방법은 트랜잭션 ID를 파티션에 정적으로 대응하는 것 뿐이었다. 이 경우, 각 파티션이 하나의 트랜잭션 ID에 의해 읽혀짐은 보장하지만, 만약 아예 기존 프로듀서(A)가 연결이 끊어졌을 때 아예 새로운 프로듀서(B)가 들어오면, A 프로듀서가 다시 살아나면 좀비이지만, B 프로듀서와 트랜잭션 ID가 달라서 펜싱되지 않는다.
아파치 카프카 버전 2.5에는 펜싱을 수행하는 두 번째 방법인 트랜잭션 ID와 컨슈머 그룹 메타데이터를 함께 사용하는 펜싱이 도입되었다. 여기서는 프로듀서의 오프셋 커밋 메소드를 호출할 때 컨슈머 그룹 ID가 아닌 컨슈머 그룹 메타데이터를 인수로 전달한다. 리밸런싱 후 트랜잭션적 레코드 프로세서
위 그림처럼 좀비가 된 A 프로듀서의 트랜잭션은 이전 세대의 컨슈머 그룹에서 온 것으로 판단하여 펜싱된다.
트랜잭션은 대부분 프로듀서 쪽 기능이다. 트랜잭션적 프로듀서 생성 및 트랜잭션 시작, 레코드 쓰기, 오프셋 쓰기 등 모두 프로듀서에서 이루어진다. 트랜잭션 기능을 사용해 쓰여진 레코드는 결과적으로 중단된 트랜잭션에 속해도 파티션에 모두 쓰여지기 때문에 컨슈머에 올바른 격리 수준이 설정되어 있어야 기대하는 '정확히 한 번' 보장이 이루어진다.
isolation.level에 따른 컨슈머 동작 방식 차이
컨슈머에서는 isolation.level 설정값을 설정하여 트랜잭션 기능을 써서 쓰여진 메시지들을 읽어오는 방법을 정의한다. isolation.level을 read_committed로 잡혀 있으면 토픽들을 구독한 뒤 consumer.poll() 호출 시 커밋된 트랜잭션에 속한 메시지나 애초부터 트랜잭션에 속하지 않은 메시지만 리턴된다. 만약, isloation.level의 기본값인 read_uncommitted로 설정한 상태로 poll()을 호출하면 진행중이거나 중단된 트랜잭션에 속한 레코드를 포함한 모든 레코드가 리턴된다.
read_committed 모드에서는 아직 진행중인 트랜잭션이 처음으로 시작된 시점(Last Stable Offset, LSO) 이후에 쓰인 메시지는 리턴되지 않는다.
read_committed 모드를 사용해도 특정 트랜잭션에 속한 모든 메시지가 리턴되는 것을 보장할 수 없다. 트랜잭션에 속하는 토픽의 일부만 구독하기 때문에, 일부 메시지만 리턴받을 수도 있다.
트랜잭션으로 해결할 수 없는 문제들
트랜잭션 기능은 다수의 파티션에 대해 원자적 쓰기 기능(읽기 X)을 제공하고, 스트림 처리 애플리케이션에서 좀비 프로듀서를 방지하기 위한 목적으로 추가되었다. 트랜잭션 기능과 관련된 자주 하는 실수는 '정확히 한 번 보장'이 카프카에 대한 쓰기 이외 동작에도 보장한다고 착각하는 것과 컨슈머가 항상 전체 트랜잭션을 읽어 온다(트랜잭션 간 경계 알고 있다)고 가정하는 것이다.
카프카의 트랜잭션 기능이 '정확히 한 번' 보장에 도움되지 않는 경우는 아래와 같다.
스트림 처리 부수 효과
스트림 처리 애플리케이션의 처리 단계에 이메일 보내는 작업(REST API 호출, 파일 쓰기 등 외부 효과 일으키는 작업)이 있다면, 이메일 발송은 취소할 수 있는 작업이 아니라서, 정확히 한 번이라고 해서 한번만 메일이 가는 것이 아니다.
카프카 토픽에서 읽어서 데이터베이스에 쓰는 경우
데이터베이스에 결과물 쓰는 것은 프로듀서가 사용되지 않는다. 하나의 트랜잭션에서 외부 데이터베이스에는 결과를 쓰고 카프카에는 오프셋을 커밋할 수 있는 기능은 없다. 물론, 트랜잭션에서 데이터와 오프셋을 동시에 데이터베이스에 커밋(데이터베이스가 트랜잭션 보장한다면)하는 것은 가능하다.
데이터베이스에서 읽어서 카프카에 쓰고, 다시 다른 데이터베이스에 쓰는 경우
카프카 트랜잭션은 종단 보장에 필요한 기능을 가지고 있지 않다.
발행/구독 패턴
read_committed 모드가 설정된 컨슈머들은 중단된 트랜잭션에 속한 레코드들을 보지 못하는 것은 맞지만, 오프셋 커밋 로직 등에 따라 컨슈머들은 메시지를 한 번 이상 처리할 수도 있다.
트랜잭션 성능
트랜잭션은 프로듀서에 약간의 오버헤드를 발생시킨다. 프로듀서 생성 시, 트랜잭션 ID 등록 요청이 발생하는 것과 트랜잭션의 일부로 파티션 등록은 추가적인 호출이 일어나는 등이 오버헤드로 볼 수 있다. 다만, 프로듀서의 오버헤드는 트랜잭션 내 메시지 수와는 무관하기 때문에 트랜잭션마다 많은 수의 메시지를 보내는 것이 상대적으로 오버헤드가 적고 동기적으로 실행되는 단계 수도 줄어든다.
컨슈머에 대해서는 커밋 마커를 읽어오는 작업과 관련하여 오버헤드가 약간 발행한다. 다만, 컨슈머는 아직 완료되지 않은 트랜잭션에 속하는 메시지들을 버퍼링할 필요는 없다. 브로커는 컨슈머가 보낸 읽기 요청을 받아도, 컨슈머가 read_committed 모드이면 이러한 메시지들을 리턴하지 않기 때문이며, 이로 인해 트랜잭션 데이터를 읽을 때 컨슈머 쪽 추가적인 작업은 없다.