💡 해당 글은 '카프카 핵심 가이드 2nd Edition'을 읽고 정리한 글입니다.
1️⃣ 클러스터 멤버십
브로커를 관리하는 클러스터는 아파치 주키퍼를 사용하여 브로커의 목록을 유지 및 관리하다.
- 브로커 프로세스가 시작되면 주키퍼에 Ephemeral 노드의 형태로 ID를 등록하게 되는데, 이는 고유한 식별자가 된다.
- 주키퍼의 `/brokers/ids` 경로를 구독 혹은 Watcher로 등록함으로써, 브로커가 추가 혹은 삭제될때마다 알림을 받을 수 있다.
- Ephemeral 노드로 등록되기 때문에, 브로커와 주키퍼 간의 연결이 끊어진다면 해당 노드는 자동으로 삭제된다.
💡 브로커가 정지되면 브로커를 나타내는 ZNode도 삭제되지만, 브로커 ID는 자료구조에 남게 되므로 만약 동일한 ID를 받은 브로커가 다시 생성된다면 해당 브로커는 이전 브로커의 토픽과 파티션을 할당받는다.
💡 ZNode?
주키퍼가 상태정보 저장을 노드 형태의 계층적 트리 구조 형식으로 저장한다. 여기서 노드를 ZNode라고 부른다. 파일시스템 구조와 같이 '/'로 구분하며, 절대적 경로만 지원한다. ZNode에 저장된 데이터를 읽으려면 전체 데이터를 읽어야 하고, 쓰기 또한 전체를 기준으로 갱신된다. (데이터 접근의 원자성)
노드는 영속 종류에 따라 아래와 같이 구분된다.
1. Persistent Nodes : 영구 노드로 명적으로 삭제하기 전까지 존재하게 된다.
2. Ephemeral Nodes : 임시노드로, 세션이 유지되는 동안 활성화되며 자식 노드를 가질 수 없다.
3. Sequence Nodes(순차 노드) : 경로의 끝에 일정하게 증가하는 카운터를 추가
카프카 주키퍼의 경우 Broker ZNode(브로커 상태, 아이디, IP 등등), Topic ZNode, Controller ZNode(각 파티션의 리더 브로커 정보)를 생성한다.
💡 Watch?
ZNode에 변경이 발생했을때, 이를 구독한 클라이언트에게 알림을 주는 역할을 수행한다. 특정 znode에 watch를 걸어놓으면, 해당 znode가 변경이 되었을때, 클라이언트로 callback 호출을 날려서 클라이언트에 해당 znode가 변경이 되었음을 알려준다. 일회성 이벤트 수신으로 다시 Watch하고 싶다면 재등록해야 한다.
2️⃣ 컨트롤러
컨트롤러는 일반적으로 카프카 브로커 기능에 더하여 파티션 리더를 선출하는 역할을 수행한다.
- 클러스터에서 가장 먼저 시작되는 브로커가 주키퍼의 /controller에 Ephemeral 노드의 형태로 생성되면서 컨트롤러가 된다.
- 컨트롤러를 제외한 다른 브로커는 컨트롤러의 변동사항을 전달받기 위해 컨트롤러에 Watch를 걸어둔다.
- 컨트롤러는 주키퍼에 하트비트를 전송하며 살아있음을 알리는데, `zookeeper.session.timeout.ms`설정값 기준으로 타임아웃을 설정한다. (타임아웃시, 노드도 같이 사라진다.)
만약 컨트롤러가 나간다면?
- 만약 컨트롤러 멈추게 되면, 다른 브로커들에 와치를 통해 이를 인지하고 주키퍼에 컨트롤러 생성 요청을 시도하여 성공한 브로커가 컨트롤러가 된다.
- 조건적 증가 연산으로 증가하는 에포크 기준으로 과거 컨트롤러와 최신 컨트롤러를 구별한다. (좀비 브로커 대비)
- 브로커가 컨트롤러가 된다면, 먼저 주키퍼로부터 최신 레플리카 상태 맵을 읽어온다.
- 비동기 API로 수행되며, 지연을 줄이기 위해 읽기요청을 여러 단계로 나눠 보낸다.
💡 에포크가 존재하는 이유 : 좀비 인스턴스
네트워크와 같은 다양한 외부 요인으로 연결이 끊겼다가 다시 연결되는 사이에, 변경사항을 인지하지 못하고 기존 동작을 수행하는 것을 좀비라고 한다. 좀비 생성자, 좀비 컨트롤러, 좀비 브로커 등이 존재하며, 이번 컨트롤러의 경우 컨트롤러 브로커가 잠깐 멈춘 사이 다른 브로커가 새로운 컨트롤러가 선출되어 기존 컨트롤러는 좀비가 되는 경우를 말한다.
만약 브로커가 나간다면?
- 컨트롤러는 해당 브로커가 리더를 맡고 있었던 모든 파티션에 대해 새로운 브로커를 할당해준다. (기본적으로 다음 레플리카를 가진 브로커가 리더가 된다.)
- 그리고 새로운 상태를 주피터에게 쓴 뒤, 새로운 리더가 할당된 파티션의 레플리카를 포함하는 브로커에게 LeaderAndISR 요청을 보낸다.
- 해당 요청에는, 토픽 파티션에 대하여 변경이 발생한 리더 브로커에 대한 정보와 ISR 정보를 담고 있다.
- 이후, 새로운 리더가 된 브로커는 클라이언트의 읽기, 쓰기 요청을 처리하고 팔로워는 새 리더의 메세지를 복제한다.
- 모든 브로커는 브로커와 레플리카 맵을 포함하는 MetadataCache를 가지고 있기 때문에, 컨트롤러는 모든 브로커에 변경사항에 대해 UpdateMetadata 요청을 보낸다.
3️⃣ KRaft : 카프카의 새로운 래프트 기반 컨트롤러
주키퍼로 메타데이터를 관리하는 것에 아래와 같은 문제점이 존재했다.
- 컨트롤러가 주키퍼에 메타데이터를 쓰는 작업은 동기적으로 이루어지는 반면에, 브로커 메세지를 보내는 작업과 주키퍼로부터 업데이트 받는 과정이 비동기로 이루어졌다. 그래서 브로커, 컨트롤러, 주키퍼 간에 메타데이터 불일치가 발생한다.
- 컨트롤러가 재시작될 때마다 주키퍼로부터 모든 메타데이터를 읽어와야 하고 이를 다시 모든 브로커에게 전달하는 병목이 존재한다.
그래서 주키퍼 기반 컨트롤러를 교체하는 쪽으로 진행되었고, 등장한 것이 바로 KRaft이다.
➕ 뗏목 합의 알고리즘(Raft Consensus Algorithm)

뗏목 합의 알고리즘(Raft Consensus Algorithm)은 분산 시스템 환경에서 모든 노드가 동일한 상태를 유지하도록 하고, 일부 노드에 결함이 생기더라도 전체 시스템이 문제 없이 동작하도록 만들기 위해 고안된 합의 알고리즘의 일종이다. 리더와 팔로워로 구성되어 있으며 리더와 팔로워 사이의 주고 받는 응답으로 합의를 보는 것이 특징이다.
위 사진과 같이 클러스터 전체에 대한 명령이 오직 리더로부터 팔로워에게 일방향으로 전파되며, 과정은 아래와 같다.
1. 리더는 수신된 명령에 대한 로그를 생성하여 모든 팔로워에게 복제하여 전달한다.
2. 전달 받은 팔로워는 로그에 대한 응답을 다시 리더에게 보낸다.
3. 리더를 수신한 정상 응답 수가 클러스터 전체 노드의 과반수에 이르면, 리더는 로그를 통해 전파된 명령을 클러스터의 모든 노드가 동일하게 수행하게 한 뒤 클라이언트에게 명령 수행 결과를 리턴 받는다.
네트워크 이슈로 제때 명령을 수행하지 못해도, 정상 상태로 복귀된 그동안 쌓인 명령을 순차적으로 처리한다. 그래서 클러스터 전체의 최신화 및 동기화가 유지된다.
링크 : https://seongjin.me/raft-consensus-algorithm/
- KRaft의 핵심 아이디어는 사용자가 이벤트 스트림으로 상태를 나타낼 수 있도록 하는 로그 기반 아키텍처를 도입하는 것이다.
- 다수의 컨슈머를 사용해 이벤트를 재생함으로써 최신 상태를 빠르게 따라잡을 수 있다는 것이 특징이다.
- 그리고 명확한 순서를 보여하며 하나의 타임라인을 따라 움직이는 것을 보장한다.
- 그림과 같이 컨트롤러가 3개로 증가하고, 이 중 하나가 액티브 컨트롤러가 되면서 리더가 된다. (주황색)
- 컨트롤러 노드들은 메타데이터 이벤트 로그를 관리하는 래프트 쿼럼이 된다.
- 이 로그에 클러스터 메타데이터와 관련된 모든 정보가 담겨 있다.
- 액티브 컨트롤러는 브로커가 보내온 RPC 호출을 처리한다.
- 팔로워 컨트롤러들은 액티브 큰트롤러에 쓰여진 데이터를 복제하며, 액티브 컨트롤러에 장애가 발생했을때를 대비한다.
- [추측] 메타 데이터를 컨트롤러끼리 뗏목 합의 알고리즘으로 공통된 메타데이터 정보를 가지는 것 같다.
- 그래서 이전과 달리 새 컨트롤러로 이전하는 리로드 시간은 필요로 하지 않는다.
🤔 쿼럼(Quorum)이란, 정족수를 의미하며 어떤 의견에 대한 합의를 이루는데에 필요한 최소 인원을 말한다. 뗏목 합의 알고리즘을 기반으로 동작하다보니, 과반수의 의미가 담긴 용어를 사용한 것 같다.
KRaft의 컨트롤러들은 외부 시스템에 의존하지 않고 아래와 같은 과정을 통해 자체적으로 리더를 선출한다.
- 모든 노드가 팔로워 상태를 유지하며 각자에게 주어진 선거 타임아웃 (Election Timeout)이 될 때까지 대기한다.
- 선거 타임아웃이란, 팔로워 상태의 노드가 후보자로 변환되기 전까지 대기하는 시간으로, 150 ~ 300ms 사이의 서로 다른 임으의 값을 주어진다.
- 선거 타임아웃이 가장 먼저 끝난 노드가 후보자가 되며, 새로운 선거 임기가 시작된다. 후보자 노드는 본인을 투표한뒤, 다른 노드에게 투표 요청을 한다.
- 만약 요청 메세지를 받은 노드가 선거 임기동안 투표한 적이 없다면, 투표 메세지를 보낸 뒤 자신의 선거 타임아웃을 초기화한다.
- 투표 과정에 있는 후보자 외에 또 다른 후보자가 출현하지 않도록 하기 위함이다.
- 전체 노드 수의 과반에 해당하는 응답을 얻은 노드가 새로운 리더 컨트롤러로 선정된다.
브로커는 액티브 컨트롤러부터 변경사항을 당겨온다.(poll)
- 컨슈머 읽기요청과 유사하게, 브로커는 마지막으로 가져온 메타데이터 변경 사항의 오프셋을 추적하고 그보다 나중 업데이트만 컨트롤러에 요청한다.
- 추후 시동 시간을 줄이기 위해 메타데이터를 디스크에 저장한다.
브로커 프로세스는 시작시 주키퍼가 아닌, 컨트롤러 쿼럼에 등록한다.
- 이는 운영자가 등록을 해제하지 않는 한 이를 유지한다.
- 따라서 브로커가 종료되면, 오프라인 상태로 돌아가는 것일 뿐 등록은 유지된다.
- 온라인 상태지만 최신 메타데이터로 최신 상태를 유지하고 있지 않은 브로커의 경우 펜스된 상태가 되어 클라이언트 요청을 받을 수 없다.
4️⃣ 복제
카프가 신뢰성과 지속성을 보장하는 방식으로 복제를 수행하고 있다.
- 토픽은 1개의 리더 파티션 와 다수의 팔로워 파티션으로 분할되어 처리된다.
- 만약 리더 파티션의 중단되었을때를 팔로워 파티션이 리더 역할을 수행하여 신뢰성과 지속성을 보장하는 것이다.
- 리더 파티션이 클라이언트의 요청을 처리하며, 설정에 따라 팔로워로부터 읽어올 수 있다. (client.rack 설정)
리더 파티션의 또 다른 역할은 어느 팔로워가 리더의 최신 상태를 유지하고 있는지를 확인하는 것이다.
- 팔로워는 새로운 메세지가 도착하는 즉시 리더로부터 메세지를 복제해옴으로써 최신 상태를 유지하는데, 네트워크 혼잡, 크래시 등의 다양한 원인으로 이러한 동기화가 깨질수 있다.
- 이떄 팔로워가 리더에 보내는 복제 요청에 다음번에 받아야 할 메세지 오프셋을 포함하여 보낸다. 그래서 리더 입장에서는 해당 팔로워가 최신상태를 유지하는지, 얼마나 뒤쳐저 있는지 확인할 수 있다.
만약 팔로워가 일정 시간 이상 요청(`replica.lag.time.max.ms`)을 보내지 않거나, 최신 상태를 따라오지 못한다면 out-of-sync replica로 동기화되지 않았음을 인식한다. 반면 최신 메세지도 잘 처리하고 있는 팔로워는 in-of-sync replica로 인식하여, 추후 리더가 중단되었을때 파티션 리더로 선출될 수 있다.
5️⃣ 요청 처리
브로커가 하는 일의 대부분은 클라이언트, 파티션 레플리카, 컨트롤러가 파티션 리더에게 보내는 요청을 처리하는 것이다. 이 요청을 어떻게 처리하는지에 대해 알아보자.
요청은 아래와 같은 내용을 포함하는 표준 헤더를 갖는다.
- 요청 유형 : 쓰기, 읽기, 어드민 등이 있다.
- 요청 버전 : 서로 다른 버전의 클라이언트로부터 요청을 받아 각각 버전에 맞는 응답을 할 수 있다.
- Correlation ID : 각각의 요청에 붙는 고유한 식별자
- 클라이언트 ID
그리고 요청은 아래와 같은 요청 처리 과정을 거친다.
- 연결 받은 각 포트별로 억셉터(acceptor) 스레드를 하나씩 실행한다.
- 억셉터 스레드는 연결을 생성하고, 들어온 요청을 프로세스(processor) 스레드에 넘겨 처리한다. (여러개의 프로세서 스레드 설정 가능)
- 네트워크 스레드는 요청을 요청 큐(Request Queue)에 넣고, 응답 큐에 응답을 가져다 클라이언트로 보낸다.
- I/O 스레드(Request Handler)가 요청을 가져와 요청을 처리한다.
이러한 요청은 상황에 따라 클라이언트에 보낼 지연이 필요할때가 있다. 이럴때는 퍼거토리(prugatory,버퍼)에 지연 응답들을 저장한다.
어드민 요청
일반적으로 토픽 생성이나 삭제와 같이 메타데이터 관련 작업과 관련있다.
- 클라이언트가 요청한 쓰기, 읽기는 모든 파티션의 리더 레플리카에서 처리된다.
- 만약 리더 레플리카를 가지고 있지 않은 브로커에게 요청하는 경우 `Leader for Partition`라는 에러 응답을 받는다.
- 그래서 클라이언트는 요청을 하기 전 원하는 토픽의 리더 레플리카 위치를 알기 위해 어드민 요청을 통해 토픽 관련 메타데이터를 읽어온다.
- 모든 브로커는 메타데이터 캐시를 가지고 있기 때문에 메타 데이터 요청은 아무 브로커에나 보내도 상관없다.
- 클라이언트는 보통 이 정보를 캐시해 두기 때문에, 주기적으로 메타데이터를 갱신할 필요가 있다.
- metadata.max.age.ms로 갱신 간격 설정 가능
- 만약 `Not a Leader`에러를 반환 받는다면, 이미 만료된 정보를 사용 중이거나 유효하지 않는 브로커에게 요청한 것을 의미한다.
쓰기 요청
요청이 들어오면 클라이언트의 요청을 처리할 리더 리플리카가 먼저 요청에 대한 유혀성 검증을 진행한다.
- 클라이언트가 토픽에 대한 쓰기 권한이 있는가?
- 요청에 지정된 acks 설정값이 올바른가?
- 만약 acks = all인 경우, 메세지를 안전하게 쓸 수 있는 만큼 충분한 인-싱크 레플리카가 있는가?
유효성 검증을 마친 후에는 브로커는 새 메세지들을 로컬 디스크에 쓴다.
- 이들은 언제 디크스에 반영될지는 보장할 수 없다. 카프카는 데이터가 디스크에 저장될 때까지 기다리지 않기 때문에, 복제에 의존한다.
메세지가 파티션 리더에 쓰여지고 나면, 브로커는 acks 설정에 따라 응답을 내려보낸다.
- acks = 0 혹은 1인 경우에는 바로 응답을 내보내고, acks = all 이라면 요청을 퍼거토리(prugatory,버퍼)에 저장한다.
💡 메세지를 안전하게 쓸 수 있는 만큼 충분한 인-싱크 레플리카가 있는지는 `min.insync.replicas`로 결정한다. 해당 옵션은 브로커 설정으로 최소 리플리카 팩터(replica factor)를 결정한다. 만약 해당 옵션보다 적은 레플리카를 가지고 있다면 메시지를 받지 않게 되며, `NotEnoughReplicasException`을 발생시킨다.
읽기 요청
클라이언트는 읽기 요청시 브로커가 리턴할 수 있는 최대 데이터 양을 지정하여 요청한다.
- ex) -- 토픽의 파티션 1의 오프셋 0 ~ 53까지의 메세지
- 브로커가 되돌려준 응답을 담을 수 있는 정도로 충분한 메모리를 클라이언트가 확보하기 위해서이다.
쓰기 요청때와 같이, 브로커는 읽기 요청을 받으면 요청에 대한 유효성 검사를 진행한다.
- 유효한 토픽인지, 유효한 오프셋 번호인지 등등
유효성 검사를 마쳤다면, 클라이언트가 지정한 크기 한도만큼 메세지를 읽어와 응답을 보낸다.
- 이때 제로 카피 (zero-copy) 최적화를 적용하였다.
- 제로 카피는 파일에서 읽어 온 메세지를 중간 버퍼를 거치지 않고 바로 네트워크 채널로 보내는 방식이다.
- 로컬 캐시에 저장하는 과정을 생략한 것으로, 데이터를 복사하고 메모리 상에 버퍼를 관리하기 위한 오버헤드를 없앨 수 있다.
➕ 제로 카피 자세히 살펴보기

- 디스크의 데이터를 커널 버퍼로 복사한다. ( 복사 1번, context switch 발생 )
- 이후 데이터를 커널 공간에서 애플리케이션 읽기 버퍼(User space)로 복사한다. ( 복사 2번, context switch 발생 )
- 다시 데이터를 커널 공간의 소켓 버퍼로 복사한다. ( 복사 3번, context switch 발생 )
- 마지막으로 네트워크를 통해 전달하기 위해 커널 공강의 네트워크 버퍼로 복사한다. ( 복사 4번 )
운영체제의 전통적인 방식은 데이터를 네트워크로 전송하기 위해 4번의 복사와 3번의 context switch가 발생한다. 중복 복사는 기본적으로 CPU 오버헤드와 큰 메모리 대역폭을 필요로 하기 때문에, 전체 시스템의 반응을 느리게하는 원인을 제공한다.
그래서 이를 보완하기 위해 아래 그림과 같이 유저 스페이스의 복사하는 과정을 생략하고, 곧바로 디스크 파일에서 소켓으로 복사하여 요청하는 것이 바로 Zero-Copy이다.

클라이언트가 응답받을 데이터의 양을 상한으로 지정하는 것 외에도 하한으로 지정할 수 있다.
- 트래픽이 그리 많지 않은 토픽들로부터 메세지를 읽어오고 있을 때 CPU와 네트워크 사용량을 감소시킬 수 있다.
- 클라이언트가 요청을 보냈을 때, 브로커는 충부한 양의 데이터가 모일 때까지 기다린 뒤 리턴한다.
- 데이터를 주고받는 횟수를 줄일 수 있다.
- 하지만 무작정 기다릴 수 만은 없기에, 타임아웃 역시 지정할 수 있다.
클라이언트가 파티션 리더에 존재하는 모든 데이터를 읽을 수 있는 것은 아니다.
- 모든 in-sync replica에 쓰여진 메세지들만 읽을 수 있다.
- 파티션 리더는 어느 레플리카로 복제되었는지를 팔로워가 리더에서 복사하는 과정에서 알 수 있으며, 모든 인-싱크 레플리카에 쓰여지기 전까지는 컨슈머들이 읽을 수 없다. ( 컨슈머가 응답받는 데이터의 일관성을 위해 )
- 그러므로 브로커 사이에 복제가 늦어지면, 응답시간 또한 지연이 발생하기 때문에 `replica.lag.time.max.ms`를 통해 지연시간을 제한한다.
컨슈머가 매우 많은 수의 파티션들로부터 이벤트를 읽어오는 경우가 있다.
- 읽고자 하는 파티션의 전체 목록을 요청을 보낼 때마다 브로커에 전송하고, 브로커는 모든 메타데이터를 돌려보내느 과정은 매우 비효율적이다.
- 읽고자 하는 파티션의 집합이나 메타데이터는 잘 바뀌지 않기 때문에, 오버헤드를 최소화하기 위해 카프카는 읽기 세션 캐시를 사용한다.
- 컨슈머는 더이상 요청을 보낼 때마다 파티션을 지정할 필요 없이가 없어진다.
- 브로커는 기존 요청은 캐시에서 처리하고, 변경 사항이 있는 경우에만 메타데이터를 포함하여 보낸다.
⚠️ 카프카는 팔로워 레플리카나 읽고 있는 파티션의 수가 더 많은 컨슈머를 우선시할 수 밖에 없기 때문에, 읽기 세션 캐시는 아예 생성되지 않거나 생성되었다가 해제될 수도 있다는 것을 주의하자.
6️⃣ 물리적 저장소
카프카의 기본 저장 단위는 파티션 레플리카이다. 더이상 분리할 수 없기 때문에, 파티션의 크기는 특정 마운트 지점에 사용 가능한 공간에 제한을 받는다고 볼 수 있다.
카프카를 설정할 때, 파티션들이 저장될 디렉토리 목록을 정의하는데 카프카가 사용할 각 마운트 지점별로 하나의 디렉토리를 포함하도록 설정하는 것이 일반적이다.
🤔 여기서 마운트는 파티션을 저장할 디스크나 특정 디렉토리를 나타내는 것 같다.
계층화된 저장소
계층화된 저장소가 등장하게된 배경은 아래와 같다.
- 카프카는 현재 대용량의 데이터를 저장하기 위한 목적으로 사용되고 있는데, 파티션별로 저장 가능한 데이터의 양은 물리적 디스크의 크기에 제한을 받는다.
- 저장 가능한 데이터의 양을 늘리기 위해선 물리적 디스크의 크기도 키울 필요가 있는데, 이는 비용으로 직결된다.
그래서 카프카 클러스터의 저장소를 로컬과 원격, 두 계층으로 나누는 계층화된 저장소가 등장했다.
- 로컬
- 카프카 저장소 계층과 똑같이 로컬 세그먼트를 저장하기 위해 카프카 브로커의 로컬 디스크를 사용한다.
- 비용이 비싸기 때문에, 보존 기한을 몇 시간 이하로 설정한다.
- 원격 저장소에 비해 지연이 짧으며, 실시간 데이터를 관리한다.
- 원격
- 완료된 로그 세그먼트를 저장하기 위해 HDFS나 S3와 같은 전용 저장소 시스템을 사용한다.
- 로컬에 비해 비용이 저렴하여, 보존 기한을 길게 설정한다.
- 보존 기한이 긴만큼, 빠진 처리 결과를 메꾸거나 복구하는 작업에 필요한 오래된 데이터를 관리한다.
이러한 이중화된 구조 덕분에 카프카 클러스터의 메모리와 CPU와 상관없이 저장소를 확장할 수 있다. 즉, 계층화된 저장소 기능은 무한한 저장 공간, 더 낮은 비용, 탄력성뿐만 아니라 오래된 데이터와 실시간 데이터를 읽는 작업을 분리시키는 역할 또한 수행한 것이다.
파티션 할당
파티션 할당에서의 목표는 아래와 같다.
- 레플리카들을 가능한 한 브로커 간에 고르게 분산시킨다.
- 각각의 레플리카는 서로 다른 브로커에 배치되도록 한다.
- 브로커에 랙 정보가 있다면, 가능한 한 각 파티션의 레플리카들을 서로 다른 랙에 할당한다.
이러한 목표를 이루기 위해 각 브로커에 라운드 로빈(Round Robin)방식으로 파티션을 할당한다.
- 파티션의 리더들을 먼저 특정 브로커부터 차례대로 할당한다.
- 이후 파티션의 레플리카를 리더가 할당된 브로커 기준 증가하는 순서로 배치한다.
만약 랙 인식 기능을 고려해야 할때는 서로 다른 랙의 브로커가 번갈아 선택되도록 순서를 정해주면 된다.
- 각 랙에 걸려있는 브로커의 정보를 알고 있다면, 파티션을 0-1-2-3이 아닌 위와 같이 0-2-1-3으로 할당하는 것이다.
- 그렇게 되면 첫번째 랙이 오프라인이 되더라도 다른 랙이 대체하여 작동할 수 있게 된다. ( 가용성 증가 )
각 파티션과 레플리카에 올바른 브로커를 선택했다면, 새 파티션을 저장할 디렉토리를 결정해야 한다.
- 디렉토리에 저장되어 있는 파티션의 수를 센 뒤, 가장 적은 파티션이 저장된 디렉토리에 새 파티션을 저장된다.
- 만약 새로운 디스크를 추가할 경우, 모든 새 파티션들은 이 디스크에 생성될 것이다.
⚠️ 파티션을 디스크에 할당해 줄때, 디스크에 저장된 파티션의 수만 고려될 뿐 크기는 고려되지 않는 다는 점을 명심하자. 즉 몇몇 파티션들이 비정상적으로 크거나, 같은 브로커에 서로 다른 크기의 디스크들이 장착되어 있을 수 있기 때문에 파티션 할당에 주의할 필요가 있다.
파일 관리
카프카는 영구히 데이터를 저장하거나 컨슈머가 메세지를 읽을때 까지 대기하지 않기 때문에, 토픽에 대한 보존 기한을 설정한다.
그리고 하나의 파티션읠 여러개의 세그먼트로 분할하여 관리한다.
- 각 세그먼트는 1GB의 데이터 혹은 최근 1주일 치의 데이터 중 적은 쪽만큼을 저장한다.
- 세그먼트 한도가 다 차면, 카프카는 세그먼트를 닫고 새 세그먼트를 생성한다.
- 활성화된 세그먼트를 액티브 세그먼트라 부르는데, 액티브 세그먼트는 어떤한 경우에도 삭제되지 않는다.
- 그래서 세그먼트안에 보존 기한이 만료된 로그가 존재해도, 계속해서 보존된다. ( 세그먼트가 삭제될때까지 )
파일 형식
각 세그먼트는 하나의 데이터 파일 형태로 저장되며, 카프카의 메세지와 오프셋를 가지고 있다.
- 특징으로 프로듀서에서 브로커로 보내지는, 브로커에서 컨슈머로 보내지는 메세지 형식과 동일하다는 것이다.
- 그래서 네트워크 버퍼로 바로 전송이 가능한 제로 카피 최적화가 가능했던 이유이기도 하다.
- 그래서 만약 메세지 형식을 바꾸고 싶다면, 네트워크 프로토콜과 디스크 저장 형식을 모두 변경해야 한다.
카프카 메세지는 사용자 페이로드와 시스템 헤더 두부분으로 나뉘어진다.
- 페이로드 : 키값과 밸류값, 헤더 모음
- 시스템 헤더 : 헤더 관련 자체적인 키/밸류 순서쌍
카프카 프로듀서는 언제나 메세지를 배치 단위로 전송한다.
- 메세지 배치를 배치 단위로 묶음으로써 공간을 절약하게 되는 만큼 네트워크 대역포고가 디스크 공간을 덜 사용하게 된다.
- 그래서 배치 단위로 많이 묶을 수록 효율을 높일 수 있다.
- linger.ms 설정으로 지연을 주어 배치에 더 많은 메세지가 묶이도록 하는 것도 이러한 이유이다.
- 카프카가 파티션별로 별도의 배치를 생성하기에, 더 적은 수의 파티션에 쓰는 프로듀서가 효율적이다.
- 카프카 프로듀서가 같은 쓰기 요청에 여러 개의 배치를 포함할 수 있다는 점을 기억하자.
메세지 배치 헤더에는 다음과 같은 것들이 포함되어 있다.
메세지 배치 헤더 | ||
매직 넘버 : 메세지 형식의 현재 버전 | 배치에 포함된 첫 번째 메세지의 오프셋과 마지막 오프셍의 차이 |
첫 번째 메세지의 타임스탬프와 배치에서 가장 큰 타임스템프 |
배치 크기 (바이트 단위) | 해당 배치를 받은 리더의 에포크 값 | 배치 오염 확인을 위한 체크섬 |
압축 유형, 타임스탬프 유형 등을 나타내는 16비트의 속성 정보 |
[정확히 한번 보장] 프로듀서 ID, 프로듀서 에포크, 배치의 첫 번째 시퀀스 넘버 |
배치에 포함된 메세지들의 집합 |
레코드 역시 자체적인 시스템 헤더를 가지고 있다.
레코드 헤더 | ||
바이트 단위로 표시한 레코드 크기 | 속성 (사용 X) | 현재 레코드의 오프셋과 배치 내 첫 번째 레코드의 오프셋과의 차이 |
현재 레코드의 타임스탬프와 배치 내 첫 번째 레코드의 타임스탬프 차이 |
사용자 페이로드 |
이렇듯 대부분의 시스템 정보는 배치 단위에서 저장되어 있다.
사용자 데이터를 저장하는 메세지 배치 외에도, 카프카에는 컨트롤러 배치가 있다.
- 트랜잭션 커밋 등을 가리키며, 현재는 어플리케이션 관점에서는 보이지 않고 버전과 타입 정보만 포함한다.
- 0은 중단된 트랜잭션, 1은 커밋이다.
인덱스
카프카는 컨슈머가 임의의 사용 가능한 오프셋부터 메세지를 읽어을 수 있도록 지원하고 있다.
- 그래서 특정 오프셋을 빠르게 찾아 해당 오프셋부터 읽어오는 것이 중요한데, 이때 인덱스를 활용하고 있다.
- 이 인덱스는 오프셋과 세그먼트 파일 및 그 안에서의 위치를 매핑한다.
뿐만 아니라, 타임스탬프와 메세지 오프셋을 매핑하는 또 다른 인덱스를 가지고 있다.
- 이 인덱스는 타임스탬프를 기준으로 메세지를 찾을 때 사용되며, 카프카 스트림즈에서 유용하게 사용되고 있다.
카프카는 인덱스의 체크섬을 유지하거나 하지 않는다.
- 인덱스가 오염될 경우, 해당하는 로그 세그먼트에 포함된 메세지들을 다시 읽어서 자동으로 재생성한다.
- 복구시간이 존재하긴 하지만, 그만큼 안전하다.
압착
카프카는 설정된 기간 동안만 메세지를 저장하며, 보존 시간이 지난 메세지들을 삭제한다. 하지만 과거 상태가 아닌 최신 상태를 더 중요시 하는 서비스의 경우 불필요하게 과거 상태를 오래 가지고 있을 필요가 없다.
그래서 이때 사용되는 것이 압착이다.
카프카는 두가지 보존 정책을 허용한다.
- 삭제(Delete) 보존 정책 : 지정된 보존 기한보다 더 오래 된 이벤트를 삭제한다.
- 압착(Compact) 보존 정책 : 토픽에서 각 키의 가장 최근값만 저장하도록 한다.
- 어플리케이션이 키와 밸류를 모두 포함하는 이벤트를 생성하는 토픽의 경우, 삭제 정책을 압착으로 설정하는 것이 합리적이다.
보존기한과 압착 설정을 동시에 적용할 수도 있다.
- 지정된 보존 기한보다 오래된 메세지들은 키에 대한 가장 최근값이 경우에도 삭제된다.
- 압착된 토픽이 지나치게 크게 자라는 것을 방지해주고, 일정 기한이 지나간 레코드들을 삭제해야 하는 경우 활용된다.
압착 작동원리
각 로그는 아래와 같이 2가지 영역으로 나뉜다.
- 클린 : Tail 영역이라고도 불리며, 압착이 완료된 메세지들이 저장되는 영역을 말한다. 압착이 완료되었다보니, 중복되는 키의 레코드가 없다는 것이 특징이다.
- 더티 : Head 영역이라고 불리며, 압착을 대기하고 있는 레코드들을 있는 영역이다. 중복된 키의 레코드들이 있을 수 있다.
카프카가 시작되었을 때, 압착 기능이 활성화 되어 있다면 브로커는 압착 매니저 스레드와 함께 다수의 압착 스레드를 시작시킨다.
- 전체 파티션 크기 대비 더티 영역 비율이 큰 파티션을 골라 압착시킨다.
압착하기 위해서는, 클리너 스레드는 파티션의 더티 영역을 읽어서 인-메모리 맵을 생성한다.
- 맵의 각 항목은 16바이트 메세지 키와 8바이트의 이전 메세지 오프셋으로 이루어진다. ( 총 24 바이트 )
- 만약 백만개의 메세지가 있다면, 필요한 전체 맵 크기는 24MB가 된다. ( 실제로는 이보다 훨씬 적게 사용된다. )
- 운영자가 오프셋 맵을 저장하기 위해 사용할 수 있는 메모리 양을 설정할 수 있는데, 실행될 클리너 스레드의 개수를 인지하여 최소한 하나의 세그멘트 전체가 들어갈 수 있게 설정하는 것을 기억하자.
압착은 아래와 같은 과정으로 진행된다.
- 클린 세그먼트들은 오래된 것부터 읽어들이면서 오프셋 맵의 내용과 대조한다.
- 만약 오프셋 맵에 없는 키값의 메세지를 확인했다면?
- 최신값이라는 의미로, 교체용 세그먼트로 복사한다.
- 만약 이미 존재하는 키값의 메세지라면?
- 파티션 내에 같은 키값을 가졌지만 더 새로운 밸류값을 갖는 메세지가 있다는 의미이므로 해당 메세지는 건너띈다.
이러한 압착과정을 거치면 키별로 하나의 메세지만 남게 된다.
삭제된 이벤트
특정 키를 갖는 모든 메세지를 삭제하고 싶을때는, 톰스톤 메세지를 활용하면 된다.
- 톰스톤 메세지란, 해당 키값과 null 밸류값을 갖는 메세지를 말한다.
- 클리너 스레드가 이 메세지를 발견하면, 평소대로 압착 작업을 한 뒤, null 밸류값을 갖는 메세지만 보존하게 된다.
- 그 기간동안 컨슈머는 이 메세지를 보고 해당 메세지가 삭제되었음을 알 수 있다.
- 그래서 컨슈머가 톰스톤 메세지를 실제로 볼 수 있도록 충분한 시간을 주는 것이 중요하다.
여기서 말하는 삭제 방식은 어드민 클라이언트에서의 deleteRecords 메서드와는 동작방식이 다르다.
- 해당 메서드는 파티션의 첫 번째 레코드를 가리키는 로우 워터 마크를 입력받은 오프셋으로 이동시키는 것이다.
- 이렇게 하면 컨슈머는 업데이트된 로우 워터 마크 이전의 메세지를 읽을 수 없게 되는 것이다.
- 이 메서드는 보존 기한이 설정되어 있거나 압착 설정이 되어 있는 토픽에 사용 가능하다.
토픽은 언제 압착될까?
- 액티브 세그먼트가 아닌 세그먼트에 저장되어있는 메세지만이 압착 대상이 된다.
- 운영자는 두 설정 매개변수를 사용해서 압착이 시작되는 시점을 조절할 수 있다.
- min.compaction.lag.ms : 메세지가 쓰여진 뒤 압착될 때까지 지나가야 하는 최소시간
- max.compaction.lag.ms : 메세지가 쓰여진 뒤 압착이 가능해질 때까지 딜레이 될 수 있는 최대 시간
- max.compaction.lag.ms의 경우, 압착이 반드시 실행된다는 것을 보장해야 하는 상황에 자주 사용한다.
➕ min. cleanable.dirty.ratio (0.1 ~ 0.9)
세그먼트들에 남아 있는 클린 영역과 더티 영역의 개수 비율을 설정하는 것이다. 0.5로 설정했다면, 클린 영역과 더티 영역의 개수가 동일해졌을때 압착을 시작한다. 만약 해당 값을 너무 높게 잡는다면 압축 효과는 좋지만 데이터가 계속해서 쌓인다는 단점이 있다. 반대로 낮게 잡았다면, 최신 데이터를 유지할 수 있지만 압착이 자주 일어나 브로커에 부담을 줄 수 있다.
'개발 서적 > 카프카 핵심 가이드' 카테고리의 다른 글
Kafka : '정확히 한 번' 의미 구조 (1) | 2025.01.26 |
---|---|
Kafka : 신뢰성 있는 데이터 전달 (0) | 2025.01.14 |
Kafka : 어드민 클라이언트(Admin Client) (0) | 2024.12.08 |
Kafka : 컨슈머(Consumer) - (2) (0) | 2024.11.24 |
Kafka : 컨슈머 (Consumer) - (1) (0) | 2024.11.24 |