ProxySQL: Giảm Kết Nối PHP-MySQL Khi Scale

Mỗi ứng dụng PHP khi kết nối với MySQL đều mở một connection. Với app nhỏ, chuyện đó không sao. Nhưng khi bạn có 50 PHP-FPM worker, deploy lên 3 server, rồi traffic tăng đột biến — thì đột nhiên có hàng trăm connection đồng thời tấn công MySQL, mỗi cái ngốn memory, một thread và file descriptor.
Giải pháp không phải là mua server to hơn. Mà là đặt một proxy thông minh trước MySQL để pool những connection đó lại. Proxy đó chính là ProxySQL.
ProxySQL Là Gì?
ProxySQL là một SQL-aware proxy hiệu năng cao, ngồi giữa ứng dụng và MySQL. Nó nói chuyện theo MySQL protocol, nên PHP của bạn không biết sự khác biệt. Từ góc độ PHP, nó chỉ đang nói chuyện với MySQL như thường.
Những thứ ProxySQL thực sự làm phía sau:
- Multiplex hàng trăm connection đến thành một pool nhỏ các backend connection bền vững
- Route query đến read replica hay write primary dựa trên rules
- Cache kết quả query
- Rewrite query on the fly
- Monitor backend health và tự failover
Trong bài này, ta tập trung vào tính năng có giá trị nhất ngay lập tức: connection pooling.
Vấn Đề: Connection Overhead Với PHP + MySQL
PHP-FPM spawn worker process. Mỗi worker giữ một MySQL connection trong suốt thời gian xử lý request (hoặc persistent, nếu dùng pconnect). Sự chênh lệch ở chỗ:
Với 100 worker trên 5 app server, đó là 500 connection đến MySQL — dù chỉ 10% trong số đó đang thực sự chạy query tại bất kỳ thời điểm nào. max_connections mặc định của MySQL là 151. Bạn chạm giới hạn đó rất nhanh.
Với ProxySQL:
500 PHP worker kết nối đến ProxySQL. ProxySQL duy trì 20 persistent connection đến MySQL và multiplex toàn bộ traffic qua đó. MySQL thấy 20 connection thay vì 500.
Cài Đặt ProxySQL
Ubuntu/Debian:
wget https://github.com/sysown/proxysql/releases/download/v2.6.3/proxysql_2.6.3-ubuntu22_amd64.deb
dpkg -i proxysql_2.6.3-ubuntu22_amd64.deb
systemctl start proxysql
systemctl enable proxysqlDocker (cho local dev/testing):
docker run -d \
--name proxysql \
-p 6033:6033 \
-p 6032:6032 \
proxysql/proxysqlProxySQL lắng nghe trên hai port:
- 6033 — MySQL protocol proxy port (app kết nối vào đây)
- 6032 — Admin interface (MySQL protocol, nhưng để config ProxySQL)
Cấu Hình ProxySQL
Config được thực hiện qua admin interface, bản thân nó là một interface tương thích MySQL. Kết nối vào:
mysql -u admin -padmin -h 127.0.0.1 -P 6032 --prompt "ProxySQL> "Credential mặc định là admin/admin — đổi ngay lập tức trên production.
Bước 1: Thêm MySQL Backend Server
INSERT INTO mysql_servers (hostgroup_id, hostname, port, max_connections)
VALUES (0, '127.0.0.1', 3306, 20);
LOAD MYSQL SERVERS TO RUNTIME;
SAVE MYSQL SERVERS TO DISK;max_connections = 20 nghĩa là ProxySQL sẽ giữ tối đa 20 connection đến MySQL server này. Tune con số này dựa trên capacity của MySQL. Điểm khởi đầu tốt: (max_connections / số_proxysql_instance) * 0.8.
Bước 2: Tạo MySQL User Cho ProxySQL
Trên MySQL, tạo user mà app sẽ dùng để authenticate:
CREATE USER 'appuser'@'%' IDENTIFIED BY 'strongpassword';
GRANT SELECT, INSERT, UPDATE, DELETE ON myapp.* TO 'appuser'@'%';
FLUSH PRIVILEGES;Rồi báo cho ProxySQL biết về user này:
-- Trong ProxySQL admin (port 6032)
INSERT INTO mysql_users (username, password, default_hostgroup)
VALUES ('appuser', 'strongpassword', 0);
LOAD MYSQL USERS TO RUNTIME;
SAVE MYSQL USERS TO DISK;Bước 3: Thiết Lập Monitoring User
ProxySQL cần một MySQL user để ping backend và kiểm tra health:
-- Trên MySQL
CREATE USER 'proxysql_monitor'@'%' IDENTIFIED BY 'monitorpassword';
GRANT USAGE ON *.* TO 'proxysql_monitor'@'%';-- Trong ProxySQL admin
SET mysql-monitor_username = 'proxysql_monitor';
SET mysql-monitor_password = 'monitorpassword';
LOAD MYSQL VARIABLES TO RUNTIME;
SAVE MYSQL VARIABLES TO DISK;Bước 4: Kích Hoạt Connection Multiplexing
Connection multiplexing bật mặc định, nhưng hãy kiểm tra các biến quan trọng:
SELECT variable_name, variable_value
FROM global_variables
WHERE variable_name IN (
'mysql-connection_max_age_ms',
'mysql-free_connections_pct',
'mysql-max_connections'
);Các biến cần hiểu:
| Biến | Mặc định | Ý nghĩa |
|---|---|---|
mysql-max_connections | 2048 | Số connection tối đa từ app đến ProxySQL |
mysql-free_connections_pct | 10 | % pool giữ idle và sẵn sàng |
mysql-connection_max_age_ms | 0 (vô hạn) | Thời gian sống tối đa của backend connection |
mysql-connect_timeout_server | 3000 | ms chờ khi connect đến backend |
Kết Nối PHP Đến ProxySQL
Không cần thay đổi code PHP. Chỉ cần trỏ connection đến host của ProxySQL và port 6033:
// Trước: kết nối trực tiếp đến MySQL
$pdo = new PDO('mysql:host=mysql-server;port=3306;dbname=myapp', 'appuser', 'strongpassword');
// Sau: kết nối qua ProxySQL
$pdo = new PDO('mysql:host=proxysql-server;port=6033;dbname=myapp', 'appuser', 'strongpassword');Vậy thôi. PHP nghĩ mình đang nói chuyện với MySQL. ProxySQL xử lý pooling một cách transparent.
Vấn đề với php.ini / PHP-FPM
Khi ProxySQL đã pool connection, bạn thường không cần pconnect (persistent connection) trong PHP nữa — ProxySQL đã giữ connection sống phía backend rồi. Dùng pconnect với ProxySQL thực ra có hại, vì PHP giữ ProxySQL connection qua các request, phá vỡ multiplexing.
Hãy dùng non-persistent connection bình thường:
// Tốt khi dùng với ProxySQL — để ProxySQL quản lý persistence
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_PERSISTENT => false, // không dùng persistent connection từ phía PHP
]);Kiểm Tra Hoạt Động
Kiểm tra số connection trên MySQL:
-- Chạy trực tiếp trên MySQL
SHOW STATUS LIKE 'Threads_connected';Trước ProxySQL: bạn thấy số gần bằng số PHP worker.
Sau ProxySQL: số thấp và ổn định, khớp với config max_connections trong pool.
Kiểm tra stats connection pool của ProxySQL:
-- Chạy trong ProxySQL admin (port 6032)
SELECT hostgroup, srv_host, srv_port, status, connused, connfree, connok, connERR
FROM stats_mysql_connection_pool;| Cột | Ý nghĩa |
|---|---|
connused | Backend connection đang được dùng |
connfree | Backend connection idle và sẵn sàng |
connok | Tổng số connection đến backend thành công |
connERR | Backend connection thất bại |
Kiểm tra query routing stats:
SELECT hostgroup, schemaname, username, digest_text, count_star, sum_time
FROM stats_mysql_query_digest
ORDER BY sum_time DESC
LIMIT 10;Đây cho bạn thấy chính xác query nào đang chạy, bao nhiêu lần, và tổng thời gian — về cơ bản là một slow query log miễn phí.
Read/Write Splitting (Bonus)
Một khi đã có ProxySQL, thêm read/write splitting rất đơn giản. Định nghĩa query rules để route SELECT đến read replica (hostgroup 1) và mọi thứ khác đến writer (hostgroup 0):
INSERT INTO mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup, apply)
VALUES
(1, 1, '^SELECT.*FOR UPDATE', 0, 1), -- SELECT FOR UPDATE vào writer
(2, 1, '^SELECT', 1, 1); -- Các SELECT khác vào reader
LOAD MYSQL QUERY RULES TO RUNTIME;
SAVE MYSQL QUERY RULES TO DISK;Thêm replica vào hostgroup 1:
INSERT INTO mysql_servers (hostgroup_id, hostname, port, max_connections)
VALUES (1, 'mysql-replica', 3306, 20);
LOAD MYSQL SERVERS TO RUNTIME;
SAVE MYSQL SERVERS TO DISK;PHP app gửi toàn bộ query đến ProxySQL trên port 6033 — ProxySQL lo phần routing. Không thay đổi gì ở application.
Docker Compose Setup Cho Development
Stack local đầy đủ để test:
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: myapp
MYSQL_USER: appuser
MYSQL_PASSWORD: strongpassword
ports:
- "3307:3306" # expose trên 3307 để 3306 không bị chiếm
proxysql:
image: proxysql/proxysql
volumes:
- ./proxysql.cnf:/etc/proxysql.cnf
ports:
- "6033:6033" # app port
- "6032:6032" # admin port
depends_on:
- mysql
php:
image: php:8.3-fpm
environment:
DB_HOST: proxysql
DB_PORT: 6033proxysql.cnf cho môi trường dev:
datadir="/var/lib/proxysql"
admin_variables=
{
admin_credentials="admin:admin"
mysql_ifaces="0.0.0.0:6032"
}
mysql_variables=
{
threads=4
max_connections=2048
default_query_delay=0
default_query_timeout=36000000
poll_timeout=2000
interfaces="0.0.0.0:6033"
default_schema="information_schema"
stacksize=1048576
connect_timeout_server=3000
monitor_username="root"
monitor_password="rootpass"
ping_timeout_server=500
sessions_sort=true
monitor_connect_interval=60000
monitor_ping_interval=10000
}
mysql_servers=
(
{ address="mysql", port=3306, hostgroup=0, max_connections=20 }
)
mysql_users=
(
{ username="appuser", password="strongpassword", default_hostgroup=0 }
)Những Điểm Cần Lưu Ý
Session state không được preserve qua các multiplexed connection. Nếu PHP code của bạn set SET @variable hoặc USE database và kỳ vọng nó tồn tại sang query tiếp theo trên một backend connection "khác" — sẽ không được. ProxySQL track session variables và replay lại khi assign connection, nhưng session state phức tạp có thể gây vấn đề. Nếu bạn phụ thuộc nhiều vào session variables, hãy test kỹ.
Transaction bị pin. Khi một transaction bắt đầu (BEGIN / START TRANSACTION), ProxySQL pin session đó vào một backend connection suốt thời gian transaction. Đây là behavior đúng — bạn không muốn ProxySQL route UPDATE của bạn đến connection khác với BEGIN. Nhưng điều đó nghĩa là transaction giữ một backend connection độc quyền, nên hãy giữ transaction ngắn.
Password hashing. ProxySQL cần store MySQL user password. Nếu MySQL dùng caching_sha2_password (mặc định trong MySQL 8.0), bạn có thể cần dùng mysql_native_password hoặc config ProxySQL đúng cách. Cách đơn giản nhất với setup mới:
-- Trên MySQL — dùng native password cho tương thích với ProxySQL
ALTER USER 'appuser'@'%' IDENTIFIED WITH mysql_native_password BY 'strongpassword';Admin password. Đổi credential mặc định admin/admin ngay lập tức:
UPDATE global_variables
SET variable_value = 'newadmin:newstrongpassword'
WHERE variable_name = 'admin-admin_credentials';
LOAD ADMIN VARIABLES TO RUNTIME;
SAVE ADMIN VARIABLES TO DISK;Khi Nào Nên (Và Không Nên) Dùng ProxySQL
Phù hợp:
- PHP-FPM với nhiều worker đánh vào một MySQL
- Nhiều app server cùng kết nối đến một database
- Muốn read/write splitting mà không cần thay code
- Cần query routing, mirroring, hoặc rewriting
Không cần thiết:
- Server đơn, traffic thấp, ít connection
- Đã dùng connection pool ở tầng app (ví dụ service Go/Node chạy dài với pool riêng)
- PgBouncer phù hợp hơn nếu bạn đang dùng PostgreSQL
Tóm Lại
ProxySQL là giải pháp battle-tested cho vấn đề "quá nhiều MySQL connection" mà mọi ứng dụng PHP sớm muộn gì cũng gặp. Các bước chính:
✅ Cài ProxySQL và config backend servers
✅ Thêm MySQL user vào user table của ProxySQL
✅ Set max_connections per backend để giới hạn load MySQL
✅ Trỏ PHP đến port 6033 của ProxySQL thay vì MySQL trực tiếp
✅ Tắt persistent connection phía PHP
✅ Monitor qua stats_mysql_connection_pool và stats_mysql_query_digest
Kết quả: MySQL thấy một pool connection nhỏ, ổn định bất kể bạn thêm bao nhiêu PHP worker hay app server. Bạn có thể scale tầng application theo chiều ngang mà không cần đụng đến cấu hình database.
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.