Hướng dẫn phân mảnh dữ liệu (Database Sharding) cho MySQL với Apache ShardingSphere: Giải pháp mở rộng ngang

MySQL tutorial - IT technology blog
MySQL tutorial - IT technology blog

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 database ds_0)
  • Node 2: 192.168.1.11:3306 (chứa database ds_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.

Share: