database sharding & capacity planning: bài học kiến trúc từ hệ thống triệu người dùng

- Published on
- /8 mins read/
Quy tắc số một của việc phân mảnh dữ liệu (Sharding) là: Đừng bao giờ làm Sharding nếu bạn vẫn còn cách khác để giải quyết bài toán.
Trong các buổi phỏng vấn kiến trúc sư hệ thống (System Design) hoặc trên các diễn đàn công nghệ như vnJUG, Database Sharding luôn là một chủ đề đầy lôi cuốn.
Ai cũng hào hứng nói về việc chia nhỏ cơ sở dữ liệu ra 16 shard, 64 shard, xử lý hàng trăm triệu giao dịch mỗi ngày như Netflix hay Cash App. Thế nhưng, trong thực tế sản xuất, Sharding giống như một "phép thuật hắc ám": Nó giải quyết được bài toán mở rộng quy mô ghi (Write Scalability), nhưng đổi lại, nó biến toàn bộ tầng nghiệp vụ đơn giản trước đây thành một cơn ác mộng phân tán.
Khi nào bạn THỰC SỰ cần Sharding? Làm thế nào để ước tính dung lượng (Capacity Planning) chính xác và chọn Sharding Key để không tự đào hố chôn mình?
Bài viết này sẽ cung cấp bản đồ toàn cảnh từ lý thuyết tính toán đến kinh nghiệm thực chiến trong các hệ thống Java/Spring Boot quy mô lớn.
# quy tắc ước tính dung lượng (back-of-the-envelope estimation)
Trước khi nghĩ đến Sharding, một Senior Engineer luôn bắt đầu bằng những phép tính cơ bản trên "vỏ bao diêm" (Back-of-the-envelope calculation) để chọn đúng công nghệ:
Bảng quy tắc dung lượng kinh điển:
- Dưới 100 GB: Nằm gọn trong RAM. Redis hoặc một máy chủ cơ sở dữ liệu phổ thông có thể phục vụ trơn tru với độ trễ sub-millisecond.
- Từ 1 TB đến 10 TB: Đừng vội Sharding! Các máy chủ hiện đại trên AWS/GCP có thể gắn 64 vCPU, 512GB RAM và ổ NVMe SSD đạt 50.000 IOPS. Kết hợp với việc tách Read Replicas (ghi vào Master, đọc từ 3-4 Replica) và Table Partitioning (chia nhỏ theo tháng/năm), một node SQL đơn lẻ có thể gánh tới 20.000 - 50.000 TPS mà không cần phân tán!
- Từ 10 TB đến 100 TB+ với Write Throughput vượt ngưỡng phần cứng: Khi một node Master duy nhất bị bão hòa IOPS ghi (Disk I/O Bottleneck), bạn mới bắt buộc phải bước vào con đường Sharding.
# phân biệt: partitioning vs sharding
Rất nhiều kỹ sư nhầm lẫn giữa hai khái niệm này:
| Tiêu chí | Table Partitioning (Phân vùng cục bộ) | Database Sharding (Phân mảnh ngang) |
|---|---|---|
| Vị trí vật lý | Nằm trên cùng một máy chủ Database | Nằm trên nhiều máy chủ / cụm Database độc lập |
| Khả năng mở rộng | Giúp index nhỏ lại, query nhanh hơn; không tăng năng lực ghi CPU/RAM | Mở rộng tuyến tính cả CPU, RAM, Disk IOPS |
| Độ phức tạp | Rất thấp (Database Engine tự xử lý ngầm) | Rất cao (Cần router, distributed transaction, khó backup) |
# 3 chiến lược chọn sharding key sống còn
Quyết định quan trọng nhất khi sharding là Chọn Sharding Key. Chọn sai Sharding Key đồng nghĩa với việc toàn bộ hệ thống sẽ phải đập đi xây lại trong tương lai!
1. Hash-Based Sharding (Phân mảnh theo băm)
- Công thức:
ShardID = Hash(ShardingKey) % TotalShards - Ưu điểm: Dữ liệu được rải cực kỳ đồng đều trên các node, triệt tiêu nguy cơ nghẽn cục bộ.
- Nhược điểm chí mạng: Cực kỳ đau khổ khi thêm node mới (Resharding)! Khi tăng từ 3 lên 4 node, hầu như toàn bộ dữ liệu phải di chuyển vị trí. Phải sử dụng giải thuật Consistent Hashing (Băm nhất quán) để giảm thiểu dữ liệu cần chuyển dịch.
2. Range-Based Sharding (Phân mảnh theo dải)
- Nguyên lý: Ví dụ Shard 1 chứa ID từ 1 - 10 triệu; Shard 2 chứa từ 10 - 20 triệu. Hoặc chia theo ngày tháng năm.
- Ưu điểm: Rất tiện lợi khi cần query theo khoảng thời gian (
BETWEEN date1 AND date2). - Nhược điểm: Hiện tượng Hotspot Shard! Mọi dữ liệu mới sinh ra hôm nay đều chỉ đổ dồn vào 1 Shard duy nhất của ngày hôm nay, làm node đó quá tải trong khi các node cũ ngồi chơi.
3. Entity-Group Sharding (Khuyên dùng cho FinTech / SaaS)
- Lấy
TenantIDhoặcCustomerIDlàm Sharding Key. - Toàn bộ dữ liệu của một khách hàng (Hồ sơ, Giao dịch, Ví tiền, Lịch sử chuyển khoản) đều nằm trọn vẹn trong cùng 1 Shard.
- Lợi ích tối thượng: Bạn vẫn có thể dùng các câu lệnh
JOINvà Transaction ACID nội bộ một cách an toàn tuyệt đối cho người dùng đó!
# 3 "cơn ác mộng" khi đưa sharding lên production
Nếu bạn nghĩ Sharding chỉ là chia bảng thì đây là những gì bạn phải đối mặt:
1. Sự sụp đổ của các câu lệnh JOIN (Cross-Shard Joins)
Trong kiến trúc đơn khối (Monolith), bạn thoải mái viết:
SELECT u.name, o.total FROM users u JOIN orders o ON u.id = o.user_id;Khi users nằm ở Shard 1 và orders nằm ở Shard 2, câu lệnh JOIN này hoàn toàn bất khả thi ở tầng Database!
- Bạn buộc phải query dữ liệu từ cả 2 Shard về tầng ứng dụng Spring Boot rồi tự dùng code Java để join chúng trong RAM (Application-side Join), hoặc phải chấp nhận phi chuẩn hóa dữ liệu (Denormalization).
2. Thảm họa Distributed Transactions (Giao dịch xuyên Shard)
Nếu người dùng A (ở Shard 1) chuyển tiền cho người dùng B (ở Shard 2):
- Không có cơ chế
@TransactionalhayBEGIN TRANSACTION ... COMMITnào của database có thể bao bọc cả 2 máy chủ độc lập đó cùng lúc. - Bạn buộc phải triển khai các mẫu thiết kế phức tạp như Saga Pattern (Orchestration/Choreography) hoặc Transactional Outbox kết hợp Kafka để đảm bảo tính nhất quán cuối cùng (Eventual Consistency).
3. Báo cáo tổng hợp toàn cục (Global Aggregations)
Câu lệnh đơn giản như SELECT COUNT(*) hoặc SELECT SUM(balance) trước đây chạy trong 10ms, nay phải gửi song song tới toàn bộ các Shard (Scatter-Gather Query) rồi tổng hợp kết quả lại, tiêu tốn lượng lớn băng thông mạng.
# triển khai routing datasource thực tế trong spring boot
Trong Java, bạn có thể triển khai định tuyến Shard linh hoạt mà không cần cài đặt thêm middleware phức tạp bằng cách kế thừa AbstractRoutingDataSource:
package com.tungdadev.datasource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class ShardedRoutingDataSource extends AbstractRoutingDataSource {
// ThreadLocal lưu Shard Key của Request hiện tại
private static final ThreadLocal<String> CURRENT_SHARD = new ThreadLocal<>();
public static void setShard(String shardKey) {
CURRENT_SHARD.set(shardKey);
}
public static void clear() {
CURRENT_SHARD.remove();
}
@Override
protected Object determineCurrentLookupKey() {
return CURRENT_SHARD.get(); // Trả về "shard_0", "shard_1", hoặc "shard_2"
}
}Mỗi khi có HTTP Request gửi đến, Interceptor sẽ bóc tách customerId, tính toán hash(customerId) % 3 và gọi ShardedRoutingDataSource.setShard("shard_" + index). Toàn bộ các truy vấn JPA/Hibernate phía sau sẽ tự động trỏ đúng vào máy chủ database tương ứng!
# tổng kết
Sharding là một kỳ tích kỹ thuật giúp các hệ thống lớn vượt qua giới hạn vật lý của một cỗ máy đơn lẻ. Nhưng hãy luôn ghi nhớ lời khuyên của các kiến trúc sư trưởng dày dạn kinh nghiệm:
\text{Optimize Queries} \xrightarrow{} \text{Add Indexes} \xrightarrow{} \text{Add Cache (Redis)} \xrightarrow{} \text{Read Replicas} \xrightarrow{} \text{Vertical Scaling} \xrightarrow{} \mathbf{Sharding (Bước đường cùng)}
Chỉ khi bạn đã tối ưu hóa câu truy vấn, đánh chỉ mục chuẩn chỉ, bổ sung Redis Cache, tách cụm Read/Write và nâng cấu hình phần cứng lên mức tối đa mà hệ thống vẫn không thể đáp ứng được tải ghi — đó mới là thời điểm chín muồi để bắt đầu hành trình Sharding.
Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!
Happy coding 😎 👍🏻 🚀 🔥.
On this page
- # quy tắc ước tính dung lượng (back-of-the-envelope estimation)
- Bảng quy tắc dung lượng kinh điển:
- # phân biệt: partitioning vs sharding
- # 3 chiến lược chọn sharding key sống còn
- 1. Hash-Based Sharding (Phân mảnh theo băm)
- 2. Range-Based Sharding (Phân mảnh theo dải)
- 3. Entity-Group Sharding (Khuyên dùng cho FinTech / SaaS)
- # 3 "cơn ác mộng" khi đưa sharding lên production
- 1. Sự sụp đổ của các câu lệnh JOIN (Cross-Shard Joins)
- 2. Thảm họa Distributed Transactions (Giao dịch xuyên Shard)
- 3. Báo cáo tổng hợp toàn cục (Global Aggregations)
- # triển khai routing datasource thực tế trong spring boot
- # tổng kết