Bài 7 giải quyết tầng đọc rất rẻ. Nhưng nó không giúp gì cho hai thứ: throughput ghi — mọi lệnh ghi vẫn dồn về một primary — và dung lượng — mọi replica đều chứa trọn bộ dữ liệu. Khi một trong hai thứ đó vượt giới hạn một máy, không còn cách nào ngoài chia dữ liệu ra: sharding.

Đây là bước đắt nhất trong toàn bộ series, và cái giá không phải tiền mà là độ phức tạp vĩnh viễn: mất transaction xuyên shard, mất JOIN, phân trang trở nên khó, và mọi truy vấn không mang shard key đều thành quét toàn bộ.

Hai phép đo trong bài đáng chú ý. Thứ nhất, tỉ lệ dữ liệu phải di trú khi thêm một shard: modulo hashing từ 4 lên 5 shard làm 79,97% dữ liệu phải chuyển chỗ, còn consistent hashing chỉ 18,78% — và cả hai khớp gần như hoàn hảo với công thức lý thuyết. Thứ hai, một shard key lệch làm p99 tệ hơn 10,3 lần trong khi p50 lại tốt hơn — hotspot sống hoàn toàn ở phần đuôi.

ℹ️ Cấu hình máy đo
Apple M1 Max, 10 lõi, 32 GB RAM, macOS 26.5.2, Docker 29.6.2. Container native arm64. Hai shard PostgreSQL 18.3 độc lập hoàn toàn (không replication, không biết gì về nhau), mỗi shard giới hạn 1 CPU / 512 MB; ba app replica mỗi replica 1 CPU; nginx 1.27-alpine; bộ đo tải 2 CPU. Phần số liệu về tỉ lệ di trú và virtual node chạy bằng node trên sysdesign-hashring.js với 100.000 key, không cần Docker.

Giới hạn 1 CPU mỗi shard không phải chi tiết trang trí — xem mục 8.2, nơi việc thiếu nó khiến lab đo ra kết quả ngược hẳn.

8.1 Khi nào buộc phải shard — và vì sao nên là bước cuối

Thứ tự ưu tiên đúng, từ rẻ tới đắt. Mỗi bước chỉ nên đi khi bước trước đã hết tác dụng:

Bước Giải quyết được Chi phí độ phức tạp
1 · Tối ưu query và thêm index Phần lớn vấn đề "database chậm" trong thực tế Gần như không
2 · Cache (Bài 5) Tải đọc; mỗi số 9 hit ratio chia 10 tải database Thấp, nhưng thêm bài toán vô hiệu hoá
3 · Read replica (Bài 7) Throughput đọc Trung bình: lag và read-your-writes
4 · Scale dọc primary Throughput ghi, tới trần phần cứng Thấp, nhưng có trần và trần đó đắt
5 · Sharding Throughput ghi và dung lượng, không còn trần Cao và VĨNH VIỄN — không có đường quay lại dễ dàng

Ba dấu hiệu cho thấy bạn thật sự cần shard, chứ không phải chỉ cảm thấy cần:

Dấu hiệu Vì sao replica và cache không giúp được
Primary bão hoà I/O ghi, không phải CPU Replica nhân tải đọc chứ không chia tải ghi; cache không đứng trước đường ghi
Dữ liệu vượt dung lượng ổ đĩa lớn nhất bạn mua được Mỗi replica đều chứa trọn bộ dữ liệu, nên thêm replica không thêm dung lượng
Thời gian backup và restore đã dài tới mức không chấp nhận được Đây là dấu hiệu bị bỏ qua nhiều nhất. Database 5 TB nghĩa là RTO (Recovery Time Objective — thời gian tối đa bạn cam kết để khôi phục sau sự cố) của bạn tính bằng nhiều giờ, dù hệ thống đang chạy hoàn hảo
⚠️ Cạm bẫy: shard quá sớm
Sharding sớm là quyết định khó đảo ngược nhất trong bài này. Bạn trả giá ngay lập tức — mất JOIN, mất transaction xuyên shard, phân trang thành bài toán riêng, mọi truy vấn không mang shard key thành scatter-gather (phải hỏi tất cả shard rồi gộp kết quả lại, nên độ trễ bằng shard chậm nhất và tải nhân với số shard) — để đổi lấy một lợi ích có thể không bao giờ cần đến.

Câu hỏi kiểm tra rất đơn giản: bạn đã đo và biết chính xác cái gì đang bão hoà chưa? Nếu câu trả lời là "chưa, nhưng chắc là database" thì bước tiếp theo là đi đo, không phải đi shard. Rất nhiều lần thứ đang bão hoà là một truy vấn thiếu index, và một dòng CREATE INDEX giải quyết xong vấn đề mà sharding phải trả giá cả năm.

8.2 Chọn shard key — và một phép đo suýt cho kết luận ngược

Shard key tốt phải đạt cả ba điều, và điều thứ ba là điều hay bị bỏ qua nhất:

Yêu cầu Nếu thiếu thì sao
Cardinality cao (số giá trị khác nhau mà cột đó có thể nhận) Ít giá trị khác nhau ⇒ không chia được thành nhiều shard, dù thuật toán băm hoàn hảo. Ví dụ: country chỉ có khoảng 200 giá trị và plan chỉ có 3 — băm kiểu gì cũng không cho ra được 50 shard cân bằng; user_id thì có bao nhiêu người dùng là bấy nhiêu giá trị
Phân bố đều Hotspot: một shard nhận gần hết tải trong khi các shard khác nằm không
Khớp với cách truy vấn Mọi truy vấn không mang shard key phải hỏi tất cả shard rồi gộp kết quả (scatter-gather) — độ trễ bằng shard chậm nhất, và tải nhân với số shard
⚠️ Cạm bẫy: shard theo thời gian hoặc ID tăng dần
Shard theo created_at hoặc theo ID tự tăng nghe rất tự nhiên và rất dễ hiểu. Hậu quả: toàn bộ lệnh ghi mới dồn vào một shard — shard của "hôm nay" — trong khi các shard cũ chỉ nhận lưu lượng đọc thưa thớt. Bạn đã trả toàn bộ chi phí phức tạp của sharding mà không nhận được thứ mình mua: chia tải ghi.

Shard theo tenant có cùng vấn đề dưới hình thức khác: nếu một tenant lớn gấp trăm lần các tenant còn lại thì shard chứa nó là một hệ thống riêng đang quá tải. Với trường hợp đó, thường phải xử lý riêng tenant khổng lồ — cho nó shard riêng — chứ không có shard key nào giải quyết được bằng cách băm.

Đo thật: shard key tốt so với shard key lệch

Lab dựng hai shard PostgreSQL độc lập hoàn toàn — không replication, không biết gì về nhau — và một router trong app quyết định key nào đi shard nào. Chế độ skew mô phỏng đúng tình huống "một tenant khổng lồ": 95% request dùng cùng một shard key.

24 kết nối, 12 giây, 20.000 key khác nhau, mỗi shard giới hạn 1 CPU, hai lần lặp:

Số liệu Shard key tốt Shard key lệch
Phân bố tải 49,3% / 50,7% 97,0% / 3,0%
Độ lệch 2,7% / 3,0% 188,0%
Throughput 12.024 / 11.982 rps 9.099 / 9.078 rps
p50 1,96 / 1,98 ms 1,73 / 1,73 ms (tốt hơn!)
p95 3,03 / 3,01 ms 2,84 / 2,82 ms (vẫn tốt hơn)
p99 3,59 / 3,59 ms 37,08 / 36,59 ms
Độ trễ lớn nhất 12,19 / 14,45 ms 48,47 / 50,62 ms

Shard key tốt cho +32% throughput. Nhưng hãy đọc kỹ ba dòng percentile: shard key lệch có p50 p95 tốt hơn, rồi p99 đột ngột tệ hơn 10,3 lần. Hotspot không làm chậm phần lớn request — nó dồn toàn bộ thiệt hại vào phần đuôi.

Đây là lần thứ ba trong series cùng một hình mẫu xuất hiện: ở Bài 3 round robin cho biểu đồ cân bằng nhưng p95 tệ hơn 340 lần; ở Bài 5 single-flight làm p50 tệ hơn nhưng cắt đuôi 11 lần; ở đây shard key lệch thắng ở p50 và p95 rồi thua 10 lần ở p99. Nếu dashboard của bạn chỉ có trung bình và p95 thì cả ba lần bạn đều kết luận ngược.

🔬 Phép đo này suýt cho kết luận ngược — và vì sao
Lần đo đầu tiên, hai shard PostgreSQL không có giới hạn CPU. Kết quả: shard key lệch nhanh hơn — 14.058 rps so với 12.090 rps của shard key tốt — và p99 cũng tốt hơn (3,06 so với 3,61 ms). Nếu dừng ở đó, bài này sẽ kết luận "shard key lệch tốt hơn", nghe vô lý nhưng có số liệu kèm theo.

Nguyên nhân: cả hai shard nằm trên cùng một máy và chia nhau CPU của máy đó. Cấu hình phân bố đều làm hai container PostgreSQL cùng hoạt động, tạo thêm tranh chấp CPU với ba app và bộ đo tải; cấu hình lệch chỉ làm một container bận. Trên một máy, "phân bố đều" là một điều bất lợi.

Nhưng đó chính là thứ mà sharding sinh ra để vượt qua: giới hạn của một máy. Một lab trên một laptop không thể đo được lợi ích đó, trừ khi ta áp giới hạn tài nguyên cho mỗi shard để mô phỏng "mỗi shard là một máy riêng". Thêm cpus: '1.0' cho mỗi shard — đúng cùng lý do đã giới hạn 1 CPU mỗi app từ Bài 2 — và kết quả đảo chiều đúng như dự đoán.

Bài học chung: khi một phép đo cho kết quả ngược với lý thuyết, khả năng cao nhất là môi trường đo đã triệt tiêu chính hiện tượng cần đo. Câu hỏi phải hỏi là "cấu hình này có cho hiện tượng đó cơ hội xuất hiện không", trước khi kết luận lý thuyết sai.

Router shard trong lab, đầy đủ như mã đang chạy:

app/app.js — router shard
// FNV-1a, then mixed with MurmurHash3's fmix32.
// Why the mixing step is needed: FNV-1a has POOR avalanche for short, similar strings
// ('user:1', 'user:2'...), so consecutive keys land on the same shard in clumps.
// The fmix32 step is cheap and removes that effect entirely.
function shardHash(key) {
  let h = 2166136261;
  for (let i = 0; i < key.length; i++) {
    h ^= key.charCodeAt(i);
    h = Math.imul(h, 16777619);
  }
  h ^= h >>> 16;
  h = Math.imul(h, 2246822507);
  h ^= h >>> 13;
  h = Math.imul(h, 3266489909);
  h ^= h >>> 16;
  return h >>> 0;
}

// WARNING: this is modulo hashing. Simple, but read section 8.3 before using it for
// real — adding one shard means migrating nearly all of the data.
function pickShard(shardKey) {
  return shardHash(String(shardKey)) % pgShards.length;
}
Hotspot sống hoàn toàn ở phần đuôi — p50 nói ngược với p99 SHARD KEY TỐT shard 1 · 49,3% 80.762 request shard 2 · 50,7% 82.989 request độ lệch 2,7% · 12.024 rps p50 1,96 ms · p99 3,59 ms đuôi sát p50 — hệ thống đang khoẻ SHARD KEY LỆCH (95% request cùng một shard key) shard 1 · 97,0% 122.964 request shard 2 · 3,0% 3.790 request · gần như rảnh độ lệch 188,0% · 9.099 rps p50 1,73 ms — TỐT HƠN cấu hình trên! p99 37,08 ms — tệ hơn 10,3 lần Cùng bốn số liệu, hai kết luận trái ngược p50: 1,73 < 1,96 · p95: 2,84 < 3,03 → "shard key lệch nhanh hơn" p99: 37,08 vs 3,59 · max: 48,47 vs 12,19 → "shard key lệch tệ hơn 10 lần" Chỉ p99 và max nói đúng.
Đây là lần thứ ba trong series cùng hình mẫu này xuất hiện (Bài 3 mục 3.3, Bài 5 mục 5.4). Dashboard chỉ có trung bình và p95 sẽ cho kết luận sai cả ba lần.

8.3 Modulo hashing và thảm hoạ resharding

Cách chia đơn giản nhất là hash(key) % N với $N$ là số shard. Nó ngắn, dễ hiểu, phân bố đều, và chính là cách router trong lab đang làm. Vấn đề xuất hiện đúng vào ngày bạn thêm shard thứ $N+1$.

Một key chỉ giữ nguyên chỗ nếu $h \bmod N = h \bmod (N+1)$. Theo định lý số dư Trung Hoa, khi $N$ và $N+1$ nguyên tố cùng nhau — mà hai số liên tiếp thì luôn vậy — cặp $(h \bmod N,\ h \bmod (N+1))$ phân bố đều trên $N(N+1)$ khả năng. Số cặp thoả điều kiện giữ nguyên là $N$, nên:

$$P(\text{stay}) = \frac{N}{N(N+1)} = \frac{1}{N+1} \quad\Rightarrow\quad P(\text{move}) = \frac{N}{N+1}$$

Lưu ý con số này là $N/(N+1)$, không phải $(N-1)/N$ như thường bị trích dẫn. Chạy thật với 100.000 key, so cả hai thuật toán, consistent hashing dùng 150 virtual node:

Thêm shard Modulo — đo Modulo — lý thuyết $\frac{N}{N+1}$ Consistent — đo Consistent — lý thuyết $\frac{1}{N+1}$
2 → 3 66,81% 66,67% 37,19% 33,33%
3 → 4 74,89% 75,00% 29,46% 25,00%
4 → 5 79,97% 80,00% 18,78% 20,00%
8 → 9 88,74% 88,89% 12,76% 11,11%
16 → 17 94,03% 94,12% 5,74% 5,88%
Thêm CÙNG một shard (4 → 5) · ô đỏ = key phải chuyển chỗ MODULO · hash(key) % N — đo được 79,97% key phải di trú 64 / 80 ô đỏ Lý thuyết N/(N+1) = 4/5 = 80,00% Đo thật (100.000 key) = 79,97% Với 16 → 17 shard thì con số là 94,03% CONSISTENT HASHING · vòng hash + 150 vnode — đo được 18,78% 15 / 80 ô đỏ Lý thuyết 1/(N+1) = 1/5 = 20,00% Đo thật (100.000 key) = 18,78% Chỉ những key trong cung của shard mới phải chuyển Chênh lệch 4,3 lần về lượng dữ liệu di trú — nhưng chi phí thật nằm ở cache miss toàn cục và ở khoảng thời gian một key có thể ở cả hai shard.
Mỗi ô đại diện 1,25% tập key; số ô đỏ được vẽ khớp với tỉ lệ đo được. Ô xanh là key giữ nguyên chỗ.

Modulo khớp lý thuyết tới hai chữ số thập phân ở mọi hàng. Và con số đáng nhớ nằm ở hàng 4 → 5: thêm một shard vào một hệ thống bốn shard buộc 80% dữ liệu phải chuyển chỗ. Với 16 shard thì con số là 94% — càng nhiều shard, thêm một shard càng tệ.

Consistent hashing sát với $1/(N+1)$ nhưng lệch rõ hơn ở $N$ nhỏ (37,19% so với 33,33% lý thuyết khi 2 → 3). Lý do: với ít node, vị trí các điểm băm trên vòng có phương sai lớn, nên tỉ lệ thực tế dao động quanh giá trị kỳ vọng. Càng nhiều node và càng nhiều virtual node thì càng khớp.

⚡ Chi phí thật của 80% không phải là băng thông
Chuyển 80% dữ liệu là việc nặng nhưng có thể lên kế hoạch. Cái đắt hơn là ba hệ quả kéo theo:

Một: mọi tầng cache dùng cùng cách băm đó cũng miss toàn bộ — nếu bạn đang có hit ratio 99% thì tải database tăng 100 lần đúng lúc nó đang phải di trú dữ liệu (nối Bài 5 mục 5.1).

Hai: trong lúc di trú, một key có thể ở shard cũ, shard mới, hoặc cả hai — nên tầng ứng dụng phải biết đọc cả hai chỗ và biết chỗ nào là chuẩn.

Ba: nếu phải dừng lại giữa đường, bạn ở một trạng thái không phải cũ cũng không phải mới. Đó là lý do consistent hashing không chỉ "tốt hơn về con số" mà là điều kiện để việc thêm shard trở thành một thao tác vận hành bình thường thay vì một dự án.

8.4 Consistent hashing và virtual node

Ý tưởng: thay vì băm key ra chỉ số shard, hãy băm cả key và cả tên shard ra cùng một không gian — một vòng tròn 32 bit. Mỗi key thuộc về shard đầu tiên gặp được khi đi theo chiều kim đồng hồ. Thêm một shard chỉ chiếm lấy phần cung nằm ngay trước nó, nên chỉ những key trong cung đó phải di trú.

Vòng hash — và vì sao virtual node là bắt buộc, không phải tối ưu thêm 1 điểm mỗi node — cung rất lệch s1 s2 s3 s4 s4 chiếm cung khổng lồ s2 chiếm cung tí xíu Nhiều virtual node mỗi node — cung xen kẽ mỗi node có nhiều cung nhỏ rải khắp vòng ⇒ tổng cung của mỗi node xấp xỉ bằng nhau Đo thật · 8 node · 100.000 key lý tưởng: 12.500 key mỗi node 1 vnode: ít nhất 66 · nhiều nhất 25.465 chênh 386 lần! 10 vnode: 8.428 · 20.368 — lệch 95,5% 150 vnode: 11.514 · 14.688 — lệch 25,4% 500 vnode: 10.887 · 13.483 — lệch 20,8% Không có virtual node, consistent hashing vẫn lệch tải nặng — đó là lý do mọi cài đặt thật đều dùng vnode. Virtual node cũng là cách xử lý node có dung lượng khác nhau: node mạnh gấp đôi thì cấp cho nó gấp đôi số vnode.
Số liệu chạy bằng node trên sysdesign-hashring.js, không cần Docker — bạn có thể lặp lại trong vài giây.

Bảng đầy đủ, cùng 8 node và 100.000 key (lý tưởng là 12.500 key mỗi node):

Virtual node / shard Node ít nhất Node nhiều nhất Độ lệch
1 66 25.465 203,2%
2 1.002 28.766 222,1%
5 5.621 18.705 104,7%
10 8.428 20.368 95,5%
50 10.599 14.722 33,0%
150 11.514 14.688 25,4%
500 10.887 13.483 20,8%

Hàng đầu tiên là lý do virtual node tồn tại: với một điểm băm mỗi node, một shard nhận 66 key trong khi một shard khác nhận 25.465 — chênh 386 lần cho một thuật toán vẫn thường được giới thiệu là "phân bố đều".

⚡ Độ lệch không giảm đơn điệu ở giá trị nhỏ
Để ý hàng 2 vnode: độ lệch 222,1%, tệ hơn cả 1 vnode (203,2%). Đây không phải lỗi đo — với rất ít điểm băm, kết quả phụ thuộc hoàn toàn vào chỗ những điểm đó tình cờ rơi vào, nên độ lệch dao động chứ không giảm đều.

Điều đó có một hệ quả thực hành: đừng chọn số vnode nhỏ rồi tăng dần cho tới khi thấy khá, vì đường đi lên không đơn điệu và bạn có thể dừng lại ở một điểm may mắn. Hãy chọn thẳng một giá trị đủ lớn (100–200 là khoảng thường dùng trong thực tế) và kiểm tra bằng đo thật với đúng tập key của bạn. Chi phí là bộ nhớ và thời gian tìm kiếm trên vòng — cả hai đều tăng theo số vnode, nên 500 không phải lúc nào cũng tốt hơn 150 (đo được 20,8% so với 25,4%, đổi lấy hơn ba lần số điểm phải giữ).

Toàn bộ hai bảng trên chạy lại được trong vài giây, không cần Docker. Lưu ý nhỏ để khỏi hoảng: Node sẽ in trước một cảnh báo MODULE_TYPELESS_PACKAGE_JSON — đó chỉ là vì package.json của repo không khai "type": "module", kết quả bên dưới vẫn đúng.

reproduce_hashring.sh
# No Docker needed — just node
cd blog/sysdesign
node --input-type=module -e "
import { compareAddNode, makeKeys, HashRing } from './sysdesign-hashring.js';
const keys = makeKeys(100000);
for (const n of [2,3,4,8,16]) {
  const nodes = Array.from({length:n},(_,i)=>'s'+(i+1));
  const r = compareAddNode(keys, nodes, 's'+(n+1), 150);
  console.log(n+'->'+(n+1),
    'modulo', (r.modulo.ratio*100).toFixed(2)+'%',
    '(theory', ((n/(n+1))*100).toFixed(2)+'%)',
    '| consistent', (r.consistent.ratio*100).toFixed(2)+'%',
    '(theory', ((1/(n+1))*100).toFixed(2)+'%)');
}
for (const v of [1,2,10,150,500]) {
  const ring = new HashRing({ vnodes: v });
  for (const n of ['s1','s2','s3','s4','s5','s6','s7','s8']) ring.addNode(n);
  const d = ring.loadDistribution(keys);
  console.log(v+' vnode: min', d.min, 'max', d.max,
    'spread', (d.spread*100).toFixed(1)+'%');
}
"

# And the self-test suite of 42 assertions, which covers the hash ring too
node sysdesign-engine-selftest.mjs

8.5 Hệ quả ở tầng ứng dụng — phần đắt thật sự

Chọn được shard key tốt và dùng consistent hashing chỉ giải quyết phần hạ tầng. Cái đắt nằm ở tầng ứng dụng, vì bốn thứ bạn vẫn coi là hiển nhiên nay không còn nữa.

Thứ mất đi Vì sao mất Phải thay bằng gì
JOIN giữa hai bảng khác shard Hai shard là hai database không biết gì về nhau Nhân bản dữ liệu nhỏ sang mọi shard, hoặc đặt hai bảng cùng shard key để chúng luôn ở cùng shard
Transaction bao nhiều shard Không có transaction coordinator giữa các database độc lập Thiết kế lại để mỗi transaction nằm trong một shard; nếu không được thì saga (Bài 14)
Khoá tự tăng toàn cục Mỗi shard có chuỗi riêng ⇒ hai shard sinh cùng ID UUID, hoặc ID có nhúng shard, hoặc Snowflake-style (Bài 10)
Truy vấn theo cột không phải shard key Không biết dữ liệu nằm shard nào Global secondary index (một bảng tra cứu ánh xạ giá trị → shard), hoặc chấp nhận scatter-gather
⚠️ Cạm bẫy: tự làm JOIN trong tầng ứng dụng
Khi JOIN không còn, phản xạ tự nhiên là kéo dữ liệu từ cả hai shard về app rồi ghép trong bộ nhớ. Với vài trăm dòng thì không sao. Với vài trăm nghìn dòng thì bạn vừa biến một database phân tán thành một nút cổ chai mạng — và tệ hơn, chỗ nghẽn nằm ở app chứ không ở database, nên mọi dashboard database vẫn xanh trong khi hệ thống chậm.

Đây cũng chính là bài toán fan-out ở Bài 4 mục 4.4 với đúng cùng hai quy tắc: gọi các shard song song (độ trễ bằng shard chậm nhất, không phải tổng), và có timeout riêng cho từng shard để một shard chậm không kéo cả truy vấn xuống.

Nhưng câu trả lời tốt nhất thường không phải tối ưu việc ghép, mà là không cần ghép: chọn shard key sao cho dữ liệu hay được truy vấn cùng nhau thì nằm cùng shard. Đó là yêu cầu thứ ba ở mục 8.2 — "khớp với cách truy vấn" — và nó quan trọng hơn cả hai yêu cầu đầu.

Còn một việc nữa mà không có lời khuyên nào làm cho dễ: phân trang xuyên shard. Muốn lấy "20 dòng mới nhất" trên 8 shard, bạn phải lấy 20 dòng mới nhất từ mỗi shard (160 dòng), gộp, sắp xếp, rồi cắt 20. Trang thứ hai thì phải lấy 40 mỗi shard, và trang thứ 100 thì mỗi shard phải trả 2.000 dòng để cuối cùng dùng 20. Chi phí tăng theo số trang, không theo số dòng cần.

Cách thoát thông thường là bỏ phân trang theo số trang và chuyển sang phân trang theo con trỏ (cursor/keyset): "cho tôi 20 dòng sau mốc này". Nó cắt được chi phí tăng theo trang, nhưng đổi lại người dùng không nhảy tới trang 100 được nữa — một thay đổi sản phẩm, không chỉ là thay đổi kỹ thuật. Đây là dạng đánh đổi rất đặc trưng của sharding: giới hạn hạ tầng đi ngược lên và định hình cả tính năng.

Tự làm lại phép đo shard

reproduce_shard_measurements.sh
cd blog/sysdesign/sysdesign-lab
docker compose --profile shard up -d   # two INDEPENDENT PostgreSQL shards + the app router

# Check that the router really does distribute
for k in a b c d e f; do curl -s "http://localhost:3001/shard?key=$k&mode=good"; echo; done

# --- GOOD shard key ---
./tools/cache-stats.sh reset
docker compose run --rm loadgen loadgen.js --url "http://lb:8080/shard?mode=good" \
  -c 24 -d 12 -w 2 --json --key-space 20000
./tools/cache-stats.sh          # look at the SHARD line + the spread

# --- SKEWED shard key (95% of requests share one shard key) ---
./tools/cache-stats.sh reset
docker compose run --rm loadgen loadgen.js --url "http://lb:8080/shard?mode=skew" \
  -c 24 -d 12 -w 2 --json --key-space 20000
./tools/cache-stats.sh

# IMPORTANT: if you drop the `cpus: '1.0'` limit on the two shards in docker-compose.yml
# the result INVERTS — the skewed key will look faster. The reason is in section 8.2.

Tóm lại

Sharding là bước cuối vì nó là bước duy nhất có chi phí vĩnh viễn: mất JOIN, mất transaction xuyên shard, phân trang thành bài toán riêng. Trước nó còn bốn bước rẻ hơn, và câu hỏi kiểm tra luôn là "bạn đã đo và biết chính xác cái gì đang bão hoà chưa".

Modulo hashing đơn giản nhưng thêm một shard buộc $N/(N+1)$ dữ liệu di trú — đo được 79,97% khi đi từ 4 lên 5 shard, khớp lý thuyết tới hai chữ số. Consistent hashing hạ con số đó xuống 18,78%, nhưng chỉ khi có virtual node: không có vnode, một shard nhận 66 key trong khi shard khác nhận 25.465.

Và bài học về đo lường lặp lại lần thứ ba trong series: shard key lệch có p50 p95 tốt hơn shard key tốt, rồi p99 tệ hơn 10,3 lần. Cộng thêm một bài học mới: lần đo đầu tiên của mục 8.2 cho kết quả ngược hoàn toàn, vì hai shard dùng chung CPU của một máy — môi trường đo đã triệt tiêu đúng hiện tượng cần đo.

Bài 9 hình thức hoá câu hỏi mà mục 7.4 để ngỏ: khi mạng bị chia, và không node nào biết bên kia đã chết hay chỉ mất liên lạc, thì phải chọn gì? Đó là CAP — và phần quan trọng hơn CAP mà ít người nói tới: lựa chọn bạn gặp hằng ngày, khi mạng hoàn toàn bình thường.

📖 Tài liệu tham khảo

Tải mã nguồn thực hành

Vòng hash và bộ so sánh modulo/consistent đã sinh ra mọi con số ở mục 8.3 và 8.4. Không phụ thuộc thư viện nào và chạy được ngay bằng node, không cần Docker:

Tải về sysdesign-hashring.js

Bài viết liên quan trong series

Bài 7: Replication & Scale Tầng Đọc Bài 9: CAP & Các Mô Hình Nhất Quán Quay lại Lộ trình Thiết Kế Hệ Thống

Bình luận