0

💬 Trận 5: Thiết kế hệ thống Chat như Facebook Messenger – Từ con số 0 đến 50 triệu người dùng mỗi ngày

"Bạn nhấn nút Gửi trên Messenger. Chỉ chưa đầy một giây sau, điện thoại của người nhận đã rung lên. Điều gì đã xảy ra phía sau màn hình?"

Đó là một trải nghiệm quá quen thuộc đến mức chúng ta hiếm khi tự hỏi:

  • Tin nhắn đã đi qua những đâu?
  • Làm sao server biết người nhận đang online?
  • Nếu người nhận tắt điện thoại thì sao?
  • Nếu có hàng chục triệu người cùng nhắn tin một lúc thì hệ thống vẫn hoạt động như thế nào?

Đó chính là bài toán mà rất nhiều công ty công nghệ lớn phải giải quyết mỗi ngày.

Trong bài viết này, chúng ta sẽ cùng thiết kế một hệ thống Chat giống như Facebook Messenger, WhatsApp hay WeChat, theo đúng cách mà Alex Xu thường tiếp cận trong các buổi phỏng vấn System Design.

Thay vì lao ngay vào vẽ kiến trúc, chúng ta sẽ đi qua từng bước giống như khi ngồi trước interviewer:

  1. Hiểu yêu cầu.
  2. Ước lượng quy mô.
  3. Vẽ kiến trúc tổng quan.
  4. Phân tích từng thành phần quan trọng.
  5. Thảo luận những đánh đổi và cách mở rộng.

Đến cuối bài, bạn sẽ không chỉ biết "hệ thống Chat được xây như thế nào", mà còn hiểu "tại sao người ta lại thiết kế như vậy."


Bước 1. Hiểu yêu cầu như trong một buổi phỏng vấn

Một sai lầm rất phổ biến của người mới học System Design là vừa nghe đề bài đã bắt đầu nói về Kafka, Redis hay WebSocket.

Trong thực tế, đó là điều interviewer không mong muốn.

Điều đầu tiên họ muốn biết là:

Bạn đã thực sự hiểu bài toán chưa?

Giả sử interviewer chỉ nói một câu rất ngắn:

Design Facebook Messenger.

Nếu lập tức vẽ kiến trúc thì gần như chắc chắn bạn sẽ thiết kế sai.

Bởi vì "Messenger" có thể mang rất nhiều ý nghĩa khác nhau.

  • Nó có hỗ trợ gọi video không?
  • Có gửi ảnh không?
  • Có tự xóa tin nhắn không?
  • Có mã hóa đầu cuối không?
  • Có chat nhóm hàng nghìn người không?

Tất cả những câu hỏi đó sẽ làm thay đổi hoàn toàn kiến trúc của hệ thống.

Vì vậy, Alex Xu luôn bắt đầu bằng việc làm rõ phạm vi thiết kế (Clarify Requirements).


Những câu hỏi cần hỏi

Một cuộc trao đổi có thể diễn ra như sau.

Interviewer: Hãy thiết kế một hệ thống Chat.

Candidate: Hệ thống có hỗ trợ chat nhóm không?

Interviewer: Có.

Candidate: Một nhóm tối đa bao nhiêu người?

Interviewer: Khoảng 100 người.

Candidate: Có cần hỗ trợ cả Web và Mobile không?

Interviewer: Có.

Candidate: Tin nhắn có lưu vĩnh viễn không?

Interviewer: Có.

Candidate: Có cần hiển thị trạng thái Online/Offline không?

Interviewer: Có.

Candidate: Có cần gửi Push Notification khi người dùng tắt ứng dụng không?

Interviewer: Có.

Candidate: Quy mô hệ thống khoảng bao nhiêu người dùng?

Interviewer: 50 triệu Daily Active Users.

Sau vài phút trao đổi, bài toán đã trở nên rõ ràng hơn rất nhiều.


Phạm vi hệ thống

Chúng ta sẽ xây dựng một hệ thống có các đặc điểm sau:

  • Hỗ trợ chat 1-1.
  • Hỗ trợ chat nhóm tối đa 100 người.
  • Có ứng dụng Web và Mobile.
  • Khoảng 50 triệu người dùng hoạt động mỗi ngày (50M DAU).
  • Hỗ trợ gửi tin nhắn văn bản.
  • Tin nhắn được lưu vĩnh viễn.
  • Hiển thị trạng thái Online/Offline.
  • Gửi Push Notification khi người nhận không mở ứng dụng.

Lưu ý rằng chúng ta chưa xét:

  • Video Call
  • Voice Call
  • Chia sẻ màn hình
  • Mã hóa đầu cuối (End-to-End Encryption)
  • Gửi file dung lượng lớn

Những tính năng này sẽ khiến thiết kế trở nên phức tạp hơn rất nhiều và có thể được xem như các bài toán riêng.


Bước 2. Ước lượng quy mô hệ thống

Sau khi hiểu yêu cầu, Alex Xu thường dành vài phút để ước lượng quy mô.

Đây không phải để có những con số tuyệt đối chính xác.

Mục đích là để biết hệ thống sẽ lớn đến mức nào và từ đó đưa ra những quyết định kiến trúc phù hợp.


1. 50 triệu DAU nghĩa là gì?

Con số đầu tiên interviewer đưa ra là:

50 triệu người dùng hoạt động mỗi ngày.

Nghe có vẻ rất lớn. Nhưng lớn đến mức nào?

Một ứng dụng chat không phải lúc nào người dùng cũng online.

Giả sử khoảng 10% người dùng đang kết nối cùng lúc.

Ta có:

50 triệu × 10% ≈ 5 triệu kết nối đồng thời

Điều này có nghĩa là hệ thống cần duy trì khoảng 5 triệu kết nối thời gian thực.

Đây là một con số cực kỳ quan trọng.

Nếu mỗi kết nối chiếm khoảng 10 KB bộ nhớ thì riêng việc giữ kết nối đã cần:

5 triệu × 10 KB ≈ 50 GB RAM

Rõ ràng, không một máy chủ nào có thể gánh nổi lượng kết nối này.

Điều đó dẫn chúng ta đến kết luận đầu tiên:

Hệ thống bắt buộc phải được phân tán trên nhiều Chat Server.


2. Bao nhiêu tin nhắn mỗi giây?

Tiếp theo là lưu lượng tin nhắn.

Giả sử trung bình mỗi người gửi: 20 tin nhắn mỗi ngày.

Tổng số tin nhắn:

50 triệu × 20 = 1 tỷ tin nhắn/ngày

Chia đều cho một ngày:

1 tỷ ÷ 86.400 ≈ 11.600 tin nhắn/giây

Nhưng lưu lượng Internet không bao giờ phân bố đều.

  • Buổi tối luôn đông hơn buổi sáng.
  • Giờ cao điểm có thể gấp 5–10 lần trung bình.

Nếu lấy hệ số gấp 10:

≈ 100.000 message/second

Đây là tốc độ ghi dữ liệu mà hệ thống cần xử lý trong thời gian cao điểm.


3. Khối lượng dữ liệu lưu trữ

Giả sử:

  • Một tin nhắn trung bình khoảng 200 byte.
  • Mỗi ngày: 1 tỷ × 200 byte ≈ 200 GB/ngày
  • Một năm: ≈ 73 TB

Đó mới chỉ là nội dung tin nhắn.

Nếu tính thêm:

  • metadata,
  • chỉ mục,
  • sao lưu,
  • nhân bản dữ liệu,

dung lượng thực tế sẽ còn lớn hơn nhiều.

Điều này giải thích vì sao các hệ thống Chat lớn thường không lưu lịch sử tin nhắn bằng một cơ sở dữ liệu quan hệ duy nhất.


4. Những thách thức lớn nhất của hệ thống Chat

Đến đây, thay vì vội vàng nghĩ đến database hay Redis, hãy tự hỏi:

Điều gì khiến một hệ thống Chat khó xây dựng đến vậy?

Thực ra chỉ có ba bài toán cốt lõi.

1. Độ trễ cực thấp

Khi bạn gửi:

"Đi ăn không?"

Bạn không muốn người nhận thấy tin nhắn sau 5 giây.

Càng không phải sau 30 giây.

Người dùng kỳ vọng phản hồi gần như ngay lập tức.

Đó là lý do độ trễ của hệ thống Chat thường chỉ được tính bằng vài chục mili giây.


2. Kết nối phải luôn được duy trì

Khác với một website thông thường, ứng dụng Chat không thể đợi người dùng bấm Refresh.

Server phải chủ động gửi dữ liệu ngay khi có tin nhắn mới.

Điều này đồng nghĩa với việc hệ thống phải duy trì hàng triệu kết nối mở cùng lúc.

Đây là một trong những khác biệt lớn nhất giữa hệ thống Chat và các ứng dụng web truyền thống.


3. Khả năng mở rộng

Hôm nay có thể chỉ có: 5 triệu kết nối.

Nhưng vài năm sau có thể là: 50 triệu kết nối.

Kiến trúc cần đủ linh hoạt để chỉ cần bổ sung thêm máy chủ là có thể phục vụ lượng người dùng tăng lên, thay vì phải thiết kế lại từ đầu.


5. Từ yêu cầu đến kiến trúc

Đến thời điểm này, chúng ta đã có đủ thông tin để bắt đầu suy nghĩ về kiến trúc của hệ thống.

Chúng ta biết rằng:

  • Cần hỗ trợ giao tiếp thời gian thực.
  • Cần duy trì hàng triệu kết nối đồng thời.
  • Cần xử lý hàng trăm nghìn tin nhắn mỗi giây vào giờ cao điểm.
  • Cần lưu trữ lịch sử tin nhắn trong nhiều năm.

Vậy câu hỏi đầu tiên là:

Làm thế nào để server có thể chủ động gửi tin nhắn đến người dùng ngay khi có dữ liệu mới?

Đó chính là nơi chúng ta sẽ bắt đầu trong phần tiếp theo, khi cùng so sánh ba phương pháp giao tiếp phổ biến: Polling, Long Polling và WebSocket, để tìm ra nền tảng phù hợp nhất cho một hệ thống Chat thời gian thực.

Bước 3. Thiết kế kiến trúc tổng quan

Ở phần trước, chúng ta đã xác định được ba thách thức lớn nhất của hệ thống Chat:

  • Độ trễ phải cực thấp.
  • Duy trì hàng triệu kết nối đồng thời.
  • Có thể mở rộng khi lượng người dùng tăng lên.

Câu hỏi đầu tiên cần trả lời là:

Làm thế nào để Server chủ động gửi tin nhắn cho Client ngay khi có tin nhắn mới?

Nghe có vẻ đơn giản, nhưng đây lại là quyết định ảnh hưởng đến toàn bộ kiến trúc của hệ thống.


1. HTTP có đủ để xây dựng Messenger không?

Nếu chỉ nhìn từ góc độ gửi tin nhắn, HTTP dường như đã đủ.

Ví dụ:

POST /messages

Body:

{
    "from": "Alice",
    "to": "Bob",
    "message": "Đi ăn không?"
}

Server nhận request → Lưu Database → Trả về:

200 OK

Mọi thứ đều ổn.

Nhưng...

2. Vấn đề nằm ở chiều ngược lại

Sau khi Alice gửi tin nhắn, làm sao Bob biết có tin nhắn mới?

HTTP hoạt động theo mô hình:

Client hỏi → Server trả lời.

Server không thể tự nhiên gửi dữ liệu xuống Client.

Nếu Bob không gửi request mới thì Server hoàn toàn không có cơ hội gửi tin nhắn.

Đây chính là bài toán lớn nhất của mọi hệ thống Chat.


3. Giải pháp đầu tiên: Polling

Cách đơn giản nhất là để Client liên tục hỏi Server.

Ví dụ cứ mỗi 3 giây:

GET /messages

Server kiểm tra:

Có tin nhắn mới không?

Sau 3 giây lại hỏi tiếp.

Client  → Server
|
3s
↓
Client  → Server
|
3s
↓
Client  → Server
...

Nguyên lý rất đơn giản.

Đó là lý do rất nhiều ứng dụng nhỏ sử dụng Polling.


4. Nhưng vấn đề xuất hiện rất nhanh

Giả sử: 5 triệu người đang online.

Mỗi người hỏi Server:

3 giây/lần

Khi đó số request mỗi giây là:

5.000.000 ÷ 3 ≈ 1.700.000 request/second

Điều đáng nói là...

Khoảng 99% request đều trả về rỗng.

Có tin mới không?
↓
Không.
↓
Có tin mới không?
↓
Không.
↓
Có tin mới không?
↓
Không.

Server phải xử lý hàng triệu request vô nghĩa chỉ để trả lời:

"Chưa có gì mới."

Điều này tiêu tốn:

  • CPU
  • Network
  • Thread
  • Load Balancer

mà gần như không tạo ra giá trị.

Polling rõ ràng không phù hợp với một hệ thống có hàng chục triệu người dùng.


5. Giải pháp thứ hai: Long Polling

Nếu Polling quá lãng phí, tại sao Server không đợi luôn đến khi có tin nhắn?

Đó chính là ý tưởng của Long Polling.

Client gửi request:

GET /messages

Server kiểm tra.

Nếu chưa có tin nhắn mới, Server không trả lời ngay.

Thay vào đó, Server giữ request mở.

Client
      │
      │
      │
      │
Server

Có thể sau:

  • 5 giây
  • 20 giây
  • 40 giây

Khi xuất hiện tin nhắn mới, Server mới phản hồi:

[
   "Hello"
]

Client nhận được tin nhắn.

Sau đó lập tức tạo một Long Polling request mới.

Chu trình lại tiếp tục.


6. Long Polling tốt hơn Polling ở điểm nào?

Nếu Polling tạo:

20 request/phút

Long Polling có thể chỉ cần:

1 request/phút

Lượng request giảm rất mạnh.

Độ trễ gần như bằng 0.

Trong nhiều năm, đây từng là giải pháp rất phổ biến.


7. Nhưng Long Polling vẫn còn nhiều nhược điểm

1. Giữ quá nhiều kết nối treo

Giả sử: 5 triệu người online.

Nếu tất cả đều đang đợi tin nhắn.

Server phải giữ 5 triệu HTTP request đang mở.

Mỗi request đều chiếm:

  • Memory
  • Socket
  • File descriptor

Đây là một áp lực rất lớn.


2. Tin nhắn có thể bị trễ

Giả sử:

  • Server vừa phản hồi → Client vừa đóng request cũ.
  • Client chưa kịp mở request mới thì đúng lúc đó có tin nhắn.
  • Server không còn kết nối nào để gửi.
  • Tin nhắn phải chờ đến request tiếp theo.
  • Độ trễ bắt đầu xuất hiện.

3. Không phải giao tiếp hai chiều

Long Polling vẫn dựa trên HTTP.

Điều đó có nghĩa: Server vẫn không thể chủ động gửi dữ liệu bất kỳ lúc nào.

Server chỉ được trả lời khi Client mở request.

Đây vẫn là một sự khác biệt rất lớn so với các ứng dụng Chat hiện đại.


8. Giải pháp cuối cùng: WebSocket

Nếu HTTP luôn bắt đầu từ Client. Nếu Long Polling vẫn phải dựa trên HTTP.

Tại sao không tạo ra một kết nối luôn mở?

Đó chính là WebSocket.


9. WebSocket hoạt động như thế nào?

Ban đầu Client vẫn gửi HTTP.

GET /chat

Nhưng trong Header có thêm yêu cầu:

Upgrade: websocket

Server đồng ý.

101 Switching Protocols

Sau thời điểm đó.

Kết nối HTTP được nâng cấp thành:

WebSocket

Đây là một kết nối:

  • Persistent
  • Full-duplex
  • Bi-directional

Nghĩa là:

Cả Client và Server đều có thể chủ động gửi dữ liệu bất kỳ lúc nào.

Client
⇅
Chat Server

Không còn:

Request
↓
Response

mà trở thành:

Message
⇅
Message
⇅
Message

giống như hai người đang gọi điện thoại.


10. Tại sao WebSocket phù hợp với hệ thống Chat?

Hãy tưởng tượng Alice và Bob đang trò chuyện.

Alice gửi:

"Đi ăn nhé?"

Tin nhắn được gửi ngay lập tức.

Bob trả lời:

"Ok."

Ngay lập tức Server lại gửi về Alice.

Không cần:

  • tạo HTTP mới,
  • thiết lập TCP mới,
  • thực hiện SSL handshake mới.

Mọi dữ liệu đều đi qua một kết nối duy nhất.

Điều này giúp giảm đáng kể:

  • độ trễ,
  • băng thông,
  • chi phí xử lý.

Đó là lý do hầu hết các ứng dụng Chat hiện đại như Messenger, WhatsApp, Discord hay Slack đều sử dụng WebSocket (hoặc các giao thức thời gian thực tương tự) để duy trì kết nối với người dùng.


11. Kiến trúc tổng quan của hệ thống Chat

Sau khi đã chọn WebSocket làm nền tảng giao tiếp, chúng ta có thể bắt đầu xây dựng kiến trúc tổng quan.

Nếu nhìn từ trên xuống, hệ thống có thể được chia thành ba nhóm dịch vụ chính.

------------------------------------------------------------------------------------------------------------------ image.png ------------------------------------------------------------------------------------------------------------------

Đừng cố gắng nhồi tất cả chức năng vào một máy chủ duy nhất.

Thay vào đó, hãy tách hệ thống thành các dịch vụ có trách nhiệm rõ ràng.


1. Stateless Services

Đây là nhóm dịch vụ mà hầu hết các ứng dụng Web đều có.

Ví dụ:

  • Đăng nhập.
  • Đăng ký.
  • Hồ sơ người dùng.
  • Danh sách bạn bè.
  • Quản lý nhóm.
  • Cài đặt.

Các request này đều sử dụng HTTP.

Ví dụ:

POST /login

GET /profile

PUT /settings

Đặc điểm của chúng là:

  • Không giữ trạng thái kết nối lâu dài.
  • Có thể mở rộng rất dễ bằng Load Balancer.
  • Bất kỳ máy chủ nào cũng có thể xử lý request.

Đó là lý do chúng được gọi là Stateless Services.


2. Chat Servers

Đây là trái tim của toàn bộ hệ thống.

Mỗi Chat Server duy trì hàng chục nghìn đến hàng trăm nghìn kết nối WebSocket.

Ví dụ:

Chat Server A
├── Alice
├── Tom
├── Lisa
├── Jack
└── ...

Một Chat Server khác có thể quản lý:

Chat Server B
├── Bob
├── Emma
├── John
└── ...

Khi Alice gửi tin nhắn cho Bob:

  • Alice gửi qua kết nối WebSocket đến Chat Server A.
  • Chat Server A xác định Bob đang kết nối với Chat Server B.
  • Tin nhắn được chuyển nội bộ sang Chat Server B.
  • Chat Server B đẩy ngay đến Bob qua WebSocket.

Nhờ cách phân chia này, mỗi Chat Server chỉ cần quan tâm đến các kết nối mà nó đang quản lý, giúp hệ thống mở rộng bằng cách bổ sung thêm máy chủ khi lượng người dùng tăng lên.


3. Push Notification Service

Điều gì xảy ra nếu Bob đã tắt ứng dụng?

  • Lúc này kết nối WebSocket không còn tồn tại.
  • Chat Server không thể gửi tin nhắn trực tiếp.
  • Thay vào đó, hệ thống sẽ chuyển yêu cầu sang dịch vụ Push Notification.
Alice
↓
Chat Server
↓
Push Service
↓
Điện thoại Bob

Điện thoại sẽ hiển thị:

Alice: "Đi ăn nhé?"

Khi Bob mở ứng dụng. Client mới thiết lập lại WebSocket và đồng bộ toàn bộ tin nhắn còn thiếu.


12. Tóm tắt kiến trúc

Đến thời điểm này, chúng ta đã có bức tranh tổng quan của hệ thống:

  • HTTP dành cho các API thông thường như đăng nhập hay quản lý hồ sơ.
  • WebSocket đảm nhiệm giao tiếp thời gian thực giữa Client và Chat Server.
  • Chat Server duy trì hàng triệu kết nối và chuyển tiếp tin nhắn.
  • Push Notification Service xử lý trường hợp người nhận đang ngoại tuyến.
  • Load Balancer phân phối lưu lượng để hệ thống có thể mở rộng theo chiều ngang.

Tuy nhiên, vẫn còn một câu hỏi rất quan trọng:

Mỗi ngày hệ thống phải lưu hàng tỷ tin nhắn. Chúng ta nên lưu dữ liệu ở đâu để vừa ghi nhanh, vừa dễ mở rộng và vẫn truy xuất lịch sử hiệu quả?

Đó sẽ là nội dung của Phần 4, nơi chúng ta đi sâu vào thiết kế cơ sở dữ liệu, mô hình dữ liệu và toàn bộ luồng gửi–nhận tin nhắn từ lúc người dùng nhấn Gửi cho đến khi tin nhắn xuất hiện trên thiết bị của người nhận.

Bước 4. Đi sâu vào thiết kế chi tiết

Ở phần trước, chúng ta đã xây dựng được kiến trúc tổng quan của hệ thống.

Client
    │
    ▼
Load Balancer
    │
    ▼
Chat Server
    │
    ▼
Internal Services

Đây mới chỉ là "bức tranh từ trên cao".

Khi bước vào vòng Deep Dive trong một buổi phỏng vấn System Design, interviewer sẽ bắt đầu đặt những câu hỏi chi tiết hơn:

  • "Tin nhắn sẽ được lưu ở đâu?"
  • "Làm sao đảm bảo không mất tin nhắn?"
  • "Nếu người nhận đang online thì sao?"
  • "Nếu họ đang offline thì sao?"
  • "Làm sao lưu trữ hàng tỷ tin nhắn mỗi ngày?"

Đây chính là lúc chúng ta cần phân tích từng thành phần của hệ thống.


1. Thiết kế cơ sở dữ liệu

Hãy bắt đầu bằng một câu hỏi tưởng chừng rất đơn giản.

Lưu tin nhắn vào MySQL có được không?

Câu trả lời là:

Có, nhưng không phải lựa chọn tốt nhất.

Để hiểu tại sao, hãy nhìn vào đặc điểm của dữ liệu trong một hệ thống Chat.


? Không phải mọi dữ liệu đều giống nhau

Một ứng dụng Messenger thực tế lưu rất nhiều loại dữ liệu khác nhau.

Ví dụ:

  • Thông tin người dùng
  • Hồ sơ cá nhân
  • Danh sách bạn bè
  • Cài đặt
  • Danh sách nhóm
  • Tin nhắn
  • Trạng thái Online

Nếu nhìn kỹ, chúng ta sẽ thấy các loại dữ liệu này có đặc tính hoàn toàn khác nhau.

Ví dụ:

1. Hồ sơ người dùng

User

ID
Tên
Email
Ngày sinh
Avatar

Đây là dữ liệu:

  • kích thước nhỏ
  • cập nhật ít
  • yêu cầu tính nhất quán cao

Rõ ràng MySQL hay PostgreSQL là lựa chọn rất phù hợp.


2. Tin nhắn

Trong khi đó, bảng Message có thể trông như sau.

message_id
conversation_id
sender
content
timestamp

Nhưng số lượng bản ghi thì hoàn toàn khác.

Một người có thể gửi:

  • vài chục tin nhắn mỗi ngày.

Hệ thống có:

  • 50 triệu người.

Điều đó đồng nghĩa với:

≈ 1 tỷ message mỗi ngày

Một năm:

≈365 tỷ message

Khối lượng dữ liệu này lớn hơn rất nhiều so với dữ liệu người dùng.


3. Đặc điểm của dữ liệu Chat

Nếu phân tích hành vi người dùng, chúng ta sẽ thấy một điều thú vị.

Giả sử bạn mở Messenger.

Bạn sẽ đọc:

  • 10 tin nhắn gần nhất.
  • 20 tin nhắn gần nhất.

Rất hiếm khi bạn cuộn lên để xem tin nhắn của... ba năm trước.

Điều này tạo nên một đặc điểm quan trọng.

Hầu hết các truy vấn đều đọc dữ liệu mới nhất.

Ví dụ:

SELECT *
FROM Message
WHERE conversation_id=123
ORDER BY message_id DESC
LIMIT 20

Đây là truy vấn xuất hiện nhiều nhất.

Rất ít khi người dùng thực hiện:

SELECT *
FROM Message
WHERE message_id=100

hoặc

SELECT *
FROM Message
WHERE created_at='2018'

Nghĩa là workload chủ yếu là:

  • Ghi rất nhiều.
  • Đọc các bản ghi mới nhất.
  • Hầu như không cập nhật.

Đây không phải kiểu dữ liệu mà RDBMS xử lý tốt nhất.


? Tại sao Cassandra hay HBase

Alex Xu lựa chọn:

  • Cassandra
  • HBase

thay vì MySQL.

Tại sao?

Bởi vì các cơ sở dữ liệu này được thiết kế riêng cho workload như Chat.

Chúng có ba ưu điểm rất lớn.


1. Ghi cực nhanh

Tin nhắn được tạo liên tục.

09:00   Hello
09:00   Hi
09:01   OK
09:01   See you

Database gần như chỉ cần:

Append
Append
Append
Append

Không phải:

UPDATE
DELETE
JOIN

Đó là điểm mạnh của Cassandra.


2. Mở rộng ngang rất dễ

Nếu dữ liệu tăng gấp đôi. Bạn không cần thay máy chủ lớn hơn.

Chỉ cần thêm node mới.

# Before
Node1
Node2
Node3

# After
Node1
Node2
Node3
Node4
Node5

Dữ liệu sẽ được phân phối lại.

Đây chính là Horizontal Scaling.


3. Tối ưu cho dữ liệu theo thời gian

Tin nhắn luôn xuất hiện theo thứ tự thời gian.

1
2
3
4
5
6
...

Việc đọc:

20 message mới nhất

gần như chỉ là đọc liên tục trên đĩa.

Đó là lý do Cassandra rất phù hợp cho hệ thống Chat.


2. Thiết kế mô hình dữ liệu

Sau khi chọn Database. Câu hỏi tiếp theo là:

Khóa chính (Primary Key) nên là gì?

Đây là một quyết định cực kỳ quan trọng.


? Thiết kế Chat 1-1

Giả sử Alice và Bob trò chuyện.

Một cách đơn giản là:

conversation_id
message_id
content

Nhưng conversation_id được tạo như thế nào?

Một cách phổ biến là:

Hash(userA,userB)

Ví dụ:

Alice
Bob

conversation_id=hash(Alice,Bob)

Nhờ vậy, toàn bộ tin nhắn của Alice và Bob sẽ nằm cùng một Partition. Đọc lịch sử rất nhanh.


Message ID

Mỗi tin nhắn cần một ID duy nhất.

Nếu dùng:

AUTO_INCREMENT

sẽ gặp vấn đề khi có nhiều Database.

Alex Xu đề xuất sử dụng:

Snowflake ID

Một Snowflake ID thường chứa:

  • Timestamp
  • Machine ID
  • Sequence Number

Ví dụ:

7283478923478234

Ưu điểm:

  • Không trùng.
  • Có thứ tự thời gian.
  • Không cần một Database trung tâm sinh ID.

Đây là giải pháp được Twitter sử dụng nhiều năm.


Luồng gửi tin nhắn

Đến đây chúng ta đã có:

  • Chat Server
  • Database
  • WebSocket

Bây giờ hãy xem điều gì xảy ra khi Alice nhấn nút Gửi.


Bước 1

Alice gửi:

"Đi ăn nhé?"

Tin nhắn đi qua WebSocket.

Alice
↓
Chat Server A

Bước 2

Chat Server tạo:

message_id

Ví dụ:

92834782374823

Bước 3

Tin nhắn được ghi xuống Database.

Chat Server
↓
Message DB

Lưu ý: Trong thực tế, việc ghi thường diễn ra thông qua Message Queue.

Chat Server
↓
Kafka
↓
Message Service
↓
Database

Tại sao?

Nếu Database bị chậm vài trăm mili giây. Chat Server không bị block. Tin nhắn vẫn được xếp hàng.

Đây là lý do Message Queue xuất hiện trong hầu hết hệ thống Chat hiện đại.


Người nhận đang Online

Giả sử Bob đang mở Messenger.

Chat Server biết điều đó.

Alice
↓
Server A
↓
Server B
↓
Bob

Quá trình diễn ra như sau.

Bước 1

Server A nhận tin nhắn.


Bước 2

Server A kiểm tra:

Bob đang ở đâu?

Ví dụ:

Bob
↓
Server B

Thông tin này thường được lưu trong Redis hoặc Presence Service.


Bước 3

Server A gửi nội bộ.

Server A
↓
Server B

Bước 4

Server B đẩy qua WebSocket tới Bob.


"Đi ăn nhé?"

Toàn bộ quá trình này thường chỉ mất vài chục mili giây.

Đó là lý do bạn thấy tin nhắn gần như xuất hiện ngay lập tức.


Người nhận đang Offline

Nếu Bob đã tắt ứng dụng. WebSocket không còn tồn tại.

Lúc này. Server A sẽ làm gì?

Rất đơn giản.

Alice
↓
Chat Server
↓
Push Notification Service
|
|        hoặc
|---------------------|
↓                     ↓
FCM                  APNs
↓                     ↓
Android             iPhone

Điện thoại hiển thị:

Alice: Đi ăn nhé?

Khi Bob mở ứng dụng. Client sẽ:

Reconnect
↓
Sync Message

Toàn bộ tin nhắn còn thiếu được tải xuống từ Database.


? Thiết kế Chat nhóm

Chat nhóm có một bài toán thú vị hơn nhiều.

Giả sử nhóm có:

A
B
C

A gửi:

Hello

Có hai cách thiết kế.


Cách 1

Lưu một bản.

Khi B mở cuộc trò chuyện.

Database phải:

JOIN
Permission
Membership
Message

Mỗi lần đọc đều phải xử lý rất nhiều logic.


Cách 2

Nhân bản tin nhắn.

Ví dụ.

Inbox B
Hello
Inbox C
Hello

Mỗi người có một hàng đợi riêng.

Khi mở Messenger, server chỉ cần đọc:

Inbox của B

Là đủ.

  • Không cần JOIN.
  • Không cần tính toán.
  • Đọc cực nhanh.

Có lãng phí không?

➥ Có.

Ví dụ nhóm: 100 người

➥ Một tin nhắn sẽ được lưu: 99 bản sao

Nghe có vẻ rất tốn bộ nhớ. Nhưng hãy nhớ phạm vi bài toán. Nhóm chỉ tối đa: 100 người

➥ Chi phí lưu trữ tăng. Đổi lại, đọc dữ liệu cực nhanh.

Đây là một trade-off rất đáng giá.

Đó cũng là lý do Alex Xu lựa chọn phương pháp này cho các nhóm nhỏ.


Tóm tắt

Đến đây, chúng ta đã giải quyết được ba bài toán quan trọng nhất của hệ thống Chat:

  • Chọn NoSQL (Cassandra/HBase) để lưu trữ lịch sử tin nhắn với khả năng ghi nhanh và mở rộng theo chiều ngang.
  • Thiết kế Message ID bằng Snowflake để đảm bảo duy nhất và có thứ tự thời gian.
  • Xây dựng luồng gửi–nhận tin nhắn cho cả trường hợp người nhận OnlineOffline, kết hợp với Message QueuePush Notification để đảm bảo độ tin cậy.

Tuy nhiên, vẫn còn một thành phần rất quan trọng mà bất kỳ ứng dụng nhắn tin nào cũng phải có:

Làm thế nào hệ thống biết ai đang Online, ai đang Offline?

Nếu người dùng mất mạng đột ngột thì sao?

Làm sao dấu chấm xanh không nhấp nháy liên tục khi kết nối chập chờn?

Đó sẽ là nội dung của Bước 5, nơi chúng ta tìm hiểu về:

  • Presence Service
  • Cơ chế Heartbeat
  • Mô hình Pub/Sub

cùng những trade-off và cách mở rộng hệ thống lên quy mô hàng trăm triệu người dùng.

Bước 5 – Presence Service, Trade-off và mở rộng hệ thống

Ở phần trước, chúng ta đã giải quyết được bài toán khó nhất của một hệ thống Chat:

  • Chọn Database phù hợp.
  • Thiết kế Message ID.
  • Gửi tin nhắn thời gian thực.
  • Xử lý người dùng Online và Offline.
  • Chat nhóm.

Đến đây, tưởng như hệ thống đã hoàn chỉnh.

Nhưng nếu bạn mở Messenger ngay lúc này, bạn sẽ thấy một tính năng tưởng chừng rất nhỏ nhưng lại cực kỳ khó xây dựng.

🟢 Dấu chấm xanh.

Bạn bè của bạn vừa online. Vài phút sau họ offline.

Có người vừa mở Messenger là dấu chấm xanh xuất hiện ngay.

Có người tắt điện thoại nhưng phải gần một phút sau mới chuyển sang offline.

Tại sao lại như vậy?

Đó chính là bài toán Presence Service.


1. Bài toán Presence

Giả sử Bob vừa mở Messenger.

Làm thế nào Server biết Bob đang online?

Một ý tưởng đơn giản là:

Bob Login
↓
Status = Online

Khi Bob bấm Logout.

Logout
↓
Status = Offline

Nghe có vẻ hoàn hảo.

Nhưng thực tế Internet không hoạt động như vậy.


2. Điều gì xảy ra nếu mất mạng?

Hãy tưởng tượng Bob đang đi tàu điện.

Đột nhiên tàu chui vào đường hầm. Điện thoại mất sóng. Ứng dụng bị ngắt kết nối.

Bob hoàn toàn không hề bấm Logout.

Nhưng lúc này, server sẽ nghĩ:

Bob vẫn đang Online.

Trong khi thực tế: Bob đã mất kết nối từ lâu.


3. Nếu chuyển Offline ngay lập tức thì sao?

Một ý tưởng khác là: Ngay khi WebSocket bị đứt.

Disconnected
↓
Offline

Điều này cũng không ổn. Hãy tưởng tượng mạng Wi-Fi chập chờn.

Online
↓
Offline
↓
Online
↓
Offline
↓
Online

Dấu chấm xanh sẽ liên tục nhấp nháy.

Trải nghiệm người dùng rất tệ. Rõ ràng, chúng ta cần một giải pháp thông minh hơn.


4. Heartbeat – Nhịp tim của hệ thống

Hầu hết các ứng dụng Chat đều sử dụng một cơ chế gọi là:

Heartbeat.

Ý tưởng rất đơn giản.

Cứ vài giây, ứng dụng sẽ gửi một gói tin rất nhỏ.

Ví dụ:

PING

Server nhận được.

PONG

Hoặc đơn giản chỉ là:

Heartbeat

Nội dung gần như không quan trọng.

Điều quan trọng là:

Server biết Client vẫn còn sống.


Ví dụ

Giả sử.

  • Ứng dụng gửi Heartbeat: 5 giây/lần
  • Server lưu: Last heartbeat 09:00:05
  • 5 giây sau: 09:00:10, lại Heartbeat.
  • Server cập nhật: Last heartbeat 09:00:10

Chu trình cứ tiếp tục.


5. Khi nào chuyển Offline?

Giả sử quy định:

  • Nếu quá 15 giây không nhận được Heartbeat.
  • Server mới chuyển trạng tháiOffline

Ví dụ:

  • 09:00:10 Heartbeat cuối
  • Sau đó không còn Heartbeat.
  • 09:00:25 Offline

Nhờ vậy, mạng chập chờn vài giây sẽ không làm dấu chấm xanh nhấp nháy liên tục.


6. Presence Server

Chúng ta không nên để Chat Server vừa gửi tin nhắn vừa quản lý trạng thái Online.

Điều đó sẽ khiến Chat Server ngày càng phức tạp.

Thay vào đó, ta tách riêng thành một dịch vụ.

          +----------------+
          | Presence Server|
          +--------+-------+
                   |
              Redis Cache

Presence Server chỉ làm đúng một việc:

Quản lý trạng thái Online/Offline.


Redis lưu những gì?

Ví dụ.

user:123
status=Online
last_seen=09:00:10

Bob.

user:456
Offline
last_seen=08:50

Redis rất phù hợp.

Vì:

  • đọc cực nhanh
  • ghi cực nhanh
  • dữ liệu nhỏ
  • không cần JOIN

Chat Server phối hợp với Presence Server

Giả sử Alice gửi tin nhắn cho Bob.

Chat Server cần biết.

Bob đang ở đâu?

Thay vì hỏi Database. Chat Server hỏi Redis.

Bob
↓
Online
↓
Server 12
  • Nếu là Offline Chat Server sẽ gọi Push Notification.
  • Nếu là Online Server biết phải gửi sang Chat Server nào.

7. Làm sao bạn bè biết bạn vừa Online?

Messenger luôn cập nhật rất nhanh. Bob vừa mở ứng dụng.

Alice gần như thấy ngay: 🟢 Bob Online

Làm sao điều đó xảy ra?


Cách đơn giản

Presence Server:

  • Sau khi đổi trạng thái.
  • Có thể gửi thông báo.
Bob Online
↓
Alice
Tom
Jack
Lisa

Nhưng, nếu Bob có 5.000 bạn bè thì sao?

Server phải gửi 5.000 Event chỉ vì một người mở ứng dụng.

Nếu hàng triệu người online cùng lúc, lượng Event sẽ khổng lồ.


8. Publish - Subscribe

Đây là lúc Pub/Sub phát huy tác dụng.

Hãy tưởng tượng, mỗi người dùng có một kênh.

Bob Channel

Bạn bè của Bob: Subscribe

Khi Bob Online, Presence Server chỉ cần:

Publish
↓
Bob Online

Redis Pub/Sub hoặc Kafka.

Sẽ tự chuyển sự kiện tới tất cả Subscriber.

            Bob Online
                 │
      ┌──────────┼──────────┐
      ▼          ▼          ▼
    Alice       Tom        Lisa

Kiến trúc này mở rộng tốt hơn rất nhiều.


9. Nhưng nếu Bob có 10 triệu follower?

Lúc này, Fan-out sẽ cực kỳ đắt.

Đây chính là lý do, Facebook không gửi Presence cho tất cả mọi người.

Thông thường chỉ những người:

  • đang mở cửa sổ chat
  • đang xem danh sách bạn bè

mới nhận Presence realtime.

Những người còn lại sẽ: Pull khi cần

Đây là một trade-off rất quan trọng.


10. Những trade-off trong hệ thống Chat

Một hệ thống lớn gần như không bao giờ có lời giải hoàn hảo.

Mỗi quyết định đều phải đánh đổi.


Trade-off 1: Polling

Ưu điểm: Đơn giản.

Nhược điểm: Rất nhiều request rỗng.

Không phù hợp.


Trade-off 2: Long Polling

Ưu điểm: Ít request hơn.

Nhược điểm:

  • giữ nhiều kết nối
  • vẫn dựa trên HTTP

Không tối ưu cho realtime.


Trade-off 3: WebSocket

Ưu điểm:

  • Realtime
  • Độ trễ thấp
  • Hai chiều

Nhược điểm:

  • Server phải duy trì hàng triệu kết nối.
  • Đòi hỏi nhiều RAM hơn.

Nhưng đây vẫn là lựa chọn tốt nhất.


Trade-off 4: MySQL

Ưu điểm:

  • ACID
  • SQL

Nhược điểm:

  • Khó scale khi dữ liệu đạt hàng trăm tỷ message.

Trade-off 5: Cassandra

Ưu điểm:

  • Scale ngang.
  • Ghi cực nhanh.

Nhược điểm:

  • Không mạnh về JOIN.
  • Không hỗ trợ transaction như RDBMS.
  • Nhưng với hệ thống Chat.

Đó lại là điều chúng ta cần.


Trade-off 6: Group Chat

Một bản Message Copy vào Inbox từng người
Ưu điểm: Ít tốn bộ nhớ Ưu điểm: Đọc cực nhanh
Nhược điểm: Đọc chậm Nhược điểm: Tốn Storage

Alex Xu chọn cách copy vào Inbox từng người này.

Vì nhóm chỉ tối đa: 100 người.

Nếu là nhóm: 100.000 người thì giải pháp sẽ hoàn toàn khác.


11. Nếu hệ thống tăng lên 500 triệu người dùng?

Điều gì sẽ xảy ra nếu Messenger tiếp tục phát triển?

Chúng ta có cần thiết kế lại không?

Câu trả lời là:

Không.

Nếu ngay từ đầu chúng ta xây dựng theo kiến trúc phân tán.

Việc mở rộng khá đơn giản.


Thêm Chat Server

Server1
Server2
Server3

Server1
Server2
Server3
Server4
Server5

Load Balancer sẽ phân phối kết nối mới.


Thêm Database Node

Node1
Node2
Node3

Node1
Node2
Node3
Node4
Node5

Dữ liệu được tự động phân mảnh.


Thêm Presence Server

Nếu Heartbeat quá nhiều. Ta chỉ cần thêm Presence Server.

Redis Cluster.

Redis
↓
Redis Cluster

Thêm Queue

Nếu Message Queue bắt đầu nghẽn.

Kafka: 3 Broker10 Broker

Thông lượng sẽ tăng lên đáng kể.


Tổng kết kiến trúc

Sau tất cả những phân tích trên.

Một hệ thống Chat quy mô lớn sẽ có kiến trúc như sau.

image.png

Mỗi thành phần đều có một trách nhiệm rất rõ ràng:

  • WebSocket mang lại khả năng giao tiếp hai chiều với độ trễ thấp.
  • Chat Server quản lý các kết nối và chuyển tiếp tin nhắn.
  • Message Queue tách việc xử lý thời gian thực khỏi việc lưu trữ, giúp hệ thống chịu tải tốt hơn.
  • Cassandra/HBase lưu lịch sử tin nhắn với khả năng ghi nhanh và mở rộng theo chiều ngang.
  • Presence Server + Redis theo dõi trạng thái Online/Offline thông qua Heartbeat.
  • Push Notification Service đảm bảo người dùng vẫn nhận được thông báo khi không còn kết nối WebSocket.

Kết luận

Qua bài toán thiết kế một hệ thống Chat, chúng ta có thể rút ra một bài học rất quan trọng về System Design:

Thiết kế hệ thống không phải là việc chọn công nghệ, mà là hiểu bài toán và chấp nhận những đánh đổi phù hợp.

Trong suốt bài viết, chúng ta không bắt đầu bằng Kafka, Redis hay Cassandra.

Thay vào đó, chúng ta đi theo đúng quy trình mà Alex Xu luôn nhấn mạnh trong các buổi phỏng vấn:

  1. Hiểu yêu cầu để xác định phạm vi bài toán.
  2. Ước lượng quy mô nhằm biết hệ thống phải chịu tải ở mức nào.
  3. Thiết kế kiến trúc tổng quan trước khi đi vào chi tiết.
  4. Phân tích từng thành phần quan trọng, từ giao thức kết nối, cơ sở dữ liệu đến luồng gửi nhận tin nhắn và Presence Service.
  5. Thảo luận các trade-off và khả năng mở rộng, bởi không có một thiết kế nào là hoàn hảo trong mọi tình huống.

Đó cũng chính là tư duy mà các công ty công nghệ lớn mong muốn nhìn thấy trong một buổi phỏng vấn System Design.


Điều quan trọng nhất sau bài viết này

Nếu có ai hỏi bạn:

"Làm sao Messenger hoạt động?"

Hy vọng câu trả lời của bạn sẽ không còn là:

"Nó dùng WebSocket."

Mà sẽ là:

"Messenger là sự kết hợp của WebSocket để giao tiếp thời gian thực, Chat Server để quản lý kết nối, Message Queue để xử lý bất đồng bộ, Cassandra để lưu lịch sử tin nhắn, Presence Service với Heartbeat để theo dõi trạng thái người dùng, cùng Push Notification để đảm bảo tin nhắn luôn đến được thiết bị ngay cả khi ứng dụng không còn chạy."

Đó mới là góc nhìn của một kỹ sư System Design.


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.