KRaft Metadata Log
สรุปในหนึ่งบรรทัด
หัวข้อที่มีชื่อว่า “สรุปในหนึ่งบรรทัด”cluster metadata ไม่ใช่ฐานข้อมูลที่ controller query — มันคือ log แบบ append-only สไตล์ Kafka ปกติ (topic ภายในชื่อ __cluster_metadata) replicate ด้วย Raft quorum ของ controller decode ได้ด้วยเครื่องมือ kafka-dump-log.sh ตัวเดียวกับที่ใช้กับ topic ข้อมูลทั่วไป แค่มี decoder ที่รู้จัก schema ของ record metadata
record type จริง decode แล้ว
หัวข้อที่มีชื่อว่า “record type จริง decode แล้ว”dump partition 0 ของ __cluster_metadata ของโมดูลนี้เองด้วย cluster-metadata decoder:
/opt/kafka/bin/kafka-dump-log.sh --cluster-metadata-decoder \ --files /tmp/kafka-logs/__cluster_metadata-0/00000000000000000000.log \ | grep -o '"type":"[A-Z_]*"' | sort -uoutput จริง ไม่แก้ไข — record type ทุกตัวที่คลัสเตอร์นี้เขียนจริงระหว่าง startup ปกติและการสร้าง topic ไม่มีตัวอย่างสังเคราะห์เลย:
"type":"BEGIN_TRANSACTION_RECORD""type":"BROKER_REGISTRATION_CHANGE_RECORD""type":"CONFIG_RECORD""type":"END_TRANSACTION_RECORD""type":"FEATURE_LEVEL_RECORD""type":"NO_OP_RECORD""type":"PARTITION_RECORD""type":"PRODUCER_IDS_RECORD""type":"REGISTER_BROKER_RECORD""type":"REGISTER_CONTROLLER_RECORD""type":"TOPIC_RECORD"PARTITION_CHANGE_RECORD (การเปลี่ยน ISR) ไม่ปรากฏที่นี่ — คลัสเตอร์ single-broker นี้ ISR ไม่เคยเปลี่ยน มันถูกพิสูจน์ตรง พร้อมเนื้อหาจริง ใน Replication and ISR Internals’s คลัสเตอร์ 3-broker: {"type":"PARTITION_CHANGE_RECORD","version":2,"data":{"partitionId":0,"topicId":"...","isr":[1,2]}} แล้วต่อมา {"...,"isr":[2,3],"leader":2} ระหว่าง ISR shrink และ controller failover ของบทนั้น
record จริงบางตัวที่คุ้มค่าอ่านให้ละเอียด ตามลำดับที่เขียนจริงตอน startup คลัสเตอร์:
baseOffset: 0 ... isControl: true ...| offset: 0 ... LeaderChange: {"version":0,"leaderId":1,"voters":[{"voterId":1}],"grantingVoters":[{"voterId":1}]}record แรกสุดใน log คือ control record ชื่อ LeaderChange — นี่คือ proof การเลือก leader ของ Raft เอง ไม่ใช่ metadata ของ application; มันมีอยู่เพราะ __cluster_metadata ถูก replicate ด้วยกลไก Raft เดียวกันที่ controller ใช้ตกลงกันเรื่องอื่นทั้งหมด ดังนั้นแม้แต่ความเป็น leader ของ log ตัวนี้เองก็ถูกบันทึกในlog ตัวนี้เอง
| offset: 2 ... payload: {"type":"FEATURE_LEVEL_RECORD","version":0,"data":{"name":"metadata.version","featureLevel":30}}| offset: 3 ... payload: {"type":"FEATURE_LEVEL_RECORD","version":0,"data":{"name":"eligible.leader.replicas.version","featureLevel":1}}| offset: 8 ... payload: {"type":"CONFIG_RECORD","version":0,"data":{"resourceType":4,"resourceName":"","name":"min.insync.replicas","value":"1"}}| offset: 10 ... payload: {"type":"REGISTER_CONTROLLER_RECORD","version":0,"data":{"controllerId":1,"incarnationId":"...","endPoints":[{"name":"CONTROLLER","host":"localhost","port":29093,...}],"features":[...]}}| offset: 11 ... payload: {"type":"REGISTER_BROKER_RECORD","version":4,"data":{"brokerId":1,"incarnationId":"...","brokerEpoch":11,"endPoints":[...],"fenced":true,"logDirs":[...]}}eligible.leader.replicas.version ที่ featureLevel:1 ที่นี่คือข้อเท็จจริง finalized-feature ของ KIP-966 เดียวกันที่ Replication and ISR Internals พิสูจน์ด้วย kafka-features.sh describe — ที่นี่มันคือ record จริงในตัว metadata log ที่ทำให้มันเป็นจริง ไม่ใช่แค่ผลลัพธ์ของ query ที่รายงานมัน "fenced":true ของ REGISTER_BROKER_RECORD ก็จริงเช่นกัน: ทุก broker เริ่มต้นแบบถูก fence (lead partition ไม่ได้) จนกว่า controller จะยืนยันว่ามันตามทัน metadata แล้ว BROKER_REGISTRATION_CHANGE_RECORD ที่มี "fenced":-1 (sentinel สำหรับ “unset/false” ใน encoding ของ record นี้) ตามมาไม่นานหลังจากนั้น unfence มัน
ต่อมา การสร้าง topic สร้าง record type สองตัวตรงตามที่คาด:
{"type":"TOPIC_RECORD","version":0,"data":{"name":"internals-pipeline-demo","topicId":"FQwkK_oVSZeJP5dIDq-bSw"}}{"type":"PARTITION_RECORD","version":2,"data":{"partitionId":0,"topicId":"FQwkK_oVSZeJP5dIDq-bSw","replicas":[1],"isr":[1], "removingReplicas":[],"addingReplicas":[],"leader":1,"leaderEpoch":0,"partitionEpoch":0,"directories":["..."]}}หนึ่ง TOPIC_RECORD สำหรับ mapping ชื่อ/ID หนึ่ง PARTITION_RECORD ต่อ partition ที่พก field replicas/isr/leader เดียวกันเป๊ะที่ kafka-topics.sh --describe อ่านกลับมา — output ของ CLI นั้นไม่ใช่แหล่งความจริงแยกต่างหาก มันคือ record นี้ ที่ถูก format แล้ว
Snapshot: config พิสูจน์แล้ว snapshot ตัวจริงไม่ (อย่างซื่อตรง)
หัวข้อที่มีชื่อว่า “Snapshot: config พิสูจน์แล้ว snapshot ตัวจริงไม่ (อย่างซื่อตรง)”/opt/kafka/bin/kafka-configs.sh --bootstrap-server $BOOTSTRAP \ --describe --entity-type brokers --entity-name 1 --all 2>&1 | grep "metadata.log.max"output จริง:
metadata.log.max.record.bytes.between.snapshots=20971520 sensitive=false synonyms={DEFAULT_CONFIG:metadata.log.max.record.bytes.between.snapshots=20971520}metadata.log.max.snapshot.interval.ms=3600000 sensitive=false synonyms={DEFAULT_CONFIG:metadata.log.max.snapshot.interval.ms=3600000}20971520 ไบต์คือ 20 MiB — snapshot (ภาพรวม state metadata ทั้งหมด ณ จุดเวลาหนึ่ง แบบ compact ให้ controller/broker ที่ restart หรือ join ใหม่ตามทันได้โดยไม่ต้อง replay log ทั้งหมดจาก offset 0) ถูก trigger เมื่อ record ใหม่สะสมถึงจำนวนไบต์นี้นับจากครั้งล่าสุด หรือ metadata.log.max.snapshot.interval.ms (1 ชั่วโมงโดย default) ผ่านไป แล้วแต่อันไหนถึงก่อน คลัสเตอร์ของโมดูลนี้เองไม่เคยเขียน metadata record ใกล้ 20 MiB เลยในช่วงชีวิตสั้น ๆ ของมัน ดังนั้น ls /tmp/kafka-logs/__cluster_metadata-0/*.checkpoint บนคลัสเตอร์นี้คืนค่าเปล่าจริง ๆ — ไม่มีการแทนที่ด้วย proof สังเคราะห์ที่นี่; ไฟล์ snapshot จริง (ชื่อ <offset>-<epoch>.checkpoint) จะปรากฏเมื่อข้ามขอบเขตไบต์หรือเวลานั้นไปแล้ว และบทนี้ซื่อตรงว่าการไปถึงจุดนั้นไม่สมเหตุสมผลภายในขอบเขตของ session พิสูจน์นี้
broker fetch metadata ยังไง: คลัสเตอร์นี้เป็น combined-mode อย่างซื่อตรง
หัวข้อที่มีชื่อว่า “broker fetch metadata ยังไง: คลัสเตอร์นี้เป็น combined-mode อย่างซื่อตรง”ทุก “broker” ใน compose ของโมดูลนี้คือ process.roles=broker,controller — node แบบ combined เดียวกับที่ KRaft & the Cluster แนะนำไปแล้ว ใน combined mode บทบาท broker และ controller รันอยู่ ใน JVM process เดียวกัน ดังนั้นไม่มี FetchRequest แบบข้าม process ที่คลัสเตอร์นี้จะแสดงใน request log ได้ — ฝั่ง broker แค่อ่าน state ของ metadata log ในหน่วยความจำของ controller ตรง ๆ ไม่ต้องมี RPC เลย บนคลัสเตอร์แบบ isolated (node controller-only และ broker-only แยกกัน — topology ที่แนะนำสำหรับ production ตาม KRaft & the Cluster) broker จะส่ง FetchRequest จริงไปหา leader ของ controller quorum เพื่อ replicate __cluster_metadata โดยใช้ API key FETCH ตัวเดียวกับที่ data replication ใช้ คลัสเตอร์ 3-broker combined ของโมดูลนี้เอง (Replication and ISR Internals) ยังแสดงสิ่งที่ใกล้เคียงและจริง: raft-client-2/raft-client-3 ส่ง request FETCH ที่ clientId ระบุ Raft client ตรง ๆ และ replicaState.replicaId ตรงกับ controller ID — proof ว่ากลไก replication ข้างใต้ของ __cluster_metadata คือคู่ FetchRequest/FetchResponse เดียวกันกับ data replication ของ topic แค่ส่งไปที่ leader ของ controller quorum แทน leader ของ partition:
"requestHeader":{"requestApiKey":1,"clientId":"raft-client-3","requestApiKeyName":"FETCH"},"request":{"clusterId":"5L6g3nShT-eMCtK--X86sw","replicaState":{"replicaId":3,"replicaEpoch":-1}, "topics":[{"topicId":"AAAAAAAAAAAAAAAAAAAAAQ","partitions":[{"partition":0,"fetchOffset":55,...,"highWatermark":55}]}]},"response":{...,"currentLeader":{"leaderId":1,"leaderEpoch":1},...}}"topicId":"AAAAAAAAAAAAAAAAAAAAAQ" ที่นี่คือ topic ID ตายตัวของ __cluster_metadata เอง (base64 ของ UUID ที่รู้จักกันดี ไม่ใช่ตัวที่สุ่มมาแบบ topic ปกติ) — นี่คือ metadata log ที่ถูก fetch ผ่านกลไกเดียวกันเป๊ะกับ partition อื่นใด
kafka-metadata-quorum.sh describe --replication: field จริง
หัวข้อที่มีชื่อว่า “kafka-metadata-quorum.sh describe --replication: field จริง”/opt/kafka/bin/kafka-metadata-quorum.sh --bootstrap-server $BOOTSTRAP describe --replicationoutput จริงบน combined node เดียวของโมดูลนี้เอง:
NodeId DirectoryId LogEndOffset Lag LastFetchTimestamp LastCaughtUpTimestamp Status1 AAAAAAAAAAAAAAAAAAAAAA 1227 0 1790650194864 1790650194864 Leaderบน quorum 3-node จริงของ Replication and ISR Internals คำสั่งเดียวกันทันทีหลัง broker 1 ถูกหยุด แสดงพอดีว่า voter ที่ lag/เข้าถึงไม่ได้หน้าตาเป็นยังไงจากมุมมองของ quorum leader เอง:
NodeId DirectoryId LogEndOffset Lag LastFetchTimestamp LastCaughtUpTimestamp Status2 AAAAAAAAAAAAAAAAAAAAAA 342 0 1790650466818 1790650466818 Leader1 AAAAAAAAAAAAAAAAAAAAAA -1 343 -1 -1 Follower3 AAAAAAAAAAAAAAAAAAAAAA 342 0 1790650466611 1790650466611 FollowerLogEndOffset:-1 และ LastFetchTimestamp:-1 ของ broker 1 (sentinel value จริง ไม่ใช่ศูนย์) แปลว่า “voter นี้ไม่ได้ fetch เลยตั้งแต่เข้าถึงไม่ได้” — ต่างจาก Lag:343 ซึ่งคือ position ล่าสุดที่รู้ของมันตามหลัง end offset ของ leader ใหม่แค่ไหน Status: Leader/Follower ที่นี่อธิบายบทบาทของแต่ละ node ใน metadata quorum โดยเฉพาะ — node หนึ่งเป็น follower ของ data-partition และเป็น leader ของ metadata quorum ในเวลาเดียวกันได้ การเลือกตั้งแบบ Raft สองอัน (leader ต่อ partition, leader ของ controller-quorum) เป็นอิสระจากกัน
Controller failover เต็มรูปแบบ
หัวข้อที่มีชื่อว่า “Controller failover เต็มรูปแบบ”Replication and ISR Internals พิสูจน์เลขหลักไปแล้ว — controller leader ตัวใหม่ถูกเลือกและให้บริการได้ภายในประมาณ 4.2 วินาทีหลังหยุดตัวเก่า LeaderEpoch เพิ่มจาก 1 → 2 มุมมองระดับ metadata-log ของเหตุการณ์เดียวกันนี้: ทันทีที่ broker 2 กลายเป็น quorum leader ใหม่ มันเขียน control record LeaderChange ใหม่ (record type เดียวกับที่เปิดบทนี้) เข้า __cluster_metadata ที่ leader epoch ใหม่ของมัน เหมือนกับ record แรกสุดที่คลัสเตอร์นี้เคยเขียนตอน startup เป๊ะ controller ตัวอื่นทุกตัว (และใน topology แบบ isolated broker ทุกตัว) รู้จัก leader ใหม่ด้วยการ replicate record ตัวนั้นตัวเดียว ผ่านกลไก FetchRequest เดียวกันที่แสดงข้างบน — ไม่มี protocol “ประกาศ leader” แยกต่างหากจาก metadata log เอง
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”__cluster_metadata คือ Kafka replicate Kafka: log แบบ append-only เดียวกัน replication ที่ขับเคลื่อนด้วย FetchRequest เดียวกัน เครื่องมือ kafka-dump-log.sh เดียวกัน แค่เอามาใช้อีกระดับหนึ่งกับข้อมูลที่อธิบาย topic อื่นทุกตัว แทนข้อมูลของ application Snapshot คือสิ่งที่เทียบเท่ากับการ collapse tombstone ของ compaction ในบริบทของ log นี้ — สรุปภาพ ณ จุดเวลาหนึ่งที่ให้ member ใหม่หรือที่กำลังฟื้นตัวข้าม replay ประวัติที่ไม่ต้องการได้ ทุกข้อเท็จจริงที่ broker หรือ client เคยรู้เกี่ยวกับ cluster state — broker ไหน lead partition ไหน, ISR คืออะไร, feature ไหน finalized แล้ว — สุดท้ายย้อนกลับไปที่ record ตัวหนึ่งที่ offset ตัวหนึ่งใน log ตัวเดียวนี้ พิสูจน์ได้ด้วย decoder ตัวเดียวกันที่ใช้ตลอดบทนี้
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| ตัวเลือก | ประโยชน์ | ต้นทุน |
|---|---|---|
| Combined mode (คลัสเตอร์ของโมดูลนี้) | รันง่ายกว่า — หนึ่ง process ต่อ node พิสูจน์ตลอดการตั้งค่า running-kafka-locally ของคอร์สนี้ | ไม่มี FetchRequest broker→controller แยกให้สังเกต (พิสูจน์แล้วข้างบน); topology production เลือก isolated mode ด้วยเหตุผลนี้และอื่น ๆ |
| Isolated mode (controller แยกเฉพาะ) | controller quorum แยกจาก load ของ data-plane; FetchRequest ที่บทนี้อธิบายเป็นของจริงและสังเกตได้ | process/node ให้รันและ monitor เยอะขึ้น |
metadata.log.max.record.bytes.between.snapshots ต่ำ | node ใหม่/ที่ฟื้นตัวตามทันเร็วขึ้น (log ให้ replay น้อยลง) | overhead การเขียน snapshot บ่อยขึ้น |
metadata.log.max.snapshot.interval.ms สูง | overhead snapshot น้อยลงบนคลัสเตอร์ที่เงียบ | node ที่ฟื้นตัวด้วย snapshot เก่า replay log มากขึ้นกว่าจะตามทัน |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “controller เก็บ cluster state ในฐานข้อมูล แยกจากกลไก log ของ Kafka เอง” พิสูจน์แล้วข้างบน: มันคือ
__cluster_metadatalog สไตล์ Kafka ปกติ decode ด้วยkafka-dump-log.shตัวเดียวกับที่ใช้กับ topic ใดก็ได้ - “
kafka-metadata.shคือเครื่องมือสำหรับตรวจ metadata quorum” แก้ไขแล้วข้างบน: binary นั้นไม่มีอยู่บน 4.3.1 เครื่องมือจริงคือkafka-metadata-quorum.sh(ใช้ตลอดบทนี้) และkafka-metadata-shell.sh - “snapshot คือกลไก backup พิเศษแยกต่างหากที่ operator ต้องตั้งค่า” พิสูจน์แล้วข้างบน: มันขับเคลื่อนด้วย config (
metadata.log.max.record.bytes.between.snapshots/...interval.ms) และเกิดอัตโนมัติ — บทนี้พูดตรง ๆ ว่าไม่ได้สังเกต snapshot ตัวจริงเพราะคลัสเตอร์นี้อายุสั้นไม่เคยข้ามขอบเขตทั้งสองเลย - “broker ที่ fetch metadata ใช้ protocol ต่างจาก replication ปกติ” พิสูจน์แล้วข้างบนด้วยบรรทัด request log จริง: request
FETCHของ Raft client คือ API keyFETCHตัวเดียวกันเป๊ะ แค่ส่งไปที่ topic ID ตายตัวของ__cluster_metadataแทน topic ข้อมูลปกติ
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”🐛 หา bug ในโค้ดนี้
# AI-suggested "faster recovery" runbook step# "Delete __cluster_metadata's snapshot files to force a clean resync# from the beginning of the log -- guarantees consistency."rm /tmp/kraft-combined-logs/__cluster_metadata-0/*.checkpointเฉลย
ปัญหาจริงสองข้อ ทั้งคู่พิสูจน์ได้จากบทนี้ ข้อแรก path ผิดสำหรับ log.dirs default จริงของ image นี้ (/tmp/kafka-logs พิสูจน์ตลอดคอร์สนี้และโมดูลนี้ — ไม่ใช่ /tmp/kraft-combined-logs ซึ่งเป็น path จาก config ตัวอย่างของ Kafka รุ่นเก่าที่ image นี้ไม่ได้ใช้เว้นแต่ตั้ง log.dirs ไว้เป็นอื่นตรง ๆ) ข้อสอง และร้ายแรงกว่า: snapshot คือการ optimize ไม่ใช่แหล่งความจริง การลบมันไม่ได้ “บังคับ resync สะอาด” มันแค่แปลว่า restart ครั้งถัดไป replay log มากขึ้น (หรือถ้า log เองถูกตัดไปแล้วโดยอิงว่า snapshot นี้มีอยู่ ก็ replay จากจุดที่อาจไม่มีประวัติเต็มแล้ว) วิธีที่ปลอดภัยจริงในการบังคับ broker เข้าร่วมใหม่จากศูนย์คือ procedure decommission/rejoin ที่บันทึกไว้ ไม่ใช่การลบไฟล์มือใน log.dirs