Database Sharding & Partitioning: Khi nào cần phân mảnh dữ liệu?
7/19/2026 · 12p đọc
title: "Database Sharding & Partitioning: Khi nào cần phân mảnh dữ liệu?"
series: "Software Architecture Masterclass"
part: "Phần 3 — Hệ thống Phân tán & Hạ tầng"
order: 24
audience: "Solutions Architect & Technical Lead"
reading_time: "13 phút"
tags: ["sharding", "partitioning", "database", "scalability", "distributed-systems", "shard-key"]
Database Sharding & Partitioning: Khi nào cần phân mảnh dữ liệu?
Có một cuộc họp mà kiến trúc sư nào rồi cũng sẽ trải qua ít nhất một lần trong sự nghiệp: đội vận hành báo cáo rằng instance database chính đang ở mức CPU 92% vào giờ cao điểm, độ trễ ghi tăng gấp ba lần trong sáu tháng qua, và bản RDS lớn nhất mà nhà cung cấp cloud bán đã được đặt mua rồi. Câu hỏi tiếp theo trong phòng luôn là: "Vậy giờ sao?" Và câu trả lời tiếp theo, gần như luôn luôn, là một từ khiến cả phòng im lặng: sharding.
Không ai muốn shard database sớm. Đó không phải vì lười — đó là vì sharding là một trong số ít quyết định kiến trúc thực sự one-way door (cửa một chiều): một khi dữ liệu đã bị phân mảnh theo một shard key nhất định, việc đổi ý gần như đồng nghĩa với việc viết lại toàn bộ tầng truy cập dữ liệu và di chuyển petabyte dữ liệu đang phục vụ traffic sống. Bài này không nói về "cách shard" theo nghĩa kỹ thuật thuần túy — nó nói về việc khi nào bạn thực sự cần bước qua cánh cửa đó, và cái giá bạn phải trả sau khi bước qua.
The Problem
Một hệ quản trị cơ sở dữ liệu quan hệ — MySQL, PostgreSQL, hay bất kỳ RDBMS nào — về bản chất chạy trên một instance duy nhất (kể cả khi có replica đọc, vẫn chỉ có một node ghi chính). Node đó bị giới hạn bởi phần cứng vật lý (hoặc ảo hóa) mà nó đang chạy: số CPU core, dung lượng RAM, băng thông I/O của ổ đĩa, và giới hạn kết nối đồng thời.
Khi ứng dụng tăng trưởng, có hai trục áp lực xuất hiện gần như đồng thời:
Trục thông lượng ghi (write throughput). Số lượng transaction ghi mỗi giây tăng tuyến tính theo số người dùng hoạt động. Một node ghi đơn lẻ có trần cứng — dù bạn nâng cấp CPU nhanh đến đâu, tại một điểm nào đó, disk I/O và lock contention trên các bảng nóng (hot table) sẽ trở thành nút thắt không thể phá vỡ bằng cách mua thêm phần cứng.
Trục dung lượng lưu trữ (storage volume). Dữ liệu tích lũy theo thời gian: log, giao dịch, sự kiện, tin nhắn. Một bảng hàng tỷ dòng khiến index không còn vừa trong bộ nhớ, mọi truy vấn full-scan trở thành thảm họa, và thời gian backup/restore kéo dài đến mức không còn khả thi trong cửa sổ bảo trì cho phép.
Phản xạ đầu tiên của mọi kiến trúc sư — đúng đắn — là scale theo chiều dọc (vertical scaling): mua instance lớn hơn, thêm RAM, chuyển sang SSD NVMe nhanh hơn. Đây luôn là bước đi rẻ nhất và ít rủi ro nhất, nên hãy làm nó trước. Bước tiếp theo là giảm tải bằng caching (bài 23) và read replica để tách traffic đọc khỏi node ghi chính. Cả ba biện pháp này đều không đụng đến cấu trúc dữ liệu.
Nhưng cả ba đều có trần. Scale dọc bị giới hạn bởi instance lớn nhất mà nhà cung cấp bán (và giá tăng phi tuyến tính khi lên gần đỉnh dòng sản phẩm). Read replica chỉ giải quyết tải đọc, không giải quyết tải ghi — vì mọi replica vẫn phải áp dụng đúng chuỗi ghi mà node chính đã thực hiện. Cache chỉ hiệu quả với dữ liệu đọc nhiều, ghi ít; nó không cứu bạn khỏi một bảng ghi hàng chục nghìn dòng mỗi giây.
Khi cả ba con đường đó đã cạn — đó là lúc câu hỏi sharding thực sự xuất hiện trên bàn.
The Theory
Partitioning là khái niệm rộng: chia một tập dữ liệu lớn thành các phần nhỏ hơn theo một tiêu chí nào đó. Có hai kiểu:
- Partitioning theo chiều dọc (vertical): chia các cột của một bảng ra nhiều bảng/nhiều database khác nhau (ví dụ tách bảng
usersthànhusers_corevàusers_profile_extended). Đây thực chất là một dạng tách module dữ liệu, gần với ý tưởng bounded context (bài 10) hơn là scale. - Partitioning theo chiều ngang (horizontal) — chính là sharding: chia các dòng của cùng một bảng logic ra nhiều instance database vật lý khác nhau, dựa trên giá trị của một hoặc nhiều cột gọi là shard key (khóa phân mảnh).
Sharding hoạt động dựa trên nguyên lý: nếu một node không chịu nổi toàn bộ dữ liệu/traffic, hãy chia dữ liệu ra N node, mỗi node chỉ chịu trách nhiệm cho 1/N phần. Ứng dụng (hoặc một lớp trung gian gọi là shard router/proxy) nhìn vào shard key của mỗi truy vấn để quyết định nó cần đi tới node nào.
Có ba chiến lược chọn shard key phổ biến, mỗi chiến lược có một hệ quả kiến trúc khác nhau:
1. Range-based sharding (phân theo dải giá trị). Ví dụ: user_id từ 1–1,000,000 vào shard 1, từ 1,000,001–2,000,000 vào shard 2. Ưu điểm: range query (ví dụ lấy tất cả user trong một khoảng ID) chỉ cần chạm vào một hoặc vài shard liền kề. Nhược điểm: nếu dữ liệu mới luôn có ID tăng dần, tất cả traffic ghi mới dồn vào shard cuối cùng — đây chính là "hot shard" kinh điển.
2. Hash-based sharding (phân theo hàm băm). Tính hash(shard_key) mod N để quyết định shard. Ưu điểm: phân bố traffic ghi đồng đều gần như tuyệt đối, triệt tiêu hot shard do pattern tăng dần. Nhược điểm: range query trở nên vô nghĩa (dữ liệu liền kề về logic bị rải ngẫu nhiên khắp các shard), và việc thêm/bớt shard (resharding) buộc phải tính lại hash cho gần như toàn bộ dữ liệu nếu dùng modulo đơn giản — đây là lý do consistent hashing hoặc virtual shard ra đời (sẽ nói ở phần Guardrails).
3. Directory-based / lookup-based sharding. Duy trì một bảng ánh xạ tường minh (thường trong một service hoặc bảng metadata riêng) từ shard key sang shard vật lý. Ưu điểm: linh hoạt tối đa — có thể di chuyển từng key riêng lẻ giữa các shard mà không cần công thức toán học cố định, hữu ích khi cần cân bằng lại tải thủ công (ví dụ dồn một khách hàng "voi" sang node riêng). Nhược điểm: bảng lookup này trở thành một điểm phụ thuộc (dependency) và tiềm năng single point of failure/bottleneck mới — bạn vừa giải quyết một nút thắt, vừa tạo ra một nút thắt khác nhỏ hơn nhưng vẫn phải quản lý cẩn thận (thường giải quyết bằng cache mạnh + replication cho chính bảng lookup đó).
Điểm chung của cả ba chiến lược: chúng đánh đổi giữa "phân bố tải đều" và "giữ được tính cục bộ (locality) của truy vấn". Không có chiến lược nào tối ưu cả hai đồng thời — đây chính là hạt nhân của phần Trade-off tiếp theo.
The Trade-off
Đây là phần quan trọng nhất, và phải nói thẳng: sharding không phải "nâng cấp", nó là một cuộc phẫu thuật cắt bỏ một khả năng để đổi lấy khả năng khác.
Được gì:
- Phá trần quy mô. Thay vì bị giới hạn bởi một node, hệ thống giờ giới hạn bởi N node — về lý thuyết, thông lượng ghi và dung lượng lưu trữ scale gần tuyến tính theo số shard (trong điều kiện shard key được chọn tốt, không có hot shard).
- Giảm bán kính nổ (blast radius) của sự cố. Một shard bị lỗi (disk hỏng, node crash) chỉ ảnh hưởng đến tập con dữ liệu/người dùng thuộc shard đó, không kéo sập toàn hệ thống — đây là một dạng cách ly lỗi tương tự bulkhead pattern.
- Cho phép song song hóa vận hành. Backup, restore, index rebuild, migration schema có thể chạy song song trên từng shard nhỏ hơn, thay vì một tác vụ khổng lồ trên một node duy nhất.
Mất gì — và đây là phần không được phép mơ hồ:
- Truy vấn xuyên shard (cross-shard query) trở nên đắt đỏ hoặc bất khả thi. Một
JOINgiữa bảngorders(sharded theocustomer_id) và bảngproducts(sharded theoproduct_id) không còn là một câu SQL — nó trở thành N lệnh gọi song song tới N shard, rồi tự viết code ứng dụng để gộp (merge) kết quả, sắp xếp lại, phân trang lại thủ công. Điều mà mộtJOIN ... ORDER BY ... LIMITlàm trong một dòng SQL giờ trở thành một tầng logic ứng dụng riêng cần viết, test, và bảo trì. - Transaction xuyên shard mất tính ACID mặc định. Chuyển tiền giữa hai tài khoản nằm trên hai shard khác nhau không còn dùng được một
BEGIN...COMMITđơn giản. Bạn buộc phải áp dụng SAGA pattern (bài 19) hoặc two-phase commit (vốn có chi phí hiệu năng và độ phức tạp vận hành rất cao), chấp nhận eventual consistency (bài 22) ở một mức độ nào đó. - Aggregation toàn cục trở thành bài toán riêng.
COUNT(*),SUM(revenue)trên toàn bộ dữ liệu giờ phải chạy trên từng shard rồi cộng dồn — hoặc tệ hơn, phải xây một pipeline ETL riêng đổ dữ liệu về data warehouse chỉ để trả lời các câu hỏi tổng hợp mà trước đây một câu SQL làm được. - Chọn sai shard key tạo ra "hot shard" — và đây là cái giá đắt nhất. Nếu shard key có phân bố lệch (ví dụ shard theo
tenant_idnhưng có một khách hàng doanh nghiệp chiếm 40% traffic), shard chứa khách hàng đó sẽ bị quá tải trong khi các shard khác gần như rảnh rỗi. Lúc này bạn có N node nhưng vẫn bị giới hạn bởi hiệu năng của một node — sharding đã tiêu tốn toàn bộ chi phí vận hành mà không giải quyết được vấn đề gốc. - Resharding (thêm/bớt shard sau này) là một dự án lớn, không phải một thao tác vận hành. Với hash-based sharding kiểu modulo đơn giản, thêm một shard mới nghĩa là gần như toàn bộ key phải được tính lại vị trí và di chuyển — trong khi hệ thống vẫn phải phục vụ traffic sống, không được downtime.
- Độ phức tạp vận hành nhân lên theo số shard. Monitoring, alerting, migration schema, backup/restore giờ phải chạy nhất quán trên N instance thay vì một. Một migration schema (thêm cột, đổi index) giờ phải chạy tuần tự hoặc song song có kiểm soát trên từng shard, và phải xử lý trường hợp một số shard migrate thành công còn số khác thất bại giữa chừng.
Nói ngắn gọn: sharding đánh đổi khả năng mở rộng vô hạn theo chiều ngang lấy sự đơn giản của một cơ sở dữ liệu quan hệ tập trung. Bạn từ bỏ JOIN tùy ý, transaction xuyên bảng, và một điểm vận hành duy nhất — để đổi lấy khả năng phá trần mà scale dọc không bao giờ cho phép.
The Architecture Diagram
Sơ đồ dưới đây minh họa một topology sharding phổ biến: shard router đứng giữa ứng dụng và các shard vật lý, sử dụng hash-based sharding trên customer_id, với virtual shard để chuẩn bị cho resharding sau này.
flowchart TB
APP["Application Layer<br/>(API Service)"]
ROUTER["Shard Router / Proxy<br/>hash(customer_id) → virtual shard → physical shard"]
subgraph VS["Virtual Shard Mapping (256 virtual shards)"]
direction LR
VSMAP["vshard 0-63 → Physical Shard A<br/>vshard 64-127 → Physical Shard B<br/>vshard 128-191 → Physical Shard C<br/>vshard 192-255 → Physical Shard D"]
end
subgraph SA["Physical Shard A"]
direction TB
A_PRI[("Primary<br/>customer_id range hash 0-63")]
A_REP[("Read Replica")]
A_PRI -.->|replication| A_REP
end
subgraph SB["Physical Shard B"]
direction TB
B_PRI[("Primary<br/>hash 64-127")]
B_REP[("Read Replica")]
B_PRI -.->|replication| B_REP
end
subgraph SC["Physical Shard C"]
direction TB
C_PRI[("Primary<br/>hash 128-191")]
C_REP[("Read Replica")]
C_PRI -.->|replication| C_REP
end
subgraph SD["Physical Shard D"]
direction TB
D_PRI[("Primary<br/>hash 192-255")]
D_REP[("Read Replica")]
D_PRI -.->|replication| D_REP
end
DIR[("Shard Directory<br/>(metadata: vshard → physical node)")]
APP --> ROUTER
ROUTER --> VS
ROUTER -.->|lookup mapping| DIR
VS --> SA
VS --> SB
VS --> SC
VS --> SD
AGG["Cross-shard Aggregator<br/>(scatter-gather cho query/report toàn cục)"]
APP -.->|"aggregation query"| AGG
AGG -.-> SA
AGG -.-> SB
AGG -.-> SC
AGG -.-> SD
Điểm mấu chốt trong sơ đồ: lớp virtual shard nằm giữa shard key và shard vật lý. Khi cần resharding (ví dụ tách Physical Shard D thành hai node vì nó đang nóng), bạn chỉ cần di chuyển một số dải virtual shard sang node mới và cập nhật Shard Directory — chứ không phải viết lại công thức hash hay di chuyển từng row một cách thủ công.
Implementation Guardrails
- Chọn shard key dựa trên pattern truy vấn thật đo được, không phải sự tiện lợi lúc thiết kế. Trước khi chọn, hãy trả lời: 90% truy vấn của hệ thống lọc/join theo cột nào? Nếu phần lớn truy vấn là "lấy toàn bộ đơn hàng của một khách hàng", shard key phải là
customer_id(hoặctenant_id) — dù ID đó có phân bố không đều — chứ không phảiorder_idchỉ vì nó là khóa chính có sẵn. Shard key sai khiến gần như mọi truy vấn trở thành scatter-gather trên toàn bộ N shard, xóa sạch lợi ích của việc sharding. - Luôn cấp phát dư virtual shard ngay từ đầu, kể cả khi chỉ cần 2-4 physical shard hiện tại. Tạo 256 hoặc 1024 virtual shard logic, ánh xạ chúng vào số physical shard thực tế hiện có. Khi cần thêm node, chỉ di chuyển một số dải virtual shard sang node mới — không phải tính lại hash cho toàn bộ dữ liệu đang sống.
- Không bao giờ thiết kế transaction nghiệp vụ core phụ thuộc vào việc ghi đồng thời lên nhiều shard. Nếu một use-case (ví dụ chuyển điểm thưởng giữa hai tài khoản) buộc phải ghi xuyên shard, phải thiết kế nó như một SAGA có bước bù trừ (compensating transaction) ngay từ đầu, không phải vá sau khi phát hiện transaction bị vỡ giữa chừng trong production.
- Xây cơ chế phát hiện hot shard trước khi nó gây sự cố, không phải sau. Giám sát riêng biệt tải CPU/I/O/độ trễ theo từng shard (không chỉ tổng hợp toàn hệ thống) — vì một dashboard trung bình cộng có thể che giấu hoàn toàn một shard đang cháy trong khi các shard khác rảnh rỗi.
Architect's Verdict
Sharding là biện pháp cuối cùng trong chuỗi leo thang scale: dọc trước, cache và read replica tiếp theo, sharding chỉ khi cả ba đã cạn kiệt và bạn có bằng chứng số liệu rõ ràng — không phải cảm giác "chắc sắp cần". Đây là cửa một chiều đắt đỏ để đảo ngược: một khi ứng dụng đã viết logic scatter-gather và SAGA xuyên shard khắp nơi, gộp lại thành một database đơn không còn là lựa chọn thực tế nữa. Nếu buộc phải shard, hãy trả giá thiết kế trước (virtual shard, shard key đúng pattern truy vấn) thay vì trả giá vận hành sau (hot shard, resharding khẩn cấp giữa production đang chạy).
🔗 Bài viết liên quan: Distributed Caching · SAGA Pattern
Bài trước: Distributed Caching · Bài tiếp theo: Message Queues