Trên Kubernetes, lỗi MySQL hết kết nối hiếm khi hiện đúng tên. Bạn thấy pod kẹt ở Terminating hoặc PodInitializing, liveness probe thất bại, người dùng nhận 502, và mọi ánh nhìn đổ vào node, vào cấu hình pod, vào ingress. Gốc thật có thể nằm ở một dòng nhỏ trong log của database: ERROR 1040 (HY000): Too many connections. Bài này hướng dẫn đi ngược từ triệu chứng phía Kubernetes xuống database, cách đếm kết nối theo user, phân biệt overcommit limit với trần kết nối, và cách stress test đúng thay vì chạy kubectl exec song song.
Mô hình nhiều website dùng chung một MySQL đã được phân tích ở bài Tầng Database: MySQL đa tenant trên VPS cấu hình nhỏ, và khung điều tra nhiều tầng có ở bài Điều tra sự cố nhiều tầng: từ balancer tới database và Kubernetes. Bài này áp dụng chúng cho cluster.
Triệu chứng đánh lạc hướng ở tầng Kubernetes
Khi MySQL hết kết nối và từ chối kết nối mới, ứng dụng PHP không phục vụ được probe, nên pod có thể bị kubelet restart; mỗi lần khởi động lại cũng có thể mở thêm kết nối. Đây là giả thuyết hợp lý nhưng chưa được xác nhận trong ca thực tế: hãy tự kiểm bằng kubectl describe pod (phần Events) và đối chiếu thời điểm restart với Threads_connected. Những phản xạ thường gặp nhưng không giải quyết được gốc:
- Tăng CPU và RAM cho pod web.
cordonrồidrainnode nghi ngờ.- Tách mỗi site một node riêng. Nếu sau khi tách mà vẫn lỗi, đó là bằng chứng node không phải gốc.
Một lưu ý về chẩn đoán: PodInitializing không có nghĩa initContainer đang kẹt vì database. Hãy kiểm tra bằng kubectl describe pod và kubectl logs pod -c tên-init thay vì suy ra từ trạng thái.
Kiểm database trước khi nghi node khi MySQL hết kết nối
Khi nghi MySQL hết kết nối, hãy vào MySQL và đọc bốn con số:
SHOW VARIABLES LIKE 'max_connections';
SHOW GLOBAL STATUS LIKE 'Threads_connected';
SHOW GLOBAL STATUS LIKE 'Max_used_connections';
SHOW GLOBAL STATUS LIKE 'Connection_errors_max_connections';max_connections là trần (mặc định của MySQL là 151). Threads_connected là số kết nối hiện tại; nếu nó sát trần thì đây là nguyên nhân. Max_used_connections là đỉnh từ lúc khởi động, và Connection_errors_max_connections đếm số lần kết nối bị từ chối vì chạm trần. MySQL luôn giữ thêm một kết nối dành cho tài khoản có quyền quản trị, nhưng nếu pipeline hoặc bạn đã dùng hết khe đó, chính lệnh mysql bạn gõ cũng báo lỗi 1040. Vì vậy hãy mở sẵn một phiên quản trị trước khi sự cố xảy ra.
Đọc processlist toàn Sleep khi MySQL hết kết nối
Tình trạng MySQL hết kết nối không có nghĩa MySQL đang bận. Một biểu hiện hay gặp là danh sách tiến trình gần như toàn Sleep: kết nối mở nhưng không làm gì. Hãy đếm theo user và theo nguồn:
SELECT user, SUBSTRING_INDEX(host, ':', 1) AS client, COUNT(*) AS n
FROM performance_schema.processlist
GROUP BY user, client
ORDER BY n DESC;
SELECT command, COUNT(*) FROM performance_schema.processlist GROUP BY command;Từ MySQL 8.0.22, information_schema.processlist bị đánh dấu deprecated nên ví dụ dùng performance_schema.processlist; SHOW PROCESSLIST cũng dùng được. Tài khoản không có quyền PROCESS chỉ thấy luồng của chính mình.
Ca thứ nhất, MySQL hết kết nối thật sự: MySQL chạy trong cluster (StatefulSet), có vài chục user, mỗi user giữ vài kết nối từ địa chỉ của các pod, hầu hết ở trạng thái Sleep. Ở đây có lỗi 1040 và Threads_connected sát trần, nên kết luận nghẽn ở số kết nối là có cơ sở.
Ca thứ hai, chưa chắc là MySQL hết kết nối: MySQL trên VPS ngoài cluster, processlist có hàng trăm dòng, đa số là Sleep, CPU máy database chỉ khoảng một phần ba. Hai ca giống nhau về hình dạng processlist, nhưng ca thứ hai có khả năng nghẽn ở số kết nối chứ chưa được xác nhận, vì chưa có giá trị max_connections hay log 1040. Cách kiểm là đối chiếu Threads_connected với max_connections.
Vì sao có nhiều kết nối mở như vậy thì chưa có câu trả lời. Hướng kiểm chứng: kết nối bền (persistent) hoặc pool ở phía ứng dụng, wait_timeout quá lớn khiến kết nối Sleep không bị đóng, và số pod nhân với số worker PHP-FPM cho mỗi site.
Vì sao dễ chạm trần khi nhiều site dùng chung
Mỗi site có thể giữ vài kết nối cho mỗi pod, và mỗi pod chạy nhiều worker. Phép nhân đơn giản: số site × số pod × số kết nối mỗi pod. Khi hàng chục site dùng chung một server, con số 151 mặc định có thể bị chạm mà không site nào bất thường. HPA tăng số pod cho một site còn làm con số này tăng đúng lúc tải cao (xem bài HPA cho Laravel). Một cách giảm nguy cơ là quy ước số database tối đa trên mỗi server; con số cụ thể hãy chọn theo đo đạc của bạn.
Để xử lý nhanh khi MySQL hết kết nối, bạn có thể nâng trần tạm thời bằng lệnh sau:
SET GLOBAL max_connections = 300;Lệnh này có hiệu lực ngay nhưng mất khi MySQL khởi động lại. Với MySQL 8.0 trở lên có thể dùng SET PERSIST max_connections = 300. Cách còn lại là ghi vào cấu hình bền vững:
[mysqld]
max_connections = 300Với MySQL chạy trong cluster, đặt đoạn [mysqld] này vào ConfigMap mount tại /etc/mysql/conf.d rồi rollout lại; sửa my.cnf trực tiếp trong container sẽ mất khi pod được tạo lại. Nâng trần cũng tốn RAM, vì mỗi kết nối cấp buffer riêng (xem sort_buffer_size, join_buffer_size). Nâng trần chỉ mua thời gian trước khi MySQL hết kết nối lần nữa; gốc là nguồn mở kết nối.
Overcommit limit ở node: rủi ro nhưng không phải thủ phạm
Khi giám sát node, bạn có thể thấy tổng limit lớn hơn nhiều lần dung lượng thật (overcommit). Đó là một rủi ro đáng ghi nhận, nhất là về RAM (xem bài Node đầy RAM: web chậm dù CPU thấp, overcommit limit và node NotReady, và cách đặt request, limit ở bài Requests và limits, QoS và nodeAffinity). Nhưng trong ca MySQL hết kết nối có lỗi 1040, overcommit không phải thủ phạm: sau khi tách mỗi site ra một node riêng, lỗi vẫn còn, tức node không phải gốc. Overcommit cao chỉ cho biết nguy cơ, không chứng minh nguyên nhân.
Stress test đúng cách bằng pod tải
Để kiểm tra MySQL hết kết nối có tái hiện được hay không, nhiều người viết một script gọi kubectl exec song song vào pod MySQL. Cách này dễ gây hiểu lầm:
- Chạy khoảng năm mươi luồng
kubectl execcùng lúc có thể báo lỗi kiểu "error sending request" tới API server. Lý do chưa xác nhận; việc cho rằng API server "giới hạn tốc độ" chỉ là phỏng đoán. - Exec là kênh quản trị, không phải kênh tải. Mọi đối số của lệnh, kể cả mật khẩu truyền bằng
-p, được đưa vào URL của yêu cầu exec; đừng đặt mật khẩu trong đối số lệnh. - So sánh
kubectl execvớidocker execkhông công bằng, vì khác kênh và khác đích; đừng coi kết quả là số đo.
Cách đúng là dựng một pod tải riêng, chạy trong cùng cluster, kết nối thẳng tới Service của MySQL như ứng dụng thật:
apiVersion: v1
kind: Pod
metadata:
name: mysql-load
namespace: database
spec:
restartPolicy: Never
containers:
- name: load
image: registry.example.com/mysql-client:<phien-ban>
envFrom:
- secretRef:
name: mysql-load-secret
command: ["/bin/sh", "-c"]
args:
- |
for i in $(seq 1 50); do
( while true; do
mysql -h db-1 -u "$MYSQL_USER" "$MYSQL_DB" -e "select sleep(60)" >/dev/null
done ) &
done
waitMật khẩu lấy từ Secret qua biến môi trường MYSQL_PWD (client mysql đọc biến này), không viết vào manifest hay đối số lệnh, nên không lộ trong ps. Truy vấn select sleep(60) giữ kết nối mở để mô phỏng kết nối Sleep; vòng lặp select 1 mở rồi đóng ngay thì không tái hiện được tình trạng giữ kết nối. Để chạm trần, số luồng phải vượt max_connections trừ đi số kết nối đang có (với trần 151, 50 luồng là chưa đủ), hoặc hạ tạm max_connections trên môi trường thử. Hãy thử trên MySQL thử nghiệm, không thử trên production, rồi tăng dần số luồng và theo dõi Threads_connected cùng Connection_errors_max_connections. Bài này không đưa số liệu mẫu vì chưa có bảng kết quả.
Cảnh báo và phòng ngừa MySQL hết kết nối
- Cảnh báo sớm trước khi MySQL hết kết nối: khi
Threads_connected / max_connectionsvượt khoảng 80% trong vài phút. Đây là khuyến nghị cần kiểm chứng trên môi trường của bạn. Metric lấy từmysqld_exporter(xem bài Cài mysqld_exporter trên Kubernetes). - Đặt
max_connectionstrong cấu hình bền vững, không chỉSET GLOBAL, để MySQL hết kết nối không tái diễn sau restart. - Đặt
wait_timeoutvàinteractive_timeouthợp lý để dọn kết nối Sleep. - Tính trước tổng kết nối tối đa của mọi site khi HPA scale lên, kẻo MySQL hết kết nối đúng lúc tải cao.
- Dùng dashboard hiển thị processlist cho dễ nhìn, với một user chỉ đọc có quyền
PROCESS.
Về việc có nên chạy MySQL trong cluster hay không, xem MySQL trong Kubernetes hay ngoài cluster. Tài liệu gốc: xử lý lỗi Too many connections của MySQL.
Điều còn chưa chốt
- Nguồn mở nhiều kết nối khiến MySQL hết kết nối ở ca đầu: chưa tìm ra.
- Cơ chế từ MySQL hết kết nối đưa tới
TerminatingvàPodInitializing: mới chỉ là liên hệ theo thời gian, chưa chứng minh nhân quả. - Ở ca MySQL ngoài cluster, web phục hồi sau khi nâng cấu hình máy database; nhưng khởi động lại cũng xoá kết nối, nên không thể viết "thêm CPU hay RAM là cách chữa". Ca đó thiếu giá trị
max_connectionsvà log 1040, nên chỉ là dấu hiệu gợi ý. - Số database tối đa mỗi server để tránh MySQL hết kết nối: chưa có quy ước, hãy chọn theo đo đạc.
Kết series
Series khép lại bằng một bài học chung, sau khi đi từ những lỗi nền (PVC, Pending, node đầy RAM, cert-manager) qua dữ liệu, bảo mật, tài nguyên và autoscaling: triệu chứng ở tầng Kubernetes thường chỉ là cái bóng của một tầng khác. Một 502 có thể bắt nguồn từ MySQL hết kết nối, một web treo có thể do HPA chạm trần, một site chậm có thể do webroot trên NFS. Thứ tự chẩn đoán nhất quán (pod, Service, node, rồi lưu trữ và database) tiết kiệm nhiều thời gian hơn bất kỳ lệnh riêng lẻ nào.
Nếu bạn mới bắt đầu, hãy đọc lại series Kubernetes và CI/CD. Để đi sâu theo chủ đề: Cluster Autoscaler trên LKE cho autoscaling, Webroot trên NFS bị treo cho lưu trữ dùng chung, và Lỗi 503 khi Service không có Endpoints cho checklist mạng.



