System Design: Chat Real-time quy mô triệu user

Posted on: 4/21/2026 8:15:59 PM

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ế.

100B+ Tin nhắn/ngày trên WhatsApp
<200ms Latency yêu cầu cho chat
10M+ WebSocket connections/server
99.99% Uptime SLA tối thiểu

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:

  1. WS Gateway nhận message từ A, gán messageId (Snowflake ID — đảm bảo unique + sortable theo thời gian) và timestamp
  2. Message Service validate, enqueue vào Kafka topic messages
  3. Consumer persist vào database (write-ahead), gửi ACK lại cho sender
  4. Router tra Redis để tìm WebSocket server của user B
  5. Nếu B online: push qua WebSocket → B gửi delivery receipt → update status thành delivered
  6. 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:

  1. Lưu trạng thái vào Redis với TTL (key: presence:{userId}, value: online, TTL: 60s)
  2. Heartbeat mỗi 30s refresh TTL — nếu hết TTL, tự động chuyển offline
  3. Chỉ query presence khi user mở conversation, không broadcast toàn bộ contact list
  4. 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

WhatsApp — Erlang/FreeBSD
WhatsApp nổi tiếng vì team chỉ 50 engineer phục vụ 2 tỷ user. Bí quyết: Erlang VM (BEAM) được thiết kế cho telecom — xử lý hàng triệu lightweight processes, hot code reload, fault-tolerant. Mỗi server WhatsApp handle 2-3 triệu connections.
Telegram — MTProto + Bare-metal
Telegram tự phát triển MTProto protocol thay vì dùng TLS tiêu chuẩn, tối ưu cho mobile (ít round-trip, packet nhỏ). Infrastructure chạy trên bare-metal server, không cloud — giảm latency và chi phí ở scale lớn.
Discord — Elixir + Rust
Discord bắt đầu với Elixir (BEAM VM, giống Erlang) cho WebSocket gateway, sau đó rewrite hot path sang Rust để tối ưu memory. Một server Discord xử lý lên đến 1 triệu concurrent connections.
Slack — Java + WebSocket
Slack dùng Java cho backend chính, WebSocket cho real-time messaging, MySQL cho persistence (sharded). Khi scale lên, Slack gặp vấn đề với MySQL sharding và chuyển dần sang Vitess (MySQL proxy).

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: