Kafka Rebalance Storm: แค่ทำงานช้าเกินเวลาข้อความเดียว ทำไมถึงสั่งหยุดงานเพื่อนร่วมงานทั้งบริษัท?
⏱️ ใช้เวลาอ่านประมาณ: 4 นาที 30 วินาที
บ่ายวันหนึ่ง กราฟ Consumer Lag ของ Apache Kafka พุ่งทะยานขึ้นเป็นกำแพงสูงชัน ข้อความค้างเติ่งในคิวนับล้านรายการ...
คุณรีบเปิดดู CPU และ RAM ของเครื่อง Consumer ปรากฏว่าทุกอย่างปกติดี ไม่มีใครแฮงก์ ไม่มีใครตาย แต่สิ่งที่น่าตกใจคือ ไม่มีเครื่องไหนยอมดึงข้อความไปประมวลผลต่อเลยแม้แต่ตัวเดียว!
พอเปิด Log ดู ก็เจอข้อความสีแดงพ่นออกมาเป็นจังหวะต่อเนื่อง:
[GroupCoordinator] PreparingRebalance: group has changed, revoking all assigned partitions...
แค่ Consumer ตัวหนึ่งประมวลผลข้อความช้าไปหน่อย ทำไม Kafka ถึงลงโทษด้วยการ สั่งหยุดงาน Consumer ทุกตัวในกลุ่มพร้อมกัน จนระบบกลายเป็นอัมพาต?
🪑 การเล่นเก้าอี้ดนตรีแห่งโลก Distributed (Rebalance)
ใน Kafka เมื่อคุณมี Topic ที่แบ่งเป็นหลาย Partition แล้วมี Consumer Group ช่วยกันรุมอ่าน จะมีผู้จัดการที่ชื่อว่า Group Coordinator คอยแจกจ่าย Partition ให้สมาชิกแต่ละคน
หัวใจของการตรวจสอบว่า "พนักงานคนไหนยังมีชีวิตอยู่" มีกลไกสำคัญ 2 ชิ้น:
- Heartbeat Thread (การเต้นของหัวใจ): เธรดเบื้องหลังที่คอยส่งสัญญาณ "ปิ๊บๆ ฉันยังไม่ตายนะ" ไปบอก Coordinator ทุกๆ 3 วินาที
- Poll Loop (การเดินไปหยิบงาน): ลูปหลักที่แอปของคุณสั่ง
consumer.poll()เพื่อดึงชุดข้อความมาประมวลผล
"แต่ Kafka มองว่า: ต่อให้หัวใจคุณยังเต้นอยู่... แต่ถ้าคุณดึงงานไปแล้ว หายหน้าไปนานเกิน max.poll.interval.ms (ค่าเริ่มต้น 5 นาที) โดยไม่กลับมาหยิบงานรอบต่อไป แปลว่าคุณเกิดอาการสมองตายหรือติดลูปนรกไปแล้ว!"
🌪️ ลูกโซ่มรณะ: พายุ Rebalance กวาดเรียบทั้งระบบ
สมมติคุณดึงข้อความมา 500 รายการ มีข้อความหนึ่งต้องยิง API ไปหา Third-party เจ้ากรรมที่ดันตอบกลับช้ามาก ใช้เวลาไป 6 นาที:
- พอครบ 5 นาที Coordinator มองว่า Consumer A ตายแล้ว จึง เตะ Consumer A ออกจากกลุ่ม
- ในโมเดลดั้งเดิม (Eager Rebalance) Coordinator จะ ยึด Partition คืนจากสมาชิกทุกคนในกลุ่มพร้อมกัน (Stop-The-World!) เพื่อจัดสรรงานใหม่
- งานที่ค้างอยู่ของ Consumer A ถูกโยนไปให้ Consumer B ทำแทน...
- Consumer B ได้รับข้อความเจ้าปัญหาชิ้นเดิมไป ก็ติดแหง็กเกิน 5 นาทีเหมือนกัน ➔ Consumer B โดนเตะออกจากกลุ่มตามไปอีกตัว!
- พายุ Rebalance โหมกระหน่ำไม่หยุด ทุกคนมัวแต่แย่งเก้าอี้ดนตรีกัน ไม่มีใครได้ทำงานจริงสักข้อความ!
🛡️ 3 ท่าปราบพายุ: จูนขนาดงานและสลับมาใช้ Cooperative Sticky
1. อย่าโลภหยิบงานก้อนใหญ่เกินไป (ลด max.poll.records):
ค่าเริ่มต้นมักยอมให้ดึงทีละ 500 รายการ ถ้างานของคุณมีการเรียก API หรือทำ I/O นาน ให้ลดลงมาเหลือ 20 - 50 รายการ เพื่อการันตีว่าแอปพลิเคชันจะกลับมาเรียก poll() ได้ทันภายในเวลา 5 นาทีเสมอ
max.poll.records = 50
max.poll.interval.ms = 600000 // ขยายเวลารอเป็น 10 นาที ถ้าจำเป็น
2. เปลี่ยนไปใช้ CooperativeStickyAssignor (การสลับงานแบบไม่หยุดโลก):
ตั้งแต่ Kafka 2.4 เป็นต้นมา มีโปรโตคอลใหม่ที่เรียกว่า Incremental Cooperative Rebalance:
org.apache.kafka.clients.consumer.CooperativeStickyAssignor
แทนที่จะริบ Partition คืนจากทุกคนในกลุ่ม... กลไกนี้จะ ย้ายเฉพาะ Partition ที่มีปัญหาเท่านั้น ส่วน Consumer ตัวอื่นๆ ที่ทำงานปกติก็ยังกินข้อมูลต่อไปได้เรื่อยๆ โดยไม่เกิดภาวะ Stop-the-world อีกต่อไป!
💡 5-Min Takeaway
"ใน Apache Kafka การมีชีวิตอยู่ (Liveness) ไม่ได้วัดแค่ว่ากระบวนการยังรันอยู่ แต่วัดว่าคุณยังกลับมารับงานใหม่อย่างสม่ำเสมอหรือไม่" อย่าปล่อยให้ข้อความที่ประมวลผลหนักก้อนเดียวมาลากทั้ง Consumer Group ให้พังทลาย จงจำกัดขนาด max.poll.records ให้สมดุลกับความเร็วของงาน และเปิดใช้งาน CooperativeStickyAssignor เพื่อให้ระบบจัดการ Partition ได้อย่างนุ่มนวลโดยไม่สะดุดทั้งบริษัท