Lỗi Gặp Phải
Fatal error: Uncaught PDOException: SQLSTATE[HY000] [2002] Connection refused
Stack trace:
#0 /var/www/html/db.php(5): PDO->__construct('mysql:host=loca...', 'root', '***')
#1 {main}
thrown in /var/www/html/db.php on line 5
Ứng dụng của bạn đang trả về lỗi 500, log tràn ngập, và các request đang dồn ứ. Lỗi này có nghĩa là PHP hoàn toàn không thể mở kết nối TCP đến MySQL — thậm chí chưa đến bước xác thực. Hãy xử lý từng bước theo thứ tự dưới đây.
Nguyên Nhân
SQLSTATE[HY000] [2002] là lỗi mạng phía client: MySQL client đã thử kết nối nhưng bị từ chối. Năm nguyên nhân sau đây chiếm 95% các trường hợp thực tế:
- Dịch vụ MySQL/MariaDB bị dừng hoặc bị crash
- Host hoặc port sai trong chuỗi PDO DSN
- Bẫy Unix socket khi dùng
localhostthay vì127.0.0.1 - Cấu hình
bind-addresscủa MySQL chặn kết nối từ IP của ứng dụng - Firewall chặn traffic trên cổng 3306
Bước 1: MySQL Có Đang Chạy Không?
Đây là nguyên nhân phổ biến nhất lúc 2 giờ sáng — một service bị crash mà không ai hay biết. Kiểm tra điều này trước tiên.
sudo systemctl status mysql
# MariaDB
sudo systemctl status mariadb
Nếu hiển thị failed hoặc inactive, hãy khởi động lại:
sudo systemctl start mysql
sudo systemctl status mysql
Nếu khởi động thất bại, hãy đọc log trước khi làm bất cứ điều gì khác:
sudo journalctl -u mysql -n 100 --no-pager
# hoặc
sudo tail -100 /var/log/mysql/error.log
Ba nguyên nhân phổ biến khiến MySQL không khởi động được: đầy ổ đĩa (dùng df -h để kiểm tra — đầy 100% ổ đĩa là thủ phạm số 1), file InnoDB bị hỏng, hoặc một tiến trình khác đang chiếm cổng 3306 (sudo ss -tlnp | grep 3306). Hãy xử lý nguyên nhân gốc rễ trước rồi mới tiếp tục.
Bước 2: Kiểm Tra PDO DSN
Gõ nhầm host, sai port, hoặc biến môi trường rỗng đều dẫn đến lỗi [2002]. Hãy in ra các giá trị thực tế mà ứng dụng đang dùng trước khi kết luận DSN đúng:
<?php
// In ra các tham số kết nối thực tế mà ứng dụng đang sử dụng
$host = $_ENV['DB_HOST'] ?? getenv('DB_HOST') ?: '(not set)';
$port = $_ENV['DB_PORT'] ?? getenv('DB_PORT') ?: '3306';
$dbname = $_ENV['DB_NAME'] ?? getenv('DB_NAME') ?: '(not set)';
$user = $_ENV['DB_USER'] ?? getenv('DB_USER') ?: '(not set)';
echo "DSN will be: mysql:host={$host};port={$port};dbname={$dbname}\n";
echo "User: {$user}\n";
Ba lỗi thường gặp: DB_HOST trỏ đến sai server (hoặc không phân giải được). Port sai — MySQL mặc định dùng 3306, nhưng các cấu hình Docker thường remap thành 3307 hoặc 33060. Hoặc file .env chưa được load, khiến mọi biến đều rỗng và DSN thành mysql:host=;port=3306;dbname=.
Bước 3: Bẫy Unix Socket khi Dùng localhost vs 127.0.0.1
Đây là điểm hay gây nhầm lẫn cho các lập trình viên PHP. Khi bạn dùng host=localhost trong PDO DSN, thư viện MySQL client sẽ âm thầm bỏ qua TCP và kết nối qua file Unix socket thay thế. Nếu PHP và MySQL không đồng nhất về vị trí socket đó, bạn sẽ nhận lỗi [2002] dù MySQL đang chạy hoàn toàn bình thường.
// Dùng Unix socket — âm thầm lỗi nếu đường dẫn socket không khớp
$dsn = 'mysql:host=localhost;dbname=myapp';
// Bắt buộc dùng TCP — hoạt động ổn định trong hầu hết mọi cấu hình
$dsn = 'mysql:host=127.0.0.1;dbname=myapp';
Kiểm tra xem PHP và MySQL có đồng nhất về đường dẫn socket không:
# Đường dẫn socket mà pdo_mysql của PHP đang tìm
php -r "echo ini_get('pdo_mysql.default_socket') . PHP_EOL;"
# Đường dẫn socket thực tế mà MySQL tạo ra
grep -E "^socket" /etc/mysql/mysql.conf.d/mysqld.cnf
# hoặc
mysqladmin -u root -p variables 2>/dev/null | grep socket
Đường dẫn không khớp? Đổi sang 127.0.0.1 trong DSN — đây là cách sửa nhanh nhất. Hoặc, đặt pdo_mysql.default_socket trong php.ini trỏ đến đúng đường dẫn socket của MySQL, sau đó reload PHP-FPM (sudo systemctl reload php8.2-fpm).
Bước 4: Kiểm Tra bind-address của MySQL
Bạn đang dùng server ứng dụng và DB server riêng biệt? Hoặc cấu hình Docker với mỗi container có network namespace riêng? Đây là lúc cấu hình bind-address gây ra vấn đề.
# Kiểm tra MySQL đang lắng nghe trên interface nào
grep bind-address /etc/mysql/mysql.conf.d/mysqld.cnf /etc/mysql/my.cnf 2>/dev/null
# Xác nhận bằng ss
sudo ss -tlnp | grep 3306
Nếu bind-address = 127.0.0.1, MySQL sẽ từ chối mọi kết nối từ bên ngoài. Hãy thay đổi để cho phép kết nối từ xa:
# /etc/mysql/mysql.conf.d/mysqld.cnf
[mysqld]
bind-address = 0.0.0.0
sudo systemctl restart mysql
Sau đó cấp quyền cho user MySQL kết nối từ IP của server ứng dụng. Cú pháp khác nhau theo phiên bản:
-- MySQL 5.7 trở xuống
GRANT ALL PRIVILEGES ON myapp.* TO 'myuser'@'APP_SERVER_IP' IDENTIFIED BY 'password';
FLUSH PRIVILEGES;
-- MySQL 8.0+: tạo user trước, sau đó mới cấp quyền
CREATE USER IF NOT EXISTS 'myuser'@'APP_SERVER_IP' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON myapp.* TO 'myuser'@'APP_SERVER_IP';
FLUSH PRIVILEGES;
Bước 5: Kiểm Tra Firewall
# UFW
sudo ufw status verbose
# iptables
sudo iptables -L -n | grep 3306
# Kiểm tra khả năng kết nối từ server ứng dụng (không phải DB server)
nc -zv DB_SERVER_IP 3306
# hoặc
telnet DB_SERVER_IP 3306
Kết nối bị từ chối hoặc timeout? Thêm rule firewall giới hạn theo IP của server ứng dụng — không mở cho toàn bộ internet:
sudo ufw allow from APP_SERVER_IP to any port 3306
sudo ufw reload
Bước 6: Môi Trường Docker / Container
Chạy ứng dụng và MySQL trong các container riêng biệt? Cả localhost lẫn 127.0.0.1 đều trỏ về chính container đó — không phải container MySQL bên cạnh. Hãy dùng tên service được định nghĩa trong docker-compose.yml:
<?php
// 'mysql' là tên service trong docker-compose.yml
$dsn = 'mysql:host=mysql;port=3306;dbname=myapp';
# Kiểm tra các container có thực sự liên lạc được với nhau không
docker exec app-container nc -zv mysql 3306
# Xác nhận container MySQL đang hoạt động bình thường
docker ps | grep mysql
docker logs mysql-container-name --tail 50
Xác Nhận Kết Quả
Sau mỗi thay đổi, hãy chạy script này trực tiếp trên server. Đừng tin vào output trên browser — có thể đang bị cache, xếp hàng, hoặc đang hit vào một instance khác:
<?php
$dsn = 'mysql:host=127.0.0.1;port=3306;dbname=myapp';
$user = 'myuser';
$pass = 'mypassword';
try {
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_TIMEOUT => 5,
]);
echo "Connected OK\n";
echo "Query OK: " . $pdo->query('SELECT 1')->fetchColumn() . "\n";
} catch (PDOException $e) {
echo "FAILED: " . $e->getMessage() . "\n";
}
php /tmp/test_db.php
Kết quả mong đợi: Connected OK rồi đến Query OK: 1. Vẫn còn lỗi [2002]? Đọc kỹ thông báo lỗi — thường có ghi rõ host và port mà PHP đã cố kết nối. Đó chính là manh mối cho thấy DSN đang sai ở đâu.
Mẹo Phòng Ngừa
- Luôn đặt
PDO::ATTR_TIMEOUT: Nếu không có, một MySQL server chết có thể khiến các PHP worker bị treo vô thời hạn và kéo sập toàn bộ ứng dụng. Năm giây là giá trị mặc định hợp lý — thất bại nhanh còn hơn bị đơ cứng. - Dùng
127.0.0.1thay vìlocalhosttrong DSN, trừ khi bạn có lý do cụ thể để dùng Unix socket. - Thêm endpoint
/healthzchạy lệnhSELECT 1và trả về HTTP 200 hoặc 503 — load balancer có thể loại instance ra khỏi pool trước khi người dùng nhìn thấy lỗi. - Bật tự động khởi động lại MySQL qua systemd: thêm
Restart=on-failurevàRestartSec=5vào unit file của MySQL. Tự phục hồi sau các sự cố thoáng qua mà không cần đánh thức ai. - Giám sát cổng 3306 bằng công cụ uptime của bạn — cảnh báo sau 1 phút tốt hơn là phát hiện qua báo cáo của người dùng đang bực bội.

