Database hiếm khi cần sharding ngay trong ngày khó khăn đầu tiên. Thông thường, đội ngũ chỉ nghĩ đến nó khi primary không còn chịu nổi lượng write, index không còn nằm gọn trong memory, maintenance window trở nên nguy hiểm, hoặc một tenant có thể làm ảnh hưởng mọi tenant khác. Sharding xử lý các giới hạn ấy bằng cách chia row cho nhiều database node độc lập.
Sự đánh đổi này chỉ đáng thực hiện khi nút thắt thật sự nằm ở kích thước hoặc write throughput của một database. Read replica có thể mở rộng workload thiên về read. Table partitioning giúp pruning, retention và bảo trì index rẻ hơn trong cùng database authority. Nếu primary vẫn giữ được working set và write rate với headroom lành mạnh, replica hoặc partitioning thường đã đủ.
Sharding hợp lý khi một failure domain, storage device, buffer pool hoặc write-ahead log là tài nguyên giới hạn, đồng thời dữ liệu có thể chia theo một ranh giới ownership ổn định. Thiết kế phải bắt đầu từ ranh giới đó, không phải từ số node mong muốn.
Chọn chiến lược từ access pattern
Bốn chiến lược phổ biến trả lời câu hỏi “Shard nào sở hữu row này?” theo các cách khác nhau.
| Chiến lược | Quy tắc routing | Điểm mạnh | Rủi ro chính |
|---|---|---|---|
| Range | Khoảng có thứ tự, chẳng hạn customer ID 1-1M | Range scan và archive hiệu quả | Key tuần tự làm nóng range mới nhất |
| Hash | hash(shard_key) mod N hoặc hash ring |
Phân phối đều với key ngẫu nhiên | Range query bị scatter; modulo remap quá nhiều |
| Directory | Metadata ánh xạ từng key hoặc tenant đến shard | Placement tường minh, dễ chuyển tenant | Availability, consistency và cache của directory |
| Geographic | Region hoặc data residency chọn regional shard group | Latency cục bộ thấp, tuân thủ residency | User toàn cầu và operation xuyên region |
Range sharding phù hợp với time-series history, catalog có thứ tự và dữ liệu mà query phổ biến đi theo chính ranh giới range. Static range dễ hiểu, nhưng ID tăng dần hoặc timestamp sẽ dồn mọi write mới vào range cuối. Pre-split, kết hợp time bucket với một chiều khác hoặc cấp các range không liền nhau có thể giảm hotspot đó.
Hash sharding là lựa chọn mặc định tốt cho point read theo account, device hoặc tenant. Hash đồng đều phân tán cả input tuần tự, nhưng làm mất ordering hữu ích. Modulo thuần đặc biệt gây khó khi mở rộng: đổi từ sang làm kết quả của phần lớn key thay đổi. Consistent hashing đặt key và virtual node lên một vòng. Khi thêm một physical shard, hệ thống chỉ chuyển các interval lân cận, xấp xỉ
với membership cân bằng, thay vì chuyển gần như mọi key. Virtual node cải thiện độ cân bằng và cho phép máy có năng lực khác nhau sở hữu số interval khác nhau.
Directory sharding lưu placement như dữ liệu. Nó phù hợp khi tenant lớn cần shard riêng, tenant nhỏ dùng chung capacity và operator phải chuyển một tenant mà không đổi identity. Directory phải có availability cao, version rõ ràng, cache với độ stale bị chặn và cơ chế ngăn hai shard cùng nhận ownership khi update.
Geographic sharding đặt dữ liệu theo yêu cầu pháp lý hoặc region truy cập chính. Thông thường, nó chọn regional shard group trước, rồi dùng range, hash hoặc directory trong group đó. Thiết kế phải trả lời khi account chuyển vùng, user đi du lịch hoặc workflow nối user từ nhiều region; “route đến region gần nhất” không phải quy tắc ownership.
Tip
Hãy kiểm tra request chủ đạo có xác định được đúng một shard trước khi chạm storage hay không. Chiến lược phân phối byte rất đều nhưng biến mọi query thường ngày thành fan-out đã tối ưu nhầm đại lượng.
Thiết kế shard key và routing contract
Shard key tốt có cardinality cao, ownership ổn định, traffic và storage tương đối đều, đồng thời khớp với ranh giới query quan trọng nhất. tenant_id thường hiệu quả trong business software vì authorization, read, write và retention đều theo tenant. Nó thất bại khi một tenant lớn hơn cả shard hoặc phần lớn query so sánh mọi tenant. user_id phân tán social workload, nhưng một tài khoản nổi tiếng vẫn có thể thành hot key: số key đồng đều không có nghĩa request rate đồng đều.
Với shard , nên định nghĩa observed load bằng nhiều yếu tố thay vì chỉ đếm row:
Mức mất cân bằng cần theo dõi là
Trọng số phải đến từ constraint thật. Shard chứa ít dữ liệu vẫn có thể bão hòa vì một hot key. Cách giảm tải gồm cache immutable read, tách tenant khổng lồ bằng key thứ hai, write bucketing cho counter có thể merge, cô lập noisy tenant và đặt admission limit theo tenant. Random suffix phân tán write nhưng buộc read fan-out, vì vậy chỉ dùng khi aggregation đã được thiết kế rõ.
Routing nên nằm trong một component dùng chung hoặc database proxy nhỏ, không rải thành conditional tự phát trong nhiều service. Hash ring TypeScript sau dùng SHA-256, virtual node, điểm 64-bit không dấu, binary search và wraparound. Membership nên đến từ versioned configuration thay vì process tự discovery cục bộ.
import { createHash } from 'node:crypto';
interface Shard {
readonly id: string;
readonly connectionString: string;
}
interface RingPoint {
readonly hash: bigint;
readonly shard: Shard;
}
function hash64(value: string): bigint {
const digest = createHash('sha256').update(value).digest();
return digest.readBigUInt64BE(0);
}
export class ConsistentHashRouter {
private readonly ring: RingPoint[];
constructor(shards: readonly Shard[], virtualNodes = 256) {
if (shards.length === 0 || virtualNodes < 1) {
throw new Error('At least one shard and one virtual node are required');
}
this.ring = shards
.flatMap((shard) =>
Array.from({ length: virtualNodes }, (_, index) => ({
hash: hash64(`${shard.id}:${index}`),
shard,
})),
)
.sort((left, right) => (left.hash < right.hash ? -1 : 1));
}
route(shardKey: string): Shard {
const target = hash64(shardKey);
let low = 0;
let high = this.ring.length;
while (low < high) {
const middle = low + Math.floor((high - low) / 2);
const point = this.ring[middle];
if (!point || point.hash >= target) high = middle;
else low = middle + 1;
}
const selected = this.ring[low] ?? this.ring[0];
if (!selected) throw new Error('Hash ring is empty');
return selected.shard;
}
}
Hash algorithm, byte encoding, số virtual node, shard ID và membership epoch đều là lựa chọn protocol phải được lưu bền vững. Thay đổi bất kỳ yếu tố nào cũng âm thầm đổi ownership. Mọi ngôn ngữ triển khai router cần được test bằng cùng một tập vector cố định.
Giữ query và transaction trong một shard
Cross-shard query rẻ nhất là query mà data model đã tránh được. Hãy co-locate các entity thường thay đổi cùng nhau và truyền shard key qua primary key, foreign key, queue message, cache key cùng API command. Trên mỗi shard, schema theo tenant có thể enforce local uniqueness:
CREATE TABLE orders (
tenant_id UUID NOT NULL,
order_id UUID NOT NULL,
status TEXT NOT NULL,
total_cents BIGINT NOT NULL CHECK (total_cents >= 0),
created_at TIMESTAMPTZ NOT NULL,
PRIMARY KEY (tenant_id, order_id)
);
CREATE INDEX orders_by_tenant_created
ON orders (tenant_id, created_at DESC);
SELECT order_id, status, total_cents
FROM orders
WHERE tenant_id = $1 AND created_at >= $2
ORDER BY created_at DESC
LIMIT 50;
Query này route bằng tenant_id rồi chạy trên một shard. Global report không có shard key phải scatter tới mọi shard, chạy bounded subquery song song rồi merge kết quả. Cần giới hạn concurrency, đẩy filter và partial aggregation xuống shard, gắn deadline và định nghĩa hành vi khi chỉ có partial result. Với global analytics thường xuyên, hãy stream change data sang warehouse hoặc search index thay vì dùng mọi transactional shard như reporting engine.
Local transaction vẫn giữ ACID guarantee thông thường của database. Transaction xuyên shard cần distributed coordination như two-phase commit, kéo theo latency, trạng thái blocking và bài toán phục hồi coordinator. Nhiều product workflow phù hợp hơn với local transaction idempotent cộng outbox, event và saga có compensating action. Đó không phải atomicity: intermediate state vẫn nhìn thấy được, nên invariant và thông báo cho user phải cho phép nó. Chuyển tiền, uniqueness và phân bổ inventory đôi khi cần một service strongly consistent riêng hoặc coordination có chủ đích.
Global ID không được phụ thuộc vào một database sequence duy nhất. UUIDv7 tạo ID phi tập trung, gần theo thứ tự thời gian; Snowflake-style ID kết hợp timestamp, worker ID và sequence nhưng cần worker lease cùng xử lý clock chặt chẽ. ID phải unique toàn cục, còn business uniqueness như một username trên toàn platform cần global registry, deterministic home shard hoặc reservation workflow.
Warning
Đừng gọi cross-shard transaction là atomic chỉ vì retry cuối cùng hoàn tất. Idempotency ngăn effect bị lặp; compensation sửa effect; cả hai đều không che intermediate state như database transaction.
Rebalance mà không đổi ownership hai lần
Khi tăng capacity, hệ thống phải chuyển range, virtual node hoặc directory entry trong lúc request vẫn chạy. Hãy xem placement như một versioned state machine. Migration an toàn copy một đơn vị hữu hạn, bắt kịp concurrent write, verify, đổi authoritative routing đúng một lần và giữ dữ liệu rollback cho tới khi đủ tin cậy.
Trong lúc copy, phải chọn một source of truth. Write vẫn authoritative ở source rồi chuyển qua change-data capture sẽ tránh kết quả dual-write mơ hồ. Nếu không thể tránh dual write, hãy dùng operation ID idempotent, ghi trạng thái cho từng destination và định nghĩa bên thắng. Khi cutover, fence router cũ bằng epoch: shard nhận request với epoch cũ phải reject hoặc redirect thay vì chấp nhận write sau khi ownership đã chuyển.
Throttle copy bandwidth theo replication lag và foreground latency, không chỉ bằng một ngưỡng tối đa cố định. Verify row count theo bucket, checksum, sampled semantic read và change-stream checkpoint. Reshard theo đơn vị nhỏ để việc pause, retry và rollback thật sự khả thi. Schema version phải tương thích trên cả source lẫn destination trong suốt quá trình chuyển.
Vận hành cho quan sát, failure và recovery
Dashboard cần phân phối theo từng shard cho QPS, write rate, storage, CPU, connection, lock time, query latency, replication lag, error rate và hottest key. Fleet average sẽ che mất một shard quá tải. Cũng cần theo dõi router epoch, directory lookup latency và cache age, fan-out width, partial-query rate, migration throughput, checksum failure, stale-epoch write bị reject và tỷ trọng tài nguyên của tenant lớn nhất.
Mọi shard đều cần replica, backup và quy trình restore đã được kiểm thử. Replication xử lý machine failure và read scale; nó không thay thế point-in-time backup vì thao tác xóa hay corruption cũng được replicate. Hãy định nghĩa recovery point objective và recovery time objective cho từng nhóm dữ liệu. Thường xuyên restore một shard vào môi trường cô lập, replay log tới timestamp, kiểm tra application invariant, rồi diễn tập publish replacement qua metadata.
Khi primary hỏng, chỉ promote replica đáp ứng durability policy và phải fence primary cũ trước khi nhận write. Network partition không được tạo ra hai writable owner. Router chỉ nên retry operation idempotent và refresh placement sau response NOT_OWNER. Kết quả commit chưa xác định cần operation ID hoặc status lookup, không phải blind retry.
Directory failure cần kế hoạch riêng. Router có thể dùng assignment last-known-good trong thời gian hữu hạn, nhưng placement change phải dừng cho tới khi metadata authority trở lại. Directory cần backup riêng, mọi mutation phải có audit và ownership record bền vững phải đủ để rebuild nó. Fault exercise nên bao gồm mất shard, mất metadata, router stale, cạn connection pool, snapshot hỏng và migration bị ngắt giữa chừng.
Biết khi nào không nên shard
Dùng read replica khi write vẫn vừa một primary còn read throughput mới là constraint. Dùng native partitioning khi vấn đề là lifecycle management, partition pruning, vacuum hoặc kích thước local index nhưng một database vẫn điều phối được workload. Archive row lạnh khi historical growth, thay vì active working set, gây áp lực. Cache repeated read, batch write, sửa query plan và scale up nếu những biện pháp này lấy lại headroom với operational cost thấp hơn.
Chỉ shard sau khi đo được một giới hạn mà các công cụ đó không thể loại bỏ với chi phí hợp lý. Trước khi quyết định, hãy chứng minh request thông thường shard-local, ước lượng tenant lớn nhất và hot key nóng nhất, benchmark traffic skew, đồng thời thực hiện trọn vẹn một lần chuyển và restore shard trên staging. Đội ngũ sẽ phải sở hữu routing metadata, global schema rollout, backup coverage, capacity balancing và cross-shard debugging trong suốt vòng đời hệ thống.
Lộ trình tăng dần thường an toàn nhất: đưa vào globally unique ID và tenant key tường minh, bỏ hidden cross-tenant constraint, đặt routing sau một interface và thu thập load theo key. Những thay đổi ấy vẫn cải thiện architecture ngay cả khi hệ thống không bao giờ shard. Nếu có, chúng ngăn migration biến thành một lần viết lại đồng thời identity, data access và operation.
Tổng kết
Sharding mở rộng storage và write bằng cách thay một database thành nhiều ownership domain. Range, hash, directory và geographic strategy tối ưu các access pattern khác nhau; không chiến lược nào cứu được shard key bỏ qua locality hoặc traffic skew. Hãy chọn key từ query cùng invariant, đo hot key bên cạnh dung lượng và giữ routing như một versioned protocol.
Thiết kế transaction và query phổ biến để nằm trong một shard. Chuyển global search cùng analytics sang derived system, đồng thời xem distributed transaction, global uniqueness và regional ownership là các bài toán coordination tường minh. Rebalance bằng snapshot, change capture, verification, một fenced cutover và rollback window.
Cuối cùng, vận hành mỗi shard như một database có thể phục hồi và directory như control-plane data trọng yếu. Nếu replica, partitioning, archiving hoặc tuning giải quyết được bottleneck đã đo, hãy dừng ở đó. Sharding thành công không phải khi row đã nằm trên nhiều máy, mà khi ownership vẫn dự đoán được qua tăng trưởng, failure và thay đổi.