2:15 sáng, chuông PagerDuty réo inh ỏi. CPU cụm MySQL production chạm ngưỡng 99%, connection pool cạn kiệt, queue đơn hàng ứ đọng hơn 12.000 request. Bảng orders vừa cán mốc 45 triệu bản ghi với dung lượng hơn 85GB.
Dù index chuẩn chỉ, một câu SELECT đơn giản theo user_id vẫn mất tới 3,8 giây vì nghẽn disk I/O. Nâng cấp RAM từ 64GB lên 128GB (Vertical Scaling) lúc này vừa đắt đỏ, vừa chỉ câu giờ được vài tuần. Muốn hệ thống trụ vững lâu dài, bạn buộc phải chia nhỏ dữ liệu ra nhiều server: Database Sharding với Apache ShardingSphere.
1. Quick start: Dựng cụm ShardingSphere-Proxy sau 5 phút
ShardingSphere-Proxy là lựa chọn tiện nhất khi cần dập lửa nhanh. Nó đứng giữa app và cụm MySQL, đóng vai trò như một proxy layer chuẩn giao thức MySQL. Nhờ vậy, team backend viết Spring Boot, Go hay Node.js không cần sửa dù chỉ một dòng code kết nối.
Chuẩn bị 2 instance MySQL vật lý chạy port 3306:
- Node 1:
192.168.1.10:3306(chứa databaseds_0) - Node 2:
192.168.1.11:3306(chứa databaseds_1)
Tạo database và bảng trên cả hai node:
CREATE DATABASE IF NOT EXISTS ds_0;
CREATE DATABASE IF NOT EXISTS ds_1;
-- Chạy DDL này trên cả node ds_0 và ds_1:
CREATE TABLE t_order (
order_id BIGINT PRIMARY KEY,
user_id INT NOT NULL,
amount DECIMAL(10, 2),
status VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Cấu hình routing rule trong file config-sharding.yaml:
databaseName: sharding_db
dataSources:
ds_0:
url: jdbc:mysql://192.168.1.10:3306/ds_0?serverTimezone=UTC&useSSL=false
username: root
password: SecretPassword123
ds_1:
url: jdbc:mysql://192.168.1.11:3306/ds_1?serverTimezone=UTC&useSSL=false
username: root
password: SecretPassword123
rules:
- !SHARDING
tables:
t_order:
actualDataNodes: ds_${0..1}.t_order
databaseStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: inline_db
shardingAlgorithms:
inline_db:
type: INLINE
props:
algorithm-expression: ds_${user_id % 2}
Khởi chạy Proxy bằng Docker container:
docker run -d --name shardingsphere-proxy \
-p 3307:3307 \
-v $(pwd)/config-sharding.yaml:/opt/shardingsphere-proxy/conf/config-sharding.yaml \
apache/shardingsphere-proxy:latest
Xong bước này, trỏ ứng dụng về 127.0.0.1:3307, database sharding_db. Khi ghi bản ghi có user_id = 101, data tự động vào ds_1 (101 % 2 = 1). Nếu user_id = 100, dữ liệu chuyển vào ds_0. Ứng dụng vẫn nhìn thấy một database duy nhất.
2. Bản chất kỹ thuật của Sharding và ShardingSphere
Vì sao Index hết tác dụng khi dữ liệu phình to?
Bảng dưới 5 triệu dòng chạy rất mượt. Đến 50 triệu dòng, cây B-Tree index phình to vượt quá dung lượng innodb_buffer_pool_size. MySQL không thể giữ trọn index trên RAM. Mỗi truy vấn buộc phải đọc ngẫu nhiên từ ổ cứng (random disk I/O), khiến I/O wait tăng vọt lên 40-60%.
Mô hình MySQL Read Replica chỉ san sẻ được query đọc. Tải ghi (Write throughput) và giới hạn dung lượng đĩa vẫn dồn hết vào Master node. Phân mảnh ngang (Horizontal Sharding) giải quyết tận gốc vấn đề này bằng cách chia đều cả tải đọc, tải ghi và dung lượng sang nhiều server độc lập.
Hai biến thể kiến trúc của Apache ShardingSphere
Hệ sinh thái ShardingSphere cung cấp hai chế độ triển khai:
- ShardingSphere-JDBC: Thư viện nhúng trực tiếp vào app như một JDBC Driver tùy chỉnh. Độ trễ gần như bằng 0 vì không qua network hop nào. Nhược điểm: gắn chặt với JVM (Java, Kotlin).
- ShardingSphere-Proxy: Một database proxy độc lập nằm trước cụm MySQL. Mọi ngôn ngữ từ Go, Python, PHP đến C# đều kết nối bình thường qua port MySQL chuẩn. Rất phù hợp khi hệ thống microservices dùng nhiều tech stack khác nhau.
Cách thuật toán định tuyến dữ liệu vận hành
Trong file cấu hình mẫu, quy tắc INLINE sử dụng công thức ds_${user_id % 2}. Cột user_id đóng vai trò là Sharding Key. Khi nhận câu lệnh:
SELECT * FROM t_order WHERE user_id = 100;
Proxy bóc tách AST (Abstract Syntax Tree), tính nhanh 100 % 2 = 0 và chuyển tiếp query thẳng tới ds_0. Node ds_1 hoàn toàn đứng ngoài cuộc, không tốn bất kỳ chu kỳ CPU hay I/O nào.
3. Xử lý ID phân tán và Sharding nhiều tầng
Khắc phục xung đột ID bằng thuật toán Snowflake
Tuyệt đối không dùng AUTO_INCREMENT mặc định của MySQL khi sharding. Nếu dùng, cả hai node ds_0 và ds_1 sẽ cùng sinh ra id = 1, gây vỡ tính toàn vẹn dữ liệu. ShardingSphere tích hợp sẵn thuật toán Snowflake, tự sinh ID số nguyên 64-bit duy nhất trên toàn cụm mà không lo trùng lặp.
Khai báo generator trong cấu hình rule:
rules:
- !SHARDING
tables:
t_order:
actualDataNodes: ds_${0..1}.t_order_${0..1}
keyGenerateStrategy:
column: order_id
keyGeneratorName: snowflake_gen
keyGenerators:
snowflake_gen:
type: SNOWFLAKE
Chiến lược Sharding 2 tầng (Database + Table)
Khi quy mô chạm mốc hàng trăm triệu record, bạn nên kết hợp chia cả database lẫn bảng vật lý. Giả sử ta có 2 database (ds_0, ds_1), mỗi database chứa 2 bảng con (t_order_0, t_order_1), tổng cộng có 4 bảng vật lý:
rules:
- !SHARDING
tables:
t_order:
actualDataNodes: ds_${0..1}.t_order_${0..1}
databaseStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: inline_db
tableStrategy:
standard:
shardingColumn: order_id
shardingAlgorithmName: inline_tbl
shardingAlgorithms:
inline_db:
type: INLINE
props:
algorithm-expression: ds_${user_id % 2}
inline_tbl:
type: INLINE
props:
algorithm-expression: t_order_${order_id % 2}
4. Kinh nghiệm thực chiến khi chạy Production
Cái giá đắt của truy vấn thiếu Sharding Key
Xem xét câu truy vấn sau:
SELECT * FROM t_order WHERE status = 'COMPLETED';
Vì câu lệnh không kèm user_id, Proxy không thể đoán dữ liệu nằm ở đâu. Nó buộc phải phát tán query đến toàn bộ các node con (cơ chế Scatter-Gather), sau đó gom hàng triệu dòng dữ liệu vào RAM để sort và merge. Hậu quả là Proxy dính lỗi Out-Of-Memory (OOM) tức thì. Hãy chặn đứng rủi ro này bằng cách bắt buộc dev truyền Sharding Key trong mọi API tra cứu đơn hàng.
Xử lý Transaction phân tán sao cho chuẩn?
Ghi dữ liệu vắt ngang 2 database vật lý sẽ làm transaction ACID cục bộ của MySQL vô hiệu hóa. ShardingSphere hỗ trợ chuẩn XA (2-Phase Commit) và Saga pattern qua Apache Seata. Đổi lại, XA kéo throughput giảm từ 4 đến 5 lần vì cơ chế lock tài nguyên kéo dài. Giải pháp thực tế tối ưu nhất là thiết kế bảng khéo léo, gom toàn bộ thao tác cùng một nghiệp vụ vào chung một Sharding Key.
Dùng Broadcast Table cho các bảng danh mục
Những bảng ít biến động nhưng bị JOIN liên tục như provinces hay product_categories nên được cấu hình dạng Broadcast Table. ShardingSphere tự nhân bản bảng này sang tất cả node. Mọi thao tác JOIN giữa đơn hàng và danh mục sẽ diễn ra ngay tại node nội bộ, triệt tiêu hoàn toàn chi phí kéo dữ liệu qua mạng.

