Scaling และ Pub/Sub
Node process เดียวสามารถถือ WebSocket ที่เปิดอยู่ได้มากอย่างน่าประหลาดใจ — หลายหมื่น connection บนเครื่องที่สุขภาพดี ดังนั้นในช่วงแรก scaling จึงรู้สึกเหมือนเป็นปัญหาที่คุณมองข้ามไปได้ แล้ว traffic ก็โตขึ้น คุณเพิ่ม server ตัวที่สองไว้หลัง load balancer และมีบางอย่างแปลก ๆ เกิดขึ้น: ข้อความที่ส่งโดยผู้ใช้บน server A ไม่เคยไปถึงผู้ใช้บน server B เลย แชทดูเหมือนพังทั้งที่ทุกบรรทัดในcodeของคุณถูกต้อง
โมดูลนี้ว่าด้วยช่องว่างนั้นและวิธีปิดช่องว่าง
connection คือสิ่งที่อยู่ใน memory
หัวข้อที่มีชื่อว่า “connection คือสิ่งที่อยู่ใน memory”รายละเอียดที่ทำให้ scaling ยากนั้นมองข้ามได้ง่าย เมื่อ client เชื่อมต่อเข้ามา server จะเก็บ object ใน memory สำหรับ socket นั้น — ตัว file descriptor, frame ที่ buffer ไว้ และมักจะมี application state บางอย่างที่คุณผูกติดไว้ เช่น id ของผู้ใช้หรือ room ที่เขาเข้าร่วม
object นั้นอยู่ใน process เดียว บนเครื่องเดียว ไม่ได้อยู่ใน database ไม่ได้ถูกแชร์ server อีกตัวไม่เคยรู้จัก object นี้เลย ถ้า process นั้น restart ทุก connection ที่ถือไว้ก็หายไป และทุก client ต้อง reconnect ใหม่
ดังนั้น “รายชื่อว่าใครเชื่อมต่ออยู่บ้าง” จริง ๆ แล้วคือ “รายชื่อว่าใครเชื่อมต่ออยู่กับ instance ตัวนี้โดยเฉพาะ” และข้อเท็จจริงเดียวนั้นเองที่ทำให้ broadcast พัง
การ broadcast ไปถึงแค่ instance นี้เท่านั้น
หัวข้อที่มีชื่อว่า “การ broadcast ไปถึงแค่ instance นี้เท่านั้น”ลองนึกถึงฟีเจอร์ real-time ที่ง่ายที่สุด: มีคนโพสต์ข้อความแชทและคุณอยากให้ทุกคนเห็น codeที่เป็นธรรมชาติคือ “loop ผ่าน client ที่เชื่อมต่ออยู่ของฉันแล้วส่งให้แต่ละคน”
ปัญหาอยู่ที่คำว่า ของฉัน แต่ละ instance สามารถ loop ได้เฉพาะ socket ที่ถือไว้เองเท่านั้น เมื่อ client กระจายอยู่บนสอง instance โดย load balancer การ broadcast บน instance หนึ่งจะไปถึงครึ่งห้อง และข้ามอีกครึ่งไปเงียบ ๆ
flowchart TB
lb["Load balancer"]
subgraph A["Instance A (in-memory sockets)"]
a1["client 1"]
a2["client 2"]
end
subgraph B["Instance B (in-memory sockets)"]
b1["client 3"]
b2["client 4"]
end
lb --> a1
lb --> a2
lb --> b1
lb --> b2
a1 -. "posts a message" .-> A
A == "broadcast reaches A only" ==> a2
A -. "client 3 and 4 never hear it" .-x B client 1 โพสต์ข้อความ Instance A ส่งให้ client 2 ได้อย่างซื่อสัตย์ ซึ่ง client 2 ก็อยู่บน A เช่นกัน ส่วน client 3 และ 4 อยู่บน instance B ที่ไม่รู้เลยว่าข้อความนี้เคยมีอยู่ จากที่นั่งของพวกเขา ห้องนั้นเงียบไป ไม่มีอะไร crash — ข้อความเพียงแค่ไม่มีเส้นทางระหว่าง instance
โมดูลนี้ครอบคลุมอะไรบ้าง
หัวข้อที่มีชื่อว่า “โมดูลนี้ครอบคลุมอะไรบ้าง”การแก้ปัญหานี้ส่วนใหญ่เป็นแบบฝึกหัดด้าน architecture ดังนั้นบทเรียนจึงพึ่งพา diagram และ TypeScript ขนาดเล็กมากกว่า server ที่รันได้ตัวเดียว เส้นทางคือ:
- ทำไม scaling จึงยาก — connection state ใน memory, ต้นทุนต่อ connection, การ balance connection ที่มีอายุยืน และปัญหา cross-instance broadcast ที่ระบุไว้อย่างแม่นยำ
- pub/sub backplane — วาง bus ที่แชร์กัน (Redis pub/sub, NATS, Kafka) ไว้ระหว่าง instance เพื่อให้ทุกข้อความที่ publish ไปถึงทุก instance ซึ่งจากนั้นจะ fan-out ไปยัง client ภายในของตัวเอง พร้อม demo ในหน้าของสอง instance ที่แชร์ bus เดียวกัน
- Presence ข้าม instance — การติดตามว่าใครออนไลน์อยู่เมื่อ “ออนไลน์” กระจายอยู่ตามเครื่องต่าง ๆ: shared store, heartbeat, การหมดอายุ และ reconnect stampede
- Sticky vs stateless — route client กลับไปยัง instance ของตัวเอง หรือปล่อยให้ instance ใดก็ได้ให้บริการจาก shared state และวิธีตั้งค่า load balancer ในแต่ละแบบ