ข้ามไปยังเนื้อหา

Flow Control & Backpressure

stream ปล่อยให้ producer ส่งเร็วเท่าที่ทำได้ แต่ถ้า consumer อ่านช้าล่ะ? ถ้าไม่มีเบรก ฝั่งที่เร็วจะกอง message ไว้ใน memory เร็วกว่าที่ฝั่งช้าจะระบายออก — จนกระทั่งบางอย่าง memory หมดแล้วล้ม

เบรกนั้นคือ flow control และแรงที่ดันกลับไปที่ producer คือ backpressure

เพราะ gRPC วิ่งบน HTTP/2 จึงได้ flow control ของ HTTP/2 มาอัตโนมัติ แต่ละฝั่งประกาศ window — จำนวน byte ที่ตอนนี้ยินดีรับ เมื่อ data มาถึงและถูกบริโภค ผู้รับก็เติม window; ถ้าผู้รับหยุดบริโภค window ก็ระบายจนเป็นศูนย์แล้วผู้ส่งต้องหยุด

flowchart LR
  prod["producer ที่เร็ว"] -->|writes| win["flow-control window"]
  win -->|reads| cons["consumer ที่ช้า"]
  win -. "window ว่าง →
Send block" .-> prod
  cons -. "consume →
เติม window" .-> win
window เต็มหยุด producer จนกว่า consumer จะตามทัน

ผลก็คือ: เมื่อคุณเรียก Send แล้ว window เต็ม Send จะ block (หรือใน callback API จะส่งสัญญาณ “not ready”/drain) จนกว่า consumer จะตามทัน transport กำลังบอก producer ว่า “ช้าลงหน่อย” นั่นคือ backpressure ที่ทำงานถูกต้อง — และเกิดขึ้นโดยที่คุณไม่ต้องเขียนอะไรเลย

อันตรายไม่ใช่ HTTP/2 — ตัว protocol handle flow control ได้ดีอยู่แล้ว อันตรายคือ code ที่ buffer อ้อม backpressure แล้วดึงปัญหา memory ไม่จำกัดกลับมา:

  • อ่านทุกอย่างเข้า slice/list ก่อนประมวลผล ถ้า receive loop ของคุณ append ทุก message เข้า list ใน memory คุณเปลี่ยน stream ที่มีขอบเขตให้กลายเป็น memory ไม่จำกัด ประมวลผลแต่ละ message ตอนที่มาถึง
  • send loop ที่ไม่เคย block ใน runtime แบบ callback (Node) การเมิน return value ของ write() / event drain ทำให้คุณ queue data ไม่จำกัดไว้ใน buffer ภายในของ library เคารพสัญญาณ “รอก่อน”
  • ไม่อ่านขณะที่เขียน บน bidi stream ถ้าคุณส่งอย่างเดียวไม่เคยรับ flow-control acknowledgement ของ peer จะกอง แล้วทั้งสองฝั่ง deadlock ได้ รัน receive loop พร้อมกันเสมอ
  • ประมวลผล อย่าสะสม จัดการแต่ละ message ที่ stream มาแล้วปล่อยไป อย่าเก็บทั้งหมดไว้ก่อน
  • ให้ backpressure ส่งถึงต้นทาง ถ้าคุณ relay stream อยู่ (อ่านจาก DB ส่งไป client) ให้ Send ที่ block หยุดการอ่านจาก DB ด้วย แล้ว consumer ที่ช้าจะ throttle ต้นทางที่เร็วเองโดยธรรมชาติ
  • จำกัดทุกอย่างที่ stream ไม่ได้ จำกัดจำนวนและขนาด message เพื่อกัน peer ที่ทำตัวไม่ดีบังคับงานไม่จำกัด
  • รู้ว่าเมื่อไรไม่ควร stream ถ้า consumer ที่ช้าจะบังคับให้ server ถือ state หรือ memory ไว้นาน stream อาจกลายเป็นภาระ บางครั้ง unary call เล็ก ๆ หลายอัน (retry และ rebalance ได้อิสระ) เป็นดีไซน์ที่ปลอดภัยกว่า stream ยาว 1 อัน — trade-off เดียวกับใน intro ของโมดูลนี้
backpressure ใน gRPC stream คืออะไร?
flow control ของ gRPC มาจากไหน?
วิธีที่ developer เผลอทำลาย backpressure บ่อยที่สุดคืออะไร?
บน bidirectional stream ทำไมต้องอ่านขณะที่เขียน?