Back to blog

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

mysqlproxysqlphpbackendperformancedatabase
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 proxysql

Docker (cho local dev/testing):

docker run -d \
  --name proxysql \
  -p 6033:6033 \
  -p 6032:6032 \
  proxysql/proxysql

ProxySQL 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ếnMặc địnhÝ nghĩa
mysql-max_connections2048Số connection tối đa từ app đến ProxySQL
mysql-free_connections_pct10% pool giữ idle và sẵn sàng
mysql-connection_max_age_ms0 (vô hạn)Thời gian sống tối đa của backend connection
mysql-connect_timeout_server3000ms 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
connusedBackend connection đang được dùng
connfreeBackend connection idle và sẵn sàng
connokTổng số connection đến backend thành công
connERRBackend 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: 6033

proxysql.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.