System Design: Chat Real-time quy mô triệu user
Posted on: 4/21/2026 8:15:59 PM
Table of contents
- Tại sao thiết kế Chat System là bài toán khó?
- Phân tích yêu cầu hệ thống
- Kiến trúc tổng quan
- WebSocket Gateway — Trái tim của hệ thống
- Message Routing — Đưa tin nhắn đến đúng người
- Database Design — Lưu trữ hàng tỷ tin nhắn
- Presence System — "Online", "Last seen", "Typing..."
- Push Notification cho Offline Users
- End-to-End Encryption (E2EE)
- Scaling Strategies
- Tech Stack gợi ý cho Production
- Monitoring & Observability
- Các bài học thực tế từ WhatsApp và Telegram
- Tổng kết
Tại sao thiết kế Chat System là bài toán khó?
Mỗi ngày, WhatsApp xử lý hơn 100 tỷ tin nhắn, Telegram phục vụ 900 triệu người dùng hoạt động. Đằng sau giao diện đơn giản — gõ, gửi, nhận — là một kiến trúc phân tán cực kỳ phức tạp phải giải quyết đồng thời: truyền tin real-time với độ trễ dưới 200ms, đảm bảo tin nhắn không bao giờ mất, scale lên hàng triệu kết nối đồng thời, và hỗ trợ group chat lên đến 200.000 thành viên.
Bài viết này sẽ đi sâu vào kiến trúc của một hệ thống Chat real-time quy mô lớn — từ WebSocket connection management, message routing, database design cho đến presence system và end-to-end encryption — dưới góc nhìn System Design cho production thực tế.
Phân tích yêu cầu hệ thống
Functional Requirements
- 1:1 Chat: Gửi và nhận tin nhắn real-time giữa hai người dùng
- Group Chat: Hỗ trợ nhóm lên đến hàng nghìn thành viên
- Online/Offline Status: Hiển thị trạng thái hoạt động của user
- Read Receipts: Xác nhận tin nhắn đã gửi, đã nhận, đã đọc (sent → delivered → read)
- Push Notification: Thông báo khi user offline
- Media Sharing: Gửi ảnh, video, file đính kèm
- Message History: Lưu trữ và đồng bộ lịch sử tin nhắn đa thiết bị
Non-functional Requirements
- Low Latency: End-to-end delivery dưới 200ms cho 99th percentile
- High Availability: 99.99% uptime (tối đa 52 phút downtime/năm)
- Message Ordering: Tin nhắn phải hiển thị đúng thứ tự trong conversation
- At-least-once Delivery: Không được mất tin nhắn, chấp nhận duplicate (client dedup)
- Scalability: Scale horizontal lên 50M+ concurrent connections
Estimation nhanh
Giả sử 50 triệu DAU, mỗi user gửi trung bình 40 tin/ngày → 2 tỷ tin/ngày → ~23.000 tin/giây. Peak load gấp 3x → ~70.000 tin/giây. Mỗi tin nhắn trung bình 200 bytes → storage ~400GB/ngày raw messages, chưa tính metadata và index.
Kiến trúc tổng quan
graph TB
subgraph Clients["📱 Clients"]
C1[Mobile App]
C2[Web App]
C3[Desktop App]
end
subgraph Edge["Edge Layer"]
LB[Load Balancer
Sticky Sessions]
end
subgraph WS["WebSocket Gateway Cluster"]
WS1[WS Server 1
~500K connections]
WS2[WS Server 2
~500K connections]
WS3[WS Server N
~500K connections]
end
subgraph Services["Application Services"]
MS[Message Service]
PS[Presence Service]
GS[Group Service]
NS[Notification Service]
SYNC[Sync Service]
end
subgraph MQ["Message Queue"]
K[Kafka / RabbitMQ]
end
subgraph Storage["Data Layer"]
DB[(Message DB
Cassandra/ScyllaDB)]
CACHE[(Session Cache
Redis Cluster)]
S3[Media Storage
S3/R2]
IDX[(Search Index
Elasticsearch)]
end
C1 & C2 & C3 --> LB
LB --> WS1 & WS2 & WS3
WS1 & WS2 & WS3 --> MS
MS --> K
K --> DB
K --> NS
WS1 & WS2 & WS3 --> PS
PS --> CACHE
MS --> GS
NS --> C1 & C2 & C3
SYNC --> DB
style LB fill:#e94560,stroke:#fff,color:#fff
style K fill:#2c3e50,stroke:#fff,color:#fff
style DB fill:#16213e,stroke:#fff,color:#fff
style CACHE fill:#e94560,stroke:#fff,color:#fff
Kiến trúc tổng quan hệ thống Chat real-time quy mô lớn
Kiến trúc chia thành 4 tầng chính: Edge Layer xử lý kết nối và phân tải; WebSocket Gateway duy trì kết nối persistent với client; Application Services xử lý business logic; và Data Layer lưu trữ tin nhắn, session, media.
WebSocket Gateway — Trái tim của hệ thống
WebSocket là giao thức nền tảng cho chat real-time vì nó duy trì kết nối hai chiều (bidirectional) liên tục giữa client và server, khác với HTTP request-response truyền thống. Khi user mở app, một WebSocket connection được thiết lập và giữ mở trong suốt phiên sử dụng.
Connection Management
Mỗi WebSocket server có thể quản lý 500K–1M kết nối đồng thời nhờ sử dụng event-driven I/O (epoll trên Linux). Với 50 triệu concurrent connections, cần khoảng 50–100 WebSocket server.
sequenceDiagram
participant C as Client
participant LB as Load Balancer
participant WS as WS Gateway
participant R as Redis (Session)
participant MS as Message Service
C->>LB: WebSocket Handshake
LB->>WS: Route (Sticky by User ID)
WS->>R: Register session
{userId: ws-server-3, connId: abc123}
R-->>WS: OK
WS-->>C: Connection Established
Note over C,WS: Heartbeat mỗi 30s để giữ connection
C->>WS: Send Message {to: user_B, text: "Xin chào"}
WS->>MS: Route message
MS->>R: Lookup user_B session
R-->>MS: {server: ws-server-7, connId: xyz789}
MS->>WS: Deliver to ws-server-7
WS->>C: Message delivered ✓
Flow gửi tin nhắn 1:1 qua WebSocket Gateway
Sticky Sessions vs Stateless Routing
Load Balancer dùng consistent hashing theo User ID để route WebSocket connection về cùng server. Khi server crash, chỉ user trên server đó bị ảnh hưởng và tự động reconnect vào server mới. Redis lưu mapping userId → wsServerId để các service khác biết gửi message đến đâu.
Heartbeat & Reconnection
Client gửi heartbeat (ping/pong) mỗi 30 giây. Nếu server không nhận được heartbeat trong 90 giây, connection được đánh dấu dead và session bị xóa khỏi Redis. Client-side có retry logic với exponential backoff: 1s → 2s → 4s → 8s → tối đa 30s.
Message Routing — Đưa tin nhắn đến đúng người
1:1 Chat Flow
Khi user A gửi tin nhắn cho user B:
- WS Gateway nhận message từ A, gán
messageId(Snowflake ID — đảm bảo unique + sortable theo thời gian) và timestamp - Message Service validate, enqueue vào Kafka topic
messages - Consumer persist vào database (write-ahead), gửi ACK lại cho sender
- Router tra Redis để tìm WebSocket server của user B
- Nếu B online: push qua WebSocket → B gửi delivery receipt → update status thành
delivered - Nếu B offline: message nằm trong inbox, trigger push notification qua FCM/APNs
Vấn đề Message Ordering
Trong hệ thống phân tán, message có thể đến không đúng thứ tự. Giải pháp: dùng Snowflake ID hoặc Lamport timestamp — client sắp xếp message theo ID thay vì thời gian server nhận. Kafka partition theo conversationId đảm bảo thứ tự trong cùng conversation.
Group Chat — Bài toán Fan-out
Group chat là thách thức lớn nhất về mặt scale. Khi 1 user gửi tin vào group 10.000 thành viên, hệ thống phải fan-out message đến tất cả — đây gọi là write amplification.
| Strategy | Mô tả | Ưu điểm | Nhược điểm |
|---|---|---|---|
| Fan-out on Write | Copy message vào inbox mỗi member ngay khi gửi | Read nhanh, đơn giản cho client | Write amplification lớn với group lớn |
| Fan-out on Read | Lưu 1 bản, member tự query khi mở chat | Write hiệu quả, tiết kiệm storage | Read chậm với group lớn, phức tạp hơn |
| Hybrid | Fan-out on Write cho group nhỏ (<500), Read cho group lớn | Cân bằng tốt nhất | Logic phức tạp hơn |
graph LR
subgraph Write["Fan-out on Write"]
A1[User gửi msg] --> B1[Message Service]
B1 --> C1[Copy → Inbox User 1]
B1 --> D1[Copy → Inbox User 2]
B1 --> E1[Copy → Inbox User N]
end
subgraph Read["Fan-out on Read"]
A2[User gửi msg] --> B2[Message Service]
B2 --> C2[Lưu 1 bản
trong Group Store]
D2[User 1 mở chat] --> C2
E2[User 2 mở chat] --> C2
end
style B1 fill:#e94560,stroke:#fff,color:#fff
style B2 fill:#e94560,stroke:#fff,color:#fff
style C2 fill:#2c3e50,stroke:#fff,color:#fff
So sánh hai chiến lược fan-out cho group chat
WhatsApp dùng fan-out on write vì group tối đa 1.024 thành viên — write amplification chấp nhận được. Telegram dùng hybrid: fan-out on write cho group nhỏ, fan-out on read cho supergroup lên đến 200.000 thành viên.
Database Design — Lưu trữ hàng tỷ tin nhắn
Chọn database phù hợp
Chat message có đặc tính: write-heavy (ghi liên tục), recent-read-heavy (đọc chủ yếu tin mới), append-only (không sửa, ít xóa), time-series-like (query theo thời gian). Các đặc tính này phù hợp với wide-column store hơn RDBMS truyền thống.
| Database | Write Throughput | Read Pattern | Use Case |
|---|---|---|---|
| Cassandra / ScyllaDB | Rất cao (100K+ writes/s) | Partition key scan | Message storage chính |
| PostgreSQL | Trung bình | Linh hoạt (index, join) | User profiles, settings |
| Redis | Cực cao (in-memory) | Key-value, sorted set | Session, presence, recent messages cache |
| S3 / R2 | Cao | Object key | Media files (ảnh, video, file) |
Schema thiết kế cho Cassandra
-- Bảng message: partition by conversation, cluster by time
CREATE TABLE messages (
conversation_id UUID,
message_id BIGINT, -- Snowflake ID (time-sortable)
sender_id UUID,
message_type TEXT, -- 'text', 'image', 'video', 'file'
content TEXT,
media_url TEXT,
status TEXT, -- 'sent', 'delivered', 'read'
created_at TIMESTAMP,
PRIMARY KEY (conversation_id, message_id)
) WITH CLUSTERING ORDER BY (message_id DESC);
-- Bảng conversation inbox: mỗi user có list conversations
CREATE TABLE user_conversations (
user_id UUID,
last_message_at TIMESTAMP,
conversation_id UUID,
conversation_type TEXT, -- '1:1' hoặc 'group'
last_message_preview TEXT,
unread_count INT,
PRIMARY KEY (user_id, last_message_at, conversation_id)
) WITH CLUSTERING ORDER BY (last_message_at DESC);
Tại sao dùng Snowflake ID thay vì UUID?
Snowflake ID (64-bit) gồm: 41 bits timestamp + 10 bits machine ID + 12 bits sequence number. Ưu điểm: sortable theo thời gian (không cần thêm cột timestamp để sort), nhỏ hơn UUID (8 bytes vs 16 bytes), và generate cực nhanh mà không cần database roundtrip. Discord, Twitter, Instagram đều dùng biến thể của Snowflake ID.
Hot/Cold Storage Tiering
Không phải tất cả tin nhắn đều được truy cập thường xuyên. Chiến lược phân tầng:
- Hot (0-7 ngày): Redis sorted set — truy vấn cực nhanh, lưu 50 tin gần nhất mỗi conversation
- Warm (7-90 ngày): Cassandra/ScyllaDB — storage chính, SSD-backed
- Cold (90+ ngày): Object storage (S3) dạng compressed Parquet — chi phí cực thấp, query khi cần qua Spark/Presto
Presence System — "Online", "Last seen", "Typing..."
Presence system theo dõi trạng thái real-time của user: online, offline, last seen, typing. Đây là tính năng tưởng đơn giản nhưng scale rất khó vì mỗi thay đổi trạng thái phải broadcast cho tất cả contact của user đó.
stateDiagram-v2
[*] --> Online: WebSocket connected
Online --> Away: Không hoạt động > 5 phút
Away --> Online: User tương tác
Online --> Offline: Disconnect / Heartbeat timeout
Away --> Offline: Heartbeat timeout
Offline --> Online: Reconnect
Online --> Typing: Bắt đầu gõ
Typing --> Online: Ngừng gõ > 3s
Typing --> Online: Gửi message
State machine của Presence System
Giải pháp scale Presence
Cách naive: mỗi khi user thay đổi status, broadcast cho tất cả friend. Nếu user có 500 friend online → 500 WebSocket push. 1 triệu user đổi status/phút → 500 triệu pushes/phút. Không khả thi.
Cách thông minh — Lazy Presence:
- Lưu trạng thái vào Redis với TTL (key:
presence:{userId}, value:online, TTL: 60s) - Heartbeat mỗi 30s refresh TTL — nếu hết TTL, tự động chuyển offline
- Chỉ query presence khi user mở conversation, không broadcast toàn bộ contact list
- Subscribe presence changes của visible contacts qua Redis Pub/Sub channel
WhatsApp Presence: "Last seen"
WhatsApp không broadcast online status liên tục. Thay vào đó, khi user A mở chat với B, app mới query "last seen" của B. Đây là thiết kế pull-based, tiết kiệm bandwidth rất nhiều so với push-based. "Typing..." indicator chỉ gửi cho user đang xem conversation đó.
Push Notification cho Offline Users
Khi receiver offline, message được persist vào database và trigger push notification qua FCM (Android) hoặc APNs (iOS). Thiết kế cần lưu ý:
- Batching: Gom nhiều tin nhắn thành 1 notification nếu đến trong khoảng ngắn ("Bạn có 5 tin nhắn mới từ Anh Tú")
- Priority: Tin nhắn 1:1 → high priority (hiển thị ngay), group chat → normal priority (có thể bị delay bởi OS)
- Unread sync: Khi user mở lại app, Sync Service pull tất cả tin chưa đọc từ database, không dựa vào push notification
- Deduplication: Client dùng
messageIdđể dedup — nếu đã nhận qua WebSocket rồi thì bỏ qua notification
End-to-End Encryption (E2EE)
E2EE đảm bảo chỉ sender và receiver đọc được nội dung tin nhắn — server chỉ thấy ciphertext. WhatsApp, Signal dùng Signal Protocol (Double Ratchet Algorithm + X3DH key agreement).
sequenceDiagram
participant A as Alice
participant S as Server
participant B as Bob
Note over A,B: Key Exchange (X3DH)
A->>S: Đăng ký Identity Key + Signed Pre-Key + One-time Pre-Keys
B->>S: Đăng ký Identity Key + Signed Pre-Key + One-time Pre-Keys
A->>S: Request Bob's Pre-Key Bundle
S-->>A: Bob's keys (Identity + Signed Pre + One-time)
Note over A: Tính Shared Secret từ
X3DH key agreement
A->>S: Encrypted message (ciphertext)
S->>B: Forward ciphertext
Note over B: Tính Shared Secret
→ Decrypt message
Note over A,B: Double Ratchet: mỗi message
dùng key khác nhau
Flow End-to-End Encryption với Signal Protocol
Tại sao Double Ratchet?
Forward secrecy: Nếu key hiện tại bị lộ, attacker không thể decrypt tin nhắn cũ vì mỗi message dùng key riêng, derived từ ratchet chain. Post-compromise security: Sau khi key bị lộ, hệ thống tự "heal" bằng cách tạo key mới qua DH ratchet ở message tiếp theo.
Scaling Strategies
WebSocket Gateway Scaling
WebSocket gateway là stateful (giữ connection), nên không thể scale đơn giản như stateless HTTP service. Chiến lược:
- Horizontal scaling: Thêm server, dùng consistent hashing để phân bổ user. Khi thêm/bớt server, chỉ ~1/N user cần reconnect
- Connection draining: Khi cần shutdown server, báo client reconnect dần (graceful) thay vì cắt đột ngột
- Regional deployment: Deploy WebSocket gateway ở nhiều region, route user đến server gần nhất bằng GeoDNS
Message Database Sharding
graph LR
subgraph Shard["Sharding by conversation_id"]
M[Message] --> H{Hash
conversation_id}
H -->|hash % 4 = 0| S0[Shard 0]
H -->|hash % 4 = 1| S1[Shard 1]
H -->|hash % 4 = 2| S2[Shard 2]
H -->|hash % 4 = 3| S3[Shard 3]
end
style H fill:#e94560,stroke:#fff,color:#fff
style S0 fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
style S1 fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
style S2 fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
style S3 fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
Message database sharding theo conversation_id
Shard key chọn conversation_id thay vì user_id vì: tất cả message trong cùng conversation nằm trên 1 shard → query "lấy 50 tin mới nhất của conversation X" chỉ hit 1 shard, không cần scatter-gather.
Tech Stack gợi ý cho Production
| Component | Technology | Lý do |
|---|---|---|
| WebSocket Gateway | .NET 10 (Kestrel) / Go | Kestrel xử lý hàng trăm nghìn concurrent connections với memory footprint thấp |
| Message Queue | Apache Kafka | Throughput cao, partition-based ordering, replay capability |
| Message Store | ScyllaDB (Cassandra-compatible) | Write throughput cực cao, linear scalability, CQL quen thuộc |
| Session/Presence Cache | Redis Cluster | Sub-millisecond latency, Pub/Sub cho presence, TTL tự động cleanup |
| User/Group Metadata | PostgreSQL | ACID compliance cho user profiles, group settings, permissions |
| Media Storage | S3 / Cloudflare R2 | S3-compatible, R2 không tính phí egress — lý tưởng cho ảnh/video chat |
| Search | Elasticsearch / Meilisearch | Full-text search trên message history |
| Push Notification | FCM + APNs | Platform-native, tin cậy, miễn phí |
Monitoring & Observability
Với hệ thống real-time, monitoring là sống còn. Các metric quan trọng cần track:
- Message delivery latency (P50, P95, P99): Thời gian từ khi sender gửi đến receiver nhận — target P99 < 200ms
- WebSocket connection count per server: Alert khi vượt 80% capacity
- Message queue lag: Consumer lag trên Kafka — nếu tăng liên tục là dấu hiệu bottleneck
- Failed delivery rate: Tỷ lệ tin nhắn không delivery được sau 3 retry
- Database write latency: P99 write latency của Cassandra — target < 10ms
Distributed Tracing cho Message Flow
Gắn traceId vào mỗi message từ khi sender gửi, propagate qua WebSocket Gateway → Kafka → Consumer → Database → Receiver. Dùng OpenTelemetry + Jaeger/Tempo để trace toàn bộ message journey — khi user report "tin nhắn gửi chậm", bạn có thể trace chính xác bottleneck nằm ở đâu.
Các bài học thực tế từ WhatsApp và Telegram
Tổng kết
Thiết kế hệ thống Chat real-time quy mô lớn đòi hỏi sự cân nhắc kỹ lưỡng ở nhiều tầng: từ WebSocket connection management với consistent hashing và heartbeat, message routing với fan-out strategy phù hợp, database design tối ưu cho write-heavy workload, đến presence system tiết kiệm bandwidth và E2E encryption đảm bảo quyền riêng tư.
Không có giải pháp "one-size-fits-all" — WhatsApp chọn Erlang và fan-out on write vì group nhỏ, Telegram chọn MTProto và hybrid fan-out vì supergroup lớn, Discord rewrite sang Rust vì memory pressure. Điểm chung là: hiểu rõ tradeoff ở từng quyết định kiến trúc, thiết kế cho failure (network partition, server crash), và luôn đo lường bằng metric thực tế thay vì phỏng đoán.
Nguồn tham khảo:
OAuth 2.1 và OpenID Connect: Xác thực API đúng cách năm 2026
Server-Sent Events — Xây dựng Real-time Dashboard với .NET 10, Vue 3 & Redis
Disclaimer: The opinions expressed in this blog are solely my own and do not reflect the views or opinions of my employer or any affiliated organizations. The content provided is for informational and educational purposes only and should not be taken as professional advice. While I strive to provide accurate and up-to-date information, I make no warranties or guarantees about the completeness, reliability, or accuracy of the content. Readers are encouraged to verify the information and seek independent advice as needed. I disclaim any liability for decisions or actions taken based on the content of this blog.