Reconnection & Backoff
ตอนนี้คุณ ตรวจจับ dead connection ได้แล้ว ปฏิกิริยาตามธรรมชาติคือ reconnect ทันที ปฏิกิริยานั้น เมื่อคูณด้วย client หลายพันตัว คือวิธีที่คุณเปลี่ยน server ที่กะพริบหายไปแวบหนึ่งให้กลายเป็นการล่มทั้งระบบ บทเรียนนี้ว่าด้วยการ reconnect ในแบบที่รักษา client ไว้ได้ โดยไม่ ไปเตะซ้ำ server ที่กำลังล้มอยู่
ทำไม “แค่ reconnect” ถึงอันตราย
หัวข้อที่มีชื่อว่า “ทำไม “แค่ reconnect” ถึงอันตราย”ลองนึกภาพ server รีสตาร์ตและ drop connection หนึ่งหมื่นตัวพร้อมกัน ถ้าทุก client retry ทันที แล้ว retry ทันทีอีกครั้งเมื่อล้มเหลว server ก็จะโดนถล่มด้วยความพยายามเชื่อมต่อหนึ่งหมื่นครั้งทุก ๆ ไม่กี่มิลลิวินาที ในวินาทีที่กำลังพยายามจะกลับมาพอดี สุดท้ายก็พังลงใต้พายุ reconnect ก่อน boot เสร็จด้วยซ้ำ นี่คือ thundering herd และ loop while (failed) reconnect() แบบไร้เดียงสาคือเครื่องยนต์ที่ขับพายุนี้
วิธีแก้มีส่วนประกอบสามอย่าง และคุณต้องการทั้งสามอย่าง:
- Exponential backoff — รอนานขึ้นหลังจากแต่ละครั้งที่ล้มเหลว ไม่ใช่ช่วงเวลาสั้น ๆ เท่าเดิม
- Jitter — สุ่มเวลารอเพื่อให้ client ไม่ retry พร้อมเพรียงกันทั้งหมด
- Cap — หยุดไม่ให้ดีเลย์โตจนยาวเกินเหตุ
Exponential backoff
หัวข้อที่มีชื่อว่า “Exponential backoff”แทนที่จะ retry ทุกวินาทีตลอดไป ให้เพิ่มดีเลย์เป็นสองเท่าทุกครั้งที่คุณล้มเหลว: 1s, 2s, 4s, 8s, 16s… คณิตศาสตร์ก็แค่ base delay คูณด้วยสองยกกำลังหมายเลขครั้ง:
const BASE_MS = 1000;const CAP_MS = 30000;
function backoffDelay(attempt: number): number { const exponential = BASE_MS * Math.pow(2, attempt); return Math.min(exponential, CAP_MS); // never wait longer than the cap}Cap สำคัญพอ ๆ กับการเติบโต ถ้าไม่ตั้ง cap ไว้ พอล้มเหลวสักโหลครั้ง คุณจะต้องรอ เป็นชั่วโมง ระหว่างความพยายามแต่ละครั้ง และผู้ใช้ที่เปิดแลปท็อปกลับมาก็จะนั่งจ้องแอปที่ disconnect อยู่ตลอดกาล Cap ที่ เช่น 30 วินาที หมายความว่า server ที่ฟื้นตัวจะถูกเจออีกครั้งอย่างรวดเร็วแม้หลังจากการล่มที่ยาวนาน
Jitter — ส่วนประกอบที่ทุกคนลืม
หัวข้อที่มีชื่อว่า “Jitter — ส่วนประกอบที่ทุกคนลืม”Backoff อย่างเดียวแก้ปัญหา ความถี่ แต่ไม่แก้ปัญหา การซิงค์กัน ถ้า client ทั้งหนึ่งหมื่นตัวโดน drop พร้อมกันและใช้ตาราง backoff ชุดเดียวกัน ก็จะรอ 1s พร้อมกันทั้งหมด แล้ว 2s ทั้งหมด แล้ว 4s ทั้งหมด — retry เป็นคลื่นที่ซิงค์กันเป๊ะ กระหน่ำ server เป็นจังหวะ ๆ ซึ่งแย่พอ ๆ กับพายุตั้งต้น
Jitter ทำลายการซิงค์กันด้วยการเพิ่มความสุ่มเข้าไปในแต่ละดีเลย์ รูปแบบที่ง่ายและได้ผลคือ “full jitter”: เลือกดีเลย์สุ่มที่ไหนก็ได้ระหว่างศูนย์กับ backoff ที่คำนวณไว้
function jittered(attempt: number): number { const ceiling = backoffDelay(attempt); return Math.random() * ceiling; // full jitter: 0..ceiling}ตอนนี้ client จะกระจาย retry ออกไปทั่วหน้าต่างเวลาอย่างราบรื่น แทนที่จะถล่ม server พร้อมกัน Jitter ราคาถูก เขียนแค่บรรทัดเดียว และเป็นตัวแปรสำคัญที่สุดที่แยกการ reconnect ที่สุภาพออกจาก DDoS ที่เราก่อขึ้นเอง
ลูป reconnect ในรูปแบบ state machine
หัวข้อที่มีชื่อว่า “ลูป reconnect ในรูปแบบ state machine”stateDiagram-v2 [*] --> Connected Connected --> Disconnected: drop detected Disconnected --> Waiting: compute min(base*2^n, cap), apply jitter Waiting --> Connecting: delay elapsed Connecting --> Connected: success, reset attempt to 0 Connecting --> Disconnected: failed, attempt += 1 Connected --> [*]: closed on purpose
รายละเอียดสองอย่างชี้เป็นชี้ตาย loop นี้ อย่างแรก รีเซ็ตตัวนับ attempt เป็นศูนย์เมื่อ connect สำเร็จ — ไม่งั้น connection ที่ต่อติดแล้วมา drop ทีหลังจะเริ่ม backoff รอบถัดไปจากดีเลย์มหึมาค้างไว้ อย่างที่สอง หยุดทั้งหมดเมื่อมีการ close อย่างตั้งใจ เพื่อที่คุณจะไม่ฝืนกับผู้ใช้ที่เลือกจะ disconnect
ดูดีเลย์โตขึ้น
หัวข้อที่มีชื่อว่า “ดูดีเลย์โตขึ้น”เดโมด้านล่างใช้ socket ในหน้าที่มี WebSocket API จริง mock ตั้งค่าให้ drop — คือยิง onclose ด้วย code 1006 ที่ผิดปกติไม่นานหลังแต่ละ open — ในความพยายามไม่กี่ครั้งแรก แล้วสุดท้ายค่อยอยู่ติด loop reconnect จะ log ดีเลย์ที่ผ่าน cap และ jitter มาแล้วก่อนลองแต่ละครั้ง คุณจะได้เห็น backoff ทำงานจริง
ดูตัวเลข backing off ...ms ไต่ขึ้นประมาณ 100, 200, 400, 800 — ผ่าน cap และสุ่มด้วย jitter แล้ว ตัวเลขจึงไม่ใช่กำลังสองเป๊ะ ๆ — สุดท้าย connection ก็อยู่ติดและตัวนับ attempt ก็รีเซ็ต ดีเลย์ที่โตขึ้น สุ่ม และถูก cap นั้นคือความแตกต่างระหว่าง client ที่ช่วย server ที่กำลังฟื้นตัว กับ client ที่ฝัง server
| Backoff Strategy | ลักษณะ | เหมาะกับ |
|---|---|---|
| Fixed interval | retry ทุก N วินาที | simple, ไม่ทนต่อ thundering herd |
| Exponential backoff | 1s, 2s, 4s, 8s… | ลด load บน server |
| Backoff + Jitter | สุ่มใน range | ป้องกัน thundering herd ได้ดีที่สุด |
| Backoff + Cap | max delay 30s | ไม่รอนานเกินไป |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”ไม่มี Jitter — Thundering Herd อาการ:
- client 10,000 ตัว drop พร้อมกัน retry ทุก 5 วินาทีพร้อมกัน
- server โดนกระหน่ำ reconnect เป็นคลื่นซ้ำ ๆ
- เพิ่ม
Math.random() * ceilingทำให้ retry กระจายตัว
ไม่ Stop เมื่อ Close ตั้งใจ อาการ:
- user logout →
ws.close(1000)แต่ reconnect logic ยังทำงาน - reconnect วนไม่หยุดทั้งที่ user ไม่ต้องการ
- check
wasCleanหรือcode === 1000→ หยุด reconnect
💡 ตัวอย่างจากของจริง
Binance WebSocket Streams:
- reconnect ด้วย exponential backoff เมื่อ stream ขาด
- cap ที่ 30 วินาที — ไม่รอนานเกินก่อน retry ใหม่
Pusher:
- backoff: 1s, 2s, 4s, 8s, 16s แล้ว cap ที่ 300s
- jitter ±20% เพื่อกระจาย reconnect จาก client หลายพัน