프로듀서가 “성공했다”고 판단하는 시점을 언제로 잡을 것인가. 카프카에서 데이터 유실이 갈리는 가장 중요한 설정 하나다.
acks 값이 정하는 성공 시점
응답 모드는 acks 값으로 고른다.
| 설정 | 성공 판단 시점 | 지연 | 유실 |
|---|---|---|---|
acks=0 | 네트워크로 보낸 순간 | 가장 낮다 | 매우 크다 |
acks=1 | 리더가 파티션 데이터 파일에 쓴 순간 | 중간 | 있다 |
acks=all | 모든 ISR이 받아간 순간 | 가장 길다 | min.insync.replicas가 1이면 있다 |
acks=0에서 에러가 나는 경우는 전송할 객체가 직렬화되지 않거나 네트워크 카드가 오작동하는 정도뿐이다. 파티션이 오프라인이어도, 리더 선출이 진행 중이어도, 클러스터 전체가 작동 불능이어도 에러가 나지 않는다. 지연이 낮아서 벤치마크를 이 설정으로 돌리는 경우가 많은데 종단 지연이 개선되지는 않는다. 컨슈머는 어차피 모든 ISR에 복제된 뒤에야 그 메시지를 볼 수 있다.
acks=1은 디스크에 반영될 필요까지는 없고 리더가 데이터 파일에 쓰기만 하면 된다. 여기서 불완전 복제 파티션(URP)이 생긴다. 리더가 팔로워로 복제되기 전에 프로듀서에게 먼저 응답하므로, 복제 속도가 쓰기 속도를 못 따라가는 상태가 만들어진다.
acks=all이 가장 안전하지만 그것만으로 유실이 사라지지는 않는다. ISR이 리더 하나뿐인 상태에서도 “모든 ISR이 받았다”가 성립하기 때문에, 최소 인-싱크 레플리카를 2 이상으로 함께 잡아야 실제 효과가 난다.
재시도 가능한 에러와 그렇지 않은 에러
프로듀서 에러는 두 종류다. 재시도 가능한 에러는 프로듀서가 자동으로 처리한다. LEADER_NOT_AVAILABLE은 새 리더가 선출되면 두 번째 시도에서 성공한다. 재시도 불가능한 에러는 INVALID_CONFIG처럼 다시 보낸다고 달라지지 않는 것이라 직접 처리해야 한다. 유실을 막는 것이 목표라면 재시도 횟수를 기본값(MAX_INT, 사실상 무한)으로 두고 delivery.timeout.ms를 최대로 잡는다.
acks=1에서 메시지가 사라지는 순간
acks=1이 데이터를 잃는 흐름은 이렇다. 브로커는 레플리카 3개에 언클린 리더 선출 false로 잘 설정되어 있고, 프로듀서가 acks=1로 보낸다. 메시지가 리더에 쓰이고 ISR에는 아직 반영되지 않은 상태에서 리더가 성공 응답을 보낸 직후 크래시가 난다. 다른 레플리카들은 아직 인-싱크 상태이므로 클린 리더 선출로 그중 하나가 리더가 되고, 복제되지 않은 메시지는 사라진다.
여기서 볼 것이 하나 있다. 시스템의 일관성은 깨지지 않았다. 어떤 컨슈머도 그 메시지를 본 적이 없고 레플리카 입장에서는 커밋된 적도 없다. 오직 프로듀서만 성공했다고 착각한다.
재시도가 남기는 중복
재시도의 대가는 중복이다. 실패한 줄 알았지만 실제로는 성공한 메시지와 재전송한 메시지가 둘 다 브로커에 쓰이는 경우가 생긴다. 재시도로 얻는 것은 최소 한 번이지 정확히 한 번이 아니고, 이 틈을 메우는 것이 멱등적 프로듀서다.
애플리케이션이 처리해야 하는 에러
재시도로 해결되지 않아 직접 처리해야 하는 에러도 있다. 메시지 크기나 인가 관련 브로커 에러, 브로커로 보내기 전에 발생한 직렬화 에러, 재전송을 다 소진했거나 재시도 도중 프로듀서의 가용 메모리가 메시지로 꽉 찬 경우, 타임아웃이 그렇다. 처리 방식은 애플리케이션이 정한다. 폐기할지, 로깅할지, 원본 시스템 읽기를 중단할지, 백프레셔를 걸어 잠시 멈출지, 로컬 디스크에 저장할지. 다만 에러 핸들러에서 하려는 일이 재전송뿐이라면 프로듀서의 재전송 기능을 쓰는 편이 낫다.