☕ 5-Min Tech Story #15

Consumer Rebalance Storm: แค่ทำงานช้า 5 นาที ทำไม Kafka ถึงเฉดหัวทิ้งทั้งแก๊ง?

⏱️ ใช้เวลาอ่านประมาณ: 4 นาที 30 วินาที

คุณเขียน Kafka Consumer ไว้อ่าน Message เพื่อสร้าง Report และส่งอีเมล ทุกอย่างดูราบรื่นดีจนกระทั่งมี Message หนึ่งแนบไฟล์ไซซ์ยักษ์เข้ามา

Consumer ของคุณก้มหน้าก้มตาประมวลผลไฟล์นั้นอย่างขยันขันแข็ง กินเวลาไป 6 นาที... ทันใดนั้น Kafka Broker ตะโกนลั่น: "ไอ้หมอนี่ตายแล้ว! ยึด Partition คืนเดี๋ยวนี้!"

แล้วความวิบัติระดับลูกโซ่ที่เรียกว่า Rebalance Storm ก็เริ่มขึ้น ทั้งระบบหยุดอ่านข้อมูล ข้อความค้างเติ่งนับล้านตัวในพริบตาเดียว!

⏱️ เส้นตายของหัวหน้าห้อง: max.poll.interval.ms

Kafka มีกลไกคอยตรวจว่า Consumer แต่ละตัวใน Group ยังมีชีวิตอยู่ไหม โดยอาศัยตัวแปรสำคัญตัวหนึ่งชื่อว่า max.poll.interval.ms (ค่าเริ่มต้นมักจะอยู่ที่ 5 นาที)

กฎเหล็กของ Kafka มีอยู่ว่า:

"หลังจากดึงข้อความรอบล่าสุดไป แกต้องกลับมาเรียกคำสั่ง poll() รอบถัดไปภายในเวลาที่กำหนด ถ้าเกินเวลา แม้หัวใจ (Heartbeat Thread) ยังเต้นอยู่ จะถือว่าโปรแกรมแกค้าง (Stuck) และโดนเตะออกจากกลุ่มทันที!"

พอตัวที่ 1 ทำงานเกินเวลาไป 6 นาที Broker ก็เตะมันทิ้ง แล้วสั่งให้ Consumer ที่เหลือในกลุ่มทำการ Rebalance (แบ่ง Partition กันใหม่)

🌪️ พายุหมุนมหาประลัย (The Poison Pill Loop)

ความพินาศที่แท้จริงไม่ได้หยุดแค่นั้น เพราะ Message ยักษ์เจ้ากรรมตัวเดิม ยังไม่ได้ถูก Commit Offset!

  1. Broker สั่ง Rebalance ส่ง Partition นั้นไปให้ Consumer ตัวที่ 2 ทำต่อ
  2. ตัวที่ 2 หยิบ Message พิษตัวเดิมขึ้นมาประมวลผล... แน่นอนว่ามันก็ใช้เวลา 6 นาทีเหมือนกัน!
  3. ตัวที่ 2 โดนเตะทิ้งตามไปอีกตัว Broker สั่ง Rebalance รอบใหม่
  4. Message พิษถูกส่งต่อไปให้ตัวที่ 3, ตัวที่ 4 ทยอยตายตามกันไปเป็นโดมิโน
  5. ระหว่างที่มีการ Rebalance เกิดขึ้นเรื่อยๆ Consumer ทุกตัวจะหยุดอ่านข้อความชั่วคราว (Stop the world)

ผลคือ ทั้งระบบค้างสนิท Message สะสมเป็นภูเขาไฟรอวันระเบิด เพียงเพราะ Message ตัวเดียวที่ใช้เวลาทำนานเกินไป

🛡️ ท่าแก้เผ็ด: แยกงานหยิบ กับงานทำ ออกจากกัน

วิธีแก้ไม่ใช่แค่การเพิ่มเวลา max.poll.interval.ms ให้ยาวเป็นชั่วโมง เพราะนั่นจะทำให้ระบบตรวจจับความตายจริงช้าลง แต่คือการ "อย่าทำงานหนักใน Consumer Thread"

1. จ้างคนงานแยก (Worker Thread Pool / Coroutine):

ให้ Consumer Thread มีหน้าที่แค่ poll() เอาข้อความมาไวๆ แล้วโยนต่อให้ Worker Pool หรือ Coroutine ไปประมวลผลหนักๆ ข้างหลัง เพื่อให้ Consumer Thread วิ่งกลับไปกดรับงานรอบถัดไปได้ทันเวลาเสมอ

2. คุมจำนวนข้อความต่อรอบด้วย max.poll.records:

// ถ้างานหนัก อย่าโลภดึงเยอะรอบละ 500 ตัว ให้ดึงแค่ 10-20 ตัวพอ
properties[ConsumerConfig.MAX_POLL_RECORDS_CONFIG] = 20

💡 5-Min Takeaway

"ในระบบ Distributed System การทำงานช้าเกินไป มีค่าเท่ากับตายแล้ว" สัญญาณชีพของ Kafka ไม่ได้ดูแค่พัลส์ของเครื่อง แต่ดูว่าคุณยังวนกลับมารับงานตามเวลาไหม อย่าเอา Message ที่ต้องประมวลผลนานมาขวางลูปหลักเด็ดขาด แยกงานยาวไปทำข้างหลัง เพื่อให้หน้าบ้านยังตอบสนองคลัสเตอร์ได้ตลอดเวลา