Giao diện
10. Mở rộng quy mô
Khi một server MySQL bắt đầu chật: dùng kết nối hiệu quả, chia bảng lớn ngay trong một server (partitioning), và chia dữ liệu ra nhiều server (sharding).
Thứ tự nên thử
Mỗi bước sau phức tạp và tốn công vận hành hơn bước trước. Chỉ đi tiếp khi bước trước đã thật sự hết dư địa:
- Tối ưu query và index (chương 4, chương 5). Thường mang lại nhiều nhất với ít công nhất.
- Scale up: thêm RAM để working set nằm vừa buffer pool, chuyển sang NVMe. Một server hiện đại chạy được vài TB dữ liệu và hàng chục nghìn QPS.
- Dùng connection hiệu quả: connection pool, proxy (mục bên dưới).
- Giảm tải đọc: cache, read replica.
- Partitioning cho bảng rất lớn theo thời gian: chủ yếu để quản lý và xoá dữ liệu cũ.
- Tách database theo chức năng (vertical split): đơn hàng, thanh toán, người dùng mỗi nhóm một cụm riêng.
- Sharding: chia dòng của cùng một bảng ra nhiều server. Mở rộng được cả ghi lẫn dung lượng, nhưng đắt nhất.
Connection pool
MySQL dùng mô hình mỗi connection một thread. Mở một connection mới tốn vài mili giây (bắt tay TCP, TLS, xác thực), và mỗi connection giữ bộ nhớ riêng (thread stack, sort/join buffer khi dùng). Mở rồi đóng connection cho từng request là lãng phí.
Connection pool ở tầng app giữ sẵn một số connection đã mở. Request mượn một connection, chạy query, rồi trả lại. Ví dụ: HikariCP (Java), database/sql của Go (SetMaxOpenConns, SetMaxIdleConns, SetConnMaxLifetime), pool của SQLAlchemy, mysql2 (Node).
Pool bao lớn là đủ
- Định luật Little: số connection bận cùng lúc = số request mỗi giây × thời gian giữ connection. 2.000 QPS, mỗi lần giữ 5 ms thì trung bình chỉ 10 connection bận.
- Pool to hơn không làm nhanh hơn. Database chỉ thật sự chạy song song được cỡ vài lần số core CPU. Nhiều query hơn thế chỉ tranh CPU, tranh lock và tăng latency. Điểm khởi đầu hay dùng cho tổng số query đồng thời: khoảng 2 × số core của DB.
- Thời gian giữ connection gồm cả lúc transaction đang mở. Gọi API ngoài 200 ms trong transaction là giữ connection 200 ms: số connection cần tăng 40 lần.
- Tổng connection = số instance app × pool size, phải nhỏ hơn
max_connections(mặc định 151). Autoscale lên 50 pod × pool 20 là 1.000 connection.
Cấu hình và theo dõi
- Đặt thời gian sống tối đa của connection trong pool ngắn hơn
wait_timeoutcủa server (mặc định 8 giờ), để pool không giữ connection mà server đã đóng. - Timeout chờ lấy connection ngắn (vài trăm ms tới vài giây): hết connection thì báo lỗi nhanh, không treo request.
Threads_connectedcao nhưng phần lớn rảnh thì chấp nhận được.Threads_runningvượt xa số core mới là dấu hiệu quá tải.- Quá nhiều instance app: đặt proxy ở giữa như ProxySQL hoặc MySQL Router. Proxy gom connection (multiplexing), chia đọc/ghi giữa source và replica, chặn query xấu.
Thử: tính connection cần dùng
Giả định DB có 16 core. Request phân bố đều; thực tế có đỉnh nên cần dư thêm.
10connection bận cùng lúc (trung bình)
100connection mở tới DB
151max_connections
Tổng 100 connection nằm dưới max_connections (151).
Pool dư: trung bình chỉ khoảng 10% connection bận.
Số query đồng thời vừa sức DB 16 core.
Partitioning
Partitioning chia một bảng thành nhiều phần vật lý trong cùng một server. Mỗi partition có B+Tree và file riêng, nhưng app vẫn thấy và truy vấn như một bảng.
- RANGE / RANGE COLUMNS: theo khoảng giá trị, phổ biến nhất là theo thời gian (mỗi tháng một partition).
- LIST: theo tập giá trị (vùng, loại).
- HASH / KEY: rải đều theo hàm băm, ít dùng vì không giúp xoá dữ liệu theo nhóm.
Lợi ích thật
- Partition pruning: query có điều kiện trên cột partition chỉ mở các partition liên quan (EXPLAIN có cột
partitions). - Xoá dữ liệu cũ tức thì:
DROP PARTITIONchỉ xoá một file, thay choDELETEhàng chục triệu dòng (sinh undo, redo, binlog, lag replica). - Bảo trì từng phần: rebuild, archive một partition mà không đụng phần còn lại.
Hiểu lầm và giới hạn
- Partitioning không tự làm query nhanh hơn. Tra cứu theo index tốt trên bảng thường đã chỉ đọc vài page. Query không có cột partition trong WHERE phải mở mọi partition, còn chậm hơn bảng thường.
- Mọi PRIMARY KEY và UNIQUE phải chứa cột partition. Vì vậy khoá chính thường thành
(id, created_at), và không còn đảm bảo unique toàn bảng cho cột khác. - Bảng partition không dùng được khoá ngoại; tối đa 8192 partition.
- Vẫn một server: CPU, RAM, disk dùng chung. Partitioning không thay được sharding.
sql
CREATE TABLE logs (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
created_at DATETIME NOT NULL,
payload JSON,
PRIMARY KEY (id, created_at), -- phải chứa cột partition
KEY idx_user (user_id, created_at)
)
PARTITION BY RANGE COLUMNS (created_at) (
PARTITION p202609 VALUES LESS THAN ('2026-10-01'),
PARTITION p202610 VALUES LESS THAN ('2026-11-01'),
PARTITION pmax VALUES LESS THAN (MAXVALUE)
);
-- mỗi tháng: tách pmax để thêm tháng mới, xoá tháng cũ nhất
ALTER TABLE logs REORGANIZE PARTITION pmax INTO (
PARTITION p202611 VALUES LESS THAN ('2026-12-01'),
PARTITION pmax VALUES LESS THAN (MAXVALUE));
ALTER TABLE logs DROP PARTITION p202511;Thử: partition pruning
Giả định mỗi tháng 30 triệu dòng log.
SELECT * FROM logs
WHERE created_at >= '2026-03-01'
AND created_at < '2026-05-01';
p202511p202512p202601p202602p202603p202604p202605p202606p202607p202608p202609p202610
phải mởbỏ qua (pruned)hết hạn lưu trữ
Optimizer chỉ mở 2/12 partition. EXPLAIN: partitions: p202603,p202604.
Đang giữ đủ 12 tháng, chưa có partition nào hết hạn. Kéo thanh lưu trữ xuống để thử xoá.
Sharding
Sharding chia các dòng của cùng một bảng ra nhiều server MySQL độc lập (shard); mỗi shard giữ một phần dữ liệu và có replica riêng. Khác với read replica (mỗi máy giữ toàn bộ dữ liệu, chỉ mở rộng đọc), sharding mở rộng được cả ghi lẫn dung lượng.
Cần đến khi: dữ liệu hoặc working set không còn nằm vừa một server, lượng ghi vượt khả năng một source dù đã tối ưu, hoặc cần cô lập dữ liệu theo khách hàng hay vùng địa lý.
Chọn shard key
Shard key là cột quyết định một dòng nằm ở shard nào. Đây là quyết định khó đổi nhất.
- Hầu hết query có shard key, để chỉ cần hỏi đúng một shard.
- Phân bố đều cả dữ liệu lẫn tải, không có shard “nóng”.
- Dữ liệu hay đi cùng nhau nằm cùng shard: shard theo
user_idthì đơn hàng, ví, thông báo của một user ở chung một shard, join và transaction vẫn chạy trong một server. - Tránh:
created_at(mọi lệnh ghi dồn vào shard mới nhất),country(một nước chiếm phần lớn), cột đổi giá trị theo thời gian.
Cách chia
| Cách chia | Ưu điểm | Nhược điểm |
|---|---|---|
| Range (id 1–10 triệu ở shard 1…) | Dễ hiểu, query theo khoảng gọn | Key tăng dần dồn mọi lệnh ghi vào shard cuối |
Hash mod N (hash(key) % N) | Phân bố đều, không cần bảng tra | Đổi N thì gần như mọi key phải chuyển shard |
| Bucket ảo (chia sẵn 1024 bucket, gán bucket vào shard) | Thêm shard chỉ chuyển một phần bucket; Vitess dùng biến thể của cách này | Cần lưu và đồng bộ bảng gán bucket |
| Directory (bảng tra key → shard) | Linh hoạt nhất, chuyển từng khách hàng lớn riêng được | Thêm một thành phần phải luôn sẵn sàng và nhanh |
Thử: thêm một shard thì bao nhiêu dữ liệu phải chuyển
Cách chia
120 user, mỗi ô là một user; số trong ô là shard sau khi thêm. Ô tô màu là user phải chuyển dữ liệu sang shard khác.
80%dữ liệu phải chuyển (ước tính trên 100.000 key)
97/120user mẫu đổi shard
20–27user mỗi shard (thấp nhất–cao nhất)
343413134414341455231141154434225451523233251311521355334151243223154215151243242533323412431532125135213325455543414154
Đổi từ hash % 4 sang hash % 5: gần như mọi key đổi shard (khoảng 4/5). Phải sao chép gần toàn bộ dữ liệu trong lúc hệ thống vẫn chạy.
Cái giá của sharding
- Query không có shard key phải hỏi mọi shard rồi gộp (scatter-gather): chậm và tải nhân N. Thường phải giữ thêm bảng tra phụ (ví dụ email → user_id) hoặc đẩy dữ liệu sang search engine.
- JOIN và transaction chéo shard không còn do database lo: phải làm ở app, dùng saga hoặc outbox (chương 9) thay cho transaction.
- ID toàn cục: AUTO_INCREMENT riêng từng shard sẽ trùng; dùng Snowflake, UUIDv7, hoặc dịch vụ cấp ID. Ràng buộc UNIQUE toàn cục cũng phải tự lo.
- Báo cáo, tổng hợp trên toàn bộ dữ liệu: đẩy qua CDC vào kho phân tích.
- Vận hành nhân N: migration schema, backup, giám sát trên mọi shard; resharding là dự án lớn.
Công cụ: Vitess (YouTube, Slack; nền của PlanetScale) lo định tuyến, resharding và migration; ProxySQL có luật định tuyến theo shard; Apache ShardingSphere chạy trong app hoặc dạng proxy. TiDB là database tương thích giao thức MySQL, tự chia dữ liệu bên dưới.
Partitioning khác sharding thế nào
| Partitioning | Sharding | |
|---|---|---|
| Phạm vi | Một bảng, một server | Nhiều server độc lập |
| Mở rộng | Không thêm CPU, RAM, disk | Thêm cả năng lực đọc, ghi và dung lượng |
| App có phải biết không | Không, vẫn là một bảng | Có, hoặc qua lớp proxy như Vitess |
| JOIN, transaction | Bình thường | Chỉ trong một shard |
| Dùng chủ yếu để | Xoá dữ liệu theo thời gian, pruning | Vượt giới hạn của một server |
Ghi nhớ
- Tối ưu query, scale up, cache, replica trước; sharding sau cùng.
- Connection bận cùng lúc = QPS × thời gian giữ; pool to không làm nhanh hơn; tổng pool phải dưới
max_connections. - Partitioning để pruning và xoá dữ liệu cũ bằng
DROP PARTITION; PK phải chứa cột partition. - Shard key: có trong hầu hết query, phân bố đều, gom dữ liệu liên quan; tránh hash mod N khi cần thêm shard.