4 คะแนน โดย GN⁺ 2024-03-19 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ในเว็บแอปแบบเรียลไทม์ การเลือกใช้ Long Polling, WebSockets, SSE, WebRTC, WebTransport สำหรับส่งอีเวนต์ระหว่างเซิร์ฟเวอร์-ไคลเอนต์ ส่งผลอย่างมากต่อ latency, การสื่อสารสองทาง, ความยากในการติดตั้งใช้งาน และข้อจำกัดด้านการปฏิบัติการ
  • WebSockets ให้การสื่อสารสองทางผ่านการเชื่อมต่อระยะยาวเพียงเส้นเดียว แต่ในการใช้งานจริงมักใช้ร่วมกับไลบรารีอย่าง Socket.IO เพราะต้องตรวจจับการเชื่อมต่อหลุด, reconnect และทำ ping-pong heartbeat
  • Server-Sent Events เป็นสตรีมทางเดียวจากเซิร์ฟเวอร์→ไคลเอนต์บน HTTP จึงติดตั้งใช้งานและจัดการ reconnect ได้ง่าย แต่ EventSource API พื้นฐานมีข้อจำกัดในการส่ง POST body หรือ custom header
  • WebTransport รองรับหลายสตรีมและการส่งแบบเชื่อถือได้/ไม่เชื่อถือได้บน HTTP/3 QUIC แต่ ณ มีนาคม 2024 ยังอยู่ในสถานะ Working Draft และยังไม่มี native support ใน Safari และ Node.js จึงยังยากจะมองว่าเป็นตัวเลือกทั่วไป
  • ในแอปจริงยังต้องมี ลอจิกกู้คืนการซิงก์และการทดสอบอินฟรา ควบคู่กัน เพราะมีปัญหาอย่างการถูกปิดเมื่ออยู่เบื้องหลังบนมือถือ, ขีดจำกัดจำนวนการเชื่อมต่อต่อโดเมน, พร็อกซี/ไฟร์วอลล์องค์กร และอีเวนต์ตกหล่นระหว่าง reconnect

พัฒนาการของเทคโนโลยีสื่อสารเซิร์ฟเวอร์-ไคลเอนต์แบบเรียลไทม์

  • ในเว็บแอปพลิเคชันแบบเรียลไทม์ ความสามารถที่เซิร์ฟเวอร์ส่งอีเวนต์ไปยังไคลเอนต์กลายเป็นข้อกำหนดหลัก
  • ช่วงแรก Long Polling ที่ทำงานบน HTTP ถูกใช้เป็นวิธีส่งข้อความระหว่างเซิร์ฟเวอร์-ไคลเอนต์ที่ทำได้บนเบราว์เซอร์
  • ต่อมา WebSockets ปรากฏขึ้นเป็นวิธีที่แข็งแรงกว่า สำหรับการสื่อสารสองทาง
  • Server-Sent Events(SSE) ให้การสื่อสารทางเดียวจากเซิร์ฟเวอร์ไปยังไคลเอนต์ในรูปแบบที่เรียบง่ายกว่า
  • WebTransport มีศักยภาพที่จะเป็นวิธีที่มีประสิทธิภาพ ยืดหยุ่น และขยายได้มากกว่า แต่ปัจจุบันยังมีขอบเขตการรองรับที่จำกัด
  • WebRTC อาจพิจารณาได้สำหรับกรณีเฉพาะบางอย่างในการส่งอีเวนต์เซิร์ฟเวอร์-ไคลเอนต์ แต่จุดประสงค์ต่างออกไปจนไม่เหมาะจะถือเป็นตัวเลือกหลัก

Long Polling

  • Long Polling คือวิธีเลียนแบบ server push communication ด้วยคำขอ XHR ทั่วไป
  • เมื่อไคลเอนต์เปิดคำขอไปยังเซิร์ฟเวอร์ เซิร์ฟเวอร์จะพักการตอบกลับไว้จนกว่าจะมีข้อมูลใหม่
  • หลังจากส่งข้อมูลใหม่แล้ว การเชื่อมต่อจะปิด และไคลเอนต์จะเริ่มคำขอถัดไปทันที
  • เมื่อเทียบกับ polling แบบเป็นช่วงตามปกติ จะอัปเดตได้เร็วกว่า และลดทราฟฟิกเครือข่ายกับภาระบนเซิร์ฟเวอร์ที่ไม่จำเป็นได้
  • อย่างไรก็ตาม ประสิทธิภาพต่ำกว่าเทคโนโลยีเรียลไทม์อย่าง WebSockets และอาจเกิด latency ได้ขึ้นกับจังหวะการส่งข้อมูล
  • การติดตั้งฝั่งไคลเอนต์ทำได้ง่าย แต่ฝั่งแบ็กเอนด์รับประกันได้ยากว่าไคลเอนต์ที่กำลัง reconnect จะไม่พลาดอีเวนต์

WebSockets

  • WebSockets สร้างการเชื่อมต่อระยะยาวเพียงเส้นเดียวระหว่างไคลเอนต์กับเซิร์ฟเวอร์ และให้ การสื่อสารแบบ full-duplex
  • หลังจากเชื่อมต่อแล้ว ทั้งสองฝั่งสามารถส่งข้อมูลได้อย่างอิสระโดยไม่มี overhead ของวงจร HTTP request-response
  • เหมาะกับแอปที่ต้องการ latency ต่ำและอัปเดตบ่อย เช่น แชตเรียลไทม์ เกม และแพลตฟอร์มซื้อขายทางการเงิน
  • WebSocket API พื้นฐานใช้งานง่าย แต่ใน production การจัดการการเชื่อมต่อหลุดและการสร้างใหม่จะซับซ้อนขึ้น
  • เนื่องจากตรวจจับได้ยากว่าการเชื่อมต่อยังใช้งานได้อยู่หรือไม่ จึงมักเพิ่ม ping-and-pong heartbeat
  • ด้วยความซับซ้อนนี้ หลายกรณีจึงใช้ไลบรารีอย่าง Socket.IO และ Socket.IO ยังมี Long Polling fallback ให้เมื่อจำเป็น

Server-Sent Events

  • Server-Sent Events(SSE) เป็นวิธีมาตรฐานสำหรับ push อัปเดตจากเซิร์ฟเวอร์ไปยังไคลเอนต์บน HTTP
  • ต่างจาก WebSockets ตรงที่ออกแบบมาเพื่อการสื่อสารทางเดียวจากเซิร์ฟเวอร์→ไคลเอนต์เท่านั้น
  • เหมาะกับสถานการณ์ที่ไคลเอนต์ไม่จำเป็นต้องส่งข้อความไปยังเซิร์ฟเวอร์ เช่น ฟีดข่าวสด คะแนนกีฬา และอัปเดตแบบเรียลไทม์
  • SSE มองได้ว่าเป็นวิธีที่แบ็กเอนด์คงการเชื่อมต่อไว้ในคำขอ HTTP หนึ่งรายการ แล้วค่อย ๆ ปล่อยการตอบกลับทีละบรรทัดเมื่อมีอีเวนต์เกิดขึ้น
  • ฝั่งไคลเอนต์บนเบราว์เซอร์จะเริ่มอินสแตนซ์ EventSource เพื่อรับ event stream
  • EventSource ต่างจาก WebSockets ตรงที่จะ reconnect อัตโนมัติเมื่อการเชื่อมต่อขาด
  • เซิร์ฟเวอร์ต้องตั้งค่า header Content-Type เป็น text/event-stream และฟอร์แมตฟิลด์อย่าง event type, data payload, event ID และ retry timing ตาม SSE specification

WebTransport

  • WebTransport เป็น API สำหรับการสื่อสารที่มีประสิทธิภาพและ latency ต่ำระหว่างเว็บไคลเอนต์กับเซิร์ฟเวอร์
  • ใช้ HTTP/3 QUIC protocol เพื่อส่งข้อมูลบนหลายสตรีมได้
  • รองรับทั้งการส่งแบบเชื่อถือได้และไม่เชื่อถือได้ รวมถึงการส่งข้อมูลแบบไม่เรียงลำดับ
  • อาจเป็นเครื่องมือทรงพลังสำหรับแอปที่ต้องการเครือข่ายประสิทธิภาพสูง เช่น เกมเรียลไทม์ ไลฟ์สตรีมมิง และแพลตฟอร์มทำงานร่วมกัน
  • ณ มีนาคม 2024 WebTransport อยู่ในสถานะ Working Draft และยังไม่ได้รับการรองรับอย่างแพร่หลาย
  • ยังใช้ไม่ได้ใน Safari browser และยังไม่มี native support ใน Node.js
  • แม้การรองรับจะกว้างขึ้น API ก็ซับซ้อนมาก จึงมีแนวโน้มว่าจะถูกใช้ในรูปแบบการสร้างไลบรารีบน WebTransport มากกว่าการเขียนตรงในโค้ดแอปพลิเคชัน

WebRTC

  • WebRTC เป็นทั้งโปรเจกต์โอเพนซอร์สและมาตรฐาน API ที่ให้ความสามารถด้านการสื่อสารเรียลไทม์ภายในเบราว์เซอร์และแอปมือถือโดยไม่ต้องใช้ปลั๊กอิน
  • รองรับการเชื่อมต่อแบบ peer-to-peer สำหรับแลกเปลี่ยนเสียง วิดีโอ และข้อมูลระหว่างเบราว์เซอร์
  • ใช้โปรโตคอลอย่าง ICE, STUN, TURN เพื่อผ่าน NAT และไฟร์วอลล์
  • WebRTC ถูกสร้างมาเพื่อการโต้ตอบระหว่างไคลเอนต์กับไคลเอนต์ แต่ก็สามารถนำไปใช้กับการสื่อสารเซิร์ฟเวอร์-ไคลเอนต์ได้โดยทำให้เซิร์ฟเวอร์ทำตัวเหมือนไคลเอนต์
  • วิธีนี้เหมาะกับ use case เฉพาะกลุ่มเท่านั้น จึงถูกตัดออกจากการเปรียบเทียบตัวเลือกหลัก
  • เพื่อให้ WebRTC ทำงานได้ อย่างไรก็ต้องมี signaling server และเซิร์ฟเวอร์นี้จะทำงานบนหนึ่งใน WebSockets, SSE หรือ WebTransport
  • ด้วยเหตุนี้ จุดประสงค์ในการใช้ WebRTC เป็นตัวทดแทนโดยตรงของเทคโนโลยีเหล่านี้จึงอ่อนลง

ข้อจำกัดหลักของแต่ละเทคโนโลยี

  • การส่งข้อมูลสองทาง

    • วิธีที่รับข้อมูลจากเซิร์ฟเวอร์และส่งข้อมูลจากไคลเอนต์บนการเชื่อมต่อเดียวกัน รองรับเฉพาะ WebSockets และ WebTransport
    • Long Polling ทำได้ในทางทฤษฎี แต่ไม่แนะนำ เพราะหากจะส่งข้อมูลใหม่ผ่านการเชื่อมต่อ long-polling ที่มีอยู่ ต้องใช้คำขอ HTTP เพิ่มเติม
    • ใน Long Polling ควรส่งข้อมูลไคลเอนต์→เซิร์ฟเวอร์ด้วยคำขอ HTTP แยกต่างหากโดยไม่รบกวนการเชื่อมต่อเดิม
    • SSE ไม่รองรับความสามารถในการส่งข้อมูลเพิ่มเติมไปยังเซิร์ฟเวอร์
    • EventSource API แบบ native พื้นฐาน ไม่สามารถส่งข้อมูลอย่าง POST ใน HTTP body ได้แม้แต่ในคำขอเริ่มต้น
    • ต้องใส่ข้อมูลไว้ใน URL parameter ซึ่งไม่ดีด้านความปลอดภัย เพราะ credentials อาจรั่วไปยัง server logs, proxy และ cache
    • RxDB ใช้ eventsource polyfill แทน EventSource API แบบ native เพื่อหลีกเลี่ยงปัญหานี้ โดยไลบรารีนี้เพิ่มความสามารถอย่าง custom HTTP header
    • fetch-event-source ของ Microsoft อนุญาตให้ส่ง body data และใช้คำขอ POST แทน GET
  • ขีดจำกัดจำนวนการเชื่อมต่อต่อโดเมน

    • เบราว์เซอร์สมัยใหม่ส่วนใหญ่อนุญาตให้มี 6 การเชื่อมต่อ ต่อโดเมน และขีดจำกัดนี้จำกัดความสามารถในการใช้งานของวิธีส่งข้อความเซิร์ฟเวอร์→ไคลเอนต์ที่เสถียรโดยรวม
    • ขีดจำกัด 6 การเชื่อมต่อนี้ใช้ร่วมกันระหว่างแท็บของเบราว์เซอร์ด้วย ดังนั้นหากเปิดหน้าเดียวกันหลายแท็บ แท็บเหล่านั้นต้องแบ่งใช้ connection pool เดียวกัน
    • HTTP/1.1 RFC แนะนำจำนวนที่ต่ำกว่านั้นคือ 2 การเชื่อมต่อ ต่อเซิร์ฟเวอร์หรือพร็อกซี
    • นโยบายนี้สมเหตุสมผลสำหรับป้องกัน DDoS ที่อาศัยผู้เยี่ยมชม แต่จะเป็นปัญหาเมื่อการสื่อสารเซิร์ฟเวอร์-ไคลเอนต์ที่ถูกต้องตามปกติต้องใช้หลายการเชื่อมต่อ
    • วิธีหลีกเลี่ยงคือต้องใช้ HTTP/2 หรือ HTTP/3 เพื่อให้เบราว์เซอร์เปิดเพียงการเชื่อมต่อเดียวต่อโดเมน และจัดการข้อมูลด้วย multiplexing
    • แม้ใน HTTP/2·HTTP/3 การตั้งค่า SETTINGS_MAX_CONCURRENT_STREAMS ก็ยังจำกัดจำนวนสตรีมพร้อมกันจริง โดยค่าเริ่มต้นของคอนฟิกส่วนใหญ่คือ 100 concurrent streams
    • เบราว์เซอร์อาจเพิ่มขีดจำกัดการเชื่อมต่อให้ API เฉพาะอย่าง EventSource ได้ แต่ issue ที่เกี่ยวกับ Chromium และ Firefox ถูกระบุว่า “won’t fix”
  • ลดจำนวนการเชื่อมต่อของแอปเบราว์เซอร์

    • ในแอปเบราว์เซอร์ต้องสมมติว่าผู้ใช้อาจเปิดแอปหลายแท็บพร้อมกัน
    • ตามค่าเริ่มต้น แต่ละแท็บสามารถเปิดการเชื่อมต่อ server stream หนึ่งเส้นได้ แต่ส่วนใหญ่ไม่จำเป็น
    • สามารถใช้วิธีเปิดการเชื่อมต่อเพียงเส้นเดียวแม้มีหลายแท็บ แล้วแชร์ระหว่างแท็บได้
    • RxDB ใช้ LeaderElection ของ broadcast-channel npm package เพื่อรักษา replication stream ระหว่างเซิร์ฟเวอร์กับไคลเอนต์ไว้เพียงเส้นเดียว
    • แพ็กเกจนี้สามารถใช้เดี่ยว ๆ ในแอปพลิเคชันอื่นได้แม้ไม่มี RxDB

ข้อจำกัดด้านปฏิบัติการจากมือถือ พร็อกซี และไฟร์วอลล์

  • ในระบบปฏิบัติการมือถืออย่าง Android และ iOS การรักษาการเชื่อมต่อที่เปิดอยู่ รวมถึง WebSockets ให้คงอยู่ตลอดเป็นเรื่องยาก
  • Mobile OS อาจส่งแอปไปทำงานเบื้องหลังและปิดการเชื่อมต่อที่เปิดอยู่หลังไม่มี activity เป็นระยะเวลาหนึ่ง
  • พฤติกรรมนี้เป็นส่วนหนึ่งของกลยุทธ์จัดการทรัพยากรเพื่อประหยัดแบตเตอรี่และปรับประสิทธิภาพให้เหมาะสม
  • นักพัฒนามักใช้ mobile push notification แทนการเชื่อมต่อถาวรเมื่อเซิร์ฟเวอร์ต้องส่งข้อมูลไปยังไคลเอนต์
  • Push notification ช่วยให้เซิร์ฟเวอร์แจ้งแอปว่ามีข้อมูลใหม่ และกระตุ้นการทำงานหรือการอัปเดตของแอปได้โดยไม่ต้องมีการเชื่อมต่อที่เปิดค้างไว้ตลอด
  • ในสภาพแวดล้อมองค์กร พร็อกซีและไฟร์วอลล์อาจบล็อกการเชื่อมต่อที่ไม่ใช่ HTTP ทำให้ใส่ WebSocket server เข้าไปในอินฟราได้ยาก
  • ในสภาพแวดล้อมเหล่านี้ SSE ที่อิง HTTP อาจเป็นวิธีที่รวมเข้ากับระบบองค์กรได้ง่ายกว่า
  • Long Polling ก็เป็นตัวเลือกได้เช่นกัน เพราะใช้เพียงคำขอ HTTP ทั่วไป

การเปรียบเทียบประสิทธิภาพ

  • เมื่อเปรียบเทียบ WebSockets, SSE, Long Polling และ WebTransport ต้องพิจารณา latency, throughput, ภาระเซิร์ฟเวอร์ และ scalability ร่วมกัน
  • realtime-web repo ที่ทดสอบเวลาข้อความใน implementation ของ Go server แสดงผลว่า WebSockets, WebRTC และ WebTransport มีประสิทธิภาพใกล้เคียงกัน
  • WebTransport เป็นเทคโนโลยีใหม่บน HTTP/3 จึงอาจมีการปรับปรุง performance optimization มากขึ้นหลังมีนาคม 2024
  • WebTransport ถูกปรับแต่งเพื่อลดการใช้พลังงาน แต่ยังไม่ได้ทดสอบเมตริกดังกล่าว
  • Latency

    • WebSockets ให้ latency ต่ำที่สุดจากการสื่อสารแบบ full-duplex บนการเชื่อมต่อถาวรเส้นเดียว
    • SSE ให้ latency ต่ำเช่นกันสำหรับการสื่อสารเซิร์ฟเวอร์→ไคลเอนต์ แต่หากไคลเอนต์ต้องส่งข้อความไปยังเซิร์ฟเวอร์ จะต้องมีคำขอ HTTP เพิ่มเติม
    • Long Polling มี latency สูงกว่า เพราะสร้างการเชื่อมต่อ HTTP ใหม่ทุกครั้งที่ส่งข้อมูล
    • ใน Long Polling หากไคลเอนต์กำลังเปิดการเชื่อมต่อใหม่ในจังหวะที่เซิร์ฟเวอร์ต้องการส่งอีเวนต์ latency อาจเพิ่มขึ้นมาก
    • WebTransport คาดว่าจะให้ latency ต่ำใกล้เคียงกับ WebSockets และใช้ประโยชน์จาก multiplexing กับ congestion control ที่มีประสิทธิภาพกว่าของ HTTP/3
  • Throughput

    • WebSockets ให้ throughput สูงได้ด้วยการเชื่อมต่อถาวร แต่ปัญหา backpressure ซึ่งไคลเอนต์ประมวลผลไม่เร็วเท่าความเร็วที่เซิร์ฟเวอร์ส่ง อาจกระทบ throughput
    • SSE มี overhead น้อยกว่า WebSockets จึงอาจให้ throughput สูงกว่าใน broadcast ทางเดียวจากเซิร์ฟเวอร์→ไคลเอนต์
    • Long Polling โดยทั่วไปมี throughput ต่ำและใช้ทรัพยากรเซิร์ฟเวอร์มากกว่า เพราะมี overhead จากการเปิดปิดการเชื่อมต่อบ่อย
    • WebTransport คาดว่าจะรองรับ throughput สูงได้ทั้งสตรีมทางเดียวและสองทางภายในการเชื่อมต่อเดียว และอาจเหนือกว่า WebSockets ในสถานการณ์ที่ต้องใช้หลายสตรีม
  • Scalability และภาระเซิร์ฟเวอร์

    • WebSockets อาจเพิ่มภาระเซิร์ฟเวอร์อย่างมากเมื่อรักษาการเชื่อมต่อจำนวนมาก ซึ่งกระทบ scalability ของแอปที่มีผู้ใช้มาก
    • SSE ขยายตัวได้ดีกว่าในสถานการณ์ที่ต้องการอัปเดตจากเซิร์ฟเวอร์→ไคลเอนต์เป็นหลัก
    • SSE ใช้คำขอ HTTP ปกติโดยไม่มีขั้นตอน WebSocket อย่าง protocol upgrade จึงมี overhead ของการเชื่อมต่อต่ำกว่า
    • Long Polling มีภาระเซิร์ฟเวอร์สูงจากการสร้างการเชื่อมต่อบ่อย จึงมี scalability ต่ำที่สุด และเหมาะเป็น fallback mechanism เท่านั้น
    • WebTransport ออกแบบมาโดยมุ่งให้ scalability สูงบนประสิทธิภาพของการจัดการ connection และ stream ของ HTTP/3 และมีโอกาสลดภาระเซิร์ฟเวอร์ได้มากกว่า WebSockets และ SSE

คำแนะนำตาม use case

  • SSE เป็นตัวเลือกที่ติดตั้งใช้งานตรงไปตรงมาที่สุด ใช้โปรโตคอล HTTP/S เดิม จึงหลีกเลี่ยงข้อจำกัดของไฟร์วอลล์องค์กรและปัญหาทางเทคนิคที่อาจเกิดกับโปรโตคอลอื่นได้ง่าย
  • ผสานเข้ากับ Node.js และ server framework อื่นได้ง่าย
  • เหมาะกับแอปที่ต้องการอัปเดตจากเซิร์ฟเวอร์→ไคลเอนต์บ่อย เช่น news feed, stock ticker และ live event streaming
  • WebSockets เด่นในสถานการณ์ที่ต้องการการสื่อสารสองทางต่อเนื่อง
  • เป็นตัวเลือกหลักเมื่อจำเป็นต้องมี interaction ต่อเนื่อง เช่น browser game, chat application และ live sports update
  • WebTransport มีศักยภาพ แต่การรองรับใน server framework ยังไม่กว้าง และยังขาดความเข้ากันได้กับ Node.js และ Safari
  • WebTransport พึ่งพา HTTP/3 ซึ่งการรองรับ HTTP/3 ของเว็บเซิร์ฟเวอร์จำนวนมาก เช่น nginx ยังอยู่ในสถานะ experimental
  • เป็นเทคโนโลยีแห่งอนาคตที่รองรับทั้งการส่งข้อมูลแบบเชื่อถือได้และไม่เชื่อถือได้ แต่สำหรับ use case ส่วนใหญ่ในปัจจุบันยังไม่ใช่ตัวเลือกที่นำไปใช้ได้จริง
  • Long Polling โดยรวมเป็นวิธีที่ล้าสมัย เพราะไม่มีประสิทธิภาพและมี overhead สูงจากการสร้างการเชื่อมต่อ HTTP ใหม่ซ้ำ ๆ
  • อาจใช้เป็น fallback ในสภาพแวดล้อมที่ไม่รองรับ WebSockets หรือ SSE ได้ แต่ไม่แนะนำให้ใช้ทั่วไปเพราะข้อจำกัดด้าน performance

ปัญหาอีเวนต์ตกหล่นระหว่าง reconnect

  • เมื่อสร้างฟีเจอร์บนเทคโนโลยี streaming แบบเรียลไทม์ใด ๆ ต้องคำนึงถึงสถานการณ์การเชื่อมต่อขาดและ reconnect
  • หากไคลเอนต์กำลังเชื่อมต่อ กำลัง reconnect หรือ offline อาจไม่ได้รับอีเวนต์ที่เกิดบนเซิร์ฟเวอร์ผ่าน stream
  • หากเซิร์ฟเวอร์สตรีมเนื้อหาทั้งหมดทุกครั้ง เช่น ราคาหุ้น อีเวนต์ที่ตกหล่นอาจไม่สำคัญ
  • หากแบ็กเอนด์สตรีมเฉพาะผลลัพธ์บางส่วน ต้องจัดการอีเวนต์ที่ตกหล่นอย่างหลีกเลี่ยงไม่ได้
  • วิธีที่แบ็กเอนด์จำว่าอีเวนต์ใดส่งสำเร็จไปยังไคลเอนต์แต่ละรายแล้วนั้นไม่ค่อย scalable
  • ควรจัดการปัญหานี้ด้วยลอจิกฝั่งไคลเอนต์มากกว่า
  • RxDB Sync Engine ใช้โหมดการทำงานสองแบบ
    • checkpoint iteration mode: ดึงข้อมูลแบ็กเอนด์ซ้ำด้วยคำขอ HTTP ปกติจนกว่าไคลเอนต์จะตามทันและซิงก์ได้อีกครั้ง
    • event observation mode: รักษาไคลเอนต์ให้อยู่ในสถานะซิงก์ด้วยอัปเดตจาก realtime stream
  • เมื่อการเชื่อมต่อของไคลเอนต์หลุดหรือเกิดข้อผิดพลาด replication จะสลับไปใช้ checkpoint iteration mode ชั่วคราว เพื่อซิงก์จนกลับมาอยู่สถานะเดียวกับเซิร์ฟเวอร์อีกครั้ง
  • วิธีนี้ช่วยชดเชยอีเวนต์ที่ตกหล่น และทำให้ไคลเอนต์ซิงก์ให้ตรงกับเซิร์ฟเวอร์ได้เสมอ

สิ่งที่ต้องตรวจสอบในอินฟราองค์กร

  • ในอินฟราองค์กร เทคโนโลยี streaming โดยรวมอาจเกิดปัญหาได้
  • พร็อกซีและไฟร์วอลล์อาจบล็อกทราฟฟิก หรือทำให้ request/response เสียหายโดยไม่ตั้งใจ
  • เมื่อทำแอปเรียลไทม์ในสภาพแวดล้อมเหล่านี้ ต้องทดสอบก่อนว่าเทคโนโลยีที่เลือกทำงานได้จริงบนอินฟรานั้นหรือไม่

1 ความคิดเห็น

 
GN⁺ 2024-03-19
ความคิดเห็นจาก Hacker News
  • โดยส่วนตัวชอบ Server-Sent Events มาตลอด เพราะเรียบง่ายและเขียน/ติดตั้งใช้งานได้ง่าย

    • ถ้ามี IPv6 ตอนนี้ก็สเกลแบบเต็มรูปแบบได้ง่าย และถ้าออกแบบดี ๆ ก็สามารถทำงานแทบจะไร้สถานะได้โดยแค่ส่งรายการบริการ SSE ให้ไคลเอนต์ จึงสเกลได้ง่ายกว่ามาก
      ส่วน WebSocket พอการใช้งานเกินระดับหนึ่งไปแล้ว การสเกลจะซับซ้อนขึ้นมาก
    • เพราะความเรียบง่ายนี่เอง จึงสเกลผ่าน CDN ได้ง่ายกว่า WebSocket มาก: https://www.fastly.com/blog/server-sent-events-fastly
    • เห็นด้วย แต่ในเบราว์เซอร์มีข้อจำกัดว่า SSE stream ต่อ origin ต่อ browser instance มีได้ 6 รายการ ดังนั้นถ้าไม่เพิ่มความซับซ้อนฝั่งไคลเอนต์ ก็อาจเปิดได้สูงสุดแค่ 6 แท็บ
      https://crbug.com/275955
    • ข้อเสียคือเพย์โหลดต้องเข้ารหัสเป็น base64 หรือไม่ก็ลบตัวขึ้นบรรทัดใหม่ออก
      เลยสงสัยว่าทำไมถึงไม่ทำเป็น multipart streaming response ไปเลย ทั้งรองรับเมทาดาทา และยังเป็นรูปแบบที่ถูกใช้งานกันทั่วไปมากด้วย
    • ใช้งานได้แม้กับ Apache prefork ธรรมดาและ PHP
  • ยังมีข้อเสียเพิ่มเติมที่ควรรู้อีก
    WebSocket ไม่มี flow control (backpressure) และไม่มี multiplexing ดังนั้นถ้าต้องใช้ก็ต้องทำเองหรือใช้พวก RSocket ส่วน SSE ก็ส่งข้อมูลไบนารีโดยตรงไม่ได้ จึงต้องใช้อินโค้ดดิ้งอย่าง base64
    WebTransport จัดการปัญหาเหล่านี้ทั้งหมดและยังแก้ HOL blocking ได้ด้วย แต่ก็กังวลว่าจะเจอปัญหาแบบเดียวกับตอนเปลี่ยน Python 2→3 หรือย้ายไป IPv6 คือคนยังคงใช้เวอร์ชันเดิมต่อไปได้ง่าย และรู้สึกว่าประโยชน์จากการอัปเกรดมีไม่มากพอ
    ตราบใดที่เบราว์เซอร์ยังทำงานบน TCP ต่อไป บางเครือข่ายก็อาจบล็อก UDP และจึงบล็อก HTTP/3/WebTransport ไปเลย

    • HOL blocking เป็นปัญหาจริง แต่ TCP มี flow control ให้อยู่แล้ว ถ้าไม่ใช้มันก็คือขยับขึ้นไปใช้บน HTTP/3
      ความกังวลว่า WebTransport อาจเปลี่ยนผ่านช้า ก็เป็นแบบเดียวกับที่เคยพูดกันได้กับ TLS transport, HTTP/3 และ XHR มาก่อน โครงสร้างตอนนี้ที่มี browser engine หลักไม่กี่ตัวครองตลาด ทำให้การปล่อยฟีเจอร์เบราว์เซอร์และโปรโตคอลใหม่ ๆ ค่อนข้างง่าย
      ถ้าจะบอกว่าเพราะ TCP ใช้งานได้ บางเครือข่ายจึงบล็อก UDP ได้ ก็คล้ายกับการบอกว่าเพราะ HTTP 1.1 แบบไม่ใช้ TLS ใช้งานได้ ดังนั้น HTTP/2 และ TLS ก็จะถูกบล็อกต่อไปเหมือนกัน ซึ่งไม่ถึงกับผิดทั้งหมด แต่เมื่อดูจากการยอมรับใช้อย่างกว้างขวางของ HTTP/2 และโดยเฉพาะ TLS แล้ว มันก็ดูไม่ใช่ปัญหาใหญ่เท่าที่คิด
    • ได้ยินเรื่องเครือข่ายที่บล็อก UDP อยู่เรื่อย ๆ แต่ไม่เคยเห็นจริงเลย หลายอย่างก็ทำงานบน UDP
      ในออฟฟิศเล็ก ๆ หรือสภาพแวดล้อมองค์กรดิสโทเปียแบบในหนังอาจปิดมันได้ แต่ก็ไม่เข้าใจว่าทำไมความจริงที่ว่าบางเครือข่ายอาจห้าม UDP ถึงเกี่ยวข้องมากนัก บางเครือข่ายยังบล็อก google.com หรือ wikipedia.com ด้วยซ้ำ แต่ก็ไม่ได้แปลว่าบริการเหล่านั้นล้มเหลว
    • WebSocketStream กำลังจะเข้ามาใน Chrome และเพิ่ม backpressure: https://chromestatus.com/feature/5189728691290112
    • ถ้าต้องการทางเลือกที่ดีกว่า base64 ก็มี binary-sse: https://github.com/luciopaiva/binary-sse
    • ถ้าเป็นสถานการณ์แบบนั้น HTTP/2 ก็ยังใช้งานได้อยู่ จึงกังวลเรื่อง multiplexing น้อยลง
  • คำอธิบายเรื่อง WebRTC ในบทความนี้ไม่ถูกต้องนัก WebRTC แบบไคลเอนต์/เซิร์ฟเวอร์ทำได้โดยไม่ต้องมี “signaling server” แยกต่างหาก เพราะให้เซิร์ฟเวอร์ทำ signaling เองได้
    แค่ต้องมีรอบรับส่งเพิ่มอีกไม่กี่ครั้งเท่านั้น ไม่จำเป็นต้องมีเซิร์ฟเวอร์แยกต่างหาก WebRTC data channel ใช้แทน WebSocket หรือ SSE ได้ค่อนข้างดี โดยเฉพาะเมื่ออยากหลีกเลี่ยง HOL blocking และก็มีไลบรารีมากมายอย่าง Pion หรือ str0m ที่จัดการงานแทบทั้งหมดให้
    ส่วนที่บอกว่า WebTransport API ซับซ้อนก็ดูพูดเกินไป ถ้าไม่ต้องใช้ความสามารถขั้นสูงก็เมินมันไปได้ และถ้าอยากใช้แบบ WebSocket ก็แค่เปิด bidirectional stream เดียวก็แทบจะพอแล้ว ถ้าอยากหลีกเลี่ยง HOL blocking ก็เปิด stream ใหม่ต่อข้อความได้เลย ซับซ้อนขึ้นนิดหน่อยแต่ยังไม่ถึงขั้นต้องพึ่งไลบรารี และมีโอกาสสูงที่ GitHub Copilot จะช่วยเขียนโค้ดให้ได้ด้วย เพียงแต่ตอนนี้ WebTransport ยังอยู่ระหว่างพัฒนา ทำให้ไลบรารีฝั่งเซิร์ฟเวอร์ยังมีไม่มาก และก็ยังรอการรองรับจาก Safari อยู่

    • ยังสงสัยกับคำบอกที่ว่า WebRTC แบบไคลเอนต์/เซิร์ฟเวอร์ทำได้โดยไม่ต้องมี “signaling server”
      ปกติ signaling server มักทำด้วย WebSocket ถ้าไม่ได้เสนอแนวทางบูตสแตรปแบบกระจายศูนย์ด้วยไคลเอนต์ที่มีอยู่แล้ว ก็ไม่สามารถทำด้วย WebRTC เองได้
  • ถ้ากำลังสร้างระบบให้ลูกค้าที่มีโครงสร้างพื้นฐาน IT แบบ “enterprise” และ “security” ดั้งเดิม การเพิ่ม ปุ่มรีเฟรช แล้วจบอาจเป็นทางเลือกที่ดีกว่า
    จากประสบการณ์ของผม สิ่งที่ล้มเหลวซ้ำ ๆ ในสภาพแวดล้อมแบบนี้ และยังแก้ไม่ได้เพราะติดขั้นตอนมากมายไม่รู้จบ ก็คือความพยายามจะสร้างฟีเจอร์เรียลไทม์ให้ลูกค้ากลุ่มนี้

    • Jetty/CometD จะ fallback ไปใช้ long polling ถ้าวิธีส่งข้อมูลแบบอื่นใช้ไม่ได้
    • พูดตรง ๆ เทคโนโลยีกลุ่มนี้ทุกตัวต่างก็มีปัญหาในแบบของตัวเอง และปุ่มรีเฟรชก็ไม่ได้เป็นข้อยกเว้น
    • ในเบราว์เซอร์มีปุ่มรีเฟรชอยู่แล้ว แต่มีโอกาสสูงว่าถ้ากดแล้วแอปพลิเคชันจะพัง
  • เมื่อขยายระบบใหญ่ขึ้น WebSocket และ SSE กลายเป็นปัญหาใหญ่ในการดูแลจัดการ โดยเฉพาะฝั่งแบ็กเอนด์ที่ต้องมี observability แยกต่างหาก และถ้าฝั่งฟรอนต์เอนด์บนอุปกรณ์พกพาไม่ได้ทำอย่างระมัดระวังมาก การดีบักจะกลายเป็นฝันร้าย
    ตัวอุปกรณ์เองอาจปิดเครือข่ายหรือทำให้ช้าลงเพื่อประหยัดแบตเตอรี่ โดยเฉพาะถ้าไม่ได้ทำ I/O อย่างชัดเจนผ่าน API เฉพาะ
    การสร้างการเชื่อมต่อใหม่มีต้นทุนสูง และเซิร์ฟเวอร์ก็ต้องเก็บสถานะไว้สักที่หนึ่ง หากเลเยอร์เก็บสถานะนี้มีปัญหา ไคลเอนต์ก็จะพยายามเชื่อมต่อซ้ำไม่หยุด ติด timeout และค้างอยู่กับงานที่มีต้นทุนสูงตลอดไป อีกทั้งมันไม่ใช่วิธีที่จะควบคุม throughput ได้ง่ายพร้อมกับค่อย ๆ เพิ่มโหลดให้ฐานข้อมูล
    ในแง่ความน่าเชื่อถือ จากประสบการณ์ของผม long polling ดีที่สุด แม้กระแสแบบ event-driven จะสำคัญมาก ฝั่งฟรอนต์เอนด์ก็ควรทำ long polling ไปยังแบ็กเอนด์ชั้นที่ 1 แล้วให้ชั้นที่ 1 นั้นไป subscribe กับแบ็กเอนด์ชั้นที่ 2 ผ่าน WebSocket จะดีกว่า และควบคุมความน่าเชื่อถือได้ดีกว่ามาก

    • เห็นด้วยเต็มที่ เคยเห็นหลายกรณีที่คนทำพลาดกับ WebSocket และ SSE เอง Long polling แม้มีต้นทุน แต่เป็นแนวทางที่อธิบายได้ง่ายและสเกลได้มากที่สุด
    • SSE รองรับ long polling ได้ เซิร์ฟเวอร์สามารถเลือกปิดการเชื่อมต่อเมื่อไรก็ได้
      SSE รองรับการเชื่อมต่อใหม่อัตโนมัติ และยังส่ง last seen ID ไปด้วย ทำให้เซิร์ฟเวอร์ต่อเนื่องจากจุดเดิมได้โดยไม่สะดุด
    • หลายประเด็นนี้ถูกพูดถึงไว้พอสมควรในบทความที่ลิงก์ไว้ และใน rxdb ก็มีสิ่งที่ช่วยลดความกังวลหลายอย่าง
  • แม้บทความจะไม่ได้พูดถึง แต่ short polling ก็เกี่ยวข้องเช่นกัน แม้จะไม่ใช่วิธีส่งข้อความจากเซิร์ฟเวอร์ไปยังไคลเอนต์ แต่ก็ยังมีประโยชน์เมื่อไม่มีทางเลือกอื่น เช่น shared hosting
    จากประสบการณ์ของผม ต่อให้ช่วง polling จะยาว เช่น 20 วินาที ก็ยังทำงานได้ดีทีเดียว หากใส่รายการข้อความกลับมาพร้อมแต่ละ response เมื่อผู้ใช้กดปุ่ม ไคลเอนต์จะส่ง request ไปยังเซิร์ฟเวอร์ และเซิร์ฟเวอร์ก็ตอบกลับทั้งข้อมูลและรายการข้อความล่าสุด ทำให้ไคลเอนต์อัปเดตเป็นสถานะล่าสุด

    • ใช้ได้แม้กับข้อมูลที่เปลี่ยนเร็ว โดยเฉพาะถ้าสัดส่วนของ polling ที่มีอัปเดตรวมอยู่ด้วยค่อนข้างสูง
  • ผมยังไม่เข้าใจว่าทำไม WebSocket กับ SSE ถึงยังไม่รองรับการส่ง headers อย่าง Authorization ใน request แรก มันเหมือนโยนภาระการทำ authentication ของบริการเรียลไทม์ให้คน implement ทั้งหมด
    อาจมีวิธีที่ดีอยู่ในสเปกก็ได้ แต่ผมเห็นแนวทางที่หลากหลายเกินไปจนตอนนี้แทบจะพูดได้ว่าในทางปฏิบัติมันไม่มีจริง

    • EventSource API มีจุดน่าผิดหวังหลายอย่าง ผมเป็นผู้ดูแล polyfill ของ EventSource ที่คนนิยมใช้มากที่สุด แต่ช่วงหลังได้เริ่มโปรเจกต์ใหม่เพื่อสร้าง EventSource client แบบสมัยใหม่ขึ้นมาอีกครั้ง: https://github.com/rexxars/eventsource-client
      นอกจากรองรับ custom headers แล้ว ยังรองรับทุก request method (POST, PATCH ฯลฯ), มี request body ได้, subscribe กับ named events ได้, และตั้งค่า last event ID เริ่มต้นได้ด้วย อีกทั้งยังใช้เป็น async iterator ได้
      ผมชอบความเรียบง่ายของ Server-Sent Events แต่ EventSource API ดูเหมือนของที่ถูกรีบทำขึ้นมาแล้วก็ปล่อยทิ้งไว้แบบนั้น
      [1]: https://github.com/eventsource/eventsource
    • request แรกส่ง HTTP headers มาตรฐานทั้งหมดรวมถึงคุกกี้ได้ไม่ใช่หรือ?
    • คุกกี้ส่งได้
    • มี TLS certificate ด้วยนะ
    • ผมใช้มันมาหลายปีแล้ว หรือว่าผมกำลังพลาดอะไรบางอย่าง?
  • อาจเป็นความคิดใสซื่อ แต่ถ้าตั้งต้นบน HTTP/2 ขึ้นไป การใช้ EventSource คู่กับ fetch() สำหรับส่งข้อความก็ดูดีพอ ๆ กับโปรโตคอลอื่นที่ใช้การเชื่อมต่อ TCP เดียวกัน ส่วน HTTP/3 ใช้ UDP ก็น่าจะยิ่งดีกว่า
    โดยตั้งสมมติฐานว่าจำเป็นต้องคงการเชื่อมต่อไว้เฉพาะตอนที่แท็บอยู่ foreground เท่านั้น อยากรู้ว่าถ้าเคยลองแนวทางนี้จริง ๆ แล้วเจอปัญหาอะไรบ้าง

    • ข้อจำกัดอย่างหนึ่งคือ SSE ใช้ได้กับข้อความแบบ text เท่านั้น จึงส่งข้อมูลไบนารีอย่างมีประสิทธิภาพไม่ได้ ต้องเข้ารหัสแบบ base64 หรืออย่างอื่น
    • มีไลบรารีที่ทำสิ่งนี้ให้ได้
      https://www.npmjs.com/package/@microsoft/fetch-event-source
    • ผมก็คิดเหมือนกันเป๊ะ ๆ เลย ว่า HTTP/2 กับ SSE น่าจะแก้ปัญหาได้ 99% แล้วไม่ใช่หรือ
      แทนที่จะไปทำอะไรที่ต่างออกไปโดยสิ้นเชิง ผมสงสัยว่ามันจะเป็นไปได้ไหมที่จะผลัก SSE ให้ไกลกว่านี้ พร้อมลด latency, การใช้หน่วยความจำ และทรัพยากร CPU ลงอีก
    • ถ้า use case หลักคือเซิร์ฟเวอร์→ไคลเอนต์ ก็ใช่
  • อ่านบทความแบบนี้แล้วก็รู้สึกขำ ๆ ผมเคยออกแบบระบบประมูลออนไลน์ตั้งแต่ปลายยุค 90 และตอนนั้นยังไม่มี XHR requests เลย
    การอัปเดตแบบเรียลไทม์ทั้งหมดทำด้วย server-push/HTTP streaming แม้ในยุคนั้นการจัดการการเชื่อมต่อที่เปิดค้างไว้ทั้งหมดจะไม่ง่ายนัก แต่ถ้ามีสถาปัตยกรรมที่เหมาะสม ก็รองรับได้ถึงขนาดที่ยอมรับได้

    • ผมใช้เวลาไปมากจริง ๆ กับการอธิบายให้คนอื่นเข้าใจความสำคัญของ HTTP streaming และมันเป็นการต่อสู้ที่ไม่ง่ายเลย
      ข้อดีของ HTTP/2 หรือ HTTP/3 นั้นยอดเยี่ยม แต่ก็ควรรู้ไว้ด้วยว่ายังมีสิ่งที่ใช้ประโยชน์ได้บน HTTP 1.1 ซึ่งแทบจะรองรับกันทุกที่อยู่แล้ว
  • คิดถึง long polling อยู่นิดหน่อย เมื่อเทียบกับเทคโนโลยีสมัยใหม่แล้วมันเรียบง่ายมาก แม้จะเป็นคนที่มองว่า WebRTC ดีที่สุดก็ยังรู้สึกแบบนั้น

    • SSE ไม่ได้ซับซ้อนไปกว่า long polling อย่างมีนัยสำคัญ ความต่างมีแค่ว่าเซิร์ฟเวอร์จะไม่ปิดการเชื่อมต่อทันทีหลังส่ง response
      แต่จะรอข้อมูลอีกครั้งแล้วส่ง response เพิ่มเติมต่อในสตรีมเดิม
    • อยากให้มันเรียบง่ายแบบนั้น แต่ความจริงไม่ใช่
      ระบบเครือข่ายของ Second Life ใช้ HTTPS แบบ long polling กับ “event channel” และเซิร์ฟเวอร์จะส่งข้อความอีเวนต์ไปยังไคลเอนต์ผ่านช่องทางนั้น ข้อความส่วนใหญ่ไปทาง UDP แต่ถ้าต้องเข้ารหัสหรือเป็นข้อความขนาดใหญ่ก็จะไปทาง event channel ของ HTTPS/TCP
      ฝั่งไคลเอนต์ C++ ใช้ libcurl แต่การตั้งค่า timeout เริ่มต้นไม่เข้ากับ long polling libcurl จะตัดการเชื่อมต่อและสร้างคำขอใหม่ ซึ่งอาจทำให้ข้อความสูญหายหรือซ้ำกันได้
      ฝั่งเซิร์ฟเวอร์ Apache ทำหน้าที่อยู่หน้าซิมูเลชันเซิร์ฟเวอร์จริงเพื่อกรองความพยายามเชื่อมต่อที่ไม่เกี่ยวข้องออก แต่ Apache เองก็มี timeout ของตัวเองเช่นกัน จึงตัดการเชื่อมต่อและทำให้ไคลเอนต์ต้องลองใหม่
      พยายามป้องกันการสูญหายด้วยหมายเลขลำดับข้อความ แต่เซิร์ฟเวอร์ของ Second Life กลับเพิกเฉยต่อหมายเลขลำดับที่ไคลเอนต์ส่งกลับมาเพื่อยืนยัน และเซิร์ฟเวอร์ที่เข้ากันได้บางตัวของ Open Simulator ก็ข้ามหมายเลขลำดับไปด้วย
      ผลลัพธ์คือกลายเป็นระบบบน HTTPS ที่อาจทำข้อความซึ่งควรเชื่อถือได้ให้สูญหายหรือซ้ำกันได้ และถ้าข้อความบางประเภทหายไป กิจกรรมของผู้ใช้ในเกมอาจหยุดชะงักทันที
      คนที่ออกแบบสิ่งนี้ได้ออกจากบริษัทไปนานแล้ว และพนักงานปัจจุบันก็ไม่รู้ว่าความยุ่งเหยิงนี้ร้ายแรงแค่ไหน ต้องให้ผู้ใช้งานภายนอกเป็นคนค้นพบและทำเอกสารปัญหาไว้ และพนักงานบริษัทก็พยายามแก้อยู่มาหลายเดือนแล้ว
      แต่มันยากพอสมควรจนตอนนี้ดูเหมือนจะเลือกเลื่อนงานนี้ออกไปก่อน
      เพราะงั้น long polling จึงไม่ใช่อะไรที่ “เรียบง่ายแบบโง่ ๆ” วิธีที่ถูกน่าจะเป็นการส่งข้อความ keep-alive ให้ถี่พอที่จะไม่ให้ชั้น TCP และ HTTPS timeout ซึ่งจะทำให้ยังอยู่ในเส้นทางการทำงานที่ Apache และ libcurl จัดการได้ดี
    • ทุกวันนี้ก็ยังใช้กันบ่อย มีแอปพลิเคชันจำนวนมากที่การยอมรับ overhead ของ request แลกกับการเก็บทุกอย่างไว้ในบริบทของ HTTP API เดิมยังถือว่าสมเหตุสมผล
    • ทุกวันนี้ก็ยังใช้ long polling บน HTTP/2 ได้ และมันคงไม่หายไปไหน