Harbor

Ứng dụng chat realtime

KhóWebSocketPub/subThứ tự tin nhắnGhi nhiềuGiống Zalo, WhatsApp, Messenger

Hiểu đề

Ứng dụng chat realtime là gì? Ứng dụng nhắn tin 1-1 và nhóm, tin đến gần như tức thì khi người nhận đang online và được giữ lại khi họ offline. Người dùng mở app nhiều lần mỗi ngày, nên kết nối lâu dài và độ trễ thấp là trọng tâm.

Yêu cầu chức năng

  1. Người dùng có thể gửi tin nhắn 1-1 cho người khác
  2. Người dùng có thể chat trong nhóm (tối đa 1.000 thành viên)
  3. Người dùng nhận được tin gửi lúc mình offline khi mở lại app, đúng thứ tự
  4. Người dùng thấy trạng thái đã gửi, đã nhận, đã đọc
  5. Người dùng có thể gửi ảnh, video, file

Yêu cầu phi chức năng

  • Tin đến người nhận đang online p95 < 300 ms trong cùng region
  • Tin đã được server ack thì không mất (ghi ít nhất 2 bản sao trước khi ack)
  • Thứ tự tin trong một cuộc trò chuyện giống nhau 100% trên mọi thiết bị
  • Sẵn sàng 99,95%, gateway chết thì client tự kết nối lại trong < 10 s
  • Đọc lại lịch sử 50 tin gần nhất p95 < 200 ms

Ngoài phạm vi: Gọi thoại, gọi video; Chi tiết giao thức mã hoá đầu cuối (chỉ nhắc ở mức ý tưởng); Tìm kiếm full-text trong lịch sử chat; Story, status, bot.

Thực thể và API

User / Device
Người dùng và từng thiết bị đăng nhập. Mỗi thiết bị có kết nối và con trỏ đồng bộ riêng.
Conversation
Cuộc trò chuyện 1-1 hoặc nhóm, giữ last_seq là số thứ tự tin mới nhất.
Member
Quan hệ user thuộc conversation, kèm last_read_seq và last_delivered_seq.
Message
Tin nhắn, khoá chính (conversation_id, seq). Có client_msg_id để chống gửi trùng, body hoặc media_key.
Connection
Bản ghi tạm trong Redis: user/device đang nối vào gateway nào. Có TTL, mất khi ngắt kết nối.
MethodEndpointGhi chú
WS/v1/wsKết nối lâu dài. Frame: send, ack, read, typing. Server đẩy new_message, receipt.
POST/v1/conversationsTạo chat 1-1 hoặc nhóm, trả về conversation_id
GET/v1/conversations?cursor=Danh sách cuộc trò chuyện, sắp theo tin mới nhất, phân trang bằng cursor
GET/v1/conversations/{id}/messages?after_seq=&limit=50Kéo tin bị lỡ hoặc cuộn lịch sử theo seq
POST/v1/media/upload-urlXin presigned URL để client upload thẳng lên blob store

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 dùng có thể gửi tin nhắn 1-1 cho người khác

Hai người thường nối vào hai gateway khác nhau. Gateway của người gửi không biết người nhận ở đâu, nên cần một registry ghi user đang ở gateway nào và một kênh để chuyển tin giữa các gateway.

  1. 1Client A gửi frame send {conversation_id, client_msg_id, body} qua WebSocket
  2. 2Message service gán seq tiếp theo của cuộc trò chuyện, ghi DB, rồi ack cho A (trạng thái đã gửi)
  3. 3Tra registry thấy B đang ở gateway 7, đẩy tin sang gateway 7
  4. 4Gateway 7 gửi tin xuống B, B ack, server báo A trạng thái đã nhận

Nguồn: Grokking Modern System Design · ch.36 WhatsApp ↗ · Detailed Design of WhatsApp · mục "Connection with a WebSocket server" ; Grokking Modern System Design · ch.36 WhatsApp ↗ · Detailed Design of WhatsApp · mục "Send or receive messages" (WebSocket server tra thông tin người nhận) ; Grokking Modern System Design · ch.36 WhatsApp ↗ · High-level Design of WhatsApp · mục "API design" (Send message) ; Chưa có nguồn trong thư viện: seq theo conversation và connection registry riêng là thiết kế của khoá học (WhatsApp trong Grokking dùng WebSocket manager và Redis)

2) Người dùng nhận được tin gửi lúc mình offline khi mở lại app, đúng thứ tự

Tin luôn được lưu trước khi giao, nên offline chỉ là chưa giao. Mỗi thiết bị nhớ seq cuối đã nhận của từng cuộc trò chuyện, khi online lại thì kéo phần còn thiếu.

  1. 1Tra registry không thấy B, gửi push notification qua FCM/APNs
  2. 2B mở app, nối WebSocket, gọi sync với danh sách {conversation_id: last_seq}
  3. 3Server trả các tin có seq lớn hơn, client sắp theo seq và bỏ trùng
  4. 4Client ack, server cập nhật last_delivered_seq của thiết bị

Nguồn: Grokking Modern System Design · ch.36 WhatsApp ↗ · Detailed Design of WhatsApp · mục "Send or receive messages" (tin của người offline được lưu rồi giao qua push notification khi online lại, xóa sau 30 ngày) ; Grokking Modern System Design · ch.36 WhatsApp ↗ · Requirements of WhatsApp's Design · mục "Functional requirements" (Push notifications) ; Chưa có nguồn trong thư viện: đồng bộ theo last_seq mỗi thiết bị là thiết kế của khoá học

3) Người dùng có thể chat trong nhóm (tối đa 1.000 thành viên)

Tin nhóm chỉ lưu một lần theo conversation. Việc nhân bản ra từng thành viên chỉ xảy ra ở bước giao, chạy bất đồng bộ để người gửi không phải chờ.

  1. 1Message service lưu tin một lần với seq của nhóm và ack người gửi
  2. 2Đẩy sự kiện vào queue/Kafka, key là conversation_id
  3. 3Worker lấy danh sách thành viên (có cache), gom theo gateway rồi đẩy một lần cho mỗi gateway
  4. 4Thành viên offline nhận push notification gộp, khi mở app thì sync như chat 1-1

Nguồn: Grokking Modern System Design · ch.36 WhatsApp ↗ · Detailed Design of WhatsApp · mục "Support for group messages" (group message handler, group service, Kafka) ; Grokking Modern System Design · ch.17 Distributed Messaging Queue ↗ · mục "Motivation" (Improved performance, Granular scalability) ; Grokking Modern System Design · ch.18 Pub-sub ↗ · mục "High-level design"

4) Người dùng có thể gửi ảnh, video, file

File lớn không đi qua WebSocket. Client upload thẳng lên blob store bằng presigned URL, tin nhắn chỉ mang media_key.

  1. 1Client gọi /media/upload-url, nhận URL có hạn 15 phút
  2. 2Client upload lên S3/GCS, có thể kèm hash để bỏ file trùng
  3. 3Client gửi tin chứa media_key và thumbnail nhỏ
  4. 4Người nhận tải file qua CDN

Nguồn: Grokking Modern System Design · ch.36 WhatsApp ↗ · Detailed Design of WhatsApp · mục "Send or receive media files" ; Grokking Modern System Design · ch.20 Blob Store ↗ · mục "High-level design" ; Grokking Modern System Design · ch.11 CDN ↗ · mục "Pull CDN" ; Chưa có nguồn trong thư viện: presigned URL là tính năng của nhà cung cấp object storage

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.

Người dùng
10.000 DAU
Tải đỉnh
≈ 46 tin/s và 3.000 kết nối đỉnh
Chi phí ước lượng
≈ 80–200 USD/tháng (ước lượng)

Hai instance Node vừa giữ WebSocket vừa phục vụ REST, một Postgres managed và một Redis nhỏ để chuyển tin giữa hai instance. Tải còn rất nhỏ, ưu tiên làm đúng thứ tự và ack.

Người dùngNgười dùngLưu trữS3 mediaCân bằng tảiLoad balancerDịch vụChat server ×2CacheRedis pub/subDatabasePostgresBên ngoàiFCM / APNsWebSocketupload trực tiếppublish/subscribelưu tin + seqngười offline

Ước lượng

  • Tin nhắn: 10.000 × 40 tin / 86.400 ≈ 4,6 tin/s, đỉnh ×10 ≈ 46 tin/s
  • Kết nối: 30% × 10.000 = 3.000 WebSocket, 1 instance chịu 20.000 → 1, giữ 2 để dự phòng
  • HTTP (lịch sử, danh sách chat): 10.000 × 30 / 86.400 ≈ 3,5 req/s, đỉnh ×10 ≈ 35 req/s → ceil(35 / 600) = 1, tối thiểu 2
  • Lưu trữ tin: 400.000 tin/ngày × 200 B = 80 MB/ngày × 365 ≈ 29 GB/năm
  • Media: 5% × 400.000 = 20.000 ảnh × 200 KB = 4 GB/ngày ≈ 1,46 TB/năm trên S3

Nguồn con số: Grokking Modern System Design · ch.5 Back-of-the-envelope Calculations ↗ · mục "Examples of Resource Estimation" (Number of servers required, Storage requirements, Bandwidth requirements) ; Grokking Modern System Design · ch.36 WhatsApp ↗ · Requirements of WhatsApp's Design · mục "Resource estimation" (30 ngày lưu tin, 100 B mỗi tin, một server giữ khoảng 10 triệu kết nối nhờ tune sâu) ; Quy tắc ước lượng của khoá học · /hoc/chon-server (1.000 req/s mỗi máy 2 vCPU, chạy tối đa 60% CPU, Redis ×1,3) ; Chưa có nguồn trong thư viện: 40 tin/người/ngày và 30% đang online là giả định của khoá học

Bảng hạ tầng

Thành phầnChọnSố lượngCấu hìnhVì sao
Load balancerALB / Cloud Load Balancing (hỗ trợ WebSocket)1managedKết nối WebSocket tự dính vào một instance suốt phiên, không cần sticky cookie.
Chat serverNode.js (NestJS + ws), Docker22 vCPU · 4 GB RAM3.000 kết nối và 46 tin/s chỉ dùng một phần nhỏ, instance thứ hai để không sập khi deploy.
Pub/subRedis managed11 GBKhi A và B nằm ở hai instance khác nhau, tin đi qua kênh user:{id}. Kèm presence có TTL.
DatabasePostgreSQL managed1 (+ backup hằng ngày)2 vCPU · 8 GB RAM · 100 GB29 GB/năm vừa thoải mái, last_seq của conversation tăng trong cùng transaction với insert tin.
MediaS3 / GCS, presigned URL1 bucket≈ 1,5 TB/nămFile không đi qua server, không bao giờ để trong DB.
PushFCM / APNsdịch vụ ngoàimiễn phíBáo cho người offline có tin mới.
Thay đổi so với mức trước
  • Điểm xuất phát
  • Gộp WebSocket và REST vào một service cho dễ vận hành
  • Dùng Redis pub/sub từ đầu vì đã có 2 instance
Nút thắt tiếp theo

Gateway và logic nghiệp vụ chung một process: deploy là ngắt hết kết nối, và khi lên hàng trăm nghìn kết nối thì RAM gateway và một kênh Redis cho mỗi user bắt đầu đắt.

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ầnMức 1Mức 2Mức 3
Người dùng10.000 DAU1.000.000 DAU50.000.000 DAU
Chi phí≈ 80–200 USD/tháng (ước lượng)≈ 3.000–6.000 USD/tháng (ước lượng)≈ 150.000–300.000 USD/tháng (ước lượng)
Load balancer
ALB / Cloud Load Balancing (hỗ trợ WebSocket)
1 × managed
NLB (L4) cho WebSocket, ALB cho REST
2 × managed
Chưa cần
Chat server
Node.js (NestJS + ws), Docker
2 × 2 vCPU · 4 GB RAM
Chưa cầnChưa cần
Pub/sub
Redis managed
1 × 1 GB
Chưa cầnChưa cần
Database
PostgreSQL managed
1 (+ backup hằng ngày) × 2 vCPU · 8 GB RAM · 100 GB
PostgreSQL managed, partition theo tháng
1 primary + 1 replica × 8 vCPU · 32 GB · 2 TB
Chưa cần
Media
S3 / GCS, presigned URL
1 bucket × ≈ 1,5 TB/năm
S3 + CDN
1 bucket + CDN × theo dung lượng
S3 + CDN, dedupe theo hash
multi-region × theo dung lượng
Push
FCM / APNs
dịch vụ ngoài × miễn phí
Chưa cầnChưa cần
WS GatewayChưa cần
Node.js (uWebSockets.js), autoscale theo số kết nối
15 (autoscale 15–20) × 2 vCPU · 4 GB RAM
Node.js (uWebSockets.js) trên Kubernetes, HPA theo số kết nối
375 pod (125/region) × 2 vCPU · 4 GB RAM
Message serviceChưa cần
Node.js, gọi từ gateway qua gRPC
4 × 2 vCPU · 4 GB RAM
Node.js hoặc Go, gRPC
117 pod × 2 vCPU · 4 GB RAM
Chat APIChưa cần
Node.js (NestJS)
3 × 2 vCPU · 4 GB RAM
Chưa cần
RedisChưa cần
ElastiCache / Memorystore, 1 primary + 1 replica
2 × 2 vCPU · 6 GB
Chưa cần
QueueChưa cần
SQS / Pub/Sub + push worker
1 queue, 2 worker × 2 vCPU · 4 GB RAM
Chưa cần
EdgeChưa cầnChưa cần
GeoDNS + NLB mỗi region
3 region × managed
Delivery workerChưa cầnChưa cần
Consumer Kafka, gọi gRPC tới gateway
42 pod × 2 vCPU · 4 GB RAM
Redis ClusterChưa cầnChưa cần
Registry, presence, seq, cache thành viên nhóm
12 shard + 12 replica × 4 vCPU · 16 GB
Message storeChưa cầnChưa cần
ScyllaDB, key (conversation_id, bucket), clustering seq
120 node (40/region) × 16 vCPU · 128 GB · 7,5 TB NVMe
KafkaChưa cầnChưa cần
Topic messages key = conversation_id, topic push
6 broker, 48 partition × 8 vCPU · 32 GB
User/Group DBChưa cầnChưa cần
PostgreSQL, primary + replica mỗi region
1 primary + 3 replica × 16 vCPU · 64 GB

Đà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) Làm sao giao tin cho người đang online khi họ nối vào một trong hàng trăm gateway?

Cách chưa tốt
Client polling HTTP mỗi vài giây

Client gọi API hỏi có tin mới không. Đơn giản nhưng phần lớn request trả về rỗng, độ trễ bằng chu kỳ poll.

Đánh đổi: 1 triệu người poll mỗi 3 s là 333.000 req/s chỉ để nghe ngóng, tốn tiền và vẫn chậm.

Nguồn: Grokking Modern System Design · ch.18 Pub-sub ↗ · mục "Motivation" (đẩy thay vì hỏi liên tục) ; Grokking Modern System Design · ch.36 WhatsApp ↗ · Detailed Design of WhatsApp · mục "Connection with a WebSocket server" ; Grokking Modern System Design · ch.5 Back-of-the-envelope Calculations ↗ · mục "Examples of Resource Estimation" (Number of servers required, Storage requirements, Bandwidth requirements) (phép tính req/s)

Cách tốt
WebSocket + Redis pub/sub theo user

Mỗi gateway subscribe kênh user:{id} cho từng người đang nối vào nó. Message service publish vào kênh của người nhận, gateway nào giữ kết nối sẽ nhận được.

Đánh đổi: Dễ làm, nhưng số kênh bằng số kết nối và Redis pub/sub không lưu tin, gateway chết giữa chừng là mất bản đẩy (vẫn còn trong DB).

Nguồn: Grokking Modern System Design · ch.18 Pub-sub ↗ · mục "High-level design" (broker, topic) ; Grokking Modern System Design · ch.36 WhatsApp ↗ · Detailed Design of WhatsApp · mục "Connection with a WebSocket server" ; Chưa có nguồn trong thư viện: Redis pub/sub không có độ bền tin và số kênh bằng số kết nối là nhận xét của khoá học (Redis in Action ch.3 §3.6 Publish/subscribe chỉ mô tả cơ chế)

Cách tốt nhất
Connection registry + định tuyến thẳng tới gateway

Khi kết nối, gateway ghi user/device → gateway_id vào Redis với TTL và gia hạn bằng heartbeat. Worker giao tin tra registry, gom người nhận theo gateway và gọi gRPC hoặc publish vào kênh gw:{id} của đúng gateway đó. Gateway cache kết quả tra cứu và nhận tín hiệu xoá cache khi user đổi gateway.

Đánh đổi: Thêm một hệ thống phải giữ đúng (registry có thể cũ vài giây), nhưng chính DB mới là nguồn sự thật nên tin không mất, chỉ có thể đến chậm qua đường sync.

Nguồn: Grokking Modern System Design · ch.36 WhatsApp ↗ · Detailed Design of WhatsApp · mục "Connection with a WebSocket server" (WebSocket manager và Redis lưu ánh xạ người dùng - server) ; Redis in Action ↗ · ch.5 · §5.4 Service discovery and configuration (tr. 104) ; Chưa có nguồn trong thư viện: gia hạn TTL bằng heartbeat và gRPC giữa worker và gateway là thiết kế của khoá học

2) Người nhận offline hoặc có nhiều thiết bị: làm sao không mất tin và đúng thứ tự?

Cách chưa tốt
Sắp theo timestamp của client, giữ tin chờ trong RAM gateway

Đồng hồ điện thoại lệch nhau nên thứ tự sai. Gateway restart là mất hàng đợi.

Đánh đổi: Không cần DB cho tin chờ, đổi lại mất tin và thứ tự lộn xộn.

Nguồn: Grokking Modern System Design · ch.36 WhatsApp ↗ · Requirements of WhatsApp's Design · mục "Non-functional requirements" (Consistency: tin phải theo thứ tự gửi) ; Grokking Modern System Design · ch.12 Sequencer ↗ · mục "Use UNIX time stamps" (Cons: đồng hồ lệch) và "Causality"

Cách tốt
Hộp thư chờ theo user, xoá khi đã giao

Lưu tin vào bảng inbox của người nhận, giao xong thì xoá, giống cách WhatsApp trong Grokking giữ tin tối đa 30 ngày trên server. ID tin sinh bởi server (Snowflake) nên thứ tự toàn cục gần đúng.

Đánh đổi: Hợp với app không lưu lịch sử trên server, nhưng nhóm 1.000 người phải ghi 1.000 bản và thiết bị thứ hai không đọc lại được lịch sử.

Nguồn: Grokking Modern System Design · ch.36 WhatsApp ↗ · Detailed Design of WhatsApp · mục "Send or receive messages" (tin offline được lưu rồi xóa sau 30 ngày) ; Grokking Modern System Design · ch.12 Sequencer ↗ · mục "Twitter Snowflake" (ID tăng theo thời gian)

Cách tốt nhất
Seq tăng dần theo conversation + con trỏ theo thiết bị

Mỗi tin có seq liên tục trong cuộc trò chuyện (Redis INCR hoặc cột last_seq trong cùng transaction). Mỗi thiết bị giữ last_delivered_seq theo conversation, khi kết nối lại thì gọi sync after_seq. Client gửi kèm client_msg_id để retry không tạo tin trùng, thấy seq bị hổng thì tự kéo lại.

Đánh đổi: Seq chỉ đúng trong một conversation, không có thứ tự toàn cục giữa các cuộc trò chuyện (và không cần). Bộ đếm seq của nhóm rất sôi động là điểm nóng phải theo dõi.

Nguồn: Grokking Modern System Design · ch.36 WhatsApp ↗ · Evaluation of WhatsApp's Design · mục "The trade-off between consistency and availability" ; Grokking Modern System Design · ch.12 Sequencer ↗ · mục "Lamport clocks" ; Redis in Action ↗ · ch.3 · §3.1 Strings (INCR, tr. 40) ; Chưa có nguồn trong thư viện: seq liên tục theo conversation và client_msg_id chống trùng là thiết kế của khoá học

3) Nhóm lớn, đã đọc và trạng thái online: làm sao không làm nổ tải?

Cách chưa tốt
Ghi một bản tin cho mỗi thành viên, broadcast mọi sự kiện

Nhóm 1.000 người: 1 tin thành 1.000 bản ghi. Mỗi lượt đọc gửi receipt cho 999 người còn lại, mỗi lần ai đó online/offline báo cho mọi liên hệ.

Đánh đổi: Một nhóm sôi nổi 10 tin/phút sinh khoảng 10 × 1.000 × 999 ≈ 10 triệu receipt/phút.

Nguồn: Grokking Modern System Design · ch.36 WhatsApp ↗ · Detailed Design of WhatsApp · mục "Support for group messages" ; Grokking Modern System Design · ch.17 Distributed Messaging Queue ↗ · mục "Motivation" (Improved performance, Granular scalability) ; Chưa có nguồn trong thư viện: phép tính 10 triệu receipt/phút là ví dụ minh họa của khoá học

Cách tốt
Lưu 1 lần, fan-out lúc giao qua queue

Tin nhóm ghi một lần theo conversation. Worker đọc danh sách thành viên từ cache, gom theo gateway rồi đẩy. Receipt lưu dạng last_read_seq cho mỗi thành viên, không lưu theo từng tin.

Đánh đổi: Ghi DB giảm mạnh, nhưng realtime receipt cho nhóm lớn vẫn tốn băng thông.

Nguồn: Grokking Modern System Design · ch.36 WhatsApp ↗ · Detailed Design of WhatsApp · mục "Support for group messages" (danh sách thành viên từ group service có Redis cache) ; Grokking Modern System Design · ch.16 Distributed Cache ↗ · Background of Distributed Cache · mục "Writing policies" (write-around: ghi vào cache sau lần trượt)

Cách tốt nhất
Watermark + gộp + chỉ đẩy cho ai đang xem

Đã đọc chỉ là một số last_read_seq, client gửi gộp mỗi vài giây. Nhóm trên 100 người không đẩy receipt realtime, chỉ hiện số người đã đọc khi mở chi tiết tin. Presence dùng heartbeat có TTL trong Redis và chỉ đẩy cho người đang mở đúng cuộc trò chuyện, phần còn lại hỏi khi cần.

Đánh đổi: Trạng thái đã đọc và online trong nhóm lớn chậm vài giây hoặc phải bấm mới thấy, đổi lại tải giảm cả trăm lần.

Nguồn: Grokking Modern System Design · ch.36 WhatsApp ↗ · Evaluation of WhatsApp's Design · mục "Fulfill the requirements" ; Chưa có nguồn trong thư viện: watermark last_read_seq và ngưỡng 100 người là thiết kế của khoá học

4) Lưu hàng tỷ tin nhắn mỗi ngày ở đâu?

Cách chưa tốt
Một bảng messages trong Postgres, không phân vùng

Ổn ở vài chục GB. Khi lên vài TB thì index không còn vừa RAM, vacuum và backup kéo dài hàng giờ.

Đánh đổi: Đơn giản nhất nhưng có trần rõ ràng ở khoảng 1-2 TB trên một node.

Nguồn: Grokking Modern System Design · ch.4 Non-functional System Characteristics ↗ · Scalability · mục "Vertical scalability--scaling up" và "Horizontal scalability--scaling out" ; Grokking Modern System Design · ch.9 Databases ↗ · Data Partitioning · mục "Why do we partition data?" ; Quy tắc ước lượng của khoá học · /hoc/chon-server (1.000 req/s mỗi máy 2 vCPU, chạy tối đa 60% CPU, Redis ×1,3)

Cách tốt
Postgres phân vùng theo tháng + archive

Partition theo thời gian, giữ nóng vài tháng, partition cũ đẩy sang object storage. Index (conversation_id, seq) nằm trong từng partition.

Đánh đổi: Kéo dài được tuổi thọ Postgres ở mức 1M DAU, nhưng vẫn một primary nhận toàn bộ lượng ghi.

Nguồn: Grokking Modern System Design · ch.9 Databases ↗ · Data Partitioning · mục "Sharding" (Key-range based sharding theo thời gian) ; Grokking Modern System Design · ch.20 Blob Store ↗ · mục "Replication" ; Chưa có nguồn trong thư viện: partition theo tháng của PostgreSQL

Cách tốt nhất
Cassandra/ScyllaDB, partition key (conversation_id, bucket)

Truy cập luôn là đọc một cuộc trò chuyện theo seq, đúng kiểu wide-row. Bucket theo tháng hoặc theo 10.000 tin để partition không phình quá lớn. Ghi phân tán đều trên nhiều node, thêm node là tăng cả dung lượng và throughput. Thông tin user, nhóm vẫn để Postgres vì cần quan hệ và transaction.

Đánh đổi: Mất JOIN và transaction nhiều dòng, phải thiết kế bảng theo truy vấn và chấp nhận vận hành một cụm NoSQL.

Nguồn: Grokking Modern System Design · ch.9 Databases ↗ · mục "Types of NoSQL databases" (Columnar database) ; Grokking Modern System Design · ch.9 Databases ↗ · Data Partitioning · mục "Sharding" (Key-range based sharding, Hash-based sharding, Consistent hashing) (hash-based sharding) ; Grokking Modern System Design · ch.10 Key-value Store ↗ · Design of a Key-value Store · mục "Consistent hashing" và "Use virtual nodes" ; Chưa có nguồn trong thư viện: wide-row, bucket của Cassandra/ScyllaDB không có sách trong thư viện

Trong phỏng vấn

Nên nói
  • “Tôi luôn lưu tin rồi mới giao. Đẩy realtime chỉ là đường nhanh, sync theo seq là đường đảm bảo.”
  • “Thứ tự chỉ cần đúng trong một cuộc trò chuyện, nên tôi dùng seq theo conversation thay vì thứ tự toàn cục.”
  • “Gateway chỉ giữ kết nối, mọi logic nằm ở service phía sau để deploy không làm rớt hàng triệu kết nối.”
  • “Đã đọc lưu thành một watermark last_read_seq cho mỗi thành viên, không lưu theo từng tin.”
Bẫy hay gặp
  • Sắp tin theo timestamp của điện thoại
  • Ghi một bản tin cho mỗi thành viên nhóm và broadcast mọi receipt
  • Quên client retry: thiếu client_msg_id thì mạng chập chờn sinh tin trùng
  • Cho file media chạy qua WebSocket hoặc lưu trong DB

Đọ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.