System Design: Cloudflare Serverless Chat App
Thiết kế hệ thống ứng dụng chat thời gian thực quy mô toàn cầu với Cloudflare Workers, Durable Objects, KV và D1.
Xây dựng một ứng dụng trò chuyện thời gian thực (Real-time Chat App) cho hàng triệu người dùng toàn cầu luôn là bài toán hóc chuẩn trong System Design. Với kiến trúc serverless truyền thống như AWS Lambda hay GCP Cloud Functions, việc duy trì kết nối WebSocket lâu dài (long-lived stateful connections) thường rất đắt đỏ hoặc phức tạp vì các hàm serverless vốn có bản chất ngắn hạn (stateless).
Tuy nhiên, với hệ sinh thái Cloudflare Edge Platform, bài toán này được giải quyết triệt me và thanh thoát nhờ sự kết hợp giữa Cloudflare Workers, Durable Objects, Cloudflare KV, và D1 Database.
1. Bài toán: Tại sao Chat App lại khó trên Serverless?#
Hãy tưởng tượng serverless như một quán ăn fast-food: nhân viên (Function) nhận order, làm đồ ăn xong rồi giải tán. Họ không ở lại bàn ăn để nói chuyện với khách cả buổi.
- WebSocket Connection State: WebSockets đòi hỏi kết nối hai chiều liên tục giữa Client và Server. Serverless thông thường ngắt kết nối ngay khi response kết thúc.
- Message Routing & Broadcast: Khi User A gửi tin nhắn trong Phòng 101, làm sao server gửi tin nhắn đó tới đúng User B và User C đang ở các máy chủ edge khác nhau?
- Persistence & History: Lưu trữ lịch sử tin nhắn với độ trễ thấp và chi phí tối ưu.
2. Kiến trúc tổng quan (Architecture Overview)#
Hệ thống của chúng ta tận dụng 4 thành phần cốt lõi của Cloudflare:
- Cloudflare Workers (Edge Gateway): Xử lý HTTP request và bắt đầu bắt tay WebSocket (Handshake) tại Pop (Point of Presence) gần người dùng nhất.
- Durable Objects (DO - Room Controller): Thành phần quan trọng nhất! DO đóng vai trò như một Singleton Actor trên Edge, duy trì kết nối WebSocket và broadcast tin nhắn trong từng phòng chat.
- Cloudflare D1 (SQLite Database): Lưu trữ thông tin người dùng, danh sách phòng, và lịch sử tin nhắn vĩnh viễn.
- Cloudflare KV (Key-Value Store): Caching session authentication và cấu hình với độ trễ siêu thấp.
+--------------+ WebSocket +---------------------+
| User Client | <------------------> | Cloudflare Workers |
+--------------+ +---------------------+
|
Forward / Stub Routing
v
+---------------------+
| Durable Object | (Chat Room State)
| (Room 101 Instance)|
+---------------------+
/ \
v v
+----------------+ +----------------+
| Cloudflare D1 | | Cloudflare KV |
| (Message History)| | (User Session) |
+----------------+ +----------------+text3. Chi tiết triển khai Code (Implementation Details)#
3.1. Durable Object: Quản lý WebSocket Room State#
Mỗi phòng chat (Chat Room) sẽ là một instance của Durable Object. DO đảm bảo tất cả kết nối WebSocket của một phòng được điều hướng về đúng một Coordinator Node.
export class ChatRoom implements DurableObject {
private state: DurableObjectState;
private sessions: Set<WebSocket> = new Set();
constructor(state: DurableObjectState) {
this.state = state;
}
async fetch(request: Request): Promise<Response> {
// Nâng cấp HTTP Request thành WebSocket Connection
const pair = new WebSocketPair();
const [client, server] = Object.values(pair);
this.handleSession(server);
return new Response(null, { status: 101, webSocket: client });
}
private handleSession(ws: WebSocket) {
ws.accept();
this.sessions.add(ws);
ws.addEventListener('message', async (msg) => {
try {
const data = JSON.parse(msg.data as string);
// Broadcast tin nhắn tới tất cả mọi người trong phòng // [!code focus]
this.broadcast(JSON.stringify({
user: data.user,
text: data.text,
timestamp: new Date().toISOString()
}));
// Ghi tin nhắn vào D1 (Async / Non-blocking)
// await this.saveToD1(data);
} catch (err) {
ws.send(JSON.stringify({ error: 'Invalid message payload' }));
}
});
ws.addEventListener('close', () => {
this.sessions.delete(ws);
});
}
private broadcast(message: string) {
for (const session of this.sessions) {
session.send(message);
}
}
}typescript[!NOTE] Durable Objects cho phép lưu trữ In-Memory State an toàn nhờ cơ chế Single-threaded Actor Model, loại bỏ hoàn toàn rủi ro Race Condition khi broadcast tin nhắn.
3.2. Worker Gateway: Điều hướng kết nối#
Worker đóng vai trò làm API Gateway kiểm tra Token và routed request đến đúng Durable Object theo roomId.
export interface Env {
CHAT_ROOM: DurableObjectNamespace;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
if (url.pathname.startsWith('/chat/')) {
const roomId = url.pathname.split('/')[2];
if (!roomId) {
return new Response('Room ID is required', { status: 400 });
}
// Tạo hoặc lấy ID duy nhất cho Durable Object dựa trên name
const id = env.CHAT_ROOM.idFromName(roomId);
const roomObject = env.CHAT_ROOM.get(id);
return roomObject.fetch(request);
}
return new Response('Not Found', { status: 404 });
}
};typescript4. Ưu điểm & Điểm lưu ý (Trade-offs)#
[!TIP] Ưu điểm:
- Zero Cold Starts: Chạy trực tiếp tại Edge (V8 Isolates).
- Global Low Latency: Người dùng ở Việt Nam hay Mỹ đều kết nối tới Pop gần nhất.
- Auto Scale: Tự động mở rộng theo phòng chat mà không lo server hết RAM/CPU.
[!WARNING] Giới hạn:
- Durable Object Limits: Mỗi DO là một single-thread instance. Nếu một phòng chat có >10.000 người gửi tin cùng lúc, ta cần dùng mô hình DO Tree/Hierarchy để scale out.