Backend

System Design — Bài 18: Thiết kế ứng dụng chat: WebSocket, ordering và reconnect

SSite Admin
10 tháng 10, 2026 5 phút đọc 0 lượt xem

Gateway chết ngay sau khi message đã lưu nhưng trước khi client nhận được. Nếu hệ thống chỉ dựa vào pub/sub tạm thời, tin có thể biến mất khỏi trải nghiệm người dùng. Thiết kế chat cần tách đường giao nhanh khỏi nguồn lịch sử bền và có cursor để nối lại cuộc trò chuyện.

Khóa học System Design: mục lục 21 bài — bạn đang đọc bài 18.

Các con số dưới đây là giả định phục vụ học thiết kế, không phải số liệu hay kiến trúc nội bộ của sản phẩm được nhắc tên. Dung lượng dùng đơn vị thập phân; capacity thực tế phải đo theo workload và mục tiêu chất lượng.

Yêu cầu & thách thức riêng

Phạm vi có chat một–một, chat nhóm, trạng thái online và biên nhận. Cần hỏi kích thước nhóm, retention, số thiết bị mỗi người và có yêu cầu sửa hoặc xóa tin không. Những điểm này thay đổi mô hình dữ liệu và fan-out.

Kết nối lâu dài tiêu tốn bộ nhớ, socket và tài nguyên TLS ngay cả khi không có message. Vì vậy số kết nối đồng thời và message mỗi giây là hai trục capacity khác nhau. Một gateway giữ nhiều kết nối nhàn rỗi chưa chứng minh chịu được cùng số kết nối đều gửi liên tục.

Ordering nên chốt theo conversation, không mặc định toàn hệ. “Đã gửi” cũng cần định nghĩa: server đã nhận byte hay message đã được lưu bền? Định nghĩa rõ từ đầu giúp UI, retry và khôi phục cùng một cách hiểu.

WebSocket gateway và bài toán định tuyến

Mỗi phiên client nối tới một gateway. Kho định tuyến có thể ánh xạ user và session tới gateway, vì một user có nhiều thiết bị. Khi có message cho B, service tra các phiên phù hợp rồi chuyển tới gateway đang giữ chúng.

Ánh xạ có thể cũ nếu gateway chết. Cần heartbeat hoặc lease cùng cơ chế reconnect. Kênh pub/sub phục vụ chuyển tin nhanh giữa gateway không nhất thiết là kho bền; nếu B offline hoặc mất thông báo, client phải có đường đọc lại lịch sử từ sequence đã biết.

Thêm gateway thường nhận kết nối mới, còn kết nối cũ có thể tiếp tục tới khi drain hoặc reconnect có kiểm soát; không phải thêm máy là phải chuyển ngay mọi kết nối. Giữ gateway nhẹ bằng cách tách xác nhận lưu message và nghiệp vụ khỏi việc quản lý socket.

Lưu tin nhắn

Luồng thường là ghi message rồi đọc theo conversation hoặc channel và một khoảng sequence. Wide-column là một phương án cho access pattern này; lựa chọn cuối vẫn dựa trên tải và nhu cầu transaction.

Channel_id là khóa gom tự nhiên, nhưng một channel rất lớn không nên làm partition phình vô hạn. Có thể kết hợp time bucket hoặc nhóm sequence, rồi xây query lịch sử qua các bucket gần nhất. Cần đo cả kênh nóng và kích thước partition.

Message_id chỉ dùng để sắp thứ tự nếu cơ chế sinh của nó đáp ứng ordering đã chốt. Snowflake hoặc timestamp phân tán không tự bảo đảm mọi tin cùng conversation được sắp theo thứ tự nghiệp vụ. Client gửi kèm ID ổn định cho lần gửi giúp server nhận diện retry và tránh lưu trùng khi response thất lạc.

Ordering, presence và delivered/seen

Ordering theo conversation có thể dựa vào sequencer hoặc phân vùng xử lý để cấp thứ tự. Client thấy một khoảng trống sequence cần đọc bù, thay vì giả định mọi message tới socket đều đầy đủ và đúng thứ tự sau reconnect.

Presence là suy luận từ heartbeat và TTL. Nếu không thấy heartbeat, ta kết luận phiên có thể offline sau ngưỡng chứ không biết chính xác người dùng đã đóng app ở thời điểm nào. User nhiều thiết bị cần tổng hợp trạng thái các phiên.

Delivered nên gắn với ack của thiết bị theo quy ước sản phẩm; seen cần hành động hoặc con trỏ đọc rõ ràng. Con trỏ chỉ nên tiến tới vùng đã đọc liên tục, không nhảy qua tin chưa nhận. Trạng thái biên nhận có thể đồng bộ chậm, còn message đã báo lưu thành công cần mức durability đã cam kết.

Gửi bền trước, giao nhanh sau, đọc bù khi cần

Client A --client_message_id--> Gateway
  -> Message Service
  -> ghi message + dedup + sequence theo conversation
  -> ACK đã lưu bền
  -> định tuyến tới các session của B

B reconnect với last_contiguous_sequence = 105
  -> đọc lịch sử sau 105
  -> dedup message đã nhận qua socket
  -> chỉ tiến cursor qua vùng liên tục đã nhận/đọc

ACK lưu bền, delivered và seen là ba mốc khác nhau. Sequencer cũng cần recovery và fencing để giữ thứ tự khi đổi owner. Presence dựa vào heartbeat chỉ là suy luận có độ trễ, đặc biệt khi một user dùng nhiều thiết bị.

Thuật ngữ cần nhớ

  • ACK — Acknowledgment: Tín hiệu xác nhận. Phải nói rõ xác nhận đã nhận vào bộ nhớ, đã lưu bền hay đã xử lý nghiệp vụ; các mốc này cho bảo đảm khác nhau.

  • TTL — Time To Live: Thời gian sống còn hiệu lực của dữ liệu, bản ghi tên miền hoặc lease. Hết hạn không tự thực hiện nghiệp vụ như hoàn tồn kho; cần luồng xử lý tương ứng.

  • TLS — Transport Layer Security: Giao thức bảo vệ kênh truyền bằng mã hóa, kiểm tra toàn vẹn và xác thực; có chi phí bắt tay và xử lý cần tính khi đo.

Bài tập tự thực hành

Gateway chết sau khi message đã lưu nhưng trước khi client nhận.

Gợi ý kiểm tra lời giải

Reconnect, resume từ cursor, đọc lịch sử và dedup; không chỉ dựa pub/sub tạm thời.

Tự kiểm tra sau bài học

  • Tính capacity theo cả số socket và message/s.

  • Client retry không tạo message mới ngoài ý muốn.

  • Mất gateway vẫn có đường đọc lại từ lịch sử bền.

Tiếp tục lộ trình

Bài trước — Thiết kế News Feed: push, pull và mô hình hybrid

Bài tiếp — Thiết kế giao đồ ăn: geo query, vị trí và dispatch an toàn

Xem toàn bộ khóa học System Design 21 bài

S

Site Admin

Engineer and writer. Building things with TypeScript and distributed systems.

Bình luận (0)

Bạn cần đăng nhập bằng Google để bình luận.

Hãy là người bình luận đầu tiên.

Bài viết liên quan

K

Học System Design qua 21 bài: yêu cầu, capacity, database, cache, hệ phân tán và 6 bài thực hành, kèm bài tập, sơ đồ luồng và mục lục đầy đủ.

10 thg 10, 20266 phút2
S

Học cách đi từ vấn đề đến kiến trúc, phân biệt architecture với design và bảo vệ lựa chọn bằng yêu cầu, số liệu và đánh đổi.

10 thg 10, 20266 phút2
S

Đặt câu hỏi về người dùng, thao tác, tải, độ trễ và tính đúng đắn trước khi chọn database hay vẽ sơ đồ kiến trúc.

10 thg 10, 20266 phút1