Kafka
글
-
Kafka 소스 파헤치기 1: AI와 함께 소스를 읽는 방법과 분석 지도
Kafka가 무엇이고 어떻게 동작하며 어디에 쓰는지를 그림과 3분 실습으로 먼저 보고 시작하는, Kafka 소스 읽기 연재의 첫 글입니다. 어떤 판(trunk, 4.5.0-SNAPSHOT)을 읽는지와 안정판 4.4.0과의 관계, 같은 소스를 받아 도커에서 Gradle로 빌드하는 법과 JSON 스키마에서 코드를 만드는 processMessages 단계, 72만 줄짜리 본체의 모듈 지도, 레코드 하나가 send()에서 디스크와 복제를 거쳐 poll()까지 지나는 큰 길, 그리고 AI가 쓴 노트를 소스로 다시 확인하며 읽는 방법을 정리합니다.
-
Kafka 소스 파헤치기 2: 전체 지도 — 클라이언트, 브로커, 컨트롤러, 그리고 파티션
Kafka 소스를 처음 여는 중급 개발자를 위한 전체 지도입니다. kafka-server-start.sh 가 부르는 kafka.Kafka 의 main 에서 시작해 process.roles 에 따라 BrokerServer·ControllerServer·SharedServer 가 만들어지는 길, 브로커가 시작할 때 만드는 부품, 컨트롤러가 메타데이터 로그로 토픽·파티션·리더를 정하고 브로커에 퍼뜨리는 길, 클라이언트가 Metadata 응답으로 리더를 찾아가는 길, 모듈 의존 방향을 실제 소스와 직접 돌린 예제로 확인하고, 3~12편이 지도의 어디를 확대하는지 짚습니다.
-
Kafka 소스 파헤치기 3: 와이어 프로토콜 — JSON 스키마에서 만들어지는 요청과 응답
프로듀서와 브로커가 TCP 위에서 주고받는 바이트는 [4바이트 길이][요청 헤더][본문] 한 가지 모양입니다. 그 모양을 정하는 JSON 스키마 203개가 빌드 때 자바 클래스 391개가 되는 과정, 판(version)을 고르는 ApiVersions 협상, 옛 판과 새 판이 함께 사는 유연한 판과 태그 필드를 소스로 따라가고, 생성된 클래스만으로 맨 소켓에 요청을 보내는 예제로 바이트를 직접 확인합니다.
-
Kafka 소스 파헤치기 4: 브로커의 네트워크 계층 — Acceptor, Processor, 요청 처리 스레드
브로커의 jstack 에 보이는 socket-acceptor, network-thread, request-handler 세 종류의 스레드가 요청 하나를 어떻게 나눠 맡는지 SocketServer.scala 로 따라갑니다. 리스너마다 Acceptor 하나, Processor 가 바이트를 읽어 요청 큐에 넣고 그 연결을 mute 하면, 요청 처리 스레드가 KafkaApis.handle() 의 match 로 갈래를 나누고, 응답은 요청을 읽은 Processor 로 돌아갑니다. 한 연결의 요청을 하나씩만 처리해 순서를 지키는 mute/unmute, num.network.threads·num.io.threads·queued.max.requests 가 걸리는 자리를 jstack·맨 소켓·요청 로그 예제로 확인하고 Tomcat 의 NIO 커넥터와 비교합니다.
-
Kafka 소스 파헤치기 5: 프로듀서 내부 — send()에서 배치, 그리고 Sender 스레드까지
KafkaProducer.send() 는 네트워크로 아무것도 보내지 않습니다. 레코드를 직렬화하고 파티션을 고른 뒤 RecordAccumulator 의 파티션별 배치에 넣고 Future 를 돌려줄 뿐이고, 실제로 보내는 것은 kafka-producer-network-thread 하나입니다. linger.ms·batch.size 가 걸리는 ready() 의 if, 키 없는 레코드를 한 파티션에 붙여 두는 내장 파티셔너, 배치 메모리를 나눠 주는 BufferPool 과 max.block.ms, 재시도와 delivery.timeout.ms, max.in.flight.requests.per.connection 을 소스로 따라가고, kafka-dump-log.sh 로 배치 수를 세는 예제와 콜백 스레드를 찍는 예제로 확인합니다.
-
Kafka 소스 파헤치기 6: 브로커가 레코드를 받아 쓰기까지 — 레코드 배치 형식과 Produce 요청 처리
프로듀서가 보낸 레코드 배치는 61바이트 헤더와 varint 로 줄인 레코드로 되어 있습니다. 그 바이트를 파일에서 직접 한 칸씩 읽어 보고, Produce 요청 하나가 KafkaApis → ReplicaManager → Partition → UnifiedLog → LogValidator → LogSegment 를 지나 파일에 닿는 길을 소스와 브로커 TRACE 로그로 따라갑니다. 브로커가 배치를 풀지 않고 헤더 몇 칸만 고쳐 그대로 쓰는 설계(CRC 가 그대로인 것을 확인), 토픽의 compression.type 이 다르면 다시 압축하는 경우, acks=0/1/all 이 응답 시점을 바꾸는 자리까지 손으로 만든 Produce 요청으로 확인합니다.
-
Kafka 소스 파헤치기 7: 로그 저장소 — 세그먼트, 인덱스, 보존과 압축
파티션 폴더의 00000000000000003830.log 같은 파일이 언제 새로 생기고, 오프셋 하나를 파일의 바이트 위치로 어떻게 바꾸고, retention.ms 가 지난 세그먼트를 누가 언제 지우고, cleanup.policy=compact 가 같은 키의 옛 값을 어떻게 걷어 내는지 UnifiedLog·LocalLog·LogSegment·OffsetIndex·LogCleaner 소스로 따라갑니다. 세그먼트 롤의 다섯 조건, 4KB마다 한 항목만 적는 희소 인덱스와 mmap, .deleted 로 이름을 바꾼 뒤 지우는 두 단계, 키의 MD5로 만드는 SkimpyOffsetMap, 브로커가 죽었다 살아날 때의 복구 지점을 예제로 확인합니다.
-
Kafka 소스 파헤치기 8: 읽기 경로 — Fetch 요청, 롱 폴링, 그리고 제로 카피
컨슈머의 poll() 이 보낸 Fetch 요청이 브로커 안에서 KafkaApis.handleFetchRequest() → ReplicaManager.fetchMessages() → UnifiedLog.read() → LogSegment.read() 를 지나 파일의 한 조각(FileRecords)이 되고, 데이터가 모자라면 fetch.max.wait.ms 동안 퍼거토리(DelayedFetch)에서 기다렸다가, 마지막에 FileChannel.transferTo() 로 JVM 을 거치지 않고 소켓으로 나가는 길을 Kafka 4.5.0-SNAPSHOT 소스로 따라갑니다. 증분 fetch 세션, 컨슈머와 팔로워가 같은 Fetch 를 쓰는 이유, TLS 에서 제로 카피가 사라지는 이유를 요청 로그·JMX·JFR 예제로 확인합니다.
-
Kafka 소스 파헤치기 9: 복제 — ISR, 하이 워터마크, 그리고 acks=all의 기다림
팔로워 브로커의 ReplicaFetcherThread 가 리더에게 Fetch 를 보내 로그를 따라 쓰고, 리더는 그 Fetch 의 오프셋으로 팔로워의 위치를 알아 하이 워터마크를 올립니다. ISR 에서 빠지는 기준(replica.lag.time.max.ms 와 lastCaughtUpTimeMs), ISR 변경을 컨트롤러에 알리는 AlterPartition, acks=all 요청이 DelayedProduce 로 퍼거토리에서 기다리는 조건과 min.insync.replicas 가 거절하는 자리, 리더가 바뀐 뒤 리더 에포크로 갈라진 로그를 잘라 내는 길을 Partition.scala·AbstractFetcherThread.scala 로 따라가고, 브로커 셋을 멈추고 죽이는 예제로 확인합니다.
-
Kafka 소스 파헤치기 10: KRaft와 메타데이터 — 컨트롤러는 클러스터를 어떻게 기억하나
토픽·파티션·리더·브로커 목록 같은 클러스터의 장부가 __cluster_metadata 라는 평범한 Kafka 로그에 레코드로 쌓이고, 컨트롤러들이 Raft 로 그 로그를 함께 지키는 과정을 소스로 따라갑니다. KafkaRaftClient 의 역할 상태(Prospective·Candidate·Leader·Follower)와 Vote·Fetch, 투표자 과반으로 정하는 하이 워터마크, 이벤트 스레드 하나로 도는 QuorumController 가 레코드를 만들고 커밋 전에 메모리에 먼저 적용했다가 되돌리는 SnapshotRegistry, 브로커가 레코드를 MetadataDelta·MetadataImage 로 바꿔 적용하는 MetadataLoader, 하트비트와 펜스를 kafka-dump-log·kafka-metadata-quorum·컨트롤러 3대 선거 예제로 확인합니다.
-
Kafka 소스 파헤치기 11: 컨슈머와 그룹 코디네이터 — poll()과 리밸런스의 안쪽
KafkaConsumer 는 group.protocol 값에 따라 ClassicKafkaConsumer 나 AsyncKafkaConsumer 에 일을 넘기는 껍데기입니다. 기본값 classic 에서는 poll() 을 부른 스레드가 JoinGroup·SyncGroup 으로 리밸런스를 직접 치르고 리더 컨슈머가 할당을 계산하며, consumer 프로토콜(KIP-848)에서는 백그라운드 스레드가 ConsumerGroupHeartbeat 하나로 브로커와 이야기하고 브로커가 할당을 정합니다. 브로커 쪽 그룹 코디네이터가 그룹 상태를 __consumer_offsets 에 레코드로 쓰고 다시 읽어 되살리는 CoordinatorRuntime 까지, 컨슈머 두 개를 차례로 붙이는 예제와 kafka-dump-log.sh 로 확인합니다.
-
Kafka 소스 파헤치기 12: 멱등성과 트랜잭션 — 정확히 한 번은 어떻게 만들어지나, 그리고 연재를 마치며
enable.idempotence=true 와 transactional.id 한 줄 뒤에서 무슨 일이 생기는지 소스로 따라갑니다. 브로커가 producer id·에포크·시퀀스로 재시도 중복을 가려내는 곳, 트랜잭션 코디네이터가 __transaction_state 에 상태를 적고 커밋·중단 마커(컨트롤 레코드)를 파티션마다 쓰게 하는 길, read_committed 컨슈머가 마지막 안정 오프셋(LSO)과 중단된 트랜잭션 목록으로 걸러 읽는 방법, 같은 transactional.id 의 옛 프로듀서를 에포크로 막는 펜싱을 맨 소켓·kafka-dump-log·kafka-transactions 예제로 확인합니다. 마지막 글이라 1~12편을 되짚으며 연재를 마칩니다.