각 파티션마다 메시지가 저장되는 위치
를 오프셋이라 합니다.
컨슈머 그룹은 __consumer_offsets라는 곳에 각 파티션별 offset을 어디까지 소비했는지 저장합니다.
이를 오프셋 커밋이라 하며, 오프셋 커밋을 통해 __consumer_offsets에는 groupId : topic : partition
별로 다음으로 전달해야 할 offset이 저장됩니다.
카프카는 레코드를 개별적으로 커밋하지 않고 성공적으로 처리해낸 마지막 메시지를 커밋함으로써 그 앞의 모든 메시지들 역시 성공적으로 처리된 것으로 간주합니다.
리밸런싱 작업이 발생하면 컨슈머는 이전에 처리하고 있던 것과는 다른 파티션을 할당받게 될 수 있고, 이때 새로운 작업을 어디서부터 재개해야 하는지 알아내기 위해 오프셋을 활용하게 됩니다.
컨슈머 그룹에 속한 컨슈머들은 자신이 구독하는 토픽의 파티션들에 대한 소유권을 공유하고 있습니다. 즉, 같은 그룹 내에서 컨슈머들은 누가 어떤 파티션을 소유하고 있는지를 알고 있습니다.
이러한 상황에서 할당된 파티션을 다른 컨슈머에게 할당해주는 작업을 리밸런스라고 합니다.
새로운 컨슈머를 컨슈머 그룹에 추가하거나, 컨슈머 종료 혹은 크래시가 발생하거나, 컨슈머 그룹이 읽고 있는 토픽에 변경이 발생했을 때 리밸런스 작업이 필요합니다.
복제는 카프카가 높은 신뢰성을 보장하기 위한 핵심 기능입니다. 하나의 파티션은 다수의 레플리카를 가질 수 있습니다.
하나의 메시지를 여러 레플리카가 함께 보관함으로써 일부 파티션에 크래시가 발생해도 메시지의 지속성을 유지할 수 있게 됩니다.
대부분의 이벤트를 처리하는(읽기 및 쓰기 작업을 진행하는) Partition을 리더 레플리카, 리더를 복사하는 작업만 하는 Partition을 팔로워 레플리카라고 합니다.
팔로워 레플리카가 리더 레플리카와 동기화를 유지하기 위해 보내는 요청은 컨슈머가 메시지를 읽어오기 위해 사용하는 요청과 동일합니다.
리더가 동작 불능 상태가 되었을 때는 인-싱크 레플리카 중 하나가 새로운 리더가 됩니다.
카프카에서는 같은 클러스터에 속한 노드(파티션)간의 데이터 교환을 복제
라고 하며, 이와 구분하기 위해 카프카 클러스터 간의 데이터 복제를 미러링
이라고 합니다.
인-싱크 레플리카(ISR)란 간단히 말해 리더를 대신할 수 있는 상태의 레플리카라고 할 수 있습니다.
다음 조건을 만족하는 팔로워 레플리카는 인-싱크 상태인 것으로 간주합니다.
그렇기 때문에 아웃-오브-싱크상태가 된 레플리카더라도 리더 파티션에 쓰여진 가장 최근 메시지까지를 따라잡기만 하면 얼마든지 다시 인-싱크 레플리카가 될 수 있습니다.
각 파티션은 ‘현재 리더’외에 ‘선호 리더’를 가지게 됩니다. 선호 리더란 토픽이 처음 생성되었을 때 리더였던(처음에 설정을 통해 미리 지정한 리더) 레플리카를 가리킵니다.
파티션이 처음 생성되던 시점에는 리더 레플리카가 모든 브로커에 걸쳐 균등하게 분포되기 때문에 선호(preferred)라는 표현이 붙었습니다.
클러스터 내의 파티션들이 가진 선호 리더가 모두 실제 리더로 선출될 경우 이는 모든 부하가 브로커 사이에 균등하게 분배될 것이라고 생각할 수 있습니다.
선호 리더를 식별하는 가장 좋은 방법은 파티션의 레플리카 목록을 살펴보는 것입니다. 레플리카 목록에 있는 첫 번째 인-싱크 레플리카가 최초에 리더 레플리카로 선출되기 때문에 이 레플리카가 곧 선호 리더입니다.
아웃-오브-싱크 레플리카 중 하나를 리더로 선출하는 언클린 리더 선출 방식도 존재합니다.
프로듀서가 쓰기 작업이 성공했다고 판단하기 위해 얼마나 많은 파티션 레플리카가 해당 레코드를 받아야 하는지를 결정하는 값입니다.
acks = 0
acks = 1
acks = all
카프카는 acks값을 조절하고 해당 브로커가 메시지를 받지 못했을 경우 다시 재전송을 통해 ‘최소 한 번’처리를 보장할 수 있습니다.
하지만 이러한 ‘최소 한 번’처리는 중복의 가능성을 내포하고 있습니다. ‘계좌 잔액 차감하기’와 같은 일부 행동은 중복 처리되면 치명적인 문제가 발생할 수도 있습니다.
동일한 작업을 여러 번 실행해도 한 번 실행한 것과 결과가 동일한 것을 멱등
하다고 합니다. 카프카의 멱등적 프로듀서는 자동으로 중복을 탐지하고 이를 처리해 줍니다. 이를 ‘정확히 한 번’이라고 합니다.
프로듀서 설정에 enable.idempotence=true를 추가함으로써 멱등적 프로듀서를 사용할 수 있습니다.
멱등적 프로듀서 기능을 켜면 모든 메시지는 고유한 프로듀서 ID(PID)와 시퀀스 넘버를 가지게 됩니다. PID와 시퀀스 넘버, 대상 토픽 및 파티션을 모두 합치면 각 메시지의 고유한 식별자가 됩니다.
각 브로커는 이 고유 식별자를 사용해 자신의 파티션들에 쓰여진 마지막 5개(변경 가능)의 메시지를 추적합니다.
추적을 통해 이전에 받은 적 있는 메시지를 또 받았다는 사실을 알게 될 경우 적절한 에러를 발생시킴으로써 중복 메시지를 거부합니다. 이 에러는 피로듀서에 로깅도 되고 지표값에도 반영되지만 예외가 발생하지는 않습니다.
따라서 사용자에게 별도의 경보가 보내지지는 않습니다.
하지만 카프카의 멱등적 프로듀서는 프로듀서의 내부 로직으로 인한 재시도가 발생할 경우 생기는 중복
만을 방지합니다.
동일한 메시지(내용은 같지만 ID값은 다름)를 가지고 producer.send()를 두 번 호출했을 때는 명등적 프로듀서가 개입하지 않으므로 중복을 방지할 수 없습니다. 또한 여러 개의 인스턴스를 띄우거나 하나의 인스턴스에서 여러 개의 프로듀서를 띄우는 경우, 이 프로듀서들 중 두 개가 동일한 메시지를 전송하려 시도해도 마찬가지로 멱등적 프로듀서는 중복을 방지하지 못합니다.
멱등적 프로듀서는 프로듀서 자체의 재시도 메커니즘(프로듀서, 네트워크, 브로커 에러로 인해 발생하는)에 의한 중복만을 방지할 뿐 그 이상의 중복 방지는 하지 못합니다.
카프카의 트랜잭션은 카프카 스트림즈를 사용해서 개발된 애플리케이션에 정확성을 보장하기 위해
만들어진 기능입니다. 그런 만큼 스트림 처리 애플리케이션의 기본 패턴인 '읽기-처리-쓰기'패턴
에서만 ‘정확히 한 번’을 보장할 수 있습니다.
카프카 트랜잭션은 원자적 다수 파티션 쓰기
를 통해 ‘정확히 한 번’을 보장합니다. '읽기-처리-쓰기'패턴에서 마지막 쓰기 작업으로 결과를 토픽에 쓰는 것과, 오프셋을 __consumer_offsets 토픽에 쓰는 것 둘 다 쓰기 작업을 진행하는 것입니다. 두 쓰기 작업을 원자적 다수 파티션 쓰기로 실행함으로써 두 작업 사이의 all or nothing을 보장합니다.
트랜잭션을 사용해 원자적 다수 파티션 쓰기를 수행하려면 트랜잭션적 프로듀서를 사용해야 합니다. 트랜잭션적 프로듀서는 transactional.id설정이 잡혀 있고 initTransactions()을 호출해서 초기화해 줍니다.
카프카 브로커에 의해 자동으로 설정되는 producer.id와 달리 transactional.id는 프로듀서 설정의 일부이며, 재시작을 하더라도 값이 유지됩니다. 카프카 브로커는 transactional.id에서 producer.id로의 대응 관계를 유지하고 있다가 만약 이미 있는 transactional.id프로듀서가 initTransactions()를 다시 호출하면 새로운 랜덤값이 아닌 이전에 쓰던 producer.id값을 할당해 줍니다.
트랜잭션 기능을 사용해서 쓰여진 레코드는 비록 결과적으로 중단된 트랜잭션에 속할지라도 다른 레코드들과 마찬가지로 파티션에 쓰여지게 됩니다. 그렇기 때문에 올바른 격리수준을 통해 메시지를 읽어드리는 방식을 제어해야 합니다.
카프카의 트랜잭션 기능은 다음과 같은 상황의 ‘정확히 한 번’은 보장하지 못합니다.