Sáu phần trước là về nhiều máy giữ cùng dữ liệu. Phần này là câu hỏi ngược: dữ liệu chia ra nhiều máy, khoá này ở máy nào? Câu trả lời ngắn nhất — key % n — đúng tuyệt đối cho tới ngày bạn thêm máy thứ 11, và lúc đó nó vẫn đúng, chỉ có điều gần như mọi khoá đã đổi chỗ. Consistent hashing là họ kỹ thuật để thêm bớt máy mà chỉ một phần nhỏ khoá phải di chuyển. Bài này đo "phần nhỏ" là bao nhiêu, và hai cái bẫy khiến nó không nhỏ như sách nói.
Ba cách trả lời "khoá này ở đâu"

Hình 1: Modulo một dòng. Vòng băm với node ảo và một bước trộn bit mà thiếu nó thì toàn bộ phép đo sai. Jump hash năm dòng, không bảng.
- Modulo:
key % n. Phân bố hoàn hảo, tra cứu một nano-giây, và khinđổi thì gần như mọi khoá đổi node. - Vòng băm (Karger et al., 1997): đặt mỗi node tại nhiều điểm trên vòng tròn 0..2⁶⁴, khoá thuộc về node có điểm kế tiếp theo chiều kim đồng hồ. Thêm node là thêm điểm, chỉ các khoá nằm ngay trước những điểm đó đổi chủ. "Nhiều điểm" — node ảo — để một node không ôm cả một cung lớn.
- Jump hash (Lamping & Veach, Google 2014): một vòng lặp số học tính thẳng số node từ khoá, không bảng, không bộ nhớ, phân bố gần hoàn hảo. Đổi lại, node phải đánh số liên tục 0..n−1 và chỉ thêm bớt được ở cuối.
Đo thật: bao nhiêu khoá đổi chỗ, lệch tải bao nhiêu

Hình 2: Kết quả thật trên một triệu khoá. Hàng "FNV thô" là phép đo đầu tiên của mình, trước khi tìm ra vì sao nó sai.
| cách | 10 → 11 node | 10 → 9 node | lệch tải (nặng nhất / nhẹ nhất) |
|---|---|---|---|
| modulo | 90,9% | 90,0% | 1,002× / 0,998× |
| vòng băm, 1 node ảo | 9,3% | 1,2% | 3,185× / 0,118× |
| vòng băm, 100 node ảo | 8,4% | 10,3% | 1,114× / 0,908× |
| vòng băm, 1000 node ảo | 9,0% | 10,1% | 1,031× / 0,953× |
| jump hash | 9,0% | 10,0% | 1,005× / 0,995× |
Modulo chuyển 90,9% khoá khi thêm một node. Không phải 9%, là 90%: với n từ 10 lên 11, khoá k ở lại node cũ chỉ khi k % 10 == k % 11, tức khoảng 1/11 trường hợp. Với cache, đó là 90% miss cùng một lúc — chính cơn bão sê-ri caching đã đo, nhân lên toàn cụm. Với database shard, là 90% dữ liệu phải chép sang máy khác trước khi cụm mới dùng được.
Vòng băm với đủ node ảo chuyển đúng 9,0% và 10,1% — sát lý tưởng 1/11 và 1/10. Lý do: thêm node là thêm 1.000 điểm rải khắp vòng, mỗi điểm "cắt" một cung nhỏ từ node đang giữ cung đó, và tổng các cung bị cắt là 1/11 vòng. Bớt node thì các cung của nó chia đều cho hàng xóm.
Bẫy thứ nhất: ít node ảo. Một node ảo cũng chuyển ~9% khi thêm, nhưng nhìn cột lệch tải: node nặng nhất gánh 3,185× trung bình, nhẹ nhất 0,118× — gấp 27 lần nhau. Với 10 điểm ngẫu nhiên trên vòng, các cung có độ dài rất không đều, và đó là lý do 10 → 9 chỉ chuyển 1,2%: node bị bỏ tình cờ là một node nhỏ. 100 node ảo đưa lệch về 1,11×, 1.000 về 1,03× — giá là bảng 100.000 điểm (6,4 MB) và tra cứu chậm hơn.
Bẫy thứ hai: hàm băm trộn kém. Phép đo đầu tiên của mình dùng FNV-1a thẳng cho vị trí node ảo "node-3#0", "node-3#1"... Kết quả: 100 node ảo mà lệch tải 3,026×, và thêm node chuyển 16,6% thay vì 9%. Nguyên nhân: các chuỗi chỉ khác ký tự cuối, FNV-1a cho ra giá trị gần nhau, nên 100 điểm của cùng một node dính chùm trên vòng — hành xử như một điểm. Thêm một bước trộn bit (splitmix64 finalizer) sau FNV là mọi con số về đúng. Hàm băm "đủ tốt cho hash map" chưa chắc đủ tốt cho vòng băm; đây là lý do các bản cài đặt thật dùng MurmurHash3, xxHash hoặc SHA-1 cắt ngắn.
Jump hash cân nhất và rẻ nhất về bộ nhớ: 1,005×, 9,0%/10,0%, không bảng, 35 ns mỗi khoá. Nhưng mục 4 cho thấy cái giá: node được đánh số 0..n−1, nên chỉ bớt được node cuối. Bỏ node 3 trong 10 node là phải đánh số lại 4..9 và kéo theo hàng loạt khoá đổi chỗ. Vòng băm thì bỏ node 3 chuyển đúng 94.377 khoá của node 3 và không động tới ai khác. Jump hash hợp với cụm chỉ to ra (hoặc co lại từ đuôi) và với thứ không có "máy chết" theo nghĩa bất kỳ — ví dụ chia dải shard logic; vòng băm hợp với cụm máy thật, nơi máy hỏng là máy bất kỳ.
Đánh đổi cần cân nhắc
Số node ảo là đánh đổi giữa cân bằng và bộ nhớ/tốc độ. 100–200 là con số nhiều hệ thống dùng (Cassandra mặc định 256 token mỗi node trước khi đổi sang 16 với thuật toán cấp token thông minh hơn). 1.000 cho 1,03× nhưng bảng 100.000 phần tử cho 100 node — vẫn nhỏ; vấn đề thật là khi có 10.000 node.
"Chuyển 9% khoá" vẫn là chuyển 9% khoá. Với cache, 9% miss có thể chấp nhận; với database, 9% của 10 TB là 900 GB phải chép, và trong lúc chép thì khoá đó ở đâu? Consistent hashing trả lời "ở đâu" nhưng không trả lời "chuyển thế nào mà không dừng" — phần 11 về sharding sẽ đo chính việc chuyển đó.
Băm đều không có nghĩa tải đều. Mọi con số lệch tải ở trên đếm số khoá; nếu một khoá nóng gấp 10.000 lần khoá khác (hot key của sê-ri caching), node giữ nó quá tải dù nhận đúng 1/10 số khoá. Băm giải quyết phân bố khoá, không giải quyết phân bố truy cập.
Có những cách khác với đánh đổi khác. Rendezvous hashing (HRW) cân tốt, bớt node bất kỳ được, không cần bảng, nhưng tra cứu O(n) theo số node. Maglev của Google cân rất đều và tra cứu O(1) nhưng xây bảng lớn. Vòng băm với node ảo vẫn là mặc định tốt vì nó là thứ duy nhất trong bảng đo ở trên vừa cân (với đủ node ảo), vừa bớt node bất kỳ, vừa tra cứu O(log n).
Một triệu khoá là mẫu thống kê, không phải tải thật. Tỉ lệ chuyển và lệch tải ổn định từ vài trăm nghìn khoá trở lên; với 10.000 khoá thì nhiễu lớn hơn nhiều và lệch tải thực tế tệ hơn bảng trên.
Ba ý mang về
- Modulo chuyển 90% khoá mỗi lần cụm đổi cỡ; vòng băm với đủ node ảo chuyển ~9% (1/n+1) — đúng lý thuyết, đo được trên một triệu khoá. Chênh lệch 10 lần đó là 90% cache miss cùng lúc hay 90% dữ liệu phải di chuyển.
- Vòng băm có hai bẫy đo được: ít node ảo thì một node gánh 27 lần node khác (1 node ảo: 3,185×/0,118×), và hàm băm trộn kém làm điểm ảo dính chùm (FNV thô: 3,026× dù có 100 node ảo). Cần ≥100 node ảo và một hàm băm trộn tốt.
- Jump hash cân nhất (1,005×) với 0 byte bảng, nhưng chỉ thêm bớt node ở cuối; vòng băm bớt node bất kỳ và chỉ chuyển đúng khoá của node đó. Chọn theo cụm của bạn to ra theo thứ tự hay có máy hỏng bất kỳ.
Nguồn
- Karger et al. — Consistent Hashing and Random Trees (1997): https://www.cs.princeton.edu/courses/archive/fall09/cos518/papers/chash.pdf
- John Lamping, Eric Veach — A Fast, Minimal Memory, Consistent Hash Algorithm (jump hash, 2014): https://arxiv.org/abs/1406.2294
- Amazon — Dynamo: Amazon's Highly Available Key-value Store (node ảo trên vòng): https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf
- Apache Cassandra docs — Virtual nodes (num_tokens): https://cassandra.apache.org/doc/latest/cassandra/architecture/dynamo.html#dataset-partitioning-consistent-hashing
- Eisenbud et al. — Maglev: A Fast and Reliable Software Network Load Balancer (2016): https://research.google/pubs/maglev-a-fast-and-reliable-software-network-load-balancer/
Phần sau quay lại chuyện ghi: khi một thao tác phải chạm hai database ở hai máy — trừ tiền ví và tạo đơn — hoặc cả hai thành công hoặc không gì cả. Hai pha commit bằng PREPARE TRANSACTION thật của PostgreSQL, đo giá của nó, và chuyện gì xảy ra khi điều phối viên chết giữa hai pha.