Bình luận livestream
Hiểu đề
Yêu cầu chức năng
- Người xem có thể gửi bình luận vào một buổi live
- Người xem thấy bình luận mới của người khác trong vòng vài giây
- Người mới vào thấy ngay các bình luận gần nhất và có thể cuộn xem bình luận cũ hơn
- Chủ kênh có thể bật chế độ chậm (slow mode) và ẩn bình luận vi phạm
Yêu cầu phi chức năng
- Bình luận hiện trên màn hình người xem khác p95 < 2 s
- Chịu được buổi live 5 triệu người xem và 5.000 bình luận/s
- Mỗi người xem nhận tối đa khoảng 10 bình luận/s, người gửi luôn thấy bình luận của mình
- Thứ tự bình luận nhất quán giữa mọi người xem; không mất bình luận đã nhận 201
- Gửi bình luận p95 < 300 ms, độ sẵn sàng 99,9%
Ngoài phạm vi: Phát video live (ingest RTMP, transcode, CDN video); Thả tim, quà tặng, donate; Trả lời lồng nhau và reaction cho từng bình luận; Mô hình kiểm duyệt nội dung bằng AI (chỉ nhắc ở mức hook lọc spam).
Thực thể và API
| Method | Endpoint | Ghi chú |
|---|---|---|
| POST | /v1/live/{videoId}/comments | Gửi bình luận, header Idempotency-Key. Trả comment_id để client hiện ngay bản của mình. |
| GET | /v1/live/{videoId}/stream | Mở kết nối SSE (hoặc WebSocket) nhận CommentBatch. Gửi lại Last-Event-ID khi kết nối lại. |
| GET | /v1/live/{videoId}/comments?before={commentId}&limit=50 | Lịch sử, phân trang bằng cursor comment_id. |
| DELETE | /v1/live/{videoId}/comments/{commentId} | Chủ kênh/mod ẩn bình luận; phát sự kiện comment_hidden. |
| PATCH | /v1/live/{videoId}/settings | Bật slow mode (ví dụ 1 bình luận/10 s mỗi người). |
Thiết kế tổng thể
Đi lần lượt từng yêu cầu chức năng. Mục tiêu ở đây là chạy đúng, việc tối ưu để dành cho phần đào sâu.
1) Người xem có thể gửi bình luận
Gửi bình luận là request HTTP thường, tách khỏi kênh nhận. Comment API kiểm tra rate limit, gán ID có thứ tự, lưu rồi phát sự kiện cho phần fan-out.
- 1Client POST bình luận kèm Idempotency-Key, hiện ngay bản của mình ở trạng thái đang gửi
- 2Comment API kiểm tra slow mode và rate limit theo user, lọc spam cơ bản
- 3Gán comment_id kiểu Snowflake (thời gian + máy + số thứ tự)
- 4Lưu vào DB và đẩy vào kênh/stream của video, trả 201
Nguồn: Grokking Modern System Design · ch.19 Rate Limiter ↗ · bài "Design of a Rate Limiter" · mục "Detailed design" ; Grokking Modern System Design · ch.12 Sequencer ↗ · mục "Twitter Snowflake" (ID có timestamp đứng đầu nên sắp được theo thời gian) ; Grokking Modern System Design · ch.23 Distributed Task Scheduler ↗ · mục "Task idempotency" (idempotence key)
2) Người xem thấy bình luận mới trong vài giây
Mỗi người xem giữ một kết nối SSE tới gateway. Gateway subscribe kênh pub/sub của các video mà người xem của nó đang xem, nhận gói bình luận và đẩy xuống từng kết nối.
- 1Client mở GET /stream, gateway ghi nhận kết nối vào danh sách của video
- 2Gateway subscribe kênh video:{id} nếu là người xem đầu tiên của video trên gateway đó
- 3Sampler gom bình luận của video mỗi 0,5-1 s, chọn tối đa ~10/s, publish một CommentBatch
- 4Gateway nhận batch, ghi một frame xuống mọi kết nối của video đó
Nguồn: Grokking Modern System Design · ch.18 Pub-sub ↗ · bài "System Design: The Pub-sub Abstraction" (publisher/subscriber qua topic) ; Node.js Design Patterns 2e ↗ (.zip) · ch.11 Messaging and Integration Patterns · mục "Using Redis as a message broker" (tr. 447) ; Grokking Modern System Design · ch.36 Design WhatsApp ↗ · mục "Connection with a WebSocket server" (kết nối lâu dài thay cho polling) ; Chưa có nguồn trong thư viện: SSE và sampler gom batch 0,5-1 s là thiết kế của khoá học
3) Người mới vào thấy bình luận gần nhất và cuộn xem cũ hơn
50 bình luận gần nhất của mỗi video đang live được giữ sẵn trong Redis để người vào phòng đọc nhanh. Cuộn lên thì phân trang bằng cursor comment_id trên DB chính.
- 1Client gọi GET /comments?limit=50, API đọc list recent:{videoId} trong Redis
- 2Client mở SSE với Last-Event-ID là comment_id mới nhất vừa nhận để không hụt
- 3Cuộn lên: GET /comments?before={id}, DB đọc theo khóa (video_id, comment_id) giảm dần
- 4Client gộp và bỏ trùng theo comment_id
Nguồn: Redis in Action ↗ · ch.3 · §3.2 Lists ; MongoDB: The Definitive Guide 2e ↗ (.zip) · ch.4 Querying · mục "Avoiding Large Skips" (tr. 70) ; Chưa có nguồn trong thư viện: Last-Event-ID của SSE
Hạ tầng theo quy mô
Cùng một sản phẩm, ba mức người dùng. Chọn từng mức để xem sơ đồ, phép tính và cấu hình server thay đổi thế nào.
Livestream bán hàng hoặc lớp học online. Một ứng dụng Node vừa nhận POST vừa giữ kết nối SSE, chạy 2 instance, dùng Redis pub/sub để hai instance thấy bình luận của nhau, Postgres lưu lịch sử.
Ước lượng
- Bình luận: 2.000 người xem × 10% có bình luận × 1 lần/30 s = 200 / 30 ≈ 6,7 bình luận/s
- Gửi hết, không lấy mẫu: 6,7 × 2.000 ≈ 13.333 tin/s; gom batch 0,5 s → 2 × 2.000 = 4.000 frame/s
- Kết nối: 2.000 / 20.000 mỗi instance → 1, chạy 2 để dự phòng
- Lưu trữ: 10.000 DAU × 5% bình luận × 10 lần = 5.000 bình luận/ngày × 300 B = 1,5 MB/ngày ≈ 548 MB/năm
- Redis pub/sub: 6,7 publish/s × 2 subscriber, không đáng kể
Nguồn con số: Grokking Modern System Design · ch.5 Back-of-the-envelope Calculations ↗ · mục "Requests estimation", "Storage requirements", "Bandwidth requirements" ; Quy tắc ước lượng của khoá học · /hoc/chon-server (số kết nối mỗi gateway 20.000-40.000, 600 req/s mỗi instance, 10 MB/s mỗi partition, Redis ×1,3 là giả định của khoá học, cần load test)
Bảng hạ tầng
| Thành phần | Chọn | Số lượng | Cấu hình | Vì sao |
|---|---|---|---|---|
| Load balancer | ALB / Cloud Load Balancing | 1 | Hỗ trợ kết nối dài, idle timeout 1 giờ | SSE cần load balancer không cắt kết nối sau 60 s. |
| App (API + SSE) | Node.js (Fastify), Docker | 2 | 2 vCPU · 4 GB RAM | 2.000 kết nối và 4.000 frame/s dư sức cho 1 instance, chạy 2 để không chết khi 1 cái lỗi. |
| Pub/sub | Redis managed | 1 | 1 vCPU · 1 GB | Để bình luận gửi vào instance A tới được người xem đang nối với instance B. |
| Database | PostgreSQL managed | 1 | 2 vCPU · 8 GB · 20 GB SSD | 6,7 ghi/s và 548 MB/năm là rất nhỏ; index (video_id, comment_id) cho phân trang. |
- Điểm xuất phát: một ứng dụng Node vừa nhận bình luận vừa giữ SSE
- Gửi mọi bình luận xuống mọi người xem, chưa cần lấy mẫu
- Redis pub/sub chỉ để nối 2 instance với nhau
Khi một buổi live có 200.000 người xem, số tin cần đẩy nhân theo bình luận × người xem và vượt xa khả năng của 2 instance. Cần tách gateway, lấy mẫu bình luận và chia kênh theo video.
Cấu hình và chi phí là ước lượng để học và trình bày trong phỏng vấn. Xem cách tính ở bài Chọn server.
So sánh nhanh ba mức
| Thành phần | Mức 1 | Mức 2 | Mức 3 |
|---|---|---|---|
| Người dùng | 10.000 DAU, buổi live lớn nhất 2.000 người xem | 1 triệu DAU; buổi live hot 200.000 người xem, 300.000 người xem đồng thời toàn nền tảng | 50 triệu DAU; trận chung kết 5 triệu người xem, 10 triệu kết nối toàn nền tảng |
| Chi phí | ≈ 100–250 USD/tháng (ước lượng) | ≈ 2.000–5.000 USD/tháng (ước lượng) | ≈ 25.000–60.000 USD/tháng (ước lượng, băng thông xuống và gateway là phần lớn) |
| Load balancer | ALB / Cloud Load Balancing 1 × Hỗ trợ kết nối dài, idle timeout 1 giờ | ALB / Envoy 1 × Định tuyến /stream theo video_id | Chưa cần |
| App (API + SSE) | Node.js (Fastify), Docker 2 × 2 vCPU · 4 GB RAM | Chưa cần | Chưa cần |
| Pub/sub | Redis managed 1 × 1 vCPU · 1 GB | Chưa cần | Redis Cluster sharded pub/sub 6 shard × (1 primary + 1 replica) × 2 vCPU · 8 GB mỗi node |
| Database | PostgreSQL managed 1 × 2 vCPU · 8 GB · 20 GB SSD | PostgreSQL managed (HA) 1 primary + 1 read replica × 4 vCPU · 16 GB · 200 GB SSD | Cassandra / ScyllaDB 6 node × 8 vCPU · 32 GB · 4 TB SSD |
| SSE gateway | Chưa cần | Node.js SSE, container 18 × 2 vCPU · 4 GB RAM | Chưa cần |
| Comment API | Chưa cần | Node.js (NestJS), autoscale 2-4 (autoscale) × 2 vCPU · 4 GB RAM | Node.js trên Kubernetes 34 pod (HPA) × 2 vCPU · 4 GB mỗi pod |
| Sampler | Chưa cần | Node.js worker đọc Redis Stream theo video 2 × 2 vCPU · 4 GB RAM | Node.js consumer group (hoặc Flink) 16 pod × 4 vCPU · 8 GB mỗi pod |
| Redis | Chưa cần | ElastiCache / Memorystore 1 primary + 1 replica × 2 vCPU · 8 GB | Chưa cần |
| Edge + LB | Chưa cần | Chưa cần | GeoDNS + Envoy, 3 region 3 cụm × Định tuyến theo region rồi theo video_id |
| Gateway | Chưa cần | Chưa cần | Node.js (uWebSockets.js) SSE/WebSocket trên Kubernetes 300 pod × 2 vCPU · 4 GB mỗi pod |
| Kafka | Chưa cần | Chưa cần | MSK / Confluent 6 broker × Topic comments 64 partition theo video_id, RF 3 |
| Writer | Chưa cần | Chưa cần | Kafka consumer ghi Cassandra 6 pod × 2 vCPU · 4 GB mỗi pod |
Đào sâu
Mỗi câu hỏi đi từ cách đơn giản đến cách người phỏng vấn mong đợi ở cấp senior.
1) Đẩy bình luận tới hàng triệu người xem đang mở cùng một buổi live thế nào?
Client gọi GET /comments?after=... liên tục. 1 triệu người xem là 500.000 query/s vào DB, phần lớn trả về cùng một kết quả.
Đánh đổi: Dễ làm, ổn cho vài trăm người xem; ở quy mô lớn thì DB sập trước.
Nguồn: Grokking Modern System Design · ch.36 Design WhatsApp ↗ · mục "Connection with a WebSocket server", Point to Ponder "Why is WebSocket preferred over HTTP(S)?" (polling tốn tài nguyên và tăng độ trễ)
Comment API publish mọi bình luận lên một kênh, mọi gateway subscribe và tự lọc theo video của kết nối mình đang giữ. Đây là mẫu chat dùng Redis làm broker trong Node.js Design Patterns.
Đánh đổi: Mỗi gateway nhận bình luận của mọi video trên nền tảng; thêm gateway là thêm tải cho mọi gateway.
Nguồn: Node.js Design Patterns 2e ↗ (.zip) · ch.11 Messaging and Integration Patterns · mục "Running and scaling the chat application" (tr. 446), "Using Redis as a message broker" (tr. 447)
Mỗi video một kênh (Redis sharded pub/sub hoặc topic chia theo video_id, Grokking ch.18). Gateway subscribe kênh khi có người xem đầu tiên và hủy khi người cuối rời đi. Load balancer định tuyến theo video_id để người xem của video nhỏ dồn vào ít gateway; video lớn thì trải ra mọi gateway, nên một batch chỉ cần gửi tới vài trăm gateway rồi mỗi gateway tự fan-out xuống hàng chục nghìn kết nối. SSE đủ vì nhận là một chiều, gửi đi bằng POST thường, chạy trên HTTP/2 và tự kết nối lại với Last-Event-ID.
Đánh đổi: Gateway có trạng thái (kết nối, danh sách subscribe) nên deploy phải xả kết nối dần; khi một gateway chết, hàng chục nghìn client kết nối lại cùng lúc cần backoff ngẫu nhiên.
Nguồn: Grokking Modern System Design · ch.18 Pub-sub ↗ · bài "Design of a Pub-sub System" (topic, partition, broker) ; Grokking Modern System Design · ch.36 Design WhatsApp ↗ · mục "Connection with a WebSocket server" (WebSocket manager ánh xạ user và server) ; Chưa có nguồn trong thư viện: Redis sharded pub/sub, SSE trên HTTP/2, định tuyến LB theo video_id
2) Một buổi live có 5.000 bình luận/s. Làm sao để người xem đọc được và hệ thống không quá tải?
5.000 bình luận/s × 5 triệu người xem là 25 tỷ tin nhắn/s. Không ai đọc kịp 5.000 dòng mỗi giây, màn hình chỉ là một dòng chữ chạy.
Đánh đổi: Tốn băng thông và CPU nhất mà trải nghiệm vẫn tệ nhất.
⚠ Chưa có nguồn trong thư viện: phép tính fan-out 5.000 × 5 triệu là số học thuần túy trên giả định của bài
Mỗi người tối đa 1 bình luận/3 s (token bucket, Grokking ch.19), chủ kênh bật slow mode khi cần. Phía phát lấy ngẫu nhiên tối đa 10 bình luận/s mỗi video, phần còn lại vẫn lưu nhưng không đẩy real-time.
Đánh đổi: Giảm tải hiệu quả nhưng chọn ngẫu nhiên nên bình luận của chủ kênh, mod hay bạn bè cũng có thể bị bỏ.
Nguồn: Grokking Modern System Design · ch.19 Rate Limiter ↗ · bài "Rate Limiter Algorithms" · mục "Token bucket algorithm" ; Grokking Modern System Design · ch.19 Rate Limiter ↗ · bài "Requirements of a Rate Limiter's Design" · mục "Types of throttling"
Mỗi video có một sampler (một partition theo video_id) gom bình luận trong 0,5-1 s, chấm điểm (chủ kênh, mod, tài khoản xác minh, không phải spam, nội dung không trùng lặp) và chọn tối đa ~10/s vào CommentBatch. Gateway chèn thêm bình luận của bạn bè người xem, còn bình luận của chính người gửi luôn hiện nhờ hiện ngay ở client. Video nhỏ dưới ngân sách thì đi thẳng không lấy mẫu. Mọi bình luận vẫn được lưu đủ cho lịch sử và replay.
Đánh đổi: Người xem thấy tập bình luận khác nhau một chút và người gửi có thể nghĩ bình luận của mình bị ẩn; cần trạng thái sampler theo video và xử lý khi sampler chuyển máy.
⚠ Chưa có nguồn trong thư viện: lấy mẫu bình luận theo ngân sách và chấm điểm ưu tiên là thiết kế của khoá học, thư viện không có sách mô tả kỹ thuật này
3) Giữ thứ tự bình luận nhất quán và phân trang lịch sử thế nào?
Đồng hồ các server lệch nhau vài chục ms nên hai người thấy thứ tự khác nhau. OFFSET 10.000 phải quét qua 10.000 hàng, và bình luận mới chèn vào làm trang bị lệch, lặp hoặc hụt.
Đánh đổi: Chạy được cho bình luận bài viết thường, không hợp với luồng ghi liên tục.
Nguồn: Grokking Modern System Design · ch.12 Sequencer ↗ · mục "Use UNIX time stamps" và phần "Time in a Distributed System" (đồng hồ vật lý lệch nhau) ; MongoDB: The Definitive Guide 2e ↗ (.zip) · ch.4 Querying · mục "Avoiding Large Skips" (tr. 70)
comment_id = thời gian ms + id máy + số thứ tự (Grokking ch.12 Sequencer), sắp xếp được theo thời gian. Truy vấn WHERE video_id = ? AND comment_id < ? ORDER BY comment_id DESC LIMIT 50 dùng index nên luôn nhanh, không bị lệch trang.
Đánh đổi: Thứ tự vẫn chỉ gần đúng giữa các máy cùng một ms, nhưng mọi người xem thấy cùng một thứ tự vì cùng sắp theo ID.
Nguồn: Grokking Modern System Design · ch.12 Sequencer ↗ · mục "Twitter Snowflake" (Pros: sắp được theo thời gian) ; MongoDB: The Definitive Guide 2e ↗ (.zip) · ch.4 Querying · mục "Avoiding Large Skips" (tr. 70, phân trang theo khóa thay cho skip)
Bình luận của một video đi vào cùng một partition Kafka (khóa video_id), sampler và writer đọc theo đúng thứ tự đó nên thứ tự phát ra và thứ tự lưu trùng nhau. Lưu trong Cassandra với partition key (video_id, giờ) để một video lớn không thành partition khổng lồ, clustering theo comment_id giảm dần. Client gộp theo comment_id và bỏ trùng; kết nối lại thì gửi Last-Event-ID để nhận phần thiếu. Khi xem lại video, bình luận được phát theo offset so với lúc bắt đầu live.
Đánh đổi: Một video cực hot dồn vào một partition, cần đủ năng lực cho partition đó (5.000 × 300 B = 1,5 MB/s vẫn dư); thứ tự hiển thị có thể lệch nhẹ so với thời điểm người dùng bấm gửi.
Nguồn: Grokking Modern System Design · ch.18 Pub-sub ↗ · bài "Design of a Pub-sub System" · Points to Ponder về strict ordering (tin cùng khóa vào cùng partition giữ thứ tự) ; Grokking Modern System Design · ch.9 Databases ↗ · mục "Columnar database", "Sharding" (partition key) ; Chưa có nguồn trong thư viện: Kafka và mô hình (video_id, giờ) của Cassandra
Trong phỏng vấn
- “Tôi tách gửi và nhận: gửi là POST thường, nhận là SSE vì luồng chỉ một chiều.”
- “Chi phí nằm ở fan-out bình luận × người xem, nên tôi lấy mẫu khoảng 10 bình luận/s mỗi video và gom batch.”
- “Pub/sub chia theo video, gateway chỉ subscribe video mà người xem của nó đang xem.”
- “Thứ tự do partition theo video_id quyết định, lịch sử phân trang bằng cursor comment_id.”
- Đẩy mọi bình luận tới mọi người xem rồi mới phát hiện con số nhân lên hàng tỷ.
- Cho mọi gateway subscribe mọi bình luận của cả nền tảng.
- Phân trang bằng OFFSET hoặc sắp theo đồng hồ của từng server.
- Quên đợt kết nối lại hàng loạt khi một gateway hoặc region chết.
Đọc thêm
Sách trong thư viện của bạn. Số chương có thể lệch nhẹ so với bản PDF.