Rào cản mở rộng: Khi Master Database chạm ngưỡng 100% CPU
Tôi nhớ chính xác khoảnh khắc nền tảng mình quản lý nhảy vọt từ 100 lên 10.000 người dùng đồng thời. Dashboard vẫn xanh trong một giờ, rồi sau đó mọi thứ trở nên hỗn loạn. Độ trễ (latency) tăng vọt từ 50ms lên hơn 2 giây. Chúng tôi có thiết lập Master-Slave tiêu chuẩn, nhưng ứng dụng lại “dội” mọi truy vấn vào Master—ngay cả những lệnh SELECT cơ bản. Slave chỉ hoạt động ở mức 2% trong khi CPU của Master luôn ở mức 98%.
Phản ứng tự nhiên thường là nâng cấp phần cứng. Bạn nghĩ đến việc thêm RAM hoặc chuyển sang ổ NVMe nhanh hơn. Nhưng đó chỉ là giải pháp tạm thời. Nút thắt cổ chai thực sự thường nằm ở kiến trúc: ứng dụng của bạn thiếu khả năng phân phối khối lượng công việc một cách thông minh trong cluster.
Chi phí tiềm ẩn của việc điều hướng ở tầng ứng dụng
Hồi còn quản lý connection pool thủ công, tôi đã cố gắng xử lý phân tách Read/Write trực tiếp trong mã nguồn PHP. Tôi định nghĩa db_master cho ghi và db_slave cho đọc. Nó hoạt động tốt—cho đến khi chúng tôi thêm slave thứ hai. Rồi master gặp sự cố, một slave được chỉ định lên thay thế, và tôi phải cập nhật file cấu hình thủ công cho 24 microservices. Chỉ một lỗi đánh máy trong địa chỉ IP đã khiến hệ thống ngoại tuyến suốt 20 phút.
Việc hardcoding cấu trúc database tạo ra ba vấn đề lớn:
- Nợ cấu hình (Configuration Debt): Mỗi thay đổi ở node yêu cầu phải deploy lại ứng dụng.
- Cạn kiệt kết nối (Connection Exhaustion): Nếu 100 instance ứng dụng, mỗi cái duy trì 20 kết nối tới 3 node, database của bạn sẽ lãng phí 6.000 kết nối chỉ để treo máy.
- Mất dấu tình trạng sức khỏe (Health Blindness): Các ứng dụng hiếm khi kiểm tra xem slave có đang bị lag 30 giây hay không trước khi gửi một truy vấn quan trọng tới đó.
Tại sao ProxySQL vượt trội hơn các lựa chọn thay thế
Trước khi chọn ProxySQL, tôi đã đánh giá các ứng cử viên quen thuộc. HAProxy là một bộ cân bằng tải TCP tuyệt vời, nhưng nó “mù SQL”. Nó không thể phân biệt DELETE với SELECT, khiến nó vô dụng trong việc phân tách loại truy vấn. MaxScale mạnh mẽ, nhưng sự thay đổi về chính sách bản quyền khiến nó khó được chấp nhận bởi các bên liên quan vốn ưu tiên ngân sách.
ProxySQL khác biệt vì nó là một proxy hiệu năng cao, am hiểu giao thức (protocol-aware). Nó hiểu ngôn ngữ MySQL. Nó có thể cache các truy vấn lặp lại, viết lại các câu lệnh SQL lộn xộn ngay lập tức và điều hướng lưu lượng dựa trên các quy tắc chi tiết. Ứng dụng không bao giờ cần biết cấu trúc database đã thay đổi.
Cách tiếp cận tốt hơn: Triển khai ProxySQL
Trong môi trường production của mình, tôi đặt ProxySQL ở giữa ứng dụng và database. Đối với ứng dụng, ProxySQL trông giống như một máy chủ MySQL duy nhất và cực kỳ bền bỉ. Đối với database, ProxySQL trông như một client rất hiệu quả và “ngoan ngoãn”.
1. Nhóm các Server
ProxySQL sử dụng Hostgroups để tổ chức các node. Tôi thường dùng Hostgroup 0 cho Master (Writes) và Hostgroup 1 cho các Slave (Reads).
-- Truy cập giao diện quản trị ProxySQL
mysql -u admin -padmin -h 127.0.0.1 -P 6032
-- Định nghĩa các node trong cluster
INSERT INTO mysql_servers(hostgroup_id, hostname, port) VALUES (0, '10.0.0.10', 3306); -- Master
INSERT INTO mysql_servers(hostgroup_id, hostname, port) VALUES (1, '10.0.0.11', 3306); -- Slave 1
INSERT INTO mysql_servers(hostgroup_id, hostname, port) VALUES (1, '10.0.0.12', 3306); -- Slave 2
LOAD MYSQL SERVERS TO RUNTIME;
SAVE MYSQL SERVERS TO DISK;
2. Quản lý người dùng bảo mật
ProxySQL đóng vai trò như người gác cổng, vì vậy nó cần phản chiếu người dùng ở backend của bạn. Khi cần migrate danh sách người dùng lớn hoặc chuyển đổi dữ liệu CSV thành các lệnh SQL insert cho các cấu hình này, tôi sử dụng toolcraft.app/vi/tools/data/csv-to-json. Vì nó xử lý mọi thứ ngay trên trình duyệt, tôi không phải lo lắng về việc rò rỉ thông tin đăng nhập database sang máy chủ của bên thứ ba.
INSERT INTO mysql_users(username, password, default_hostgroup) VALUES ('app_user', 'mat_khau_manh', 0);
LOAD MYSQL USERS TO RUNTIME;
SAVE MYSQL USERS TO DISK;
3. Điều hướng lưu lượng truy cập chính xác
Đây là nơi các logic phát huy tác dụng. Chúng ta hướng dẫn ProxySQL gửi các lệnh SELECT tới slave, với một ngoại lệ quan trọng: SELECT ... FOR UPDATE. Những lệnh này phải nằm ở master để đảm bảo cơ chế khóa dòng (row locking) hoạt động chính xác.
-- Giữ các truy vấn đọc có khóa (locking reads) trên Master
INSERT INTO mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup, apply)
VALUES (1, 1, '^SELECT.*FOR UPDATE$', 0, 1);
-- Điều hướng tất cả các lệnh SELECT khác tới nhóm Slave
INSERT INTO mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup, apply)
VALUES (2, 1, '^SELECT', 1, 1);
LOAD MYSQL QUERY RULES TO RUNTIME;
SAVE MYSQL QUERY RULES TO DISK;
Những bài học xương máu từ thực tế Production
Thiết lập các quy tắc là phần dễ dàng. Giữ cho hệ thống ổn định dưới tải trọng lớn mới cần một vài tinh chỉnh bổ sung.
Giải quyết tình trạng Race Condition khi tạo bản ghi mới
Người dùng rất ghét việc tạo một hồ sơ, nhấn lưu, rồi sau đó thấy lỗi “404 Not Found” chỉ vì Slave mà họ được chuyển hướng tới đang bị chậm 500ms so với Master. ProxySQL có thể theo dõi Seconds_Behind_Master. Tôi thiết lập một ngưỡng nghiêm ngặt—thường là 10 giây—để tự động loại bỏ các slave bị lag ra khỏi vòng quay đọc dữ liệu.
UPDATE mysql_servers SET max_replication_lag = 10 WHERE hostgroup_id = 1;
LOAD MYSQL SERVERS TO RUNTIME;
Lợi ích từ Connection Pooling
Một trong những cải tiến đáng kể nhất mà tôi từng thấy là ở cơ chế dồn kênh kết nối (connection multiplexing). Trong một dự án, chúng tôi có 2.000 luồng ứng dụng tranh giành kết nối. Bằng cách đặt ProxySQL phía trước, chúng tôi đã cô đọng chúng thành chỉ còn 150 kết nối backend bền bỉ. Chỉ riêng thay đổi này đã giúp giảm 18% mức sử dụng CPU của Master mà không cần viết lại bất kỳ dòng code ứng dụng nào.
Kiểm tra truy vấn theo thời gian thực
Cơ sở dữ liệu stats là mỏ vàng để xử lý sự cố. Bạn không cần bật log truy vấn chậm (slow query logs) tốn kém trên các node database. Thay vào đó, hãy truy vấn trực tiếp proxy để tìm ra những truy vấn “nặng” nhất:
SELECT count_star, sum_time, hostgroup, digest_text
FROM stats_mysql_query_digest
ORDER BY sum_time DESC LIMIT 5;
Lời kết
Việc tách logic cân bằng tải ra khỏi ứng dụng và đưa vào ProxySQL là một bước ngoặt cho khả năng mở rộng. Nó cung cấp một lớp trừu tượng sạch sẽ. Bạn có thể thực hiện bảo trì, mở rộng dung lượng đọc hoặc xử lý failover lúc 3 giờ sáng mà ứng dụng không bao giờ bị rớt một gói tin nào. Hãy bắt đầu với việc điều hướng cơ bản, và một khi bạn thấy hiệu quả về hiệu suất, bạn có thể khám phá các tính năng nâng cao như query mirroring và caching.

