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

Reconnection & Backoff

ตอนนี้คุณ ตรวจจับ dead connection ได้แล้ว ปฏิกิริยาตามธรรมชาติคือ reconnect ทันที ปฏิกิริยานั้น เมื่อคูณด้วย client หลายพันตัว คือวิธีที่คุณเปลี่ยน server ที่กะพริบหายไปแวบหนึ่งให้กลายเป็นการล่มทั้งระบบ บทเรียนนี้ว่าด้วยการ reconnect ในแบบที่รักษา client ไว้ได้ โดยไม่ ไปเตะซ้ำ server ที่กำลังล้มอยู่

ลองนึกภาพ server รีสตาร์ตและ drop connection หนึ่งหมื่นตัวพร้อมกัน ถ้าทุก client retry ทันที แล้ว retry ทันทีอีกครั้งเมื่อล้มเหลว server ก็จะโดนถล่มด้วยความพยายามเชื่อมต่อหนึ่งหมื่นครั้งทุก ๆ ไม่กี่มิลลิวินาที ในวินาทีที่กำลังพยายามจะกลับมาพอดี สุดท้ายก็พังลงใต้พายุ reconnect ก่อน boot เสร็จด้วยซ้ำ นี่คือ thundering herd และ loop while (failed) reconnect() แบบไร้เดียงสาคือเครื่องยนต์ที่ขับพายุนี้

วิธีแก้มีส่วนประกอบสามอย่าง และคุณต้องการทั้งสามอย่าง:

  • Exponential backoff — รอนานขึ้นหลังจากแต่ละครั้งที่ล้มเหลว ไม่ใช่ช่วงเวลาสั้น ๆ เท่าเดิม
  • Jitter — สุ่มเวลารอเพื่อให้ client ไม่ retry พร้อมเพรียงกันทั้งหมด
  • Cap — หยุดไม่ให้ดีเลย์โตจนยาวเกินเหตุ

แทนที่จะ 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 ที่ฟื้นตัวจะถูกเจออีกครั้งอย่างรวดเร็วแม้หลังจากการล่มที่ยาวนาน

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 ที่เราก่อขึ้นเอง

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
Reconnect ด้วย capped exponential backoff และ jitter

รายละเอียดสองอย่างชี้เป็นชี้ตาย loop นี้ อย่างแรก รีเซ็ตตัวนับ attempt เป็นศูนย์เมื่อ connect สำเร็จ — ไม่งั้น connection ที่ต่อติดแล้วมา drop ทีหลังจะเริ่ม backoff รอบถัดไปจากดีเลย์มหึมาค้างไว้ อย่างที่สอง หยุดทั้งหมดเมื่อมีการ close อย่างตั้งใจ เพื่อที่คุณจะไม่ฝืนกับผู้ใช้ที่เลือกจะ disconnect

เดโมด้านล่างใช้ socket ในหน้าที่มี WebSocket API จริง mock ตั้งค่าให้ drop — คือยิง onclose ด้วย code 1006 ที่ผิดปกติไม่นานหลังแต่ละ open — ในความพยายามไม่กี่ครั้งแรก แล้วสุดท้ายค่อยอยู่ติด loop reconnect จะ log ดีเลย์ที่ผ่าน cap และ jitter มาแล้วก่อนลองแต่ละครั้ง คุณจะได้เห็น backoff ทำงานจริง

JavaScript

ดูตัวเลข backing off ...ms ไต่ขึ้นประมาณ 100, 200, 400, 800 — ผ่าน cap และสุ่มด้วย jitter แล้ว ตัวเลขจึงไม่ใช่กำลังสองเป๊ะ ๆ — สุดท้าย connection ก็อยู่ติดและตัวนับ attempt ก็รีเซ็ต ดีเลย์ที่โตขึ้น สุ่ม และถูก cap นั้นคือความแตกต่างระหว่าง client ที่ช่วย server ที่กำลังฟื้นตัว กับ client ที่ฝัง server

Backoff Strategyลักษณะเหมาะกับ
Fixed intervalretry ทุก N วินาทีsimple, ไม่ทนต่อ thundering herd
Exponential backoff1s, 2s, 4s, 8s…ลด load บน server
Backoff + Jitterสุ่มใน rangeป้องกัน thundering herd ได้ดีที่สุด
Backoff + Capmax 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 หลายพัน
ทำไมการ reconnect ทันทีใน loop ที่กระชั้นถึงอันตราย?
Jitter แก้ปัญหาอะไรโดยเฉพาะที่ backoff อย่างเดียวแก้ไม่ได้?
ทำไมคุณต้องรีเซ็ตตัวนับ attempt เป็นศูนย์หลังจาก connect สำเร็จ?