- 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ếp và sự đơ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
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.. 😭
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.
"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.
Ý kiến Hacker News
Long polling có những vấn đề riêng
libcurl, và có thể xảy ra timeoutThật vui khi được dùng Phoenix và LiveView mỗi ngày
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)
Bài này đang gắn "Websocket" và "Long-polling" như những quyết định độc lập
Cách dễ hơn để dùng
setTimeouttrong Node.jsimport { 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
Server-Sent Events hay WebSockets không thể thay thế mọi trường hợp dùng của long polling
Nên dùng tính năng thông báo bất đồng bộ của Postgres
LISTENtrên kênh, và khi dữ liệu thay đổi thì PG có thểTRIGGERvàNOTIFYKhô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
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ôi muốn thử dùng WebSockets thông qua Elixir, framework Phoenix và LiveView.