프로듀서의 재시도 때문에 생기는 중복을 브로커가 알아서 걸러내게 하는 기능. 멱등성이란 같은 작업을 여러 번 해도 한 번 한 것과 결과가 같은 성질로, UPDATE t SET x=x+1 WHERE y=5는 비멱등이고 UPDATE t SET w=18 WHERE y=5는 멱등이다.
재시도가 만드는 중복의 모양
재시도가 만드는 중복은 이런 모양이다. 파티션 리더가 레코드를 받아 팔로워들에게 성공적으로 복제했는데, 프로듀서에게 응답을 보내기 직전에 리더가 있는 브로커에 크래시가 난다. 프로듀서는 응답을 받지 못한 채 타임아웃이 나서 메시지를 재전송하고, 재전송된 메시지가 새 리더에 도착한다. 그런데 그 메시지는 이미 저장되어 있다.
프로듀서 에러, 네트워크 에러, 브로커 에러로 인한 중복이 전부 이 모양이고, 재고 처리나 재무제표나 중복 배송처럼 그것이 곧 사고가 되는 자리가 많다.
PID와 시퀀스 넘버
기능을 켜면 모든 메시지가 고유한 식별자를 가진다. 프로듀서가 시작될 때 브로커로부터 받는 PID(프로듀서 ID, long)와, 특정 토픽-파티션으로 가는 각 메시지 배치마다 0부터 1씩 증가하는 SID(시퀀스 넘버, integer)를 합친 것이다. 브로커는 PID와 토픽-파티션 조합별로 마지막으로 성공 처리한 시퀀스 넘버를 기억하고, 다음 메시지의 시퀀스 넘버가 마지막 값에 1을 더한 것일 때만 정상 처리한다.
리더가 바뀌어도 유지되는 상태
새 리더가 된 팔로워도 어디까지 왔는지 안다. 리더는 새 메시지가 쓰일 때마다 인메모리 프로듀서 상태에 최근 5개의 SID를 갱신하고, 팔로워도 복제할 때마다 자체 인메모리 버퍼를 갱신하기 때문에, 팔로워가 리더로 선출되는 시점에는 이미 메모리 안에 최근 SID를 들고 있다. 재시작해서 메모리가 날아간 경우를 위해 브로커는 종료되거나 새 세그먼트가 생성될 때마다 프로듀서 상태 스냅샷을 파일로 저장한다. 스냅샷마저 최신이 아니면 PID와 SID가 카프카 로그의 메시지 형식에 포함되어 있으므로 최신 세그먼트의 메시지들로 복구한다.
켜는 비용
켜는 방법은 enable.idempotence=true이고 카프카 3.0부터 기본값이 true다. 이미 acks=all을 쓰고 있다면 성능 차이는 거의 없다. 켜면 PID를 받기 위한 API 호출이 시동 과정에 하나 늘고, 각 레코드 배치에 96비트가 추가된다.
Out Of Order Sequence Number
가장 자주 보이는 것이 Out Of Order Sequence Number 에러인데, 시퀀스 번호의 간격을 보고 판단해야 한다.
간격이 작고 비정기적이면 정상이다. max.in.flight.requests.per.connection이 1보다 크면(기본값 5) 이전 요청의 응답을 기다리지 않고 다음 배치를 먼저 보내기 때문이다. 배치 A(SID 3), B(SID 4), C(SID 5)를 동시에 보냈는데 네트워크 지연으로 A, C, B 순서로 도착하면 브로커는 4를 기대했는데 5를 받았으므로 C를 거절하고 에러를 낸다. 그 사이 B가 도착해 처리되고 프로듀서가 재전송한 C도 처리되어, 유실 없이 순서대로 정확히 한 번 저장된다. 프로듀서가 이 에러를 재시도 가능한 에러로 알고 있으므로 알아서 복구되고, 트랜잭션을 쓰지 않는다면 무시해도 된다.
간격이 크고 지속적이면 심각하다. 2번 뒤에 27번이 왔다면 중간 메시지들이 유실된 것이다. 원인은 대개 뒤처진 레플리카가 리더로 올라간 것이다. 프로듀서는 옛 리더에 저장했다고 알고 있어서 재전송할 것이 없으므로 3부터 26까지는 영구 유실이다. 이때는 unclean.leader.election.enable이 false인지, acks=all인지를 먼저 확인한다.
트랜잭션에서의 펜싱
카프카 트랜잭션을 쓸 때는 이 에러를 무시하면 안 된다. 트랜잭션은 여러 메시지 전송을 하나의 원자적 단위로 묶으므로 순서를 절대적으로 지켜야 하고, 순서가 깨졌다는 것은 정합성이 깨질 수 있다는 뜻이다. 카프카는 프로듀서를 신뢰할 수 없다고 판단해 펜싱하고, ProducerFencedException을 받은 쪽은 abortTransaction()으로 트랜잭션을 중단하고 인스턴스를 새로 만들어야 한다. 브로커가 이 상황을 눈감아주면 잠시 단절되었던 좀비 프로듀서가 뒤늦게 커밋을 시도하고, 새 프로듀서는 이미 중단을 보낸 상태라서 일부는 실패하고 일부는 성공하는 상태가 만들어지기 때문이다.
순서 보장의 범위
순서 보장의 범위도 정확히 알아야 한다. 단일 프로듀서 세션 안에서 동일한 토픽-파티션에 대해서만 보장된다. 파티션 간 순서는 보장되지 않아서 메시지 A를 1번 파티션에, B를 2번 파티션에 보냈을 때 A가 먼저 도착한다는 보장이 없다. 프로듀서가 재시작하면 새 PID를 받으므로 이전 세션과의 순서 연속성도 없다.
막지 못하는 중복
막지 못하는 중복도 있다. 애플리케이션이 같은 메시지로 producer.send()를 두 번 호출하는 것, 여러 인스턴스의 프로듀서 둘이 같은 메시지를 전송하는 것, 트랜잭션 없이 프로듀서가 재시작해 새 PID를 받는 것은 전부 브로커가 알아차리지 못한다. 막는 것은 프로듀서 내부 로직의 재시도로 생기는 중복뿐이다.