Chọn database, cache và queue
Bảng chọn kho dữ liệu theo nhu cầu đọc ghi, giới hạn của một node và việc cần làm khi vượt ngưỡng; so sánh managed với tự vận hành, chọn queue, object storage và CDN, kèm ví dụ tính cấu hình.
Chọn kho dữ liệu theo cách dữ liệu được đọc và ghi. Hỏi bốn câu trước khi chọn: dữ liệu có quan hệ và cần transaction không, tỉ lệ đọc ghi thế nào, truy vấn theo khoá hay theo điều kiện phức tạp, và dữ liệu lớn bao nhiêu sau vài năm. Mọi con số giới hạn trong bài là ước lượng cho máy cỡ vừa, thuộc quy tắc ước lượng của khoá học trừ chỗ có ghi nguồn sách.1 Grokking ch.5 cho vài mốc tham chiếu: một MySQL đơn lẻ khoảng 1.000 QPS, key-value store khoảng 10.000 QPS, cache server từ 100.000 đến 1 triệu QPS, và nhấn mạnh các số này thay đổi nhiều theo loại truy vấn, cấu hình máy, thiết kế DB và index.2
Mẹo: Đọc tiếp về chọn database: So sánh database quan hệ (Oracle, PostgreSQL, SQL Server, MySQL), Database scale (replication, sharding, NewSQL), Chọn NoSQL (vì sao Cassandra hay DB scale) và So sánh Message Queue.
Bảng chọn nhanh
| Nhu cầu | Chọn | Một node chịu được (ước lượng) | Khi vượt ngưỡng |
|---|---|---|---|
| Quan hệ, transaction, ràng buộc | PostgreSQL / MySQL | Vài nghìn QPS đơn giản có index, 1–2 TB | Read replica, partition bảng, rồi Citus hoặc Vitess |
| Document, schema linh hoạt | MongoDB | Tương tự Postgres | Shard theo khoá có nhiều giá trị |
| Ghi cực nhiều, đọc theo khoá | Cassandra / ScyllaDB / DynamoDB | Mở rộng gần tuyến tính theo node | Thêm node |
| Cache, đếm, khoá, rate limit | Redis | ~100.000 thao tác/s | Redis Cluster |
| Tìm kiếm full-text | Elasticsearch / OpenSearch | Shard 10–50 GB, node 8 vCPU · 32 GB | Thêm node và shard |
| Ảnh, video, file | S3 / GCS + CDN | Gần như không giới hạn | Không cần làm gì |
| Vị trí, tìm gần | Redis GEO / PostGIS | Tuỳ số điểm cập nhật mỗi giây | Chia theo vùng địa lý |
| Metric theo thời gian | TimescaleDB / Prometheus / InfluxDB | Hàng trăm nghìn điểm/s | Downsample, chia theo thời gian |
Mặc định an toàn cho hầu hết sản phẩm là PostgreSQL. Nó đủ cho transaction, JSON, full-text cơ bản, và với PostGIS thì cả dữ liệu vị trí. Chỉ thêm kho khác khi có con số cho thấy Postgres không đáp ứng. Grokking ch.9 so sánh SQL và NoSQL theo cùng tinh thần: chọn relational khi dữ liệu có cấu trúc, cần ACID và vừa một node; chọn non-relational khi dữ liệu phi cấu trúc, rất lớn, cần scale ngang dễ và chấp nhận mô hình nhất quán yếu hơn như eventual consistency.3 Sách mô tả key-value hợp với tải theo phiên (session), document hợp với dữ liệu catalog không cố định cấu trúc, columnar hợp với tổng hợp nhiều dòng, graph hợp với dữ liệu quan hệ.4 Bảng nhanh phía trên, gồm cả cột ngưỡng một node, là bảng ước lượng của khoá học, còn tên sản phẩm cụ thể (Cassandra, TimescaleDB…) chưa có sách trong thư viện.5
Trong phỏng vấn: Khi chọn Cassandra hay DynamoDB, nói luôn cái giá: không join, phải thiết kế bảng theo truy vấn, và nhất quán có thể là eventual. Xem CAP và Mô hình dữ liệu.
Đọc nhiều hay ghi nhiều
Tỉ lệ đọc ghi quyết định hướng scale. Building Microservices ch.11 tách riêng hai trường hợp: đọc dễ scale hơn ghi nhiều, còn scale ghi là chỗ mọi thứ trở nên khó.67
- Đọc nhiều: cache trước. Building Microservices khuyên nghĩ đến caching trước read replica vì thường cải thiện hiệu năng nhiều hơn với ít công hơn.6 Hit 90% nghĩa là DB chỉ nhận 10% lượt đọc (phép tính). Sau đó thêm read replica: mô hình primary-secondary hợp với tải đọc nhiều, thêm follower để chia tải đọc, nhưng replicate bất đồng bộ thì đọc từ replica có thể thấy dữ liệu cũ.8 Con số mỗi replica lập kế hoạch khoảng 3.000 truy vấn đọc/s và "trễ vài trăm ms" là ước lượng của khoá học.1 Chi tiết ở Scale đọc.
- Ghi nhiều: replica không giúp vì mọi ghi vẫn vào primary; Grokking nói primary-secondary không phù hợp khi tải ghi nặng.8 Gom ghi qua queue để hấp thụ đỉnh,9 partition bảng, rồi shard: chia dữ liệu ra nhiều node, mỗi node giữ một phần.10 Building Microservices cảnh báo sharding làm truy vấn xuyên shard phức tạp, và khi chạm giới hạn ghi thì mua máy to hơn thường là cách nhanh nhất trong lúc tìm giải pháp lâu dài.7 Chi tiết ở Scale ghi và Sharding.
Ngưỡng cấu hình Postgres khoá dùng (theo tải DB sau cache, quy tắc ước lượng của khoá học):1
| Tải DB | Cấu hình | Bố trí |
|---|---|---|
| < 300 q/s | 2 vCPU · 8 GB | Một instance, backup tự động |
| < 1.500 q/s | 4 vCPU · 16 GB | Primary + standby ở zone khác |
| < 4.000 q/s | 8 vCPU · 32 GB | Primary + standby + 1–2 read replica |
| < 15.000 q/s | 16 vCPU · 64 GB | Primary + 2–3 read replica, chuyển ghi nặng sang queue |
| ≥ 15.000 q/s | Nhiều node | Sharding (Citus, Vitess) hoặc DB phân tán |
Cache
- Cache-aside là mặc định: đọc cache, trượt thì đọc DB rồi ghi lại cache với TTL. Grokking mô tả chính sách gần nhất là write-around: ghi thẳng DB, khi đọc trượt cache thì mới nạp vào cache, nhưng không tốt để đọc lại dữ liệu vừa ghi.11
- Write-through khi cần cache luôn mới: ghi cả cache và DB, tăng độ trễ ghi nhưng giữ nhất quán mạnh giữa hai nơi. Write-back ghi cache trước rồi ghi DB bất đồng bộ, độ trễ ghi thấp nhưng có thể không nhất quán.11
- Đo bằng tỉ lệ hit. Hit 85% là mức khởi đầu hợp lý cho dữ liệu đọc nhiều (giả định của khoá học).1
- Cẩn thận cache stampede khi key nóng hết hạn: dùng khoá đơn (lock) hoặc làm mới sớm có xác suất (cách của khoá học). Building Microservices mô tả nguy cơ tương tự ở dạng "thundering herd" khi cache mất, và đề xuất để origin tự nạp lại cache bất đồng bộ thay vì để mọi request đổ vào origin.12
- Cẩn thận hot key: một key nhận quá nhiều lượt đọc dồn vào một node. Grokking gọi là "hotkey problem"; giải pháp trong sách là thêm replica (primary-secondary) cho shard nóng để chia tải đọc.13 Sao chép key ra nhiều bản hoặc cache thêm trong process là cách của khoá học.1
Grokking ch.16 bàn các chính sách thay thế (LRU, LFU) và cách chia cache ra nhiều node.14 Khoá này chi tiết hơn ở bài Caching.
async function getProduct(id: string) {
const key = `product:${id}`;
const hit = await redis.get(key);
if (hit) return JSON.parse(hit);
const row = await db.query("SELECT * FROM products WHERE id = $1", [id]);
await redis.set(key, JSON.stringify(row), "EX", 300);
return row;
}
Queue
Dùng queue khi việc có thể làm sau, khi cần hấp thụ đỉnh, hoặc khi một sự kiện cần nhiều bên xử lý. Grokking ch.17 nêu đúng các lợi ích này: xử lý bất đồng bộ để tách việc chậm khỏi đường đi chính, tăng độ tin cậy vì producer và consumer hỏng độc lập, decoupling, và hấp thụ đỉnh tải như một dạng rate limiting thô.9
| Tiêu chí | BullMQ (trên Redis) | SQS / Pub/Sub | Kafka |
|---|---|---|---|
| Mô hình | Job queue | Queue / topic managed | Log có partition, giữ lịch sử |
| Thông lượng (ước lượng của khoá học) | Vài nghìn job/s | Rất cao, gần như tự scale | Partition ~10 MB/s, broker 50–100 MB/s |
| Thứ tự | Theo queue | SQS FIFO có giới hạn; standard thì không | Đảm bảo trong một partition |
| Đọc lại lịch sử | Không | Không (Pub/Sub có seek giới hạn) | Có, theo retention |
| Vận hành | Dễ nếu đã có Redis | Không phải vận hành | Nặng nếu tự chạy, dùng MSK hoặc Confluent |
| Hợp với | Mức 1–2, job nền trong app Node.js | Mức 1–2, hệ thống trên AWS/GCP | Mức 3, event backbone, stream processing |
Gần như mọi queue giao ít nhất một lần: Grokking giải thích message chỉ bị xoá khi consumer gọi xoá, nên nếu worker lỗi thì worker khác nhận lại, tức at-least-once.15 Vì vậy consumer phải idempotent: lưu id đã xử lý, hoặc dùng upsert theo khoá tự nhiên (hướng xử lý của khoá học).1 Các so sánh sản phẩm trong bảng (BullMQ, SQS, Kafka, số MB/s) chưa có sách trong thư viện.16 Xem Kafka và Tác vụ dài.
Sai lầm hay gặp: Chọn Kafka cho một app 10 nghìn người dùng chỉ để gửi email. Một queue managed hoặc BullMQ là đủ, và không ai phải trực đêm vì broker.
Object storage và CDN
File, ảnh, video không nên nằm trong database. Đặt ở object storage (S3, GCS, R2), database chỉ giữ metadata và đường dẫn. Grokking ch.20 mô tả blob store là kho cho dữ liệu phi cấu trúc (ảnh, video, tệp nhị phân), chia dữ liệu thành các chunk trên data node và giữ metadata (tài khoản, container, vị trí blob) ở một kho metadata riêng, đúng cách tách này.17
- Client upload thẳng lên storage bằng presigned URL, API không phải trung chuyển byte.18
- File lớn dùng multipart upload (chia thành chunk, khớp cách blob store chia chunk). Xem File lớn.17
- Đặt CDN phía trước cho nội dung đọc nhiều. Grokking ch.11 nêu ba vấn đề CDN giải quyết: độ trễ cao do khoảng cách tới data center, ứng dụng nhiều dữ liệu khiến origin phải gửi dữ liệu trùng lặp, và tài nguyên data center có hạn (kèm nguy cơ điểm chết duy nhất); CDN là nhóm proxy server phân tán địa lý để phục vụ từ gần người dùng.19
- Chi phí lưu trữ object khoảng 0,02 USD/GB/tháng và chi phí băng thông ra thường lớn hơn chi phí lưu trữ là số ước lượng theo nhà cung cấp, không có trong sách.18
Managed hay tự vận hành
Phần này là đánh giá vận hành của khoá học; sách trong thư viện không so sánh managed với tự vận hành, và hệ số chi phí 1,5–2 lần là giả định.20
| Tiêu chí | Managed (RDS, Cloud SQL, ElastiCache, MSK) | Tự vận hành trên VM / Kubernetes |
|---|---|---|
| Backup, failover, patch | Nhà cung cấp lo | Đội bạn lo |
| Giá máy | Đắt hơn khoảng 1,5–2 lần cùng cấu hình (ước lượng) | Rẻ hơn trên hoá đơn |
| Chi phí người | Thấp | Cần người hiểu sâu, trực sự cố |
| Tuỳ biến | Giới hạn extension, cấu hình | Toàn quyền |
| Hợp với | Mức 1–2, và phần lớn mức 3 | Quy mô rất lớn, yêu cầu đặc thù, đội platform mạnh |
Một kỹ sư vận hành tốn hơn chênh lệch hoá đơn của vài database cỡ vừa. Vì vậy mặc định là managed. Tự vận hành chỉ hợp lý khi hoá đơn đủ lớn để trả cho một đội chuyên trách.
Ví dụ tính cấu hình
Phép tính dưới đây dùng quy tắc ước lượng của khoá học (xem Chọn server).1 Giả định như bài đó: 30 request/người/ngày, đỉnh ×5, 10% ghi, mỗi ghi 1 KB, cache hit 85%, 2 triệu item nóng mỗi item 1 KB.
1 triệu DAU:
- Đỉnh ≈ 1.736 req/s, gồm ≈ 174 ghi và ≈ 1.562 đọc.
- Đọc xuống DB sau cache: 1.562 × 0,15 ≈ 234. Tổng tải DB ≈ 408 q/s.
- Database: 4 vCPU · 16 GB, primary + standby. Read replica: ceil(234 ÷ 3.000) = 1, tức 0–1 replica.
- Redis: 2.000.000 × 1.100 × 1,3 ≈ 2,9 GB, node 4 GB có replica.
- Lưu trữ: 1.000.000 × 30 × 10% × 1 KB × 365 ≈ 1,1 TB/năm. Sau khoảng 1,5 năm chạm ngưỡng 1–2 TB của một node, cần partition theo thời gian và chuyển dữ liệu cũ ra kho lạnh.
- Queue: SQS hoặc BullMQ cho job nền.
50 triệu DAU:
- Đỉnh ≈ 86.806 req/s, ghi ≈ 8.681/s.
- Tải DB sau cache ≈ 8.681 + 78.125 × 0,15 ≈ 20.400 q/s, vượt 15.000: cần shard hoặc DB phân tán.
- Lưu trữ ≈ 55 TB/năm: dữ liệu ghi nhiều theo khoá hợp với Cassandra hoặc DynamoDB, dữ liệu giao dịch giữ ở Postgres đã shard.
- Redis Cluster vì đỉnh vượt 80.000 thao tác/s trên một node.
- Kafka làm xương sống sự kiện, đọc lại được khi thêm consumer mới.
Bạn có thể đổi giả định trong Máy tính hạ tầng.
Nguồn trích dẫn
-
Quy tắc ước lượng của khoá học, xem bài Chọn server. Không phải số liệu trích từ sách; cần load test với ứng dụng thật. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Grokking Modern System Design · ch.5 Back-of-the-envelope Calculations · bài "Put Back-of-the-envelope Numbers in Perspective", mục "Standard numbers to remember", bảng "Important Rates". ↩
-
Grokking Modern System Design · ch.9 Databases · bài "Types of Databases", mục "Choose the right database" và "Drawbacks of NoSQL databases". ↩
-
Grokking Modern System Design · ch.9 Databases · bài "Types of Databases", mục "Types of NoSQL databases" (Key-value, Document, Graph, Columnar database, phần "Use case"). ↩
-
Chưa có nguồn trong thư viện: thư viện có MongoDB: The Definitive Guide và Seven Databases in Seven Weeks nhưng không có sách về PostgreSQL, Cassandra, DynamoDB, Elasticsearch hay TimescaleDB; cột "một node chịu được" là ước lượng khởi đầu. ↩
-
Building Microservices (Sam Newman) (file .zip, tải về giải nén) · ch.11 Microservices at Scale · mục "Scaling Databases" › "Scaling for Reads" (tr. 222–223). ↩ ↩2
-
Building Microservices (file .zip, tải về giải nén) · ch.11 Microservices at Scale · mục "Scaling Databases" › "Scaling for Writes" (tr. 223–224). ↩ ↩2
-
Grokking Modern System Design · ch.9 Databases · bài "Data Replication", mục "Single leader/primary-secondary replication" và "Synchronous versus asynchronous replication". ↩ ↩2
-
Grokking Modern System Design · ch.17 Distributed Messaging Queue · bài "System Design: The Distributed Messaging Queue", mục "Motivation". ↩ ↩2
-
Grokking Modern System Design · ch.9 Databases · bài "Data Partitioning", mục "Why do we partition data?" và "Sharding". ↩
-
Grokking Modern System Design · ch.16 Distributed Cache · bài "Background of Distributed Cache", mục "Writing policies". ↩ ↩2
-
Building Microservices (file .zip, tải về giải nén) · ch.11 Microservices at Scale · mục "Caching" › "Hiding the Origin" (tr. 228–229). ↩
-
Grokking Modern System Design · ch.16 Distributed Cache · bài "Detailed Design of a Distributed Cache", mục "Find and remove limitations" (hotkey problem) và "Improve availability". ↩
-
Grokking Modern System Design · ch.16 Distributed Cache · bài "Background of Distributed Cache", mục "Eviction policies" và "Sharding in cache clusters". ↩
-
Grokking Modern System Design · ch.17 Distributed Messaging Queue · bài "Detailed design" (back-end service), đoạn về việc consumer gọi API xoá message và "at-least-once delivery semantic", ngay trước "Point to Ponder" về visibility timeout. ↩
-
Chưa có nguồn trong thư viện: thư viện không có sách về BullMQ, SQS hay Kafka; bảng chỉ là hướng chọn ban đầu, không nêu số liệu chính thức của sản phẩm. ↩
-
Grokking Modern System Design · ch.20 Blob Store · bài "System Design: A Blob Store", mục "What is a blob store?"; bài "Design of a Blob Store", mục "Components" (data node, chunk, metadata storage). ↩ ↩2
-
Chưa có nguồn trong thư viện: presigned URL và giá lưu trữ object là đặc thù nhà cung cấp cloud, sách trong thư viện không đề cập. ↩ ↩2
-
Grokking Modern System Design · ch.11 Content Delivery Network (CDN) · bài "System Design: Content Delivery Network (CDN)", mục "Problem statement"; và bài "Introduction to a CDN", mục "Proposed solution". ↩
-
Chưa có nguồn trong thư viện: so sánh managed và tự vận hành dựa trên kinh nghiệm vận hành, không có sách trong thư viện bàn chủ đề này. ↩