14 điểm bởi GN⁺ 2025-01-09 | 5 bình luận | Chia sẻ qua WhatsApp
  • Trong một backend dựa trên Node.js/TypeScript, chúng tôi cần xử lý các bản cập nhật thời gian thực ở quy mô lớn
  • Sử dụng PostgreSQL làm backend, hàng trăm worker node phải liên tục kiểm tra tác vụ mới, và các agent cần nhận cập nhật trạng thái thực thi và trò chuyện
  • Ban đầu bắt đầu bằng việc tìm hiểu WebSocket, nhưng cuối cùng lại đi đến một giải pháp “kiểu cũ” nhưng hiệu quả đáng ngạc nhiên
    → "HTTP Long Polling dùng Postgres"

Bối cảnh vấn đề: cập nhật thời gian thực ở quy mô lớn

  • Cập nhật cho worker node :
    • Có hàng trăm worker node chạy SDK Node.js/Golang/C#
    • Vì cần biết ngay khi có tác vụ mới được phân phối, nên cần một chiến lược truy vấn không làm quá tải cơ sở dữ liệu Postgres
  • Đồng bộ trạng thái agent :
    • Agent cần các bản cập nhật thời gian thực về trạng thái thực thi và trò chuyện, và cần truyền chúng đi một cách hiệu quả

So sánh Long Polling và WebSocket

  • Short polling giống như một chuyến tàu khởi hành đúng giờ cố định, cứ đến lịch là rời bến bất kể có hành khách hay không
  • Long polling là cách máy chủ chờ phản hồi, rồi trả về ngay khi có dữ liệu; nếu qua một khoảng thời gian nhất định thì phản hồi bằng timeout
    • Nói cách khác, nó giống một chuyến tàu “đợi đến khi có khách thì chạy”. Chỉ khi không có hành khách xuất hiện trong một khoảng thời gian nhất định (TTL) thì tàu mới rời bến trong trạng thái trống
    • Khi có dữ liệu (hành khách) thì khởi hành ngay, còn khi không có thì vẫn sử dụng tài nguyên hiệu quả — mang lại cả hai lợi ích cùng lúc
  • WebSocket là phương thức giữ kết nối mở liên tục để trao đổi dữ liệu hai chiều
    • Trong môi trường tổ chức, hạ tầng và các vấn đề tường lửa, long polling đơn giản hơn và tương thích tốt hơn so với việc cấu hình WebSocket

Chi tiết triển khai Long Polling

  • Hàm getJobStatusSync giữ vai trò quan trọng
    • Nhận các tham số như jobId, owner, ttl và lặp lại việc truy vấn trạng thái của một tác vụ cụ thể trong một khoảng thời gian nhất định
  • Tiếp tục truy vấn lặp cho đến khi thỏa một trong các điều kiện sau
    • Trạng thái tác vụ trở thành success hoặc failure
    • ttl (timeout) hết hạn
  • Truy vấn cơ sở dữ liệu theo chu kỳ 500ms; nếu kết quả chưa xác định thì chờ rồi truy vấn lại
  • Nếu vượt quá thời gian timeout thì ném lỗi, còn nếu thành công thì trả về kết quả

Tối ưu hóa cơ sở dữ liệu

  • Đặt chỉ mục phù hợp trên Postgres để giảm thiểu chi phí truy vấn
  • Ví dụ: CREATE INDEX idx_jobs_status ON jobs(id, cluster_id);

Lợi ích của Long Polling

  • Dễ duy trì giám sát : có thể tận dụng nguyên trạng stack logging và monitoring dựa trên HTTP hiện có
  • Đơn giản hóa xác thực : không cần triển khai cơ chế xác thực mới, có thể dùng nguyên hệ thống xác thực HTTP hiện có
  • Tương thích hạ tầng : không cần cấu hình riêng cho tường lửa hay load balancer, vì được xử lý như lưu lượng HTTP thông thường
  • Đơn giản trong vận hành : khi máy chủ khởi động lại cũng không cần xử lý riêng trạng thái kết nối, và việc debug dễ hơn
  • Triển khai phía client dễ dàng : chỉ cần thêm logic retry vào cấu trúc request-response HTTP tiêu chuẩn là có thể hoạt động

So sánh với ElectricSQL

  • ElectricSQL là giải pháp đồng bộ dữ liệu Postgres với frontend
  • Nó có cấu trúc đảm bảo tính thời gian thực dù dùng HTTP thay vì WebSocket
  • Trong thực tế, nếu không cần mức kiểm soát cực đoan hoặc cấu trúc quá thấp tầng để xử lý cập nhật thời gian thực, thì ElectricSQL là lựa chọn nên dùng

Vì sao chúng tôi chọn Raw Long Polling

  • Cơ chế truyền tải thông điệp không chỉ là một chi tiết triển khai đơn giản mà là cốt lõi của sản phẩm
  • Không thể để tính năng cốt lõi phụ thuộc vào thư viện bên thứ ba (dù thư viện đó có xuất sắc đến đâu)
  • Yêu cầu
    • Kiểm soát cốt lõi sản phẩm : phải kiểm soát hoàn toàn cơ chế truyền tải thông điệp. Đây không phải vấn đề ở tầng hạ tầng mà là bản thân sản phẩm
    • Loại bỏ phụ thuộc bên ngoài : giảm thiểu phụ thuộc bên ngoài để đơn giản hóa việc self-hosting
    • Kiểm soát mức thấp : trực tiếp kiểm soát cơ chế polling và quản lý kết nối
    • Khả năng kiểm soát tối đa : phải có thể tinh chỉnh chi tiết như triển khai khoảng polling động
    • Đơn giản về mã nguồn : thiết kế đủ đơn giản để người dùng dễ hiểu và dễ chỉnh sửa codebase
  • Kết luận, bằng cách chọn một triển khai HTTP Long Polling đơn giản, chúng tôi đạt được cả quyền kiểm soát trực tiếpsự đơn giản

Những điểm cần lưu ý khi triển khai Long Polling

  • Thiết lập TTL : phía máy chủ phải luôn áp dụng TTL tối đa, và không cho phép TTL do client yêu cầu vượt quá giới hạn này
  • Cân nhắc timeout của hạ tầng : TTL phải ngắn hơn đáng kể so với thiết lập timeout của load balancer, edge server, proxy, v.v.
  • Khoảng polling DB : dùng độ trễ khoảng 500ms để giảm tải cho DB
  • Chiến lược backoff (tùy chọn) : có thể tăng dần khoảng polling để sử dụng tài nguyên hệ thống hiệu quả hơn

Khi nào nên cân nhắc WebSocket

  • Bản thân WebSocket không sai, và vẫn hữu ích ở những khía cạnh khác
    • Khi cần giám sát nhiều kết nối có trạng thái và liên tục trao đổi các sự kiện phức tạp
    • Khi có đủ thời gian và nguồn lực để giải quyết các vấn đề về xác thực, hạ tầng và quan sát hệ thống
  • Tuy nhiên, vẫn tồn tại độ phức tạp do phải tự xây dựng phần vận hành, logging, xử lý reconnect, cơ chế xác thực, v.v.

WebSockets: câu chuyện về một lựa chọn khác

  • Long Polling phù hợp với nhu cầu của chúng tôi, nhưng WebSockets cũng hoàn toàn đáng để cân nhắc
  • WebSockets tự thân không tệ, chỉ là đòi hỏi nhiều sự chú ý và quản lý hơn
  • Các thách thức chính của WebSockets và hướng xử lý
    • Khả năng quan sát : vì WebSockets dựa trên trạng thái, cần bổ sung logging và monitoring cho các kết nối kéo dài
    • Xác thực : cần triển khai cơ chế xác thực mới cho kết nối WebSocket
    • Hạ tầng : cần cấu hình phù hợp load balancer, firewall và hạ tầng khác để hỗ trợ WebSocket
    • Vận hành : quản lý kết nối và tái kết nối WebSocket; xử lý timeout kết nối và lỗi
    • Triển khai phía client : xây dựng thư viện WebSocket phía client, bao gồm chức năng reconnect và quản lý trạng thái

5 bình luận

 
jhj0517 2025-01-10

Hiện tôi đang dùng cấu trúc "short polling" được nói đến ở đây cho việc phục vụ mô hình ML, nên cũng đang rất băn khoăn không biết phương án nào sẽ hiệu quả hơn. Theo những gì tự tìm hiểu ở nhiều nơi, tôi thấy có ý kiến cho rằng short polling nhìn chung an toàn hơn vì chi phí lớn của việc xử lý tái kết nối với WebSocket hay SSE, nên cuối cùng đã chọn short polling.. 😭

 
bbulbum 2025-01-10

Có vẻ mọi người ngại dùng long polling vì nó mang cảm giác hơi mang tính chắp vá. Trên trình duyệt thì có lẽ nó sẽ cứ hiện như thể yêu cầu chưa hoàn thành. Thỉnh thoảng có những trang cứ không tải xong, và tôi lại nghĩ kiểu “nội dung vẫn chưa được tải hết à?”, nên cá nhân tôi không thích lắm.
Trong ứng dụng thì rốt cuộc cũng sẽ thành trạng thái treo ở đâu đó và chờ phản hồi, nên nhìn hơi gượng gạo.

 
joyfui 2025-01-09

"Tác nhân cần nhận cập nhật trạng thái thực thi và trò chuyện"
Nhìn câu này là tôi nghĩ ngay tới SSE, đúng là trong ý kiến trên Hacker News cũng có nhiều người nhắc tới SSE.

 
GN⁺ 2025-01-09
Ý kiến Hacker News
  • Long polling có những vấn đề riêng

    • Second Life sử dụng kênh HTTPS long polling giữa client và server
    • Ở phía client dùng libcurl, và có thể xảy ra timeout
    • Nếu server cố gửi tin nhắn giữa lúc timeout và yêu cầu kế tiếp, có thể phát sinh race condition làm mất tin nhắn
    • Phía trước có Apache server để chặn các yêu cầu không cần thiết, nhưng vẫn có thể xảy ra timeout
    • Các middlebox và proxy server có thể không thích long polling
    • Có nhiều thành phần không thích giữ kết nối HTTP mở quá lâu
    • Kết quả là nó trở thành một kênh tin nhắn không đáng tin cậy, cần số thứ tự để phát hiện bản sao và vẫn có thể mất tin nhắn
    • Phần biểu đồ được ghi là "loop" trong bài gốc không nhắc đến xử lý timeout
    • Khi dùng long polling, cần gửi dữ liệu mỗi vài giây để giữ kết nối
  • Thật vui khi được dùng Phoenix và LiveView mỗi ngày

    • Dùng WebSockets nên không phải bận tâm nhiều
  • Tò mò không biết nó có lợi thế kỹ thuật gì so với việc dùng Server-Sent Events (SSE)

    • Cả hai đều giữ kết nối HTTP mở và có ưu điểm là HTTP đơn giản
    • SSE có vẻ phù hợp hơn khi có thể stream các cập nhật hoặc kết quả
    • Một trường hợp sử dụng phù hợp có thể là theo dõi mọi job ID thay cho một client cụ thể
  • Bài này đang gắn "Websocket" và "Long-polling" như những quyết định độc lập

    • Server long-polling có thể xử lý websocket client với một ít công việc bổ sung
    • Nếu kiến trúc hiện có là websocket, để hỗ trợ long-polling client thì cần hai tầng server
  • Cách dễ hơn để dùng setTimeout trong Node.js

    • Dùng import { setTimeout } from "node:timers/promises"; await setTimeout(500);
  • Thích long polling vì dễ hiểu và từ góc nhìn client thì nó hoạt động như một kết nối rất chậm

    • Cần theo dõi việc retry và các kết nối bị client hủy
    • Vòng lặp query dữ liệu lặp đi lặp lại trong ví dụ code trông hơi gượng gạo
  • Server-Sent Events hay WebSockets không thể thay thế mọi trường hợp dùng của long polling

    • Giới hạn kết nối của SSE thường xuyên trở thành vấn đề
    • WebSockets không đáng tin cậy trong phần lớn môi trường
    • Bài toán phát hiện thay đổi ở backend và truyền đúng đến client phù hợp vẫn chưa được giải quyết
  • Nên dùng tính năng thông báo bất đồng bộ của Postgres

    • Server có thể LISTEN trên kênh, và khi dữ liệu thay đổi thì PG có thể TRIGGERNOTIFY
  • Không chắc long polling với timeout ngắn và các request kết thúc êm ái còn nhiều ý nghĩa hay không

    • Nếu không dùng HTTP/2 hay QUIC thì mẹo này có thể vẫn còn ý nghĩa
  • Thật mới mẻ khi được nhắc lại về một lựa chọn thay thế tương đối đơn giản cho WebSockets

    • Từng làm ở một startup chọn WebSockets, và rất khó test trên Wi‑Fi khách sạn và nhà hàng
 
luminance 2025-01-10

Tôi muốn thử dùng WebSockets thông qua Elixir, framework Phoenix và LiveView.