- เป็นสถานการณ์ที่ต้องจัดการการอัปเดตแบบเรียลไทม์จำนวนมากในแบ็กเอนด์ที่พัฒนาด้วย Node.js/TypeScript
- ใช้ PostgreSQL เป็นแบ็กเอนด์ โดยมีโหนดเวิร์กเกอร์หลายร้อยตัวคอยตรวจสอบงานใหม่อย่างต่อเนื่อง และเอเจนต์ต้องได้รับอัปเดตสถานะการรันและการแชต
- เริ่มต้นจากการสำรวจ WebSocket แต่สุดท้ายกลับมาจบที่โซลูชันแบบ 'ดั้งเดิม' ที่ได้ผลอย่างน่าประหลาดใจ
→ "HTTP Long Polling with Postgres"
สถานการณ์ปัญหา: การอัปเดตแบบเรียลไทม์ในระดับใหญ่
- การอัปเดตของโหนดเวิร์กเกอร์ :
- มีโหนดเวิร์กเกอร์หลายร้อยตัวที่รัน SDK ของ Node.js/Golang/C#
- จำเป็นต้องรู้ทันทีเมื่อมีงานใหม่เข้ามา จึงต้องมีกลยุทธ์การคิวรีที่ไม่ทำให้ฐานข้อมูล Postgres ล่ม
- การซิงก์สถานะเอเจนต์ :
- เอเจนต์ต้องการการอัปเดตแบบเรียลไทม์เกี่ยวกับสถานะการรันและการแชต และต้องสตรีมข้อมูลเหล่านี้อย่างมีประสิทธิภาพ
เปรียบเทียบ Long Polling กับ WebSocket
- Short polling เหมือนรถไฟที่ออกตามตารางเวลาอย่างเคร่งครัด ไม่ว่าจะมีผู้โดยสารหรือไม่ก็ออกตามช่วงเวลาที่กำหนด
- Long polling คือเซิร์ฟเวอร์จะรอการตอบกลับไว้ก่อน แล้วส่งกลับทันทีเมื่อมีข้อมูล และหากเวลาผ่านไปตามกำหนดก็จะตอบกลับด้วย timeout
- กล่าวคือเหมือนรถไฟที่ “รอจนกว่าจะมีข้อมูลแล้วค่อยออก” และจะออกแบบว่างเปล่าก็ต่อเมื่อไม่มีผู้โดยสารภายในเวลาที่กำหนด (TTL)
- จึงได้ข้อดีสองอย่างพร้อมกัน คือออกได้ทันทีเมื่อมีข้อมูล (ผู้โดยสาร) และใช้ทรัพยากรได้อย่างมีประสิทธิภาพเมื่อไม่มีข้อมูล
- WebSocket คือการคงการเชื่อมต่อไว้ตลอดเวลาเพื่อรับส่งข้อมูลแบบสองทาง
- ในแง่สภาพแวดล้อมองค์กร อินฟราสตรักเจอร์ และปัญหาไฟร์วอลล์ Long polling เรียบง่ายและเข้ากันได้ดีกว่าการตั้งค่า WebSocket
รายละเอียดการติดตั้งใช้งาน Long Polling
- ฟังก์ชัน
getJobStatusSync ทำหน้าที่สำคัญ
- รับพารามิเตอร์อย่าง
jobId, owner, ttl แล้วคอยคิวรีสถานะของงานที่กำหนดซ้ำ ๆ ภายในช่วงเวลาที่ระบุ
- จะคิวรีซ้ำไปเรื่อย ๆ จนกว่าจะเข้าเงื่อนไขข้อใดข้อหนึ่งต่อไปนี้
- สถานะงานกลายเป็น
success หรือ failure
ttl (timeout) หมดลง
- คิวรีฐานข้อมูลทุก ๆ 500ms และหากผลลัพธ์ยังไม่สิ้นสุดก็จะรอแล้วคิวรีใหม่
- หากเกินเวลา timeout จะ throw error และหากสำเร็จจะคืนผลลัพธ์
การปรับแต่งฐานข้อมูลให้เหมาะสม
- วางดัชนีที่เหมาะสมใน Postgres เพื่อลดต้นทุนของการคิวรีให้ต่ำที่สุด
- ตัวอย่าง:
CREATE INDEX idx_jobs_status ON jobs(id, cluster_id);
ข้อดีของ Long Polling
- ดูแลการมอนิเตอร์ได้ง่าย : ยังใช้สแตก logging และ monitoring เดิมที่อิงกับ HTTP ได้ตามเดิม
- การยืนยันตัวตนเรียบง่าย : ไม่จำเป็นต้องสร้างวิธียืนยันตัวตนใหม่ และใช้การยืนยันตัวตน HTTP เดิมได้ทันที
- เข้ากันได้กับอินฟราสตรักเจอร์ : ไม่ต้องมีการตั้งค่าเพิ่มสำหรับไฟร์วอลล์หรือ load balancer และถูกมองเป็นทราฟฟิก HTTP ปกติ
- ความเรียบง่ายในการปฏิบัติการ : แม้รีสตาร์ตเซิร์ฟเวอร์ก็ไม่ต้องจัดการสถานะการเชื่อมต่อเป็นพิเศษ และดีบักได้ง่าย
- ฝั่งไคลเอนต์ติดตั้งง่าย : ทำงานได้ด้วยโครงสร้างมาตรฐานแบบ HTTP request-response เพียงเพิ่มตรรกะ retry เข้าไป
เปรียบเทียบกับ ElectricSQL
- ElectricSQL เป็นโซลูชันสำหรับซิงก์ข้อมูล Postgres กับฟรอนต์เอนด์
- มีโครงสร้างที่รับประกันความเป็นเรียลไทม์ได้แม้จะใช้ HTTP แทน WebSocket
- หากไม่ได้ต้องการการควบคุมแบบสุดขั้วหรือโครงสร้างระดับล่างเพื่อจัดการอัปเดตเรียลไทม์โดยตรง ก็แนะนำ ElectricSQL
เหตุผลที่เราเลือก Raw Long Polling
- กลไกการส่งต่อข้อความไม่ใช่แค่รายละเอียดของการติดตั้งใช้งาน แต่เป็นองค์ประกอบ แกนหลักของผลิตภัณฑ์
- ไม่สามารถพึ่งพาไลบรารีของบุคคลที่สามสำหรับฟังก์ชันหลักนี้ได้ (แม้จะเป็นไลบรารีที่ยอดเยี่ยมก็ตาม)
- ข้อกำหนด
- การควบคุมผลิตภัณฑ์หลัก : ต้องควบคุมกลไกการส่งต่อข้อความได้อย่างสมบูรณ์ นี่ไม่ใช่เรื่องระดับอินฟราสตรักเจอร์ แต่เป็นตัวผลิตภัณฑ์เอง
- ตัดการพึ่งพาภายนอก : ลดการพึ่งพาภายนอกให้มากที่สุดเพื่อให้ self-hosting ง่ายขึ้น
- การควบคุมระดับล่าง : ควบคุมกลไก polling และการจัดการการเชื่อมต่อได้โดยตรง
- ความสามารถในการควบคุมสูงสุด : ต้องปรับรายละเอียดได้ละเอียด เช่น การกำหนดช่วง polling แบบไดนามิก
- ความเรียบง่ายของโค้ด : ออกแบบให้เรียบง่ายเพื่อให้ผู้ใช้เข้าใจและแก้ไขโค้ดเบสได้ง่าย
- สรุปคือ ด้วยการเลือกติดตั้งใช้งาน HTTP Long Polling แบบเรียบง่าย เราจึงได้ทั้ง การควบคุมโดยตรง และ ความเรียบง่าย
ข้อควรระวังเมื่อใช้งาน Long Polling
- การตั้งค่า TTL : ฝั่งเซิร์ฟเวอร์ต้องบังคับ TTL สูงสุดเสมอ และต้องจัดการไม่ให้ TTL ที่ไคลเอนต์ร้องขอเกินค่านั้น
- คำนึงถึง timeout ของอินฟราสตรักเจอร์ : TTL ต้องสั้นกว่าค่าตั้ง timeout ของ load balancer, edge server, proxy ฯลฯ อย่างเพียงพอ
- ช่วงเวลาในการ polling DB : หน่วงราว 500ms เพื่อลดภาระของ DB
- กลยุทธ์ backoff (ทางเลือก) : สามารถเพิ่มช่วงเวลาของ polling แบบค่อยเป็นค่อยไปเพื่อใช้ทรัพยากรระบบได้อย่างมีประสิทธิภาพมากขึ้น
สถานการณ์ที่ควรพิจารณา WebSocket
- ตัว WebSocket เองไม่ใช่สิ่งที่ผิด และยังมีประโยชน์ในมุมอื่น
- กรณีที่ต้องมอนิเตอร์การเชื่อมต่อจำนวนมากที่มีสถานะ และต้องรับส่งอีเวนต์ที่ซับซ้อนอย่างต่อเนื่อง
- กรณีที่มีทรัพยากรและเวลามากพอสำหรับแก้ปัญหาเรื่องการยืนยันตัวตน อินฟราสตรักเจอร์ และการสังเกตการณ์ระบบ
- แต่ก็มีความซับซ้อนที่ต้องสร้างเอง ทั้งด้านการปฏิบัติการ logging การจัดการการเชื่อมต่อใหม่ และกลไกการยืนยันตัวตน
WebSockets: อีกเรื่องหนึ่งเกี่ยวกับตัวเลือกทางเลือก
- แม้ Long Polling จะเหมาะกับความต้องการของเรา แต่ WebSockets ก็ยังเป็นทางเลือกที่ควรพิจารณาอย่างจริงจัง
- WebSockets ไม่ได้แย่ในตัวเอง เพียงแต่ต้องการ ความใส่ใจและการดูแล อย่างมาก
- โจทย์หลักของ WebSockets และแนวทางรับมือ
- การมองเห็นระบบ : WebSockets เป็นแบบมีสถานะ จึงต้องเพิ่ม logging และ monitoring สำหรับการเชื่อมต่อที่ต่อเนื่อง
- การยืนยันตัวตน : จำเป็นต้องสร้างกลไกการยืนยันตัวตนใหม่สำหรับการเชื่อมต่อ WebSocket
- อินฟราสตรักเจอร์ : ต้องตั้งค่าอินฟราสตรักเจอร์อย่างเหมาะสม เช่น load balancer และไฟร์วอลล์ เพื่อรองรับ WebSocket
- การจัดการปฏิบัติการ : การจัดการการเชื่อมต่อและการเชื่อมต่อใหม่ของ WebSocket รวมถึง timeout และการจัดการข้อผิดพลาด
- การติดตั้งฝั่งไคลเอนต์ : การติดตั้งไลบรารี WebSocket ฝั่งไคลเอนต์ พร้อมความสามารถในการเชื่อมต่อใหม่และการจัดการสถานะ
5 ความคิดเห็น
ตอนนี้ผมใช้โครงสร้าง "short polling" ตามที่พูดถึงในนี้กับการเสิร์ฟโมเดล ML อยู่ เลยคิดหนักมากว่าแบบไหนจะมีประสิทธิภาพกว่ากัน เท่าที่ผมหาข้อมูลมาหลายที่ ดูเหมือนว่าจะมีความเห็นกันว่า short polling ปลอดภัยกว่าโดยทั่วไป เพราะมีต้นทุนสูงจากการจัดการการเชื่อมต่อใหม่ของ WebSocket หรือ SSE เลยเลือก short polling ไปก่อน.. 😭
ดูเหมือนว่าหลายคนจะเลี่ยง
Long pollingเพราะมันให้ความรู้สึกค่อนข้างแฮ็ก ๆ นะครับ ในเบราว์เซอร์ก็คงจะแสดงเป็นเหมือนว่าคำขอยังไม่เสร็จตลอดเวลาเหมือนกัน แล้วบางทีก็มีเว็บที่โหลดไม่เสร็จอยู่บ่อย ๆ ซึ่งสำหรับผมก็จะรู้สึกว่า เอ๊ะ คอนเทนต์มันยังโหลดไม่ครบหรือเปล่า? เลยไม่ค่อยชอบเท่าไหร่ในแอปพลิเคชันเองสุดท้ายก็ต้องมีบางส่วนที่ถูกแขวนไว้แล้วรอการตอบกลับอยู่ดีด้วย,, เลยดูแปลก ๆ เหมือนกันครับ
"เอเจนต์ต้องได้รับการอัปเดตสถานะการทำงานและการแชต"
เห็นประโยคนี้แล้วก็นึกถึง SSE ขึ้นมาทันทีเลย แต่ก็ตามคาด ในความเห็นบน Hacker News ก็มีการพูดถึง SSE กันเยอะเหมือนกัน
ความคิดเห็นจาก Hacker News
Long polling ก็มีปัญหาในตัวเอง
รู้สึกดีที่ได้ใช้ Phoenix และ LiveView ทุกวัน
สงสัยว่ามีข้อได้เปรียบทางเทคนิคเมื่อเทียบกับการใช้ Server-Sent Events (SSE) หรือไม่
บทความนี้เชื่อมโยง "Websocket" กับ "Long-polling" เหมือนเป็นการตัดสินใจที่แยกจากกัน
วิธีที่ง่ายกว่าสำหรับการใช้ setTimeout ใน Node.js
import { setTimeout } from "node:timers/promises"; await setTimeout(500);ชอบ long polling เพราะเข้าใจง่าย และจากมุมมองของไคลเอนต์มันทำงานเหมือนการเชื่อมต่อที่ช้ามาก
Server-Sent Events หรือ WebSockets ไม่ได้มาแทนทุกกรณีใช้งานของ long polling
ควรใช้ฟีเจอร์ asynchronous notification ของ Postgres
ไม่แน่ใจว่า long polling ที่มี timeout สั้น ๆ และคำขอที่ปิดจบอย่างสวยงามยังมีความหมายอยู่หรือไม่
เป็นการเตือนความจำที่สดใหม่ถึงทางเลือกที่ค่อนข้างเรียบง่ายกว่าของ WebSockets
ผมอยากลองใช้ WebSockets ผ่าน Elixir, Phoenix framework และ LiveView ดูครับ