카프카는 고가용성 분산 스트리밍 플랫폼으로써 데이터 파이프라인의 중앙에서 메인 허브 역할을 합니다. 따라서 일시적인 하드웨어 이슈 등으로 특정 브로커에 장애가 발생하더라도 안정적으로 서비스가 운영될 수 있도록 구상됐습니다.
리더는 리플리케이션 중 하나가 선정되며, 모든 읽기와 쓰기는 리더를 통해야만 합니다. 즉 프로듀서는 모든 리플리케이션에 메시지를 보내는 것이 아니라 리더에게만 전송합니다. 또한 컨슈머도 리더에서만 메시지를 가져옵니다.

팔로워는 리더에 문제가 발생했을 때를 대비해 언제든지 새로운 리더가 될 준비를 해야 합니다. 따라서 지속적으로 파티션의 리더가 새로운 메시지를 받았는지 확인하고, 새로운 메시지가 있다면 해당 메시지를 리더로부터 복제합니다.
리더와 팔로워는 ISR(InSyncReplica)라는 논리적 그룹으로 묶여 있습니다. 그 이유는 그룹에 속한 팔로워들만이 새로운 리더의 자격을 가질 수 있기 때문입니다.
ISR 내의 팔로워들은 리더와의 데이터 일치를 유지하기 위해 지속적으로 리더의 데이터를 따라가고, 리더는 ISR 내 모든 팔로워가 메시지를 받을 때까지 대기합니다. 그러나 팔로워가 예상치 못한 이유로 리더로부터 복제에 실패할 수 있습니다. 이런 팔로워가 리더가 될 경우 데이터 정합성이 깨질 가능성이 있습니다. 따라서 리더는 팔로워를 감시하며 리플리케이션의 동작을 감지합니다. 제대로 리플리케이션 동작을 하지 않는 팔로워가 있다면 리더는 해당 팔로워를 ISR에서 추방합니다.
ISR 내에서 모든 팔로워의 복제가 완료되면 리더는 내부적으로 커밋되었다는 표시를 합니다. 마지막 커밋 오프셋 위치를 하이워터마크(high water mark)라고 부릅니다. 커밋되었다는 것은 리플리케이션 팩터 수의 모든 리플리케이션이 전부 메시지를 저장했음을 의미하며, 이렇게 커밋된 메시지만 컨슈머가 읽을 수 있습니다. 이를 통해 메시지의 일관성을 유지하게 됩니다.

만약 커밋되지 않은 메시지를 컨슈머가 읽은 상태에서 파티션의 리더 선출이 발생하면 메시지가 불일치하는 현상이 발생할 수 있습니다.

카프카에서 리더는 메시지 처리와 팔로워 감시 등 많은 작업을 하게 됩니다. 따라서 리더와 팔로워 간의 리플리케이션 동작을 처리할 때 서로의 통신을 최소화할 수 있도록 설계하여 리더의 부하를 줄였습니다.
리더가 메시지를 가진 상태에서 팔로워에게 리플리케이션 요청을 보낼 경우, 메시지를 받았는지에 대한 ACK를 받지 않도록 하여 성능을 높였습니다. 대신 팔로워들은 리플리케이션 동작을 마친 후 리더에게 다음 오프셋에 대한 리플리케이션을 요청합니다. 만약 팔로워가 실패할 경우 실패 오프셋 리플리케이션 요청을 리더에게 보냅니다. 따라서 리더는 팔로워들의 리플리케이션 요청을 받으며 특정 메시지가 커밋되었는지 판단하고 하이워터마크를 증가시키게 됩니다. 추가로 팔로워가 리더를 풀(pull)하는 방식으로 동작하여 리더의 부하를 줄였습니다.
리더에포크는 32비트 숫자로 표현되며 파티션들이 복구 동작을 할 때 메시지의 일관성을 유지하기 위한 용도로 이용됩니다. 리플리케이션 프로토콜에 의해 전파되고, 리더가 변경된 후 변경된 리더에 대한 정보는 팔로워에게 전달됩니다.
리더에포크를 사용하지 않는다면 복구를 위해 리더 선출을 하는 과정에서 메시지 유실이 발생할 수 있습니다.
리더 선출을 담당합니다. 리더 선출을 하기 위해 ISR 리스트 정보가 필요하며, 이는 가용성 보장을 위해 주키퍼에 저장되어 있습니다. 컨트롤러는 브로커가 실패하는 것을 예의주시하며 브러커의 실패가 감지되면 즉시 ISR 리스트 중 하나를 새로운 파티션 리더로 선출합니다. 파티션에 리더가 없는 상태에서는 메시지를 쓰거나 읽는 작업이 불가능하므로 서비스 장애로 이어집니다. 따라서 빠른 리더 선출이 필요합니다. 카프카 버전 1.1.0부터 리더 선출 과정이 단축되어 과거 6분 30초가 걸리던 것을 약 3초로 단축했다고 합니다. 따라서 리더 선출이 오래 걸릴까 걱정할 필요는 없어졌습니다.

제어된 브로커 종료란 예기치 않은 브로커의 실패나 장애가 아니라 관리자에 의해 이뤄지는 자연스러운(graceful) 종료를 의미합니다.
제어된 종료와 급작스러운 종료의 차이점은 다운타임입니다. 제어된 종료는 브로커가 종료되기 전, 컨트롤러가 해당 브로커가 리더로 할당된 전체 파티션에 대해 리더 선출 작업을 진행합니다. 하지만 리더들이 활성화된 상태에서 각 파티션마다 리더를 선출하게 되므로 다운타임을 최소화합니다.
브로커 장애로 인한 리더 선출 작업에서는 이미 파티션의 리더가 종료된 상태이고, 파티션을 순회하며 리더를 선출합니다. 첫 번째 파티션의 다운타임은 짧고 마지막 파티션의 다운타임은 제일 깁니다. 따라서 제어된 종료 사용을 권장하며 브로커의 설정 파일인 server.properties에 controlled.shutdown.enable = true 설정이 되어있는지 확인하면 됩니다.
카프카의 토픽으로 들어오는 메시지는 세그먼트(로그 세그먼트)라는 파일에 저장됩니다. 로그 세그먼트에는 메시지의 내용 뿐만 아니라 키, 밸류, 오프셋, 메시지 크기 같은 정보도 함께 저장되며, 로그 세그먼트 파일은 브로커의 로컬 디스크에 보관됩니다.
로그 세그먼트는 최대 1GB로 크기 제한이 걸려있고, 이를 초과할 경우 다음 로그 세그먼트를 생성하는 롤링 방식으로 저장합니다. 카프카 관리자는 무한히 세그먼트가 늘어나는 경우를 대비해 전략을 수립해둬야 합니다. 관리 방법으로 크게 로그 세그먼트 삭제와 컴팩션으로 구분할 수 있습니다.
이 옵션은 server.properties에서 log.cleanup.policy 값이 delete로 명시되어야 합니다. 이 값은 기본값입니다.
로그 세그먼트 삭제 작업은 일정 주기를 가지고 체크하며, 카프카 기본 값은 5분입니다. 따라서 세그먼트 삭제 명령어를 실행하면 5분 후에 삭제 작업이 실행됩니다.
또한 로그 세그먼트 보관 주기도 설정이 가능한데, server.properties에 retention.ms 값을 조정하면 됩니다. 이 값은 로그 세그먼트 보관 시간이 해당 숫자보다 크면 삭제한다는 것을 의미합니다. 만약 0으로 설정하면 모든 로그를 삭제할 수 있습니다. 기본값은 일주일로 설정되어 있습니다.
로그를 삭제하지 않고 컴팩션하여 보관할 수 있습니다. 무기한으로 디스크에 저장하는 상황을 방지하기 위해 컴팩션은 메시지의 키값을 기준으로 마지막의 데이터만 보관합니다.
CG01 컨슈머 그룹이 T01 토픽을 컨슘하고 첫 번째 메시지를 커밋한 경우, _consumer_offset 토픽에 (CG01, T01) 키에 1(오프셋)이 밸류로 저장됩니다. 두 번째 메시지를 읽으면 (CG01, T01) 키에 2(오프셋)이 밸류로 저장됩니다. 세 번째 메시지까지 읽으면 3(오프셋)이 밸류로 저장되고 총 3개의 밸류가 (CG01, T01) 키에 저장됩니다. 이때 컴팩션이 실행되면 마지막 오프셋 3을 제외한 이전 데이터를 삭제합니다. 컨슈머 그룹은 항상 마지막 오프셋부터 메시지를 읽기 때문에 무관합니다.
실제 예시를 들자면 구매 현황 상태를 조회하는 시스템에서 사용자 아이디를 메시지의 키, 현재의 구매 상태 정보를 메시지의 밸류로 사용할 수 있습니다. 구매 상태 정보는 주문 완료 -> 배송 준비 -> 배송 중 -> 배송 완료 총 4단계로 구분된다고 가정합니다. 결국 최종 상태만 알고 있으면 되므로 컴팩션을 사용할 수 있는 상황입니다.
프로듀서 입장에서 메시지의 밸류는 필수로 입력해야 하지만 키값은 선택적입니다. 컴팩션을 이용하기 위해서는 반드시 메시지를 전송할 때 키값을 지정해야 합니다.

이러한 로그 컴팩션의 장점은 빠른 장애 복구입니다. 전체 로그를 복구하지 않고 최신의 상태만 복구하게 되어 복구 시간을 단축할 수 있습니다.