카프카를 프로덕션 환경에서 운용하기 위해 내부를 알아야 하는 건 아니지만, 내부 작동 방식을 알고 있으면 트러블슈팅을 하거나 카프카가 실행되는 방식을 이해하는 데 도움이 된다. 기본적인 카프카 사전 지식이 필요한 글이니, 참고 부탁드립니다.
카프카를 실제로 사용하는 사용자 입장에서 중요한 주제에 초점을 맞춰 설명할 예정이다.
일반적인 카프카 브로커의 기능에 더해서 파티션 리더를 선출하는 역할을 추가적으로 맡는다.
클러스터에서 가장 먼저 시작되는 브로커는 주키퍼의 /controller에 ^Ephemeral 노드를 생성함으로써 컨트롤러가 된다. 다른 브로커 역시 시작할 때 해당 위치에 노드를 생성하려고 시도하지만 ‘노드가 이미 존재함’ 예외를 받게 되기 때문에 컨트롤러 노드가 이미 존재한다는 것, 즉 클러스터에 이미 컨트롤러가 있다는 것을 알아차리게 된다. 브로커들은 주키퍼의 컨트롤러 노드에 뭔가 변동이 생겼을 때 알람을 받기 위해서 이 노드에 ^와치를 설정한다.
컨트롤러 브로커가 멈추거나 주키퍼와의 연결이 끊어질 경우, 이 Ephemeral 노드는 삭제된다. Ephemeral 노드가 삭제될 경우, 클러스터 안의 다른 브로커들은 주키퍼에 설정된 와치를 총해 컨트롤러가 없어졌다는 것을 알아차리게 되며 주키퍼에 컨트롤러 노드를 생성하려고 시도하게 된다. 주키퍼에 가장 먼저 새로운 노드를 생성하는 데 성공한 브로커가 다음 컨트롤러가 되며, 다른 브로커들은 ‘노드가 이미 존재함’ 예외를 받고 새 컨트롤러 노드에 대한 와치를 다시 생성하게 된다.
브로커는 새로운 컨트롤러가 선출될 때마다 주키퍼의 조건적 증가 연산에 의해 증가된 ^에포크 값을 전달받게 된다. 브로커는 현재 컨트롤러의 에포크 값을 알고 있기 때문에, 만약 더 낮은 에포크 값을 가진 컨트롤러로부터 메시지를 받을 경우 무시한다.
특히 이건 컨트롤러 브로커가 오랫동안 가비지 수집 때문에 멈춘 사이 주키퍼 사이의 연결이 끊길 수 있어 중요하다. 만약 새로운 컨트롤러가 선출되었다는 사실을 모르고 브로커에 메시지를 보낼 수도 있다. 이런 컨트롤러를 좀비라고 부르는데 결국 ‘좀비’를 방지하는 방법이기도 한 것이다.
브로커가 컨트롤러가 되면, 클러스터 메타데이터 관리와 리더 선출을 시작하기 전에 먼저 주키퍼로부터 최신 레플리카 상태 맵을 읽어온다. 이 작업은 비동기 API를 사용해서 수행되며, 지연을 줄이기 위해 읽기 요청을 여러 단계로 나눠서 주키퍼로 보낸다. 그렇지만 파티션 수가 매우 많은 클러스터에서는 적재 작업이 몇 초씩 걸릴 순 있다.
브로커가 클러스터를 나갔다는 사실을 컨트롤러가 알아차리면, 컨트롤러는 해당 브로커가 리더를 맡고 있었던 모든 파티션에 대해 새로운 브로커를 할당해주게 된다. 컨트롤러는 새로운 리더가 필요한 모든 파티션을 순회해 가면서 새로운 리더가 될 브로커를 결정한다. 그 후, 새로운 상태를 주키퍼에 쓴 뒤 새로 리더가 할당된 파티션의 레플리카를 포함하는 모든 브로커에 ^LeaderAndISR 요청을 보낸다. 즉, 각각의 요청은 같은 브로커에 레플리카가 있는 다수의 파티션에 대한 새 리더십 정보를 포함하게 되는 것이다.
새로 리더가 된 브로커 각각은 클라이언트로부터의 쓰기 혹은 읽기 요청을 처리하기 시작한다. 반면 팔로워들은 새 리더로부터 메시지를 복제하기 시작한다. 클러스터 안의 모든 브로커는 클러스터 내 전체 블로커와 레플리카의 맵을 포함하는 메타데이터 캐시를 가지고 있기 때문에, 컨트롤러는 모든 브로커에 리더십 변경 정보를 포함하는 업데이트 요청을 보내서 각각의 캐시를 업데이트 하도록 한다. 브로커가 백업을 시작할 때도 비슷한 과정이 반복된다.
최근 주기퍼 기반 컨트롤러에서 주키퍼를 걷어내고 레프트 기반 컨트롤러 쿼럼으로 옮기기 시작했다. 3.3 버전부턴 프로덕션 환경에 사용할 수 있을 만큼 안정적으로 운영이 가능해졌다.
Ephemeral 노드란?
Ephemeral(휘발성) 노드는 ZooKeeper 클라이언트가 연결된 동안에만 존재하는 임시 노드이다. 클라이언트(예: Kafka 브로커)가 ZooKeeper와의 연결을 종료하거나 세션이 만료되면 이 노드는 자동으로 삭제된다.
Kafka 브로커 등록, 파티션 리더십 관리, 컨트롤러 브로커 관리 등에 사용된다.
와치란?
Watch는 일종의 이벤트 리스너로, ZooKeeper 클라이언트가 특정 ZNode에 대해 관심을 등록하면, 해당 ZNode의 상태가 변경될 때 클라이언트에게 알림을 보낸다. 변경이 발생하면 클라이언트는 알림을 받고, 필요에 따라 다시 데이터를 읽거나 다른 작업을 수행할 수 있다.
에포크 값이란?
증가하는 정수로, Kafka의 클러스터 상태 변경과 관련된 중요한 버전 정보로, 브로커나 파티션 리더가 변경될 때 데이터를 안전하고 일관되게 관리위함이다.
쉽게 말해, 에포크 값은 컨트롤러나 파티션 리더의 "버전 관리"를 위한 값입니다.
아래와 같은 상황에서 사용된다.
LeaderAndISR이란?
파티션 리더십과 복제 상태를 관리하는 데 사용되는 중요한 메타데이터 정보이다. Kafka의 분산 환경에서, 각 파티션은 리더와 복제본을 가지며, LeaderAndISR 정보는 ZooKeeper 또는 KRaft(Kafka Raft) 메커니즘을 통해 저장되고 관리된다.
이 둘을 합친게 LeaderAndISR 인 것이다.
우스겟소리로 ‘분산되고, 분할되고, 복제된 커밋 로그 서비스’ 라는 수식어가 있을만큼 복제는 중요하다.
카프카에서 복제는 개별적인 노드에 필연적으로 장애가 발생할 수밖에 없는 상황에서 카프카가 신뢰성과 지속성을 보장하는 방식이기 때문이다.
카프카에 저장되는 데이터는 토픽을 단위로 해서 조직화된다. 각 토픽은 1개 이상의 파티션으로 분할되며, 각 파티션은 다시 다수의 레플리카를 가질 수 있다. 각각의 레플리카는 브로거에 저장되는데, 대개 하나의 브로커는 수백 개에서 심지어 수천 개의 레플리카를 저장한다.
각 파티션에는 리더 역할을 하는 레플리카가 하나씩 있다. 일관성을 보장하기 위해, 모든 쓰기 요청은 리더 레플리카로 주어진다. 클라이언트들은 리더 레플리카나 팔로워로부터 레코드를 읽어올 수 있다.
파티션에 속한 모든 레플리카 중에서 리더 레플리카를 제외한 나머지를 팔로워 레플리카라고 한다. 별도로 설정을 잡아주지 않는 한, 팔로워는 클라이언트의 요청을 처리할 수 없다. 이들이 주로 하는 일은 리더 레플리카로 들어온 최근 메시지들을 복제함으로써 최신 상태를 유지하는 것이다. 만약 해당 파티션의 리더 레플리카에 크래쉬가 날 경우, 팔로워 레플리카 중 하나가 파티션의 새 리더 파티션으로 승격된다.
리더 페플리카의 또 다른 일은 어느 팔로워 레플리카가 리더 레플리카의 최신 상태를 유지하고 있는지를 확인하는 것이다. 팔로워 레플리카는 새로운 메시지가 도작하는 즉시 리더 레플리카로부터 모든 메시지를 복제해 옴으로써 최신 상태를 유지할 수 있도록 하지만, 다양한 원인으로 인해 동기화가 깨질 수 있다.
리더 레플리카와의 동기화를 유지하기 위해 팔로워 레플리카들은 리더 레플리카에 읽기 요청을 보낸다. 이러한 요청에 응답으로 리더 레플리카는 메시지를 되돌려 준다. 읽기 요청들은 언제나 메시지를 순서대로 돌려준다. 즉, 리더 레플리카 입장에서는 팔로워 레플리카가 요청한 마지막 메시지까지 복제를 제대로 완료했는지, 이후 새로 추가된 메시지가 없는지의 여부를 알 수 있는 것이다. 리더 레플리카는 각 팔로워 레플리카가 마지막으로 요청한 오프셋 값을 확인함으로써 각 팔로워 레플리카가 얼마나 뒤쳐져 있는지를 알 수 있다.
팔로워 레플리카가 리더 레플리카를 따라가는 데 실패했다면 (이를 아웃-오브-싱크 레플리카라고 부른다) 해당 레플리카는 더 이상 장애 상황에서 리더가 될 수 없다. 반대로, 지속적으로 최신 메시지를 요청하고 있는 레플리카는 ‘인-싱크 레플리카’라고 부른다. 현재 리더에 장애가 발생할 경우 인-싱크 레플리카만이 파티션 리더로 선출될 수 있다.
현재 리더에 더하여, 각 파티션은 ‘선호 리더’를 갖는다. 선호리더는 토픽이 처음 생성 되었을 때 리터 레플리카였던 레플리카를 가리킨다. 파티션이 처음 생성되던 시점에서는 리더 레플리카가 모든 브로커에 걸쳐 균등하게 분포되기 때문에 ‘선호’라는 표현이 붙었다. 결과적으로, 클러스터 내의 모든 파티션에 대해 선호 리더가 실제 리더가 될 경우 부하가 브로커 사이에 균등하게 분배된 것이라고 예상할 수 있다.
TCP로 전달되는 이진 프로토콜로 요청을 처리한다.
언제나 클라이언트가 연결을 시작하고 요청을 전송하며, 브로커는 요청을 처리하고 클라이언트로 응답을 보낸다. 특정 클라이언트가 브로커로 전송한 모든 요청은 브로커가 받은 순서대로 처리된다. 즉, 그렇기 때문에 카프카가 저장하는 메시지는 순서가 보장되며, 카프카를 메시지 큐로 사용할 수도 있는 것이다.
요청 헤더
프로토콜의 상세한 내용은 카프카 공식 문서 에 있으니 확인해보면 좋을 것 같다.
브로커는 연결을 받는 각 포트별로 ^억셉터 스레드를 하나씩 실행시킨다. ^억셉터 스레드는 연결을 생성하고 들어온 요청을 프로세서 스레드에 넘겨 처리하도록 한다. 프로세서 스레드의 수는 설정이 가능하다. 프로세서 스레드의 수는 설정이 가능하다. 네트워크 스레드는 클라이언트 연결로부터 들어온 요청들을 받아서 요청 큐에 넣고, 응답 큐에서 응답을 가져다 클라이언트로 보낸다. 가끔은 클라이언트로 보낼 응답에 지연이 필요한 때가 있다. 즉, 컨슈머의 경우 브로커 쪽에 데이터가 준비되었을 때에만 응답을 보낼 수 있고, 어드민 클라이언트의 경우 토픽 삭제가 진행 중인 상황에서만 DeleteTopicrequest 요청에 대한 응답을 보낼 수 있는 것이다. 지연된 응답들은 완료될 때까지 ^퍼커토리에 저장된다.

억셉터 스레드란?
네트워크 기반 서버에서 클라이언트 연결을 수락(accept) 하는 역할을 담당하는 스레드
퍼거토리란?
지연된 요청을 관리하기 위해 설계된 내부 메커니즘
요청이 바로 처리되지 않고 일정 조건이 충족될 때까지 대기 상태로 보류되는 공간이다.
Kafka 브로커가 다음과 같은 요청들을 처리하기 위해 사용하는 지연 처리 대기열(Delayed Queue)이다.
Kafka의 분산 환경에서 요청이 즉시 처리되지 못하는 경우가 빈번하기 때문에, 퍼거토리는 효율적인 비동기 처리를 지원하기 위한 핵심 메커니즘이다.
요청은 크게 쓰기, 읽기 어드민 3가지 요청으로 나눌 수 있다.
카프카 브로커로 메시지를 쓰고 있는 프로듀서가 보낸 요청
acks 설정 매개변수는 쓰기 작업이 성공한 것으로 간주되기 전 메시지에 대한 응답을 보내야 하는 브로커의 수를 가리킨다. 어느 시점에서 메시지가 ‘성공적으로 쓰여진다’라고 간주되는지는 프로듀서 설정을 통해 바꿀 수 있다.
acks=1: 리더만이 메시지를 받았을 때
acks=all: 모든 인-싱크 레플리카들이 메시지를 받았을 때
acks=0: 메시지가 보내졌을 때, 즉, 브로커의 응답을 기다리지 않음
파티션의 리더 레플리카를 가지고 있는 브로커가 해당 파티션에 대한 쓰기 요청을 받게 되면 유효성 검증을 한다.
그러고 나서 브로커는 새 메시지들을 로컬 디스크에 쓴다. 카프카는 데이터가 디스크에 저장될 때까지 기다리지 않는다. 즉, 메시지의 지속성을 위해 복제에 의존하는 것이다.
메시지가 파티션 리더에 쓰여지고 나면, 브로커는 acks 설정에 따라 응답을 내려보낸다. 만약 0이나 1로 설정되어 있다면 바로 응답을 내려보내지만, all로 설정되어 있다면 일단 요청을 퍼거토리라 불리는 버퍼에 저장한다. 그리고 팔로워 레플리카들이 메시지를 복제한 것을 확인한 다음에야 클라이언트에 응답을 돌려보낸다.
카프카 브로커로부터 메시지를 읽어오고 있는 컨슈머나 팔로워 레플리카가 보낸 요청
브로커는 쓰기 요청이 처리되는 것과 매우 유사한 방식으로 읽기 요청을 처리한다. 클라이언트는 브로커에 토픽, 파티션 그리고 오프셋 목록에 해당하는 메시지들을 보내 달라는 요청을 보낸다. 클라이언트는 각 파티션에 대해 브로커가 리턴할 수 있는 최대 데이터의 양 역시 지정한다. 왜냐면, 클라이언트는 브로커가 되돌려준 응답을 담을 수 있을 정도로 충분히 큰 메모리를 할당해야 하기 때문이다. 만약 한도값이 없다면 브로커는 클라이언트가 메모리 부족에 처할 수 있을 정도로 큰 응답을 보낼 수 있을 것이다.
요청은 요청에 지정된 파티션들의 리더를 맡고 있는 브로커에 전송되어야 하며, 클라이언트는 읽기 요청을 정확히 라우팅할 수 있도록 필요한 메타데이터에 대한 요청을 보내게 된다. 요청을 받은 파티션 리더는 먼저 요청이 유효한지를 확인한다. 만약 오프셋이 존재한다면, 브로커는 파티션으로부터 클라이언트가 요청에 지정한 크기 한도만큼의 메시지를 읽어서 클라이언트에게 보내 준다. 카프카는 파일에서 읽어온 메시지들을 중간 버퍼를 거치지 않고 바로 네트워크 채널로 보낸다. 그래서 클라이언트에게 보내는 메시지에 제로카피 최적화를 할 수 있다. 이부분이 대부분의 데이터 베이스와의 차이점이다. 결국 ‘제로카피’를 택하면서 데이터 복사, 메모리 상의 버퍼 관리 오버헤드가 사라져 성능이 향상된다.
또한, 클라이언트는 리턴될 데이터의 양의 하한을 지정할 수 있다. 예를 들어 하한을 10K로 잡는다면, 클라이언트가 브로커에게 “보낼 데이터가 최소한 10K 바이트가 쌓이면 결과를 리턴해라.”라고 이야기하는 것과 같다. 클라이언트가 트래픽이 그리 많지 않은 토픽들로부터 메시지를 읽어오고 있을 때 CPU와 네트워크 사용량을 감소시키는 좋은 방법이다.
파티션 리더는 어느 메시지가 어느 레플리카로 복제되었는지 알고 있으며, 특정 메시지가 모든 인-싱크 레플리카에 쓰여지기 전까지는 컨슈머들이 읽을 수 없다. 이렇게 작동되는 이유는 다음과 같다.
충분한 수의 레플리카에 복제가 완료되지 않은 메시지는 ‘불안전한’ 것으로 간주된다. 만약 리더에 크래시가 발생해서 다른 레플리카가 리더 역할을 이어받는다면, 이 메시지들은 더 이상 카프카에 존재하지 않게 된다. 만약 클라이언트가 이렇게 리더에만 존재하는 메시지들을 읽을 수 있도록 한다면, 크래시 상황에서 일관성이 결여될 수 있다.
컨슈머가 매우 많은 수의 파티션들로부터 이벤트를 읽어오는 경우가 있다. 이 경우 읽고자 하는 파티션의 전체 목록을 요청을 보낼 때마다 브로커에 전송하고, 다시 브로커는 모든 메타데이터를 돌려 보내는 방식은 매우 비효율적일 수 있다. 즉, 읽고자 하는 파티션의 집합이나 여기에 연관된 메타데이터는 여간해서는 잘 바뀌지는 않는 데다가, 많은 경우 리턴해야 할 메타데이터가 그렇게 많지도 않다. 이런 오버헤드를 최소화하기 위해 카프카는 읽기 세션 캐시를 사용한다. 세션이 한번 생성되면, 컨슈머들은 더 이상 요청을 보낼 때마다 모든 파티션을 지정할 필요 없이 점진적으로 읽기 요청을 보낼 수 있다. 브로커는 변경 사항이 있는 경우에만 응답에 메타데이터를 포함하면 된다.
토픽 생성이나 삭제와 같이 메타데이터 작업을 수행중인 어드민 클라이언트가 보낸 요청
카프카 클라이언트는 요청에 맞는 파티션의 리더를 맡고 있는 브로커에 쓰기나 읽기 요청을 전송할 책임을 진다.
그럼 클라이언트는 어디로 요청을 보내야 하는지 어떻게 아는 것일까? 카프카 클라이언트는 메타데이터 요청이라 불리는 또 다른 유형의 요청을 사용한다. 이 요청은 클라이언트가 다루고자 하는 토픽들의 목록을 포함한다. 메타데이터 요청은 아무 브로커에나 보내도 상관없다. 모든 브로커들이 이러한 정보를 포함하는 메타데이터 캐시를 가지고 있기 때문이다. 클라이언트는 보통 이 정보를 캐시해 두었다가 이 정보를 사용해서 각 파티션의 리더 역할을 맡고 있는 브로커에 바로 쓰거나 읽는다. 클라이언트는 토픽 메타데이터가 변경될 경우에도 최신값을 유지해야 하는 만큼 때때로 새로운 메타데이터 요청을 보내서 이 정보를 새로고침할 필요가 있다.
카프카의 기본 저장 단위는 파티션 레플리카이다. 파티션은 서로 다른 브로커들 사이에 분리될 수 없으며, 같은 브로커의 서로 다른 디스크에 분할 저장되는 것조차 불가능하다. 따라서 파티션의 크기는 특정 마운트 지점에 사용 가능한 공간에 제한을 받는다고 볼 수 있다. 카프카를 설정할 때는 운영자가 파티션들이 저장될 디렉토리 목록을 정의한다. 이것은 log.dirs 매개변수에 지정된다. 카프카가 사용할 각 마운트 지점별로 하나의 디렉토리를 포함하도록 설정하는 것이 일반적이다.
그럼 카프카가 데이터를 저장하기 위해 사용 가능한 디렉토리들을 어떻게 활용하는지를 살펴보자.
첫번째로, 데이터가 클러스터 안의 브로커, 브로커 안의 디렉토리들을 어떻게 활용하는지 알아보자.
2023년 10월, 카프카 3.6.0 릴리즈 부터 계층 저장소(Tiered storage)를 지원한다.
왜 계층 저장소가 필요했을까?
카프카는 현재 대량의 데이터를 저장하기 위한 목적으로 사용되고 있다.
- 물리적인 디스크 크기에 영향을 받는다.
- 디스크와 클러스터 크기는 저장소 요구 조건에 의해 결정된다.
지연과 처리량이 주 고려사항일 경우 클러스터는 필요한 것 이상으로 커지는 경우가 많다.- 파티션의 크기가 클수록 클러스터의 탄력성은 줄어든다.
클라우드 환경의 유연한 옵션을 활용할 수 있도록 최대한의 탄력성을 가지는 것이 아키텍처 설계의 추세다.
카프카 클러스터의 저장소를 로컬과 원격 두 계층으로 나눈다.
로컬 계층은 현재의 카프카 저장소 계층과 똑같이 로컬 세그먼트를 저장하기 위해 카프카 브로커의 로컬 디스크를 사용한다. 새로운 원격 계층은 완료된 로그 세크먼트를 저장하기 위해 HDFS나 S3와 같은 전용 저장소 시스템을 사용한다. 사용자는 계층별로 서로 다른 보존 정책을 설정할 수 있다. 로컬 저장소가 리모트 계층 저장소에 비해 훨씬 비싼 것이 보통이므로 로컬 계층의 보존 기한은 대개 몇 시간 이하로 설정하고, 원격 계층의 보존 기한은 그보다 길게 설정하는 것이다. 로컬 저장소는 원격 저장소에 비해 지연이 훨씬 짧다. 지연에 민감한 애플리케이션들은 로컬 계층에 저장되어 있는 최신 레코드를 읽어오는 만큼, 데이터를 전달하기 위해 페이지 캐시를 효율적으로 활용하는 카프카의 메커니즘에 의해 문제없이 작동한다.
이런 계층화된 저장소 기능의 이중화된 구조 덕분에 카프카 클러스터의 메모리와 CPU에 상관없이 저장소를 확장할 수 있다. 따라서 이 기능은 무한한 저장 공간, 더 낮은 비용, 탄력성뿐만 아니라 오래 된 데이터와 실시간 데이터를 읽는 작업을 분리시키는 기능이 있다.
사용자가 토픽을 생성하면, 카프카는 우선 이 파티션을 브로커 중 하나에 할당한다.
만약 브로커가 6개 있고 여기에 있던 파티션이 10개, 복제 팩터가 3인 토픽을 생성하기로 했다고 하자.
그럼 카프카는 30개의 파티션 레플리카를 6개의 브로커에 할당해 줘야 한다.
그럼 이제 파티션을 할당할 때 목표는 다음과 같다.
여기서는 임의의 브로커부터 시작해서 각 브로커에 라운드 로빈 방식으로 파티션을 할당함으로써 리더를 결정한다.
만약 ^랙 인식 기능을 고려할 때는 단순히 순서대로 브로커를 선택하는 대신, 서로 다른 랙의 브로커가 번갈아 선택되도록 순서를 정해야 한다. 즉, 각 브로커는 다른 랙에 있는 브로커 다음에 온다.

다음의 경우 첫번째 랙이 오프라인이 되더라도 여전히 작동 가능한 레플리카가 있는 만큼 파티션은 여전히 사용 가능하기 때문이다. 이것은 모든 레플리카에도 마찬가지이므로, 우리는 랙에 장애가 발생할 경우에도 가용성을 보장할 수 있다.
각 디렉토리에 저장되어 있는 파티션의 수를 센 뒤, 가장 적은 파티션이 저장된 디렉토리에 새 파티션을 저장하는 것이다. 이는 만약 새로운 디스크를 추가할 경우, 모든 새 파티션들은 이 디시크에 생성될 것이라는 것을 의미한다.
랙 인식 기능이란?
파티션 복제를 브로커의 물리적 위치를 고려해 배치하는 것
고가용성과 장애 복구를 강화하기 위한 기능
클러스터가 여러 데이터 센터나 랙에 분산되어 있는 경우에 유용하다.
카프카는 영구히 데이터를 저장하지도, 데이터를 지우기 전에 모든 컨슈머들이 메시지를 읽어갈 수 있도록 기다리지도 않는다. 대신, 카프카 운영자는 각각의 토픽에 대해 보존 기한을 설정할 수 있다. 즉, “이만큼 오래된 메시지는 지운다” 혹은 “이 용량이 넘어가면 지운다”와 같은 것을 설정할 수 있는 것이다.
큰 파일에서 삭제해야 할 메시지를 찾아서 지우는 작업은 시간과 에러의 가능성이 높아, 하나의 파티션을 여러 개의 세그먼트로 분할한다. 카프카가 파티션 단위로 메시지를 쓰는 만큼 각 세그먼트 한도가 다 차면 세그먼트를 닫고 새 세그먼트를 생성한다.
현재 쓰여지고 있는 세그먼트를 액티브 세그먼트라 부른다. 액티브 세그먼트는 어떠한 경우에도 삭제되지 않기 때문에, 로그 보존 기한을 하루로 설정했는데 각 세그먼트가 5일치의 데이터를 저장하고 있을 경우, 실제로는 5일치의 데이터가 보존되게 된다. 세그먼트가 닫히기 전까지는 데이터를 삭제할 수도 없기 때문이다.
카프카 브로커는 각 파티션의 모든 세그먼트에 대해 파일 핸들을 연다. 이것 때문에 사용중인 파일 핸들 수가 매우 높게 유지될 수 있는 만큼 운영체제 역시 여기에 맞춰서 튜닝해 줄 필요가 있다.
각 세그먼트는 하나의 데이터 파일 형태로 저장된다. 파일 안에는 카프카의 메시지와 오프셋이 저장된다. 디스크에 저장되는 데이터의 형식은 사용자가 프로듀서를 통해서 브로커로 보내는, 그리고 나중에 브로커로부터 컨슈머로 보내지는 메시지의 형식과 동일하다. 네트워크를 통해 전달되는 형식과 디스크에 저장되는 형식을 통일함으로써 카프카는 컨슈머에 메시지를 전송할 때 제로카피 최적화를 달성할 수 있으며, 프로듀서가 이미 압축한 메시지들을 압축 해제해서 다시 압축하는 수고 역시 덜 수 있다.
결과적으로, 만약 우리가 메시지 형식을 변경하고자 한다면, 네트워크 프로토콜과 디스크 저장 형식이 모두 변경되어야 하며 카프카 브로커들은 업그레이드로 인해 2개의 파일 형식이 뒤섞여있는 파일을 처리할 방법을 알아야한다.
카프카 메시지는 사용자 페이로드와 시스템 헤더, 두 부분으로 나뉘어진다. 사용자 페이로드는 기값과 밸류값, 헤더 모음을 포함한다. 각각의 헤더는 자체적인 키/밸류 순서쌍이다.
버전 0.11부터 카프카 프로듀서는 언제나 메시지를 배치 단위로 전송한다. 만약 하나의 메시지만을 보내고자 할 경우 배치는 약간의 오버헤드를 발생시킨다. 하지만 배치당 2개 이상의 메시지를 보낼 경우, 메시지를 배치 단위로 묶음으로써 공간을 절약하게 되는 만큼 네트워크 대역폭과 디스크 공간을 덜 사용하게 된다. 카프카가 파티션별로 별도의 배치를 생성하는 만큼, 더 적은 수의 파티션에 쓰는 프로듀서가 더 효율적이다. 카프카 프로듀서가 같은 쓰기 요청에 여러 개의 배치를 포함할 수 있다. 즉, 프로듀서에서 압축 기능을 사용할 경우 더 큰 배치를 전송할수록 네트워크를 통해 전송되고 브로커의 디스크에 저장되는 데이터가 더 잘 압축된다는 이야기도 된다.
카프카는 컨슈머가 임의의 사용 가능한 오프셋에서부터 메시지를 읽어오기 시작할 수 있도록 한다. 즉, 만약 컨슈머가 오프셋 100에서부터 시작되는 1MB 어치의 메시지를 요청할 경우, 브로커는 오프셋 100에 해당하는 메시지가 저장된 위치를 빠르게 찾아서 해당 오프셋에서부터 메시지를 읽기 시작할 수 있어야 한다. 브로커가 주어진 오프셋의 메시지를 빠르게 찾을 수 있도록 하기 위해 카프카는 각 파티션에 대해 오프셋을 유지한다. 이 인덱스는 오프셋과 세그먼트 파일 및 그 안에서의 위치를 매핑한다. 또 이와 유사하게 카프카는 타임스탬프와 메시지 오프셋을 매핑하는 또 다른 인덱스를 가지고 있다. 이 인덱스는 타임스탬프를 기준으로 메시지를 찾을 때 사용된다.
대개 카프카는 설정된 기간 동안만 메시지를 저장하며, 보존 시간이 지나간 메시지들은 삭제한다.
하지만 예를 들어서, 고객의 배송지 주소를 저장하기 위해 카프카를 사용한다고 해보자. 이 경우, 각 고객의 주소 변경 내역 중 가장 마지막의 것만 저장하는 것이 지난 1주일 동안, 혹은 1년 동안의 변경 내역 데이터를 저장하는 것보다 더 합리적이다. 이렇게 하면 오래 된 주소에 대해 신경 쓸 필요가 없으면서도 한동안 주소를 바꾸지 않은 사용자의 주소를 여전히 유지할 수 있다. 또 다른 예로 현재 상태를 저장하기 위해 카프카를 사용하는 애플리케이션을 생각할 수 있다. 상태가 변할 때마다 애플리케이션은 새 상태를 카프카에 쓴다. 크래시가 발생한 뒤 복구 과정에서 애플리케이션은 이 메시지들을 카프카에서 읽어와서 최근 상태를 복원한다. 중요한 것은 애플리케이션이 돌아가는 와중에 발생한 모든 상태 변경이 아닌 크래시 나기 직전의 마지막 상태다.
카프카는 두 가지 보존 정책을 허용함으로써 앞에서 이야기한 활용 사례들을 모두 지원한다. 삭제 보존 정책에서는 지정된 보존 기한보다 더 오래 된 이벤트들을 삭제한다. 압착 보존 정책에서는 토픽에서 각 키의 가장 최근값만 저장하도록 한다. 당연히 애플리케이션이 키와 밸류를 모두 포함하는 이벤트를 생성하는 토픽의 경우 압착 설정을 잡아주는 것이 합리적이다. 만약 토픽에 키값이 null인 메시지가 있을 경우 압착은 실패한다.