Harbor

Scale ghi

Khi lượng ghi vượt sức một database: gom batch, ghi bất đồng bộ qua hàng đợi, chọn storage tối ưu cho ghi, chia dữ liệu theo key, xử lý partition nóng bằng salting, sharded counter và tổng hợp trước. Kèm con số cho từng quy mô.

15 phút đọcPattern thường gặp

Bài toán: khi ghi trở thành nút thắt

Đọc thì dễ scale. Bạn thêm cache, thêm read replica, đẩy nội dung tĩnh ra CDN, và mỗi lớp đó gánh bớt phần lớn lượng đọc. Ghi thì khác. Mỗi lệnh ghi phải tới được bản gốc của dữ liệu, được ghi xuống đĩa an toàn, rồi mới được sao chép đi. Thêm replica không giúp gì cho ghi, thậm chí còn làm mỗi lệnh ghi tốn thêm công sao chép: Grokking ghi rằng mọi lệnh ghi đều đi qua primary, nên primary-secondary không hợp với tải nặng về ghi.1 Building Microservices cũng nhận xét scale cho ghi khó hơn nhiều so với scale cho đọc.2

Các bài thiết kế có lượng ghi lớn thường rơi vào vài dạng quen thuộc:

  • Sự kiện dày đặc: click quảng cáo, view video, log, metrics, vị trí GPS gửi lên mỗi vài giây.
  • Bộ đếm nóng: like của một bài viết viral, số người xem một livestream, số vé còn lại.
  • Dữ liệu người dùng tạo ra liên tục: tin nhắn chat, bình luận live, bài viết.

Câu hỏi cần trả lời trong phỏng vấn là: lượng ghi đỉnh bao nhiêu lệnh mỗi giây, mỗi lệnh to bao nhiêu, và dữ liệu có cần đọc lại ngay sau khi ghi hay chấp nhận trễ vài giây. Ba con số đó quyết định bạn cần giải pháp nào trong bài này.

Cách ngây thơ và vì sao nó gãy

Cách đầu tiên ai cũng nghĩ tới: mỗi request từ client gọi API, API mở transaction, INSERT hoặc UPDATE một dòng vào PostgreSQL, commit, trả về 200.

// Mỗi click quảng cáo = một transaction riêng
app.post("/click", async (req, res) => {
  await db.query(
    "INSERT INTO clicks(ad_id, user_id, ts) VALUES ($1, $2, now())",
    [req.body.adId, req.user.id]
  );
  await db.query("UPDATE ads SET clicks = clicks + 1 WHERE id = $1", [req.body.adId]);
  res.sendStatus(200);
});

Ở vài chục lệnh mỗi giây, cách này chạy tốt. Khi lên vài nghìn lệnh mỗi giây, nó gãy theo ba kiểu:

  1. Chi phí cố định cho mỗi commit. Mỗi commit phải đợi WAL được fsync xuống đĩa. Grokking ghi một máy MySQL xử lý khoảng 1.000 QPS (xấp xỉ, tuỳ query và cấu hình);3 với Postgres ta dùng cùng bậc độ lớn, vài nghìn lệnh ghi đơn giản mỗi giây (giả định của khoá học, cần load test). Phần lớn thời gian đó tốn cho round trip và fsync, ít khi cho chính dữ liệu.
  2. Tranh chấp trên một dòng. Lệnh UPDATE ads SET clicks = clicks + 1 trên một quảng cáo hot khóa đúng một dòng. Mọi request khác phải xếp hàng chờ khóa đó. Bạn có 32 core nhưng chỉ một core làm việc có ích.
  3. Một node là trần. Dù tối ưu tới đâu, toàn bộ lượng ghi vẫn dồn vào một primary. Khi primary đầy, bạn không có chỗ nào để thêm sức.

Mỗi giải pháp dưới đây gỡ một trong ba nút thắt đó. Thứ tự đi từ rẻ và đơn giản tới đắt và phức tạp.

Giải pháp 1: gom batch và ghi bất đồng bộ qua hàng đợi

Batching

Thay vì 1.000 transaction mỗi transaction một dòng, bạn gửi 1 transaction chứa 1.000 dòng. Chi phí round trip và fsync được chia đều cho cả lô. Redis in Action §4.5 minh hoạ cùng ý: gộp nhiều lệnh vào một lần gửi (lệnh nhiều đối số như MSET, hoặc pipeline) giảm số round trip và cải thiện hiệu năng đáng kể.4 Với Postgres, INSERT ... VALUES (...), (...), ... hoặc COPY được kỳ vọng tăng throughput nhiều lần so với insert từng dòng (ước tính của khoá học, cần load test; tuỳ kích thước dòng và index).5

Batch có thể làm ở client (SDK gom sự kiện 5 giây rồi gửi một lần), ở API server (gom trong RAM 100 ms), hoặc ở worker đọc từ hàng đợi. Đánh đổi: dữ liệu trong batch chưa bền cho tới khi lô được ghi. Nếu process chết, lô đó mất. Vì vậy batch trong RAM của API server chỉ hợp với dữ liệu chấp nhận mất một ít, như view count hay analytics.

Ghi bất đồng bộ qua hàng đợi

Bước tiếp theo là tách việc nhận ghi khỏi việc lưu ghi. API chỉ đẩy sự kiện vào Kafka hoặc SQS rồi trả về ngay. Một nhóm consumer đọc từ hàng đợi, gom batch, ghi vào database với tốc độ database chịu được.

Đang vẽ sơ đồ…

Hàng đợi làm ba việc. Theo Grokking ch.17, hàng đợi giúp tách (decouple) producer khỏi consumer, hấp thụ đỉnh tải để tránh quá tải service, tăng độ tin cậy nhờ replicate message và cho phép tăng số consumer khi tải tăng.6 Ví dụ: 50.000 sự kiện mỗi giây trong 10 phút có thể được ghi dần với 10.000 mỗi giây (phép tính minh hoạ). Tính bền đến từ việc message được replicate trên nhiều host trước khi xác nhận.7 Việc nhiều hệ thống cùng đọc một luồng (database chính, Elasticsearch, data warehouse) là cách dùng log/stream của khoá học, xem Kafka.

Đánh đổi:

  • Đọc ngay sau khi ghi sẽ không thấy. Người dùng đăng bình luận rồi reload có thể chưa thấy. Cách xử lý thường gặp là hiển thị lạc quan ở client, hoặc ghi đồng bộ cho đúng dữ liệu cần thấy ngay và chỉ đẩy phần còn lại vào hàng đợi.
  • Trùng lặp. Hàng đợi giao ít nhất một lần (message chỉ bị xoá khi consumer xác nhận, nên lỗi worker dẫn tới giao lại),8 nên consumer phải idempotent. Gắn event_id cho mỗi sự kiện và loại bản trùng; Grokking mô tả việc kiểm tra khoá trong một store để nhận ra bản trùng,9 còn INSERT ... ON CONFLICT DO NOTHING là cách cụ thể trong Postgres (thực hành của khoá học).
  • Lag cần được theo dõi. Nếu consumer chậm hơn producer quá lâu, hàng đợi đầy và độ trễ tăng không ngừng. Consumer lag là metric nên theo dõi (thực hành của khoá học).5

Mẹo: Kafka partition chịu khoảng 10 MB/s hoặc vài nghìn message mỗi giây (giả định của khoá học, cần load test). Số partition đặt ra trần cho số consumer song song trong một consumer group. Chọn số partition dư ra từ đầu, vì tăng sau sẽ làm lệch thứ tự theo key.10

Giải pháp 2: chọn storage tối ưu cho ghi

B-tree (Postgres, MySQL) cập nhật dữ liệu tại chỗ. Mỗi lệnh ghi có thể chạm vào nhiều trang ngẫu nhiên trên đĩa: trang dữ liệu, trang của từng index. Ghi ngẫu nhiên là thứ đĩa làm kém nhất.11

LSM-tree (Cassandra, ScyllaDB, RocksDB, HBase) đi hướng ngược lại. Phần mô tả chi tiết dưới đây là kiến thức cơ bản của khoá học;11 sách Seven Databases in Seven Weeks có nói HBase dùng write-ahead log và Bloom filter để biết một key có thể có trong file hay không:12

  1. Ghi vào commit log tuần tự (append-only, rất nhanh).
  2. Ghi vào memtable trong RAM.
  3. Khi memtable đầy, flush nguyên khối xuống đĩa thành một SSTable bất biến, đã sắp xếp.
  4. Chạy compaction ở nền để gộp các SSTable nhỏ và xóa bản ghi cũ.

Mọi thao tác đĩa trên đường ghi đều tuần tự, nên một node Cassandra có thể nhận khoảng 10.000 tới vài chục nghìn lệnh ghi mỗi giây (giả định của khoá học, cần load test; phụ thuộc phần cứng và kích thước bản ghi). Cái giá phải trả nằm ở phía đọc: một key có thể nằm rải ở nhiều SSTable, nên đọc phải tra nhiều file, dựa vào bloom filter và compaction để giữ tốc độ.

Tiêu chíB-tree (Postgres)LSM (Cassandra)
Đường ghiCập nhật tại chỗ, ghi ngẫu nhiênAppend tuần tự, flush theo khối
Throughput ghi 1 node (ước tính)Vài nghìn/s10k tới vài chục nghìn/s
Đọc theo keyNhanh, ổn địnhNhanh nếu compaction theo kịp
Truy vấn linh hoạt, JOIN, transactionMạnhYếu, phải thiết kế bảng theo truy vấn
Scale ngangPhải tự shard hoặc dùng extensionCó sẵn, thêm node là thêm sức ghi

Building Microservices cũng nói khi chạm trần ghi, nên xem các hệ như Cassandra, Mongo, Riak có mô hình scale khác, nhưng "mua máy to hơn" thường là cách nhanh nhất trước mắt.2 Khi nào đổi sang LSM: dữ liệu dạng sự kiện hoặc time series, truy vấn biết trước (theo user_id và khoảng thời gian), lượng ghi đỉnh vượt vài chục nghìn mỗi giây. Xem thêm Cassandra, DynamoDB và Time-series database.

Sai lầm hay gặp: Đổi sang Cassandra rồi vẫn mô hình hóa như SQL, cần truy vấn theo cột không phải partition key. Kết quả là scan toàn cụm, chậm hơn cả Postgres. Với LSM store, bạn thiết kế bảng theo từng truy vấn.

Giải pháp 3: chia dữ liệu theo key và xử lý partition nóng

Sharding theo key

Khi một node không đủ, bạn chia dữ liệu ra nhiều node, mỗi node giữ một phần keyspace; lượng ghi được chia theo. Grokking ch.9 nhấn mạnh phân vùng phải cân bằng, nếu không sẽ có hotspot, và nêu các cách chia theo key-range, theo hash và consistent hashing.13 Building Microservices mô tả sharding cho ghi: băm khoá để biết gửi dữ liệu tới đâu, và khó khăn nằm ở truy vấn chạy xuyên nhiều shard.2 Chi tiết cơ chế có ở bài Sharding và Consistent hashing. Phần quan trọng nhất là chọn shard key.

MongoDB: The Definitive Guide (ch.15) mô tả ba kiểu shard key và hệ quả của từng kiểu, áp dụng được cho mọi hệ thống sharded:14

  • Key tăng dần (timestamp, ObjectId, auto-increment): mọi bản ghi mới đều rơi vào shard chứa khoảng cuối. Một shard nhận toàn bộ lượng ghi, các shard khác ngồi chơi (sách: nếu không có cơ chế đặc biệt thì không nên dùng key tăng dần làm shard key).
  • Key phân bố ngẫu nhiên (hash của id, UUID): ghi trải đều mọi shard. Đổi lại, truy vấn theo khoảng phải hỏi tất cả shard.
  • Key theo vị trí hoặc nhóm (quốc gia, tenant): dữ liệu liên quan nằm gần nhau, nhưng nhóm lớn sẽ thành shard nặng.

Cuốn sách cũng gợi ý dạng ghép ("multi-hotspot"): phần đầu là giá trị gần ngẫu nhiên có cardinality vừa phải để phân tán ghi, phần sau tăng dần để ghi trong từng shard vẫn tuần tự, và hashed shard key khi cần nạp dữ liệu nhanh nhưng chấp nhận không có range query hiệu quả.14 Ví dụ (user_id, created_at): ghi trải theo user, còn truy vấn "tin nhắn mới nhất của user X" vẫn nằm trên một shard.

Lưu ý: Shard key cũng phải có cardinality đủ lớn: sách nêu key chỉ có vài giá trị (ví dụ logLevel) thì dữ liệu không thể tách quá số giá trị đó, và gợi ý ghép với một key biến thiên hơn.15 Nếu bạn shard theo country và 70% user ở Việt Nam (ví dụ minh hoạ), bạn chỉ có một shard thật sự làm việc, dù cụm có 20 node.

Hot partition và key salting

Ngay cả khi key phân bố tốt, một key cụ thể vẫn có thể nóng: bài viết của người nổi tiếng, sản phẩm flash sale, quảng cáo của một chiến dịch lớn. Mọi lệnh ghi cho key đó đều về một partition, đúng bài toán heavy hitter trong Grokking ch.24.16

Salting là thêm một hậu tố ngẫu nhiên vào key nóng để rải nó ra N partition (kỹ thuật của khoá học, cùng ý tưởng với sharded counter bên dưới):17

const N = 16;
// Ghi: rải đều sang 16 partition
const key = `post:${postId}#${Math.floor(Math.random() * N)}`;
// Đọc: phải gom từ cả 16 key
const keys = Array.from({ length: N }, (_, i) => `post:${postId}#${i}`);

Đánh đổi rõ ràng: ghi rẻ hơn N lần trên mỗi partition, đọc đắt hơn N lần. Vì vậy chỉ salt những key thực sự nóng. Cách làm thực tế: đếm tần suất ghi theo key trong cửa sổ ngắn, key nào vượt ngưỡng thì chuyển sang dạng salted, và ghi lại danh sách key nóng để bên đọc biết phải gom.

Giải pháp 4: sharded counter và tổng hợp trước

Sharded counter

Bộ đếm là trường hợp riêng rất hay gặp của partition nóng. Grokking (ch.24) mô tả bài toán heavy hitter: một tweet của tài khoản lớn nhận hàng triệu like, mọi lệnh tăng đều dồn vào một bộ đếm và bị xếp hàng tuần tự. Giải pháp là chia một bộ đếm logic thành nhiều shard counter. Mỗi lệnh tăng chọn một shard khả dụng, lệnh đọc cộng tất cả shard lại.16

Đang vẽ sơ đồ…

Grokking cũng nhấn mạnh hai điểm thiết kế: số shard nên phụ thuộc mức độ nóng của từng đối tượng (tweet của người có nhiều follower có nhiều shard hơn; ít shard thì ghi tranh chấp, nhiều shard thì đọc tốn kém tuyến tính theo số shard), và việc đọc tổng nên được làm định kỳ rồi cache lại, thay vì cộng mỗi lần có người xem.18 Con số hiển thị trễ vài giây là chấp nhận được với hầu hết sản phẩm (nhận định của khoá học).

Với Redis, cách đơn giản là INCR trên N key likes:{postId}:{i}, một job mỗi vài giây cộng lại và ghi vào database; Redis in Action §5.2 trình bày lưu counter trong Redis.19 Grokking ghi cache server xử lý khoảng 100.000 tới 1 triệu QPS,3 nên chỉ riêng việc chuyển bộ đếm từ Postgres sang Redis đã gỡ được rất nhiều tải.

Tổng hợp trước (pre-aggregation)

Nhiều khi bạn không cần lưu từng sự kiện vào database phục vụ. Hệ thống ad click không cần đọc lại từng click khi vẽ dashboard. Nó cần "số click của quảng cáo X trong phút Y". Vậy hãy tính sẵn con số đó trước khi ghi:

  1. Sự kiện thô vào Kafka, được lưu nguyên ở object storage cho mục đích audit và tính lại.
  2. Một stream processor (Flink, Kafka Streams, hoặc consumer tự viết) gom theo (ad_id, minute) trong RAM.
  3. Hết mỗi cửa sổ, ghi một dòng tổng vào database.

Đây là kỹ thuật thiết kế của khoá học; ý "gom trước khi ghi" giống việc Grokking ch.24 đọc định kỳ các shard rồi lưu tổng đã tính vào kho bền.1820

Nếu có 1 triệu click mỗi phút trên 10.000 quảng cáo, database chỉ nhận 10.000 dòng mỗi phút thay vì 1 triệu. Lượng ghi giảm 100 lần. Đánh đổi: dữ liệu trễ bằng độ dài cửa sổ, và bạn mất khả năng truy vấn tùy ý trên sự kiện thô từ database phục vụ. Xem Flink để biết cách xử lý sự kiện đến muộn và cửa sổ.

Trong phỏng vấn: Khi người phỏng vấn hỏi "nếu có 1 triệu lệnh ghi mỗi giây thì sao", trước khi nói sharding, hãy hỏi lại: dữ liệu này có cần đọc từng bản ghi không? Nếu chỉ cần tổng, tổng hợp trước thường giải quyết bài toán rẻ hơn nhiều so với một cụm database khổng lồ.

Scale theo quy mô người dùng

Giả định của khoá học: mỗi user hoạt động tạo khoảng 50 lệnh ghi mỗi ngày (like, view, sự kiện, tin nhắn). Một ngày có 86.400 giây, đỉnh gấp 5 lần trung bình. Mọi con số dưới đây là ước tính theo quy tắc ước lượng của khoá học.21

Quy môGhi trung bìnhGhi đỉnhKiến trúc gợi ý
~10k user500k/ngày ≈ 6/s~30/s1 Postgres, ghi đồng bộ, index hợp lý. Không cần gì thêm
~1M user50M/ngày ≈ 580/s~3.000/sPostgres vẫn được nếu batch. Đẩy sự kiện phụ (view, analytics) qua hàng đợi. Bộ đếm sang Redis
50M+ user2,5 tỷ/ngày ≈ 29.000/s~150.000/sKafka làm cửa vào. Dữ liệu sự kiện ở Cassandra hoặc DynamoDB, sharded theo key ghép. Sharded counter, salting cho key nóng, tổng hợp trước cho analytics

Một vài phép tính kiểm tra cho tầng 50M+ (dùng 10 MB/s mỗi partition, 15.000 ghi/s mỗi node Cassandra, 600 req/s mỗi instance 2 vCPU: các giả định của khoá học2110):

  • Kafka: 150.000 message/s × 500 byte ≈ 75 MB/s. Với khoảng 10 MB/s mỗi partition, cần ít nhất 8 partition. Chọn 32 tới 64 partition để còn chỗ cho consumer song song và tăng trưởng. 3 tới 6 broker là đủ, cộng replication factor 3.
  • Cassandra: 150.000 lệnh ghi/s, mỗi node khoảng 15.000/s, nhân replication factor 3 thì tổng lệnh ghi vật lý là 450.000/s. Cần khoảng 30 node, chưa tính dư địa. Nếu tổng hợp trước được một nửa lượng ghi, số node giảm theo.
  • Dung lượng: 2,5 tỷ bản ghi × 200 byte ≈ 500 GB/ngày, nhân 3 bản sao là 1,5 TB/ngày. Sau một năm là hơn 500 TB. Lúc này cần TTL cho dữ liệu thô hoặc dồn dữ liệu cũ sang object storage.
  • API server: 150.000 req/s, mỗi instance 2 vCPU chạy ở 60% khoảng 600 req/s, cần khoảng 250 instance. Nếu client gom batch 10 sự kiện mỗi request, con số giảm còn khoảng 25.

Nhìn bảng này, bạn thấy mỗi tầng chỉ thêm đúng thứ cần thêm. Đừng vẽ Kafka và Cassandra cho bài có 30 lệnh ghi mỗi giây. Xem thêm Con số cần thuộc và Hạ tầng theo quy mô.

Bài thiết kế nào dùng pattern này

BàiKỹ thuật chính
Ad click aggregatorKafka làm cửa vào, tổng hợp trước theo phút, idempotent theo click id
Top-KĐếm trong stream processor, gộp top-k theo cửa sổ thay vì ghi từng view
Metrics monitoringTime-series database, batch ở agent, downsampling
Live commentsGhi bình luận theo partition video_id, salting cho livestream cực hot
ChatShard theo conversation_id, LSM store cho lịch sử tin nhắn
Strava và Ride-hailingGhi vị trí GPS tần suất cao, chỉ giữ vị trí mới nhất trong Redis, lưu lịch sử theo batch
Instagram và News feedSharded counter cho like, fan-out ghi qua hàng đợi
Flash saleBộ đếm tồn kho nóng, xếp hàng ghi qua queue

Các bài này áp dụng những chương Grokking về hàng đợi và sharded counter.22 Khi trình bày, đi theo đúng trình tự: ước tính lượng ghi đỉnh, chỉ ra nút thắt cụ thể (commit, khóa dòng, hay trần một node), rồi chọn giải pháp rẻ nhất gỡ được nút thắt đó. Bài liên quan: Scale đọc, Kafka, Tranh chấp tài nguyên.

Nguồn trích dẫn

  1. Grokking Modern System Design · ch.9 Databases · bài "Data Replication" · mục "Single leader/primary-secondary replication" (không phù hợp với workload nặng ghi). ↩

  2. Building Microservices (file .zip, tải về giải nén) · ch.11 Microservices at Scale · mục "Scaling for Writes" (tr. 223–224). ↩ ↩2 ↩3

  3. Grokking Modern System Design · ch.5 Back-of-the-envelope Calculations · mục "Standard numbers to remember" (bảng "Important Rates": MySQL 1.000 QPS, cache server 100.000 tới 1 triệu QPS). ↩ ↩2

  4. Redis in Action · ch.4 · §4.5 Non-transactional pipelines (tr. 84–85). ↩

  5. Chưa có nguồn trong thư viện: mức tăng throughput khi batch Postgres và việc theo dõi consumer lag là kinh nghiệm thực hành của khoá học; thư viện chỉ có ví dụ pipeline của Redis. ↩ ↩2

  6. Grokking Modern System Design · ch.17 Distributed Messaging Queue · bài mở đầu · phần lợi ích của messaging queue (độ tin cậy, scalability, decoupling, rate limiting). ↩

  7. Grokking Modern System Design · ch.17 Distributed Messaging Queue · bài "Design of a Distributed Messaging Queue" (Back-end service, replicate message trên nhiều host) và bài "Evaluation" (Durability). ↩

  8. Grokking Modern System Design · ch.17 Distributed Messaging Queue · bài "Design of a Distributed Messaging Queue" (phần consumer lấy message: consumer xoá message, at-least-once, visibility timeout). ↩

  9. Grokking Modern System Design · ch.17 Distributed Messaging Queue · mục "Front-end service" (Request deduplication). ↩

  10. Chưa có nguồn trong thư viện: thư viện không có sách về Kafka; số liệu 10 MB/s mỗi partition, quy tắc số partition và thứ tự theo key chỉ là giả định/kinh nghiệm của khoá học. ↩ ↩2

  11. Chưa có nguồn trong thư viện: thư viện không có sách về cấu trúc LSM-tree/Cassandra; phần này chỉ nêu khái niệm cơ bản (commit log, memtable, SSTable, compaction), không có số liệu sản phẩm. ↩ ↩2

  12. Seven Databases in Seven Weeks (file .zip, tải về giải nén) · ch.4 HBase · mục "Compression and Bloom Filters" và phần nói về write-ahead log (WAL) của HBase. ↩

  13. Grokking Modern System Design · ch.9 Databases · bài "Data Partitioning" · mục "Sharding" (vertical/horizontal, key-range, hash-based, consistent hashing, hotspot). ↩

  14. MongoDB: The Definitive Guide 2e (file .zip, tải về giải nén) · ch.15 Choosing a Shard Key · mục "Ascending Shard Keys", "Randomly Distributed Shard Keys", "Location-Based Shard Keys", "Shard Key Strategies" (Hashed Shard Key, Multi-Hotspot) (tr. 257–268). ↩ ↩2

  15. MongoDB: The Definitive Guide 2e (file .zip, tải về giải nén) · ch.15 · mục "Shard Key Rules and Guidelines" · "Shard Key Cardinality" (tr. 271). ↩

  16. Grokking Modern System Design · ch.24 Sharded Counters · mục "Problem statement" (heavy hitters) và "High-level solution sketch". ↩ ↩2

  17. Chưa có nguồn trong thư viện: key salting là kỹ thuật thực hành của khoá học; thư viện chỉ có ý tưởng tương đương là sharded counter (Grokking ch.24) và replicate/shard tiếp key nóng (Grokking ch.16). ↩

  18. Grokking Modern System Design · ch.24 Sharded Counters · bài "Design of Sharded Counters" · mục "Number of shards" và "Manage read requests". ↩ ↩2

  19. Redis in Action · ch.5 · §5.2 Counters and statistics (tr. 93–94). ↩

  20. Quy tắc ước lượng của khoá học · /hoc/chon-server (hệ số tải đỉnh, 600 req/s mỗi instance 2 vCPU ở 60% CPU). ↩ ↩2

  21. Grokking Modern System Design · ch.17 Distributed Messaging Queue; ch.24 Sharded Counters; ch.9 Databases. ↩