Skip to main content

Topic Design

การตัดสินใจเลือกจำนวน Partition​

จำนวน partition กำหนด เพดาน parallelism สูงสุด ของ topic นั้น (consumer ในกลุ่มเดียวกัน ทำงานพร้อมกันได้สูงสุดเท่าจำนวน partition) แต่เพิ่มจำนวน partition มากเกินไปก็มีต้นทุน: เปลือง file handle/memory บน broker, เพิ่ม latency ของ leader election ตอน broker ล่ม, และเพิ่มภาระ ZooKeeper/KRaft metadata

กฎคร่าวๆ: partition count = max(target throughput / per-partition throughput,
max consumer parallelism ที่ต้องการ)
🧠 จำง่ายๆ

"Partition เยอะ = เลนถนนเยอะ วิ่งเร็วขึ้นแต่สร้างแพงขึ้น" — เลนเยอะช่วยให้รถ (consumer) วิ่งพร้อม กันได้มากขึ้นไม่ติดขัด แต่สร้าง/ดูแลถนนเยอะเลนก็แพงกว่า (resource บน broker) และถ้าเลนเยอะเกิน จำนวนรถที่มีจริง เลนที่เหลือก็ว่างเปล่าไม่มีประโยชน์

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

เพิ่ม partition ทำได้ แต่ลดไม่ได้ (ต้องสร้าง topic ใหม่แล้วย้ายข้อมูลเองถ้าต้องการลด) และ การเพิ่ม partition ให้ topic ที่มีข้อมูลอยู่แล้วทำลาย ordering guarantee ของ key ที่มีอยู่เดิม ทันที — เพราะสูตร hash คำนวณ partition จากจำนวน partition ปัจจุบัน ถ้าจำนวนเปลี่ยน key เดียวกันที่เคย hash ไปลง partition 1 อาจเริ่ม hash ไปลง partition ใหม่ (partition 5 ที่เพิ่ง เพิ่ม) แทน ทำให้ message ของ key เดียวกันกระจัดกระจายอยู่คนละ partition ทั้งที่เคยอยู่ที่เดียวกัน มาตลอด — เป็นเหตุผลสำคัญที่ทีม production มักกำหนด partition count ให้ "เผื่ออนาคต" ไว้ตั้งแต่ สร้าง topic ครั้งแรก (over-provision พอประมาณ) แทนที่จะคิดว่า "ค่อยเพิ่มทีหลังได้"

Retention Policy — เก็บข้อมูลนานแค่ไหน​

log.retention.hours=168 # เก็บ 7 วัน (default)
log.retention.bytes=-1 # ไม่จำกัดขนาด (default)

กำหนดได้ทั้งแบบ เวลา (retention.ms) และ ขนาด (retention.bytes) — อย่างไหนถึงก่อน ข้อมูลถูกลบก่อน ตั้งได้ต่อ topic แยกกัน ไม่ต้องใช้ค่าเดียวกันทั้ง cluster

🧠 จำง่ายๆ

"Retention = วันหมดอายุนม สองแบบ: ตามวันที่ หรือ ตามจำนวนกล่องในตู้เย็น อย่างไหนถึงก่อนทิ้งก่อน" — ตั้งทั้งคู่พร้อมกันได้ และไม่จำเป็นต้องเหมือนกันทุก topic (log บาง topic อาจเก็บแค่ 1 วัน ส่วน audit trail อาจเก็บเป็นปี)

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

Retention ไม่ได้ลบข้อมูลทันทีที่หมดอายุ — Kafka เช็ค segment file (ไม่ใช่ message ทีละตัว) ตามรอบที่ตั้งไว้ใน log.retention.check.interval.ms (default 5 นาที) และจะลบได้ก็ต่อเมื่อ ทั้ง segment หมดอายุ (segment ที่มี message ใหม่ปนอยู่แม้แต่ตัวเดียวจะไม่ถูกลบทั้ง segment) หมายความว่าข้อมูลอาจอยู่เกิน retention ที่ตั้งไว้จริงๆ ได้นานถึงเกือบ 2 เท่าของ log.segment.ms ในกรณีเลวร้ายที่สุด (segment เพิ่งเริ่มเขียน message เก่าสุดก่อนหมดอายุนิดเดียว แล้วต้องรอจน segment นั้นเต็ม/หมดอายุทั้งก้อนก่อนถึงจะลบได้) — ทีมที่คาดหวัง "ลบทันทีเป๊ะตาม retention.ms ที่ตั้ง" มักแปลกใจว่าทำไม disk usage ไม่ลดลงตามที่คำนวณไว้

Log Compaction — เก็บแค่ค่าล่าสุดของแต่ละ Key​

cleanup.policy=compact ต่างจาก delete (retention ปกติ) โดยสิ้นเชิง — compaction ไม่ลบ ตามเวลา/ขนาด แต่เก็บเฉพาะ message ล่าสุดของแต่ละ key ไว้ตลอดไป (ลบ message เก่าที่มี key ซ้ำกับตัวใหม่กว่าทิ้ง) เหมาะกับ use case ที่ Kafka ทำหน้าที่เป็น changelog ของ state ปัจจุบัน (เช่น "user ID 123 ล่าสุดชื่ออะไร" ไม่สนใจว่าเคยเปลี่ยนชื่อมากี่ครั้ง)

ก่อน compact: [user1=Alice][user2=Bob][user1=Alicia][user1=Alice2][user2=Bobby]
หลัง compact: [user1=Alice2][user2=Bobby] // เก็บแค่ล่าสุดของแต่ละ key
🧠 จำง่ายๆ

"Compaction = กระดานไวท์บอร์ดที่เขียนทับชื่อเดิม ไม่ใช่สมุดจดที่เขียนต่อท้าย" — ถ้าเขียนชื่อ "Alice" ซ้ำ 3 ครั้งบนกระดาน สุดท้ายก็เห็นแค่ค่าล่าสุดที่เขียนทับไว้ ไม่เห็นประวัติการเขียนก่อน หน้า — ต่างจาก topic ปกติที่เหมือนสมุดจดที่เก็บทุกบรรทัดที่เคยเขียนไว้ครบ

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

การ "ลบ key ออกจาก compacted topic ให้หมดจริงๆ" ทำได้ด้วยการส่ง message ที่มี key เดิมแต่ value เป็น null เรียกว่า tombstone — compaction จะเก็บ tombstone ไว้ชั่วคราว (ตาม delete.retention.ms) เพื่อให้ downstream consumer ที่ยังไม่ทัน consume ได้เห็นว่า key นี้ ถูกลบจริงๆ (ไม่ใช่แค่หายไปเฉยๆ ซึ่งจะแยกไม่ออกว่า "ไม่เคยมี" กับ "เคยมีแต่ถูกลบ") หลังจากนั้น tombstone เองก็จะถูกลบไปในที่สุด — Kafka Streams และระบบที่ใช้ compacted topic เป็น KTable (state store) ใช้กลไก tombstone นี้เป็นมาตรฐานในการลบ record ออกจาก state ที่ derive มาจาก topic เสมอ ไม่มีวิธี "DELETE" แบบ database ทั่วไป

Schema Evolution — จัดการ message format ที่เปลี่ยนแปลง​

Producer/consumer หลายตัวที่ deploy คนละเวลากันต้องอ่าน/เขียน message format ที่เข้ากันได้ — นิยมใช้ Avro หรือ Protobuf คู่กับ Schema Registry (เก็บ schema แยกจาก message เอง ส่ง message แค่ schema ID ตัวเล็กๆ แทนที่จะแนบ schema เต็มทุกครั้ง)

🧠 จำง่ายๆ

"Schema Registry = พจนานุกรมกลางที่ทุกคนอ้างอิงเลขหน้า" — แทนที่จะแนบคำอธิบายศัพท์เต็มทุกครั้ง (แนบ schema เต็มในทุก message ซึ่งเปลือง) ก็ส่งแค่ "เลขหน้า" (schema ID) แล้วให้ทุกฝ่ายไปเปิด พจนานุกรมกลาง (registry) เพื่อแปลความหมายเอง

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

Schema Registry บังคับ compatibility mode ที่ตั้งได้หลายแบบ (BACKWARD, FORWARD, FULL) ซึ่งเป็นจุดที่ทีมจำนวนมากตั้งค่าผิดโดยไม่รู้ตัว: BACKWARD (ค่า default ของหลาย registry) หมายถึง consumer ที่ใช้ schema ใหม่ยังอ่าน message เก่าได้ — เหมาะกับการ deploy consumer ก่อน producer แต่ถ้าทีม deploy producer ก่อน consumer (เพิ่ม field ใหม่ใน producer แล้วหวังว่า consumer เก่าจะยังอ่านได้) ต้องใช้ FORWARD compatibility แทน สลับกันใช้ compatibility mode ผิดประเภทกับลำดับการ deploy จริงคือสาเหตุของ "schema incompatible" error ที่พบบ่อยที่สุดในทีมที่เพิ่งเริ่มใช้ Schema Registry

English Summary

Partition count sets the hard ceiling on consumer parallelism but costs broker resources as it grows — and increasing it later reshuffles key-to-partition hashing, breaking prior ordering guarantees for existing keys, so production teams typically over-provision upfront. Retention is checked per-segment on an interval, not per-message instantly, so data can outlive its nominal retention by nearly a full segment cycle. Log compaction (cleanup.policy=compact) retains only the latest value per key forever, rather than deleting by age/size — true key deletion requires a null-value tombstone message, which Kafka Streams' KTable state stores rely on as their only delete mechanism. Schema evolution via Avro/Protobuf + Schema Registry sends compact schema IDs instead of full schemas, but picking the wrong compatibility mode (BACKWARD vs FORWARD) relative to your producer/consumer deploy order is the most common cause of schema-incompatible errors.