Cấu hình Read/Write Splitting MySQL bằng ProxySQL: Tách tải đọc ghi không cần sửa code

2:15 sáng và hồi chuông PagerDuty: Khi Replica dựng lên chỉ để “ngắm”

2:15 sáng, điện thoại rung bần bật vì chuông PagerDuty. Dashboard Grafana đỏ rực: CPU node MySQL Master chạm trần 100%, Threads_running vọt từ 15 lên hơn 350. API gateway bắt đầu nhả lỗi 504 Gateway Timeout hàng loạt. Trong khi đó, hai node Replica 8 core 32GB RAM đứng cạnh lại ngồi chơi xơi nước với mức CPU chưa tới 3%.

Kịch bản này rất quen thuộc. Cụm Replication Master-Slave dựng rất chuẩn chỉ. Dẫu vậy, toàn bộ microservice vẫn trỏ chung vào một connection string duy nhất của Master. Toàn bộ query đọc lẫn ghi dồn hết vào một chỗ. Nếu không tách tải đọc ghi (Read/Write Splitting) ngay trong đêm, đợt sale 9h sáng chắc chắn vỡ trận.

Ba hướng tiếp cận phân chia Read/Write trên bàn cân

Xử lý bài toán này thường có 3 phương án:

1. Tách connection pool ở tầng ứng dụng (Application Routing)

Backend khai báo 2 DataSource riêng: db_master cho ghi và db_replica cho đọc. Developer phải tự bọc query hoặc cấu hình annotation (như @Transactional(readOnly = true) trong Spring Boot hay dùng read-replica pool trong Laravel/Django).

2. Dùng HAProxy tách port

Dựng HAProxy mở port 3306 forward về Master và port 3307 cân bằng tải cho cụm Replica. Ứng dụng vẫn phải tự quản lý logic gọi đúng port tương ứng.

3. Đặt Database Proxy thông minh (ProxySQL) ở giữa

ProxySQL đóng vai trò cổng giao tiếp duy nhất (port 6033). Nó phân tích cú pháp SQL (Layer 7) theo thời gian thực: thấy SELECT thì đẩy sang Replica, thấy INSERT/UPDATE thì giữ lại ở Master. Ứng dụng hoàn toàn không hay biết gì về sự thay đổi phía sau.

Tại sao ProxySQL thắng thế khi cứu hỏa Production?

Mỗi giải pháp đều có chỗ đứng riêng tùy giai đoạn hệ thống:

  • Sửa code ứng dụng: Giải pháp chuẩn về lâu dài. Tuy nhiên, lúc nửa đêm đè 15 microservice ra sửa connection pool, kiểm tra context transaction rồi deploy là tự sát. Chỉ cần sót một query đọc sau ghi (read-your-own-writes) trong transaction, dữ liệu sẽ lệch ngay.
  • HAProxy: Cực nhẹ và chịu tải trâu bò (Layer 4). Đáng tiếc, HAProxy không đọc được nội dung câu lệnh SQL. Nó không phân biệt được SELECT hay UPDATE, đẩy toàn bộ trách nhiệm phân luồng ngược lại cho app.
  • ProxySQL: Hiểu tường tận giao thức MySQL ở Layer 7. Ngoài tự động định tuyến qua regex, ProxySQL còn có connection pooling cực mạnh, query caching trực tiếp trên RAM và kiểm tra độ trễ replication. Quan trọng nhất: app chỉ cần đổi đúng một dòng host và port.

Mô hình thực tế

Hạ tầng giả định cho bài viết gồm 3 node:

  • MySQL Master (Writer): 192.168.1.10:3306
  • MySQL Replica (Reader): 192.168.1.11:3306
  • ProxySQL Node: 192.168.1.5 (Port client: 6033, Port admin: 6032)

Các bước triển khai ProxySQL Read/Write Splitting

Bước 1: Cài đặt ProxySQL trên server trung gian

Cài đặt ProxySQL bản 2.5.x trên node Ubuntu/Debian:

sudo apt-get update
sudo apt-get install -y lsb-release curl apt-transport-https
curl -fsSL https://repo.proxysql.com/repo/proxysql.repo.gpg | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/proxysql.gpg
echo "deb https://repo.proxysql.com/ProxySQL/proxysql-2.5.x/$(lsb_release -sc)/ ./" | sudo tee /etc/apt/sources.list.d/proxysql.list
sudo apt-get update
sudo apt-get install -y proxysql
sudo systemctl enable --now proxysql

Bước 2: Tạo User giám sát trên MySQL Master

ProxySQL cần một user chuyên dụng để healthcheck và theo dõi replication lag. Chạy lệnh sau trên Master (lệnh sẽ tự replicate sang node Replica):

CREATE USER 'monitor'@'%' IDENTIFIED BY 'MonitorPass123!';
GRANT REPLICATION CLIENT, PROCESS ON *.* TO 'monitor'@'%';
FLUSH PRIVILEGES;

Bước 3: Cấu hình Backend Server vào Hostgroup trong ProxySQL

Đăng nhập vào giao diện quản trị ProxySQL (mặc định user/pass là admin:admin tại port 6032):

mysql -u admin -padmin -h 127.0.0.1 -P 6032 --prompt='ProxySQLAdmin> '

Chia các server thành 2 nhóm: Hostgroup 10 cho Writer (Master) và Hostgroup 20 cho Reader (Replica):

-- 1. Khai báo user giám sát
UPDATE global_variables SET variable_value='monitor' WHERE variable_name='mysql-monitor_username';
UPDATE global_variables SET variable_value='MonitorPass123!' WHERE variable_name='mysql-monitor_password';
LOAD MYSQL VARIABLES TO RUNTIME;
SAVE MYSQL VARIABLES TO DISK;

-- 2. Thêm các node database vào hostgroup
INSERT INTO mysql_servers (hostgroup_id, hostname, port, max_connections, weight, max_replication_lag) VALUES 
(10, '192.168.1.10', 3306, 1000, 1, 0),
(20, '192.168.1.11', 3306, 1000, 1, 10);

LOAD MYSQL SERVERS TO RUNTIME;
SAVE MYSQL SERVERS TO DISK;

Tham số max_replication_lag = 10 cực kỳ đáng giá. Nếu Replica bị trễ quá 10 giây so với Master, ProxySQL lập tức ngắt traffic đọc tới node này để tránh lỗi đọc dữ liệu cũ.

Bước 4: Đồng bộ User ứng dụng vào ProxySQL

Khai báo tài khoản mà backend đang dùng để kết nối database:

INSERT INTO mysql_users (username, password, default_hostgroup, transaction_persistent) 
VALUES ('app_user', 'SecretAppPass', 10, 1);

LOAD MYSQL USERS TO RUNTIME;
SAVE MYSQL USERS TO DISK;

Cờ transaction_persistent = 1 là chi tiết sống còn. Nó đảm bảo mọi truy vấn (kể cả SELECT) nằm trong một transaction mở (BEGIN ... COMMIT) đều được ghim lại trên Master. Nhờ đó, bạn tránh được hoàn toàn lỗi dirty read hoặc race condition do replication delay.

Bước 5: Thiết lập Query Rules định tuyến tự động

Đây là trái tim của cơ chế phân luồng. Thứ tự ưu tiên (rule_id) quyết định tính đúng đắn của dữ liệu:

-- Rule 1: Giữ nguyên SELECT ... FOR UPDATE trên Master để lock dòng an toàn
INSERT INTO mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup, apply) 
VALUES (1, 1, '^SELECT.*FOR UPDATE', 10, 1);

-- Rule 2: Đẩy toàn bộ các lệnh SELECT còn lại sang Replica
INSERT INTO mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup, apply) 
VALUES (2, 1, '^SELECT', 20, 1);

LOAD MYSQL QUERY RULES TO RUNTIME;
SAVE MYSQL QUERY RULES TO DISK;

Thuộc tính apply = 1 ở Rule 1 có ý nghĩa cốt lõi: khi query khớp mẫu FOR UPDATE, ProxySQL gán ngay nó vào Hostgroup 10 và dừng quét các rule tiếp theo. Query này sẽ không bao giờ bị Rule 2 đẩy nhầm sang Replica.

Bước 6: Nghiệm thu và kiểm tra tải

Đổi connection string trên backend từ IP Master cũ sang 192.168.1.5:6033. Mở terminal admin ProxySQL để xem luồng query được bóc tách:

SELECT hostgroup, schemaname, digest_text, count_star 
FROM stats_mysql_query_digest 
ORDER BY count_star DESC LIMIT 10;

Tại cột hostgroup, các lệnh SELECT phân bổ đều đặn sang Hostgroup 20, trong khi INSERT/UPDATE vẫn đi thẳng vào Hostgroup 10. Khoảng 75% tải đọc được san phẳng sang node Replica. CPU node Master hạ nhiệt nhanh chóng từ 100% xuống quanh mốc 25%, Threads_running tụt về mức an toàn 18 threads. Hệ thống ổn định trở lại mà không cần đụng tới một dòng code ứng dụng nào.

Share: