Kafka Best Practices & Code Review Checklist
Consumer Lag Monitoring
Consumer lag คือระยะห่างระหว่าง offset ล่าสุดที่มีใน partition กับ offset ที่ consumer อ่านถึง — lag ที่สูงขึ้นเรื่อยๆ (ไม่ใช่แค่สูงนิ่งๆ) คือสัญญาณว่า consumer ตามไม่ทัน producer ซึ่งต้องแก้ไขก่อนข้อมูลสะสมจนกระทบระบบ downstream
kafka-consumer-groups --bootstrap-server localhost:9092 \
--describe --group order-processor
# ดู column LAG ของแต่ละ partition
"Lag คือจำนวนอีเมลที่ค้างอยู่ในกล่องจดหมายที่ยังไม่เปิดอ่าน" — lag นิ่งๆ (ไม่เพิ่มไม่ลด) อาจ แค่มีอีเมลค้างอยู่บ้างแต่ไม่แย่ลง แต่ lag ที่ เพิ่มขึ้นเรื่อยๆ ไม่หยุด คือสัญญาณว่าอ่าน อีเมลช้ากว่าที่อีเมลเข้ามาใหม่ ต้องรีบจัดการก่อนกล่องจดหมายล้น
Lag สูงไม่ได้แปลว่า "consumer ช้า" เสมอไป — ต้องแยกให้ออกระหว่าง lag ที่เกิดจาก consumer ประมวลผลช้า (CPU-bound, I/O-bound ใน consumer logic เอง) กับ lag ที่เกิดจาก producer ยิงเร็วกว่าปกติชั่วคราว (traffic spike) ซึ่งทั้งสองแบบแก้คนละวิธี วิธีแยกให้ชัดคือดู อัตราการเพิ่มของ lag เทียบกับอัตราการเพิ่มของ producer throughput พร้อมกัน: ถ้า lag โต เร็วกว่า throughput ของ producer มาก แปลว่า consumer เองมีปัญหา (ต้อง scale consumer หรือ optimize โค้ด) แต่ถ้า lag โตพอๆ กับ spike ของ producer แล้วค่อยๆ ลดกลับมาเองหลัง spike จบ แปลว่าระบบทำงานถูกต้องตามปกติ (แค่ต้องมี buffer เผื่อ spike เพียงพอ) — เครื่องมืออย่าง Burrow หรือ dashboard ที่ plot lag เทียบเวลาช่วยให้เห็น pattern นี้ได้ชัดกว่าดูตัวเลข snapshot เดียว
Rebalancing Strategy
เมื่อ consumer เข้า/ออกจาก group (หรือ crash) Kafka จะ rebalance (แจก partition ใหม่ให้ consumer ที่เหลือ) — ระหว่าง rebalance ทุก consumer ใน group หยุดประมวลผลชั่วคราว ("stop-the-world" แบบเดิม) ซึ่งกระทบ throughput ถ้า rebalance เกิดบ่อย
"Rebalance = หยุดงานทั้งทีมเพื่อแบ่งโต๊ะใหม่" — เดิมเวลามีพนักงานเข้า/ออกทีม ต้องหยุดงานทั้งทีม ชั่วคราวเพื่อจัดโต๊ะใหม่ให้ทุกคน (stop-the-world rebalance) ซึ่งเสียเวลามากถ้าเกิดบ่อย
Kafka รุ่นใหม่ (2.4+) มี cooperative rebalancing (ผ่าน CooperativeStickyAssignor)
ที่แก้ปัญหา "หยุดงานทั้งทีม" — แทนที่จะเอา partition assignment ของทุก consumer คืนหมดแล้ว
แจกใหม่ทั้งหมด (eager rebalancing แบบเดิม) cooperative rebalancing จะ ย้ายเฉพาะ
partition ที่จำเป็นต้องย้ายจริงๆ เท่านั้น consumer ที่ partition assignment ของตัวเองไม่
เปลี่ยนก็ยังทำงานต่อได้ไม่สะดุด ลด downtime จาก rebalance ได้มาก — แต่ default assignor ของ
หลาย client library ยังคงเป็นแบบ eager (RangeAssignor) อยู่ ต้องตั้งค่าเปลี่ยน
assignor เอง ถึงจะได้ประโยชน์นี้ ทีมจำนวนมากไม่รู้ว่าต้องเปลี่ยน default และยังทน
rebalance แบบเก่าอยู่โดยไม่จำเป็น
Dead-Letter Topic Pattern
เมื่อ consumer process message ไม่สำเร็จซ้ำๆ (เช่น message format ผิด, business logic error ที่ retry แล้วก็ยัง fail) อย่า retry วนลูปไม่หยุด (จะบล็อก partition ทั้งเส้นไม่ให้ message ตัวถัดไปถูกประมวลผลเลย) — ให้ route message ที่ fail ไปยัง dead-letter topic แยกต่างหากแทน แล้วให้ consumer หลักเดินหน้าต่อ
main-topic → consumer process fail 3 ครั้ง → ส่งไป main-topic-dlq → แจ้งเตือน/review ทีหลัง
"Dead-letter topic = ถาดจดหมายส่งไม่ถึงของไปรษณีย์" — จดหมายที่ส่งไม่ถึงผู้รับ (process ไม่ สำเร็จ) ไม่ควรกองสุมอยู่หน้าประตู (block queue หลัก) แต่ถูกแยกไปไว้ถาดต่างหากให้คนมาตรวจสอบ ทีหลัง ส่วนจดหมายอื่นยังส่งต่อไปได้ตามปกติไม่ติดขัด
Dead-letter topic ที่ดีต้องแนบ metadata ของความล้มเหลว ไปด้วย ไม่ใช่แค่ forward message เดิมไปเฉยๆ — metadata ที่ควรแนบคือ: exception message/stack trace, จำนวนครั้งที่ retry ไปแล้ว, timestamp ของแต่ละความพยายาม, และ original topic/partition/offset ของ message ต้นฉบับ (เพื่อให้ replay กลับไปที่ตำแหน่งเดิมได้ถ้าแก้บั๊กแล้ว) หลายทีมลืมแนบ offset ต้นฉบับไว้ ทำให้ ตอนแก้บั๊กเสร็จแล้วอยากจะ "replay" message จาก DLQ กลับเข้า main topic ใหม่ ไม่สามารถรู้ได้ว่า message นั้นควรไปอยู่ตำแหน่งไหนเทียบกับ message อื่นที่ประมวลผลสำเร็จไปแล้ว ทำให้ reprocessing กลายเป็นงานที่ทำได้ยากกว่าที่ควรจะเป็น
Consumer lag should be monitored for its growth trend, not just absolute value — distinguish
consumer-side slowness from a temporary producer traffic spike by comparing lag growth rate to
producer throughput. Rebalancing historically stops the entire consumer group ("stop-the-world");
Kafka 2.4+'s cooperative rebalancing (via CooperativeStickyAssignor) only reassigns partitions
that actually need to move, but most client libraries still default to the older eager assignor,
so this has to be opted into explicitly. Route repeatedly-failing messages to a dead-letter topic
rather than retrying forever (which blocks the whole partition) — and always attach failure
metadata plus the original topic/partition/offset, so fixed messages can be replayed back to
their correct position later.