การตรวจจับ Dead Connection
Heartbeat จากบทเรียนที่แล้วส่งชีพจรออกไป แต่ชีพจรที่คุณส่งนั้นไร้ค่าหากคุณไม่ ตรวจสอบว่าคำตอบกลับมา บทเรียนนี้ว่าด้วยการตรวจสอบนั้น: วิธีเปลี่ยน “pong ไม่มาถึง” ให้กลายเป็นการตัดสินใจที่มั่นใจว่าจะประกาศว่า connection ตายแล้วรื้อทิ้ง
การส่งคือครึ่งหนึ่ง การคาดหวังคืออีกครึ่ง
หัวข้อที่มีชื่อว่า “การส่งคือครึ่งหนึ่ง การคาดหวังคืออีกครึ่ง”ความพยายามแรกที่ผิดและพบบ่อยคือการส่ง ping บน timer แล้วหยุดแค่นั้น นั่นไม่ตรวจจับอะไรเลย การตรวจจับทั้งหมดเกิดขึ้นใน ช่องว่างระหว่าง ping ที่คุณส่งกับ pong ที่คุณคาดหวัง Logic คือ:
- ส่ง ping จดไว้ว่าตอนนี้มี pong ค้าง อยู่กับคุณ
- เริ่ม deadline — เช่น interval ถัดไปหรือไม่กี่วินาที
- ถ้า pong ที่ตรงกันมาถึงก่อน deadline ดีแล้ว: เคลียร์หนี้ connection ยังมีชีวิต
- ถ้า deadline ผ่านไปโดยที่หนี้ยังค้างอยู่ แสดงว่าคุณมี missed pong ให้ถือว่า connection ตายแล้ว
ดังนั้นตัวแปรที่มีประโยชน์ที่สุดใน client ของคุณคือสิ่งที่คล้าย ๆ pongOutstanding (หรือจำนวน ping ที่ยังไม่ได้รับคำตอบ) พอค่านี้ข้ามเกณฑ์เมื่อไหร่ก็ลงมือได้ client ใน production ส่วนใหญ่ยอมให้ miss สักหนึ่งถึงสองครั้งก่อนประกาศการตาย — pong ที่หายไปครั้งเดียวอาจเป็นแค่อาการสะดุดชั่วขณะ แต่สามครั้งติดกันคือศพ
Half-open connection — เหตุผลว่าทำไมเรื่องนี้ถึงยาก
หัวข้อที่มีชื่อว่า “Half-open connection — เหตุผลว่าทำไมเรื่องนี้ถึงยาก”ตัวร้ายเบื้องหลังทั้งหมดนี้คือ connection แบบ half-open connection จะเป็น half-open เมื่อฝ่ายหนึ่งยังเชื่อว่าต่ออยู่ ส่วนอีกฝ่ายหายไปแล้วอย่างเงียบ ๆ ลองนึกภาพการโทรศัพท์ที่โทรศัพท์ของอีกฝ่ายดับไปกลางประโยค: คุณยังถือหูฟังอยู่ พูดอยู่ ไม่ได้ยินอะไรเลย โดยไม่มีเสียงสัญญาณบอกว่าสายตัดไปแล้ว
สิ่งนี้เกิดขึ้นตลอดเวลาในเน็ตเวิร์กจริง:
- ฝาแลปท็อปปิด OS แช่แข็ง socket โดยไม่มีการ close อย่างสะอาด
- อุปกรณ์มือถือสลับจาก Wi-Fi ไปเป็นเซลลูลาร์ ทิ้ง socket เก่าไป
- NAT หรือ load balancer ขับ connection ที่ idle ออกจากตารางอย่างเงียบ ๆ
ในทุกกรณี ไม่มี event close ยิงขึ้น บนฝ่ายที่ยังอยู่รอด เพราะไม่มี FIN packet มาถึงเลยสักตัว WebSocket ของคุณจึงยังบอกว่า OPEN วิธี เดียว ที่จะรู้ความจริงคือส่งอะไรบางอย่างออกไปแล้วดูว่าไม่มีอะไรกลับมา — ซึ่งนั่นคือสิ่งที่ heartbeat บวก deadline มอบให้คุณพอดี
จาก missed pong สู่การ termination
หัวข้อที่มีชื่อว่า “จาก missed pong สู่การ termination”เมื่อตัดสินใจแล้วว่า connection ตาย ก็ไม่ต้องสุภาพ การ close() อย่างสะอาดจะพยายามทำ handshake กับ peer ที่ หายไปแล้ว — ไม่มีใครให้ handshake ด้วย จึงมีสิทธิ์ค้าง ให้ terminate แทน: ทิ้ง socket ที่ตายแล้วทันทีแล้วไป reconnect ตรง ๆ เบราว์เซอร์เปิดเผย close() ส่วน library ws ของ Node เพิ่ม terminate() มาเพื่อกรณีโหด ๆ แบบนี้โดยเฉพาะ Flow การตัดสินใจ:
stateDiagram-v2 [*] --> Healthy Healthy --> Waiting: send ping, pong now owed Waiting --> Healthy: pong arrived in time Waiting --> Suspect: deadline passed, pong missed Suspect --> Healthy: a later pong arrived Suspect --> Dead: misses exceeded threshold Dead --> Terminate: abandon socket, do not handshake Terminate --> [*]: hand off to reconnect
ดู missed pong ถูกตั้งธง
หัวข้อที่มีชื่อว่า “ดู missed pong ถูกตั้งธง”เดโมด้านล่างใช้ socket ในหน้าที่มี WebSocket API จริง mock นี้ตอบ ping สองสามตัวแรกตามปกติ — แล้วก็เงียบไป จำลอง connection ที่ตายไปแล้วโดยไม่ยิง onclose (half-open connection) client รัน heartbeat ที่มี deadline และตั้งธงว่า connection ตายในวินาทีที่ pong เกินกำหนด
คุณจะเห็น ping/pong ที่แข็งแรงสองคู่ จากนั้น mock ก็เงียบไป client ส่งต่อไปเรื่อย ๆ จำนวน pong ที่ไม่ได้รับคำตอบไต่สูงขึ้น พอข้ามเกณฑ์ client ก็ประกาศว่า connection ตายแล้ว terminate ทิ้ง — โดยไม่ต้องรอ onclose ที่ไม่มีวันมา นั่นคือการตรวจจับ: คุณหยุดเชื่อความเงียบและลงมือกับคำตอบที่หายไป
| Detection Method | ความแม่นยำ | Overhead |
|---|---|---|
| Heartbeat + missed pong counter | สูง — ตรวจจับ half-open ได้ | ส่ง ping ทุก interval |
| OS TCP keepalive | ต่ำ — อาจใช้เวลาชั่วโมง | ไม่มี application code |
| Connection timeout บน server | ปานกลาง | server ต้องตรวจ idle time |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”รอ onclose Event สำหรับ Dead Connection อาการ:
- half-open connection ไม่ยิง
onclose— รอตลอดไป - server ถือ zombie connection โดยไม่รู้ตัว
- ใช้ heartbeat + missed pong เพื่อตรวจจับ ไม่ต้องรอ event
Threshold สูงเกินไป — ตรวจจับช้า อาการ:
- threshold 10 missed pong — รอนานเกินก่อนประกาศว่าตาย
- UX แย่ — user รู้สึกว่า app ค้าง
- threshold 2-3 missed pong เหมาะสมสำหรับส่วนใหญ่
💡 ตัวอย่างจากของจริง
Discord Gateway:
- ถ้าไม่ได้รับ Heartbeat ACK — ส่ง non-resumable close code แล้ว reconnect ทันที
- ไม่รอ TCP timeout
Cloudflare Durable Objects:
- ตรวจสอบ WebSocket activity ทุก 60 วินาที
- ถ้าไม่มี activity → close connection เพื่อประหยัด resource
| Dead Connection Scenario | สาเหตุ | Detection Method |
|---|---|---|
| Client network drop | wifi ขาด, mobile network เปลี่ยน | Heartbeat timeout |
| Firewall/NAT timeout | idle connection ถูก reset โดยไม่แจ้ง | Heartbeat ไม่ได้ pong |
| Process crash | browser tab close, app kill | OS ส่ง TCP RST — onclose fire |
| Server-side timeout | server close idle connection | Close frame opcode |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”เชื่อ TCP Connected ว่า Application Connected อาการ:
- TCP connection ยังอยู่แต่ data ไม่ผ่าน (half-open connection)
- server ส่ง message ไปแต่ client ไม่ได้รับ — ไม่มี error
- heartbeat ที่ application level จำเป็น — TCP status ไม่เพียงพอ
Zombie Connection ที่ Server ไม่รู้ อาการ:
- client disconnect แบบ ungraceful (network cut, process kill)
- server ยัง track connection อยู่ใน map — memory leak
- ตั้ง heartbeat timeout: ถ้าไม่มี pong ใน N วินาที → remove และ close
💡 ตัวอย่างจากของจริง
AWS API Gateway WebSocket:
- ตั้ง idle connection timeout — ปิด connection ที่ไม่มี message นาน 10 นาที
- แอปต้องส่ง heartbeat ถ้าต้องการ connection ค้างนาน
nginx WebSocket proxy:
proxy_read_timeout 3600s— เพิ่ม timeout สำหรับ long-lived WebSocket connection- ถ้าไม่ตั้ง → nginx ปิด connection ทุก 60 วินาที (default)