Kafka ทำงานอย่างไรข้างใน
โมดูลนี้มีไว้ทำอะไร
หัวข้อที่มีชื่อว่า “โมดูลนี้มีไว้ทำอะไร”โมดูลก่อน ๆ สอนให้คุณ ใช้ Kafka: produce, consume, ตั้งค่า topic, จูน batching, รัน transaction แต่ไม่มีบทไหนอธิบายว่าจริง ๆ แล้วมีอะไรวิ่งผ่านสายตอน producer.send() คืนค่า, rebalance คืออะไรในระดับ RPC จริง ๆ, follower ทำอะไรอยู่ระหว่างที่คุณ produce จนถึงตอนที่ acks=all คืนค่า, __cluster_metadata record หน้าตาเป็นยังไงจริง ๆ, หรือทำไม .log segment ถึงถูกจัดวางแบบที่เป็นอยู่ ช่องว่างนี้เองที่ทำให้คำอธิบาย Kafka ที่ฟังดูมั่นใจ (จากคนหรือจาก AI) ผิดพลาดได้ในแบบที่ kafka-topics.sh --describe เพียงอย่างเดียวจับไม่ได้
โมดูลนี้ตอบคำถามภายใน 5 ข้อ แต่ละข้อพิสูจน์กับ broker จริง ไม่ใช่แค่กล่าวอ้าง:
- RPC อะไรเกิดขึ้นจริงระหว่าง
producer.send()กับcommitOffsetsของ consumer? (Produce-to-Fetch Pipeline — Mermaid sequence เดียว พิสูจน์ทีละ hop ด้วยบรรทัด request log จริง) - consumer group rebalance หน้าตาเป็นยังไงจริง ๆ บนสาย ระหว่าง classic กับ KIP-848? (Consumer Group Protocol Internals —
JoinGroup/SyncGroupจริงจาก KafkaJS,ConsumerGroupHeartbeatจริงจาก confluent client) - follower ทำอะไรอยู่จริง ๆ และ ISR shrink หน้าตาข้างในเป็นยังไง? (Replication and ISR Internals — คลัสเตอร์ KRaft 3-broker จริง, controller failover จริง,
PartitionChangeRecordจริง) - cluster metadata คืออะไรจริง ๆ และ controller quorum ตกลงกันยังไง? (KRaft Metadata Log —
__cluster_metadatarecord จริงที่ decode ด้วยkafka-dump-log.sh --cluster-metadata-decoder) - ไบต์บนดิสก์หน้าตาเป็นยังไงจริง ๆ และทำไม fsync ไม่ใช่กลไก durability? (Storage and Segments —
hexdump -Cจริงของไฟล์.logdecode ด้วยมือแล้ว cross-check กับkafka-dump-log.sh)
ทุก proof ในโมดูลนี้ทำมายังไง
หัวข้อที่มีชื่อว่า “ทุก proof ในโมดูลนี้ทำมายังไง”ทุกคำกล่าวอ้างระดับ RPC ด้านล่างคือบรรทัดจริง ตัดมาสั้น ๆ จาก request log ของ broker — log stream คนละตัวจาก server.log ปิดอยู่โดย default (kafka.request.logger ที่ WARN ซึ่ง log แค่ request ที่พังหรือช้า) broker scratch ของโมดูลนี้เปิดขึ้นเป็น TRACE โดย mount log4j2.yaml ที่แก้แล้วเข้าไปที่ /mnt/shared/config/log4j2.yaml — จุด merge เดียวกันที่สคริปต์ launch ของ image apache/kafka:4.3.1 เองใช้อยู่แล้ว (kafka.docker.KafkaDockerWrapper setup --mounted-configs-dir /mnt/shared/config) เพื่อซ้อน override ของผู้ดูแลระบบทับค่า default ของ image ไม่มี environment variable ชื่อ KAFKA_LOG4J_OPTS บน image นี้ (ชื่อนี้ไม่มีจริงใน apache/kafka:4.3.1) — bind-mount คือกลไกจริงที่ตรวจสอบแล้ว ที่ log level นี้ ทุก request ที่เสร็จแล้วคือ JSON หนึ่งบรรทัดที่บอก requestApiKeyName, correlationId, clientId, และ field ของ request/response ตรง ๆ (acks, fetchOffset, highWatermark, memberEpoch, …) — โมดูลนี้ยกบรรทัดเหล่านั้นมาตรง ๆ ไม่แก้ไขนอกจากตัดให้สั้น
เพราะการเปิดสิ่งนี้ต้องแก้ log4j ที่ harness verification กลางไม่มี proof ที่มาจาก request log ทุกอันในโมดูลนี้จึงเป็น text transcript จากการรันจริงบน broker scratch ของโมดูลนี้เอง (KRaft, single combined node, พอร์ต 29092) ไม่ใช่ fence ที่ harness รันจริง ส่วนอะไรที่ต้องการแค่ broker ปกติ — kafka-dump-log.sh, kafka-topics.sh --describe, kafka-metadata-quorum.sh — จะเป็น bash fence ที่ harness รันจริง เหมือนบทอื่นในคอร์ส Replication and ISR Internals ใช้ compose KRaft 3-broker ชั่วคราวเพิ่ม (ปิดทันทีหลังใช้) เพราะพฤติกรรม ISR/failover ไม่มีทางเกิดบน node เดียว
flowchart TD p["Produce-to-Fetch Pipeline\n(ลำดับ RPC)"] --> g["Consumer Group Protocol Internals\n(JoinGroup/SyncGroup vs ConsumerGroupHeartbeat)"] p --> r["Replication and ISR Internals\n(follower fetch, ISR shrink, failover)"] r --> k["KRaft Metadata Log\n(log ที่ทำให้ r เป็นไปได้)"] p --> s["Storage and Segments\n(ไบต์ที่ p append จริง)"] k --> s
ลำดับการอ่านและสิ่งที่แต่ละบทสมมติไว้แล้ว
หัวข้อที่มีชื่อว่า “ลำดับการอ่านและสิ่งที่แต่ละบทสมมติไว้แล้ว”| บท | ต่อจาก (Phase 3) | ตอบคำถาม |
|---|---|---|
| Produce-to-Fetch Pipeline | The Log Abstraction, Acks and Durability | RPC อะไรวิ่งผ่านสายจริงจากหนึ่ง producer.send() ถึงหนึ่ง commitOffsets()? |
| Consumer Group Protocol Internals | Consumer Groups and Rebalancing, Consumer API | JoinGroup/SyncGroup เทียบกับ ConsumerGroupHeartbeat หน้าตาเป็นยังไงจริง ๆ ทีละ RPC? |
| Replication and ISR Internals | Replication and ISR | FetchRequest ของ follower มีอะไรอยู่ข้างใน และ ISR shrink เกิดอะไรขึ้นจริง? |
| KRaft Metadata Log | KRaft & the Cluster | __cluster_metadata record จริงหน้าตาเป็นยังไง และ controller failover จริงเร็วแค่ไหน? |
| Storage and Segments | The Log Abstraction | ไบต์ดิบของไฟล์ .log/.index เก็บอะไรจริง ๆ และทำไม fsync ไม่ได้ให้ durability? |
บท Phase 3 ที่ลิงก์ไว้ข้างบนแสดงมุมมอง ข้างนอก ให้คุณแล้ว (error message, CLI describe, timing ที่ client มองเห็น) โมดูลนี้แสดงกลไก ข้างใน ที่อยู่หลังมุมมองนั้น — ไม่สอนมุมมองข้างนอกซ้ำ แค่ลิงก์ไปหา
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”ทุกบทในโมดูลนี้คือไอเดียเดียวกันที่เอาไปใช้กับ subsystem ต่างกัน: พฤติกรรมของ Kafka ไม่ใช่ black box — มันคือ RPC และ on-disk record ชุดตายตัวที่ request log, metadata dump, หรือ hex editor แสดงให้คุณเห็นตรง ๆ ได้เสมอ “rebalance” ไม่ใช่การหยุดชะงักปริศนา มันคือลำดับ RPC ที่เรียกชื่อได้ชัดเจน (JoinGroup→SyncGroup หรือ ConsumerGroupHeartbeat หนึ่งตัว) ที่ request log แสดงให้เห็นทั้งหมด “ISR shrink” ไม่ใช่เหตุการณ์คลัสเตอร์ที่มองไม่เห็น มันคือ PartitionChangeRecord ที่ถูก append เข้า __cluster_metadata ดูได้ด้วย kafka-dump-log.sh --cluster-metadata-decoder “.log segment” ไม่ใช่ abstraction มันคือไบต์ที่ offset ตายตัวที่ hexdump -C กับ kafka-dump-log.sh เห็นตรงกันไบต์ต่อไบต์ พอรู้ว่าเครื่องมือไหนพิสูจน์คำกล่าวอ้างไหน คำว่า “เชื่อผมเถอะ Kafka ทำงานแบบนี้” ก็ไม่จำเป็นอีกต่อไป
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “
acks=allแปลว่า replica ทุกตัวยืนยันการเขียนก่อน producer จะได้ response” Produce-to-Fetch Pipeline พิสูจน์กลไกจริง: leader append แล้วรอให้ ISR (ไม่ใช่ “ทุก replica” แน่นอน — ดู Replication and ISR) เลื่อน high watermark ผ่าน offset ของ record ใหม่ ผ่านFetchRequestของ follower เอง ไม่ใช่ leader push - “
ConsumerGroupHeartbeatของ KIP-848 ก็แค่JoinGroupเปลี่ยนชื่อ” Consumer Group Protocol Internals พิสูจน์ว่ามันต่างกันโดยโครงสร้าง: classic ใช้คู่ RPC สองตัวJoinGroup→SyncGroup(แต่ละตัวคือ barrier synchronize ทั้งกลุ่ม); KIP-848 ใช้ RPC เดียวที่วนซ้ำ พร้อม assignment ของ member เองอยู่ใน response เลย ไม่มีSyncGroupเลย - “ISR shrink กับ broker fencing เป็นกลไกเดียวกัน” Replication and ISR Internals พิสูจน์ด้วย controller log จริงว่าทั้งสองเป็นโค้ดพาธคนละเส้นใน KRaft controller ที่บังเอิญให้อาการที่เห็นเหมือนกัน (replica หลุดจาก ISR)
- “cluster metadata อยู่ในฐานข้อมูลที่ controller query” KRaft Metadata Log พิสูจน์ว่า metadata คือ log แบบ append-only สไตล์ Kafka ปกติ (topic
__cluster_metadata) อ่านได้ด้วยเครื่องมือkafka-dump-log.shตัวเดียวกันที่ใช้กับ topic ข้อมูลทั่วไป - “fsync คือสิ่งที่ทำให้การเขียนของ Kafka durable” Storage and Segments พิสูจน์ตรงกันข้ามจาก config จริงของคลัสเตอร์นี้:
log.flush.interval.messagesdefault เป็นLong.MAX_VALUE(คือไม่เกิดขึ้นเลย) เพราะ durability มาจาก replication (ISR) ไม่ใช่การบังคับ flush ทุกครั้งก่อน ack
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”ตัวอย่างของแพทเทิร์นที่ทุกบทในโมดูลนี้ทำซ้ำอย่างละครั้ง: คำกล่าวอ้างเกี่ยวกับ Kafka internals ที่ฟังดูสมเหตุสมผล แต่การตรวจสอบระดับ RPC หรือไบต์หักล้างได้
🐛 หา bug ในโค้ดนี้
// A comment an AI assistant added above a producer config// "acks: 'all' waits for every replica in the cluster to write the// record before returning -- this guarantees zero data loss."const producer = kafka.producer();await producer.send({topic: 'orders',acks: -1,messages: [{ key: orderId, value: JSON.stringify(order) }],});เฉลย
คอมเมนต์นี้มีปัญหาจริง 2 อย่างที่พิสูจน์แล้วในโมดูลนี้ ข้อแรก Produce-to-Fetch Pipeline แสดงว่า acks=all (-1) รอ ISR ไม่ใช่ “replica ทุกตัวในคลัสเตอร์” — ถ้า ISR shrink ลงเหลือแค่ leader (เป็นไปได้จริง พิสูจน์ใน Replication and ISR Internals) acks=all จะคืนค่าหลังจาก broker เดียวมี record เท่านั้น ข้อสอง “zero data loss” จะจริงก็ต่อเมื่อตั้ง min.insync.replicas สูงพอที่ ISR จะ shrink ลงเล็กเกินไปไม่ได้โดยที่ producer จะได้ NotEnoughReplicasException จริงที่มองเห็นได้ แทนที่จะสำเร็จแบบเงียบ ๆ — acks เพียงอย่างเดียวไม่ได้บังคับขนาดขั้นต่ำของ ISR