14 คะแนน โดย GN⁺ 2025-01-09 | 5 ความคิดเห็น | แชร์ทาง WhatsApp
  • เป็นสถานการณ์ที่ต้องจัดการการอัปเดตแบบเรียลไทม์จำนวนมากในแบ็กเอนด์ที่พัฒนาด้วย 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 ความคิดเห็น

 
jhj0517 2025-01-10

ตอนนี้ผมใช้โครงสร้าง "short polling" ตามที่พูดถึงในนี้กับการเสิร์ฟโมเดล ML อยู่ เลยคิดหนักมากว่าแบบไหนจะมีประสิทธิภาพกว่ากัน เท่าที่ผมหาข้อมูลมาหลายที่ ดูเหมือนว่าจะมีความเห็นกันว่า short polling ปลอดภัยกว่าโดยทั่วไป เพราะมีต้นทุนสูงจากการจัดการการเชื่อมต่อใหม่ของ WebSocket หรือ SSE เลยเลือก short polling ไปก่อน.. 😭

 
bbulbum 2025-01-10

ดูเหมือนว่าหลายคนจะเลี่ยง Long polling เพราะมันให้ความรู้สึกค่อนข้างแฮ็ก ๆ นะครับ ในเบราว์เซอร์ก็คงจะแสดงเป็นเหมือนว่าคำขอยังไม่เสร็จตลอดเวลาเหมือนกัน แล้วบางทีก็มีเว็บที่โหลดไม่เสร็จอยู่บ่อย ๆ ซึ่งสำหรับผมก็จะรู้สึกว่า เอ๊ะ คอนเทนต์มันยังโหลดไม่ครบหรือเปล่า? เลยไม่ค่อยชอบเท่าไหร่
ในแอปพลิเคชันเองสุดท้ายก็ต้องมีบางส่วนที่ถูกแขวนไว้แล้วรอการตอบกลับอยู่ดีด้วย,, เลยดูแปลก ๆ เหมือนกันครับ

 
joyfui 2025-01-09

"เอเจนต์ต้องได้รับการอัปเดตสถานะการทำงานและการแชต"
เห็นประโยคนี้แล้วก็นึกถึง SSE ขึ้นมาทันทีเลย แต่ก็ตามคาด ในความเห็นบน Hacker News ก็มีการพูดถึง SSE กันเยอะเหมือนกัน

 
GN⁺ 2025-01-09
ความคิดเห็นจาก Hacker News
  • Long polling ก็มีปัญหาในตัวเอง

    • Second Life ใช้ช่องทาง HTTPS long polling ระหว่างไคลเอนต์กับเซิร์ฟเวอร์
    • ฝั่งไคลเอนต์ใช้ libcurl และอาจเกิด timeout ได้
    • หากเซิร์ฟเวอร์พยายามส่งข้อความระหว่างที่ timeout กับคำขอถัดไป อาจเกิด race condition จนข้อความสูญหาย
    • มี Apache อยู่ด้านหน้าเพื่อกรองคำขอที่ไม่จำเป็น แต่ก็อาจเกิด timeout ได้
    • middlebox และ proxy server อาจไม่ชอบ long polling
    • มีหลายองค์ประกอบที่ไม่ชอบการคง HTTP connection ไว้นาน
    • สุดท้ายแล้วมันกลายเป็นช่องทางส่งข้อความที่เชื่อถือไม่ได้ ต้องมี sequence number เพื่อตรวจจับข้อมูลซ้ำ และยังอาจทำข้อความหายได้
    • ส่วนกราฟในบทความต้นฉบับที่ระบุว่า "loop" ไม่ได้พูดถึงการจัดการ timeout
    • หากใช้ long polling ก็ต้องส่งข้อมูลทุก ๆ ไม่กี่วินาทีเพื่อคงการเชื่อมต่อไว้
  • รู้สึกดีที่ได้ใช้ Phoenix และ LiveView ทุกวัน

    • ใช้ WebSockets เลยไม่ต้องกังวลเรื่องนี้
  • สงสัยว่ามีข้อได้เปรียบทางเทคนิคเมื่อเทียบกับการใช้ Server-Sent Events (SSE) หรือไม่

    • ทั้งคู่ต่างก็เปิด HTTP connection ค้างไว้และมีข้อดีตรงที่เป็น HTTP แบบเรียบง่าย
    • SSE ดูจะเหมาะกว่าเมื่อสามารถสตรีมอัปเดตหรือผลลัพธ์ได้
    • กรณีใช้งานที่เหมาะอาจเป็นการมอนิเตอร์งานทุก ID แทนไคลเอนต์รายหนึ่ง
  • บทความนี้เชื่อมโยง "Websocket" กับ "Long-polling" เหมือนเป็นการตัดสินใจที่แยกจากกัน

    • เซิร์ฟเวอร์ long-polling สามารถรองรับไคลเอนต์ websocket ได้ด้วยงานเพิ่มอีกเล็กน้อย
    • หากสถาปัตยกรรมเดิมเป็น websocket การรองรับไคลเอนต์ long-polling จะต้องมีเซิร์ฟเวอร์สองชั้น
  • วิธีที่ง่ายกว่าสำหรับการใช้ setTimeout ใน Node.js

    • ใช้ import { setTimeout } from "node:timers/promises"; await setTimeout(500);
  • ชอบ long polling เพราะเข้าใจง่าย และจากมุมมองของไคลเอนต์มันทำงานเหมือนการเชื่อมต่อที่ช้ามาก

    • ต้องติดตามการ retry และการเชื่อมต่อที่ถูกยกเลิกจากฝั่งไคลเอนต์
    • ลูปที่ query ข้อมูลซ้ำ ๆ ในตัวอย่างโค้ดดูแปลก ๆ
  • Server-Sent Events หรือ WebSockets ไม่ได้มาแทนทุกกรณีใช้งานของ long polling

    • ข้อจำกัดด้านจำนวนการเชื่อมต่อของ SSE มักเป็นปัญหาอยู่บ่อยครั้ง
    • WebSockets ไม่ได้เชื่อถือได้ในสภาพแวดล้อมส่วนใหญ่
    • ปัญหาเรื่องการตรวจจับการเปลี่ยนแปลงจากฝั่งแบ็กเอนด์และกระจายไปยังไคลเอนต์ที่เหมาะสมก็ยังไม่ได้รับการแก้ไข
  • ควรใช้ฟีเจอร์ asynchronous notification ของ Postgres

    • เซิร์ฟเวอร์สามารถ LISTEN ที่ channel ได้ และเมื่อข้อมูลเปลี่ยน PG ก็สามารถ TRIGGER และ NOTIFY ได้
  • ไม่แน่ใจว่า long polling ที่มี timeout สั้น ๆ และคำขอที่ปิดจบอย่างสวยงามยังมีความหมายอยู่หรือไม่

    • หากไม่ได้ใช้ HTTP/2 หรือ QUIC กลเม็ดนี้ก็อาจยังมีความหมายอยู่
  • เป็นการเตือนความจำที่สดใหม่ถึงทางเลือกที่ค่อนข้างเรียบง่ายกว่าของ WebSockets

    • เคยทำงานในสตาร์ทอัปที่เลือก WebSockets และทดสอบบน Wi‑Fi ของโรงแรมกับร้านอาหารได้ยากมาก
 
luminance 2025-01-10

ผมอยากลองใช้ WebSockets ผ่าน Elixir, Phoenix framework และ LiveView ดูครับ