Heartbeats
ในบทเรียนที่แล้วเราบอกว่าการไถลเงียบ ๆ จาก live ไปสู่ dropped คือ transition ที่แอปลืม บทเรียนนี้อธิบายว่า ทำไมถึงเงียบ — และมอบเครื่องมือหนึ่งเดียวที่ทำลายความเงียบนั้น: heartbeat
Connection ที่เงียบและ connection ที่ตายดูเหมือนกัน
หัวข้อที่มีชื่อว่า “Connection ที่เงียบและ connection ที่ตายดูเหมือนกัน”นี่คือความจริงที่อึดอัดเกี่ยวกับ socket ที่เปิดอยู่: การไม่ทำอะไรเลยหน้าตาเหมือนการพังเป๊ะ ๆ ถ้าไม่มีฝ่ายไหนส่ง message เลยเป็นเวลาสามสิบวินาที คำอธิบายที่เป็นไปได้มีสองอย่าง และมองจากข้างในแล้วแยกไม่ออกเลย:
- ทุกอย่างปกติดี และก็แค่ไม่มีใครมีอะไรจะพูด
- Connection ตายไปเมื่อสิบวินาทีที่แล้ว — Wi-Fi handoff แลปท็อปที่หลับ load balancer ที่เก็บกวาด stream ที่ idle ทิ้ง — แล้ว packet ที่ประกาศการตายก็ไม่เคยมาถึง
TCP เองก็ช่วยตรงนี้ไม่ได้ TCP จะรู้ว่า connection พังก็ต่อเมื่อลองส่งแล้ว acknowledgement ไม่กลับมา ถ้าแอปของคุณ idle TCP ก็ idle ตาม จึงไม่เคยลองส่ง และไม่เคยรู้ ส่วน TCP keepalive ที่ติดมากับ operating system ค่าเริ่มต้นอาจใช้เวลาเป็น ชั่วโมง กว่าจะทำงาน — ไร้ประโยชน์สำหรับแอป real-time ผลลัพธ์คือ object WebSocket ของคุณรายงานอย่างมีความสุขว่า readyState === 1 (OPEN) บน connection ที่กลายเป็นซากมาหลายนาทีแล้ว
ทางรักษาเดียวคือ จงใจสร้าง traffic ตามช่วงเวลาที่กำหนด แล้วตรวจว่าคำตอบกลับมาไหม นั่นคือ heartbeat
Heartbeat สองชนิด
หัวข้อที่มีชื่อว่า “Heartbeat สองชนิด”มีกลไกที่แตกต่างกันสองอย่าง และผู้คนสับสนระหว่างสองอย่างนี้อยู่เสมอ
- Protocol ping/pong frame WebSocket protocol นิยาม control frame พิเศษไว้: ping frame และ pong frame ตาม spec แล้ว endpoint ที่รับ ping ต้องตอบด้วย pong เสมอ frame พวกนี้อยู่ต่ำกว่าระดับ message handler ของคุณ — เป็นส่วนหนึ่งของ framing layer ที่พูดถึงในโมดูล foundations
- Application-level heartbeat ข้อความธรรมดาที่codeของคุณส่งและตอบ เช่น JSON
{"type":"ping"}ที่ server ของคุณตอบกลับด้วย{"type":"pong"}
ทำไมถึงต้องสร้างเวอร์ชัน app-level ในเมื่อ protocol ให้ ping/pong มาฟรี ๆ อยู่แล้ว? เพราะ เบราว์เซอร์ไม่อนุญาตให้คุณใช้ protocol frame เหล่านั้น WebSocket API ของเบราว์เซอร์ไม่มี method ping() และไม่ยิง event ใดเลยเมื่อ ping หรือ pong มาถึง — ทุกอย่างถูกจัดการอย่างมองไม่เห็นอยู่ภายในเบราว์เซอร์ ฝั่ง server ส่ง protocol ping ไปยังเบราว์เซอร์ได้ แต่ code ฝั่ง client ในเบราว์เซอร์ทั้งส่งและสังเกตไม่ได้เลย ดังนั้นในทางปฏิบัติ:
- Server ไปยังเบราว์เซอร์: server อาจใช้ protocol ping/pong จริงเพื่อตรวจสอบเบราว์เซอร์
- เบราว์เซอร์ไปยัง server: client ต้องใช้ message ระดับ app เพราะเห็นได้แค่นั้น
ดังนั้นระบบ production ส่วนใหญ่จึงรัน heartbeat แบบ app-level เพื่อให้ logic เดียวกันทำงานได้บนทุกแพลตฟอร์ม นั่นคือสิ่งที่เราจะสร้างด้านล่าง
The heartbeat loop
หัวข้อที่มีชื่อว่า “The heartbeat loop”รูปแบบของ app-level heartbeat มักจะเหมือนกันเสมอ:
sequenceDiagram
participant C as Client
participant S as Server
loop every interval
C->>S: ping
S-->>C: pong
Note over C: pong seen — mark connection healthy
end ใน code ก็แค่ interval timer บวก state ชิ้นเล็ก ๆ: ส่ง ping ทุก N วินาที แล้วจำว่า pong ที่คู่กันกลับมาหรือยัง นี่คือหัวใจของตัวเองในรูป TypeScript ธรรมดา:
const HEARTBEAT_MS = 5000;
function startHeartbeat(ws: WebSocket) { const timer = setInterval(() => { if (ws.readyState !== WebSocket.OPEN) return; ws.send(JSON.stringify({ type: 'ping', t: Date.now() })); }, HEARTBEAT_MS);
ws.addEventListener('message', (event) => { const msg = JSON.parse(event.data as string); if (msg.type === 'pong') { // The connection answered — it is genuinely alive right now. } });
ws.addEventListener('close', () => clearInterval(timer));}การส่ง ping คือครึ่งที่ง่าย ครึ่งที่มีค่า — การสังเกตว่า pong ไม่เคยมา — คือบทเรียนถัดไปทั้งบท สำหรับตอนนี้ ให้โฟกัสที่จังหวะ: ชีพจรที่สม่ำเสมอ และคำตอบสำหรับทุกครั้งที่เต้น
ping/pong แบบสด
หัวข้อที่มีชื่อว่า “ping/pong แบบสด”เดโมด้านล่างใช้ socket ในหน้าที่มี WebSocket API จริง mock เล่นบทเป็น server ที่ประพฤติดี: ทุกครั้งที่รับ message ping ระดับ app ก็จะตอบ pong กลับ ส่วน client ส่ง ping ทุกหนึ่งวินาทีแล้ว log การไปกลับ ลองดู heartbeat เข้าจังหวะของตัวเอง
คุณจะเห็น ping ออกไปและ pong กลับมา หนึ่งคู่ต่อวินาที ทุก pong ที่กลับมาคือหลักฐานชัดเจนว่า connection ยังมีชีวิต ในขณะนั้น — ไม่ใช่เมื่อสามสิบวินาทีที่แล้ว ไม่ใช่ “น่าจะ” นั่นคือจุดประสงค์ทั้งหมดของ heartbeat: เปลี่ยนความเงียบซึ่งไม่บอกอะไรเลย ให้เป็นสัญญาณสม่ำเสมอที่เชื่อถือได้จริง
| Heartbeat Type | ทำงานอย่างไร | ข้อดี |
|---|---|---|
| Protocol-level Ping/Pong | WebSocket frame opcode 0x9/0xA | browser handle อัตโนมัติ, lightweight |
| Application-level Ping | ส่ง JSON { "type": "ping" } | flexible, บน binary หรือ text frame |
| Keepalive TCP | OS-level TCP keepalive | ไม่ต้องใช้ application code |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”ไม่มี Heartbeat เลย อาการ:
- connection ดูเหมือนเปิดอยู่ใน server แต่ client จริงหายไป (zombie connection)
- load balancer หรือ firewall ปิด idle connection โดยไม่แจ้ง
- implement server-side ping ทุก 30 วินาที, ถ้าไม่มี pong → ปิด connection
Heartbeat Interval สั้นเกิน อาการ:
- ping ทุก 1 วินาท — server และ client เสียเวลา ping/pong แทนที่จะส่งข้อมูล
- battery drain บน mobile device
- interval 30-60 วินาทีเหมาะสำหรับส่วนใหญ่ — ขึ้นกับ firewall timeout
💡 ตัวอย่างจากของจริง
Discord Gateway:
- ส่ง
{ "op": 1, "d": null }(Heartbeat opcode) ตาม interval ที่ server กำหนด- server ตอบ
{ "op": 11 }(Heartbeat ACK) — client รู้ว่า connection aliveSlack RTM API:
- ping/pong ทุก 30 วินาที
- ถ้าไม่มี pong ใน 10 วินาที → reconnect อัตโนมัติ