2 giờ sáng và app của bạn đang báo lỗi
Hệ thống giám sát kêu. Log đầy những dòng Error: MySQL server has gone away. Các query chạy tốt cả ngày nay bỗng dưng thất bại. Database vẫn đang chạy — bạn kết nối thủ công được — vậy chuyện gì đang xảy ra?
MySQL đã ngắt kết nối trước khi app của bạn dùng xong. Client gửi query, nhưng server không còn lắng nghe nữa. Không phải crash. Chỉ là mất kết nối.
Chẩn đoán nhanh: tìm nguyên nhân thật sự trước
Trước khi đụng vào bất kỳ config nào, hãy xác định tại sao kết nối bị đứt. Ba thủ phạm chiếm 95% trường hợp:
- Connection timeout — MySQL đóng kết nối nhàn rỗi trước khi app dùng lại
- Packet quá lớn — một query hoặc blob vượt quá
max_allowed_packet - MySQL crash hoặc khởi động lại — server tự tắt giữa chừng
Kiểm tra error log của MySQL
# Ubuntu/Debian
tail -100 /var/log/mysql/error.log
# CentOS/RHEL
tail -100 /var/log/mysqld.log
# Hoặc hỏi thẳng MySQL
SHOW VARIABLES LIKE 'log_error';
Kiểm tra cài đặt timeout hiện tại
SHOW VARIABLES LIKE '%timeout%';
SHOW VARIABLES LIKE 'max_allowed_packet';
Chú ý wait_timeout và interactive_timeout. Mặc định của MySQL là 28800 giây (8 tiếng). Nhiều nhà cung cấp cloud đặt thấp hơn nhiều — AWS RDS thường mặc định 600 giây, một số database managed xuống thấp đến 60 giây.
Kiểm tra xem server có khởi động lại không
-- Thời gian kể từ lần khởi động lại gần nhất
SHOW STATUS LIKE 'Uptime';
# Trên Linux, kiểm tra system log
journalctl -u mysql --since "1 hour ago"
Cách sửa 1: Connection timeout (nguyên nhân phổ biến nhất)
App của bạn mở một kết nối, để nó nhàn rỗi, và MySQL đã kill nó. Query tiếp theo thất bại với lỗi MySQL server has gone away vì app vẫn nghĩ kết nối còn sống.
Phương án A — Tăng timeout của MySQL (phía server)
Sửa file /etc/mysql/mysql.conf.d/mysqld.cnf (hoặc /etc/my.cnf):
[mysqld]
wait_timeout = 3600
interactive_timeout = 3600
Áp dụng mà không cần restart:
SET GLOBAL wait_timeout = 3600;
SET GLOBAL interactive_timeout = 3600;
Phương án B — Sửa connection pooling trong app (cách làm đúng)
Tăng timeout chỉ trì hoãn vấn đề. Sớm hay muộn, một kết nối vẫn sẽ bị cũ. Cách sửa bền vững là dạy app nhận biết kết nối chết và tự kết nối lại.
Python (SQLAlchemy):
engine = create_engine(
DATABASE_URL,
pool_pre_ping=True, # Kiểm tra kết nối trước khi dùng
pool_recycle=1800, # Tái sử dụng kết nối mỗi 30 phút
pool_timeout=30,
pool_size=10
)
Node.js (mysql2):
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
database: 'mydb',
waitForConnections: true,
connectionLimit: 10,
enableKeepAlive: true, // Gửi gói keepalive
keepAliveInitialDelay: 10000
});
// Luôn dùng pool.execute(), không dùng một kết nối persistent duy nhất
const [rows] = await pool.execute('SELECT 1');
PHP (PDO):
// Tránh dùng PDO::ATTR_PERSISTENT — nó tái sử dụng kết nối giữa các request mà không kiểm tra còn sống không
// Bắt lỗi và kết nối lại thay vào đó:
try {
$stmt = $pdo->query($sql);
} catch (PDOException $e) {
if (str_contains($e->getMessage(), 'server has gone away')) {
$pdo = new PDO($dsn, $user, $pass, $options); // kết nối lại
$stmt = $pdo->query($sql);
} else {
throw $e;
}
}
Cách sửa 2: max_allowed_packet quá nhỏ
Lỗi xảy ra khi insert văn bản lớn, ảnh hoặc JSON blob — nhưng không xảy ra khi kết nối nhàn rỗi? Kích thước packet mới là thủ phạm, không phải timeout.
-- Kiểm tra giới hạn hiện tại
SHOW VARIABLES LIKE 'max_allowed_packet';
-- Mặc định là 4MB (4194304) trong MySQL 5.7, 64MB trong MySQL 8.0
Tăng lên trong my.cnf:
[mysqld]
max_allowed_packet = 64M
Hoặc áp dụng ngay (chỉ có hiệu lực với kết nối mới):
SET GLOBAL max_allowed_packet = 67108864;
Restart MySQL để thay đổi tồn tại sau khi khởi động lại:
sudo systemctl restart mysql
Cách sửa 3: MySQL crash hoặc bị OOM-kill
Uptime thấp hoặc có mục restart trong error log nghĩa là server đã tắt — thường do áp lực bộ nhớ. Hệ điều hành đã kill MySQL trước khi nó kịp đóng các kết nối sạch sẽ.
# Kiểm tra xem OOM killer có nhắm vào MySQL không
dmesg | grep -i 'killed process'
grep -i 'oom' /var/log/syslog | tail -20
Nếu MySQL bị OOM-kill, hãy giới hạn innodb_buffer_pool_size ở mức 70–75% RAM khả dụng. Trên server 4 GB, đó là khoảng 2–3 GB:
[mysqld]
# Cho server có 4GB RAM:
innodb_buffer_pool_size = 2G
Xác nhận lại sau khi sửa
Sau khi áp dụng thay đổi, hãy kiểm tra các giá trị mới có thực sự có hiệu lực không:
-- Xác nhận giá trị timeout mới
SHOW VARIABLES LIKE 'wait_timeout';
SHOW VARIABLES LIKE 'max_allowed_packet';
-- Theo dõi lỗi kết nối theo thời gian thực
SHOW STATUS LIKE 'Aborted_clients';
SHOW STATUS LIKE 'Aborted_connects';
Số Aborted_clients tăng liên tục nghĩa là các client đang ngắt kết nối mà không đóng đúng cách — cấu hình connection pool của bạn cần xem lại. Ngược lại, nếu Aborted_connects tăng thì vấn đề nằm ở xác thực hoặc mạng.
Muốn stress-test logic kết nối lại phía app? Giảm wait_timeout xuống 10 giây, chờ 15 giây, rồi gửi query qua app:
SET GLOBAL wait_timeout = 10;
-- Chờ 15 giây, sau đó chạy một query qua app
-- Không có lỗi = pool_pre_ping / logic kết nối lại đang hoạt động tốt
Những điều cần ghi nhớ
- Không bao giờ giữ kết nối DB nhàn rỗi — luôn dùng connection pool với
pre_pinghoặc keepalive. Dù timeout của MySQL thấp, pool vẫn tự xử lý kết nối lại một cách minh bạch. - Đặt max_allowed_packet thành 64M trước khi deploy — nếu app xử lý upload file hoặc payload JSON lớn, mặc định 4 MB sẽ gây bất ngờ trên môi trường production.
- Theo dõi Aborted_clients — tăng đều đặn là dấu hiệu cảnh báo sớm rằng có gì đó đang giữ kết nối hoặc làm rò rỉ kết nối.
- Managed database có timeout rất ngắn — RDS, PlanetScale và Cloud SQL thường mặc định 60–600 giây. Đặt
pool_recyclebằng một nửa giá trị đó để an toàn.

