Cảnh báo php-fpm max_children: tìm nguyên nhân trong log trước khi tăng worker

Cảnh báo php-fpm max_children là dấu hiệu cho thấy mọi tiến trình worker PHP đều đang xử lý yêu cầu nên yêu cầu mới phải vào hàng đợi. Trên một máy chủ WordPress chạy Nginx, Apache và php-fpm, nơi cảnh báo này tích tụ suốt nhiều tuần, các yêu cầu làm đầy worker đã được lọc ra từ log, và vấn đề được giải quyết bằng giới hạn tốc độ của nginx thay vì nâng giá trị cài đặt.
Trong đợt kiểm tra hằng tuần, cảnh báo được ghi 27–187 lần mỗi ngày, nhưng trong bốn ngày cùng kỳ, khách truy cập nhận 0 lỗi 502·504. Thứ làm đầy worker không phải khách truy cập mà là một bot liên tục yêu cầu địa chỉ quản trị và một máy quét yêu cầu những đường dẫn không tồn tại.
1️⃣ php-fpm max_children & seems busy
Khi cảnh báo tích lại 66 lần một ngày, rất dễ hiểu thành worker đã thiếu 66 lần. Log của php-fpm trộn lẫn hai loại cảnh báo khác nhau, và phần lớn là cảnh báo sớm được ghi trước khi chạm giới hạn.
[09-Oct-2026 00:16:18] WARNING: [pool www] seems busy (you may need to increase pm.start_servers, or pm.min/max_spare_servers), spawning 8 children, there are 0 idle, and 15 total children
[09-Oct-2026 13:02:05] WARNING: [pool www] server reached pm.max_children setting (26), consider raising it[seems busy] là thông báo cho biết số worker rảnh đã về 0 và một tiến trình mới đang được tạo; nếu cách quản lý tiến trình là dynamic, chỉ cần yêu cầu tăng lên một chút cũng bị ghi lại. [server reached pm.max_children] là thời điểm đã chạm giới hạn 26 và không thể tạo thêm, từ lúc đó yêu cầu mới phải chờ.
Giống như sự khác nhau giữa việc gọi thêm một nhân viên trong nhà hàng và việc để khách đứng ngoài vì đã hết chỗ ngồi.
Đếm dữ liệu một ngày theo từng loại thì có 58 lần seems busy và 6 lần max_children. Quy trình kiểm tra cộng cả hai cảnh báo và báo cáo 66 lần, nhưng thực tế chỉ chạm giới hạn sáu lần.
grep '09-Oct-2026' /var/log/php-fpm/error.log | grep 'pool www' | grep -c 'seems busy'58grep '09-Oct-2026' /var/log/php-fpm/error.log | grep 'max_children' | cut -c2-2109-Oct-2026 13:02:05
09-Oct-2026 13:52:10
09-Oct-2026 18:09:20
09-Oct-2026 18:42:17
09-Oct-2026 18:46:07
09-Oct-2026 19:12:44Sáu thời điểm do lệnh thứ hai in ra là mốc để truy nguyên nhân, và bước tiếp theo chỉ dùng những thời điểm này.
2️⃣ Vì sao tăng max_children không phải là lời giải
Nâng giới hạn 26 có thể làm giảm số cảnh báo, nhưng không thay đổi việc yêu cầu nào đã làm đầy worker.
Cách tính thông thường là lấy bộ nhớ khả dụng chia cho bộ nhớ trung bình của một worker. Trên máy chủ được đo, 26 worker dùng trung bình 131MB, tối đa 214MB, và nếu tính theo bộ nhớ khả dụng còn lại thì về mặt con số vẫn còn dư để tạo thêm worker.
ps -o rss= -u www -C php-fpm | awk '{s+=$1; n++; if($1>m) m=$1} END{print n, s/n/1024, m/1024, s/1024}'26 130.579 214.27 3395.05free -mBốn giá trị ở kết quả đầu tiên là số worker, bộ nhớ trung bình (MB), bộ nhớ tối đa (MB) và tổng (MB); cột available trong kết quả của free là bộ nhớ khả dụng.
Khi số worker đã nhiều hơn số lõi CPU, tăng worker không làm tăng số yêu cầu được xử lý đồng thời mà chỉ kéo dài thời gian cho mỗi yêu cầu. Khi khách truy cập nhận 0 lỗi 502·504, nâng giới hạn không đem lại gì, chỉ cấp thêm tiến trình cho bot và máy quét vốn đang chiếm worker.
Trước khi nâng giới hạn, cần kiểm tra xem các yêu cầu chiếm worker vào thời điểm cảnh báo được ghi có phải là khách truy cập hay không; trình tự kiểm tra điều đó bằng access.log được trình bày ở phần tiếp theo.
3️⃣ Truy nguyên nhân bằng access.log: thời điểm, IP, đường dẫn, mã phản hồi
Chọn một thời điểm có cảnh báo, câu trả lời nằm trong access.log của 1–2 phút đó. Trình tự là lọc các dòng theo thời điểm, đếm theo IP, rồi xem đường dẫn và mã phản hồi của các IP đứng đầu.
grep '09/Oct/2026:18:09' /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head 115 140.245.x.x
61 187.15.x.x
17 3.36.x.x
8 91.239.x.x
4 79.127.x.x
3 109.248.x.x
3 109.248.x.x
3 109.248.x.x
2 79.127.x.x
2 40.77.x.xgrep '09/Oct/2026:18:09' /var/log/nginx/access.log | awk '{print $7, $9}' | sort | uniq -c | sort -rn | head 55 / 200
13 /wp-login.php 404
13 /wp-admin/plugins.php 302
13 /wp-admin/index.php 302
13 /wp-admin/edit.php 302
12 /wp-admin/profile.php 302
4 / 499
3 /xmlrpc.php 302
2 /xmlrpc.php 499
2 /wp-content/themes/kinsta/images/duos/small/wheelchair.svg 404Lúc 18:09 ngày 9 tháng 10, có 286 yêu cầu được ghi trong một phút; một IP chiếm 115 yêu cầu trong số đó, đường dẫn là địa chỉ quản trị như /wp-admin/profile.php, và mã phản hồi đều là 302.
Mã phản hồi cho biết yêu cầu có tới PHP hay không. Mã 200 do nginx trả thẳng HTML đã cache, cùng 444·400, không đi qua PHP; còn 302·404 và 200 chưa cache thì mỗi cái dùng một worker. Khi yêu cầu địa chỉ quản trị mà chưa đăng nhập, WordPress trả 302 về trang đăng nhập, và một 302 đó là một lần chạy PHP.
Nếu một máy chủ chạy từ hai website trở lên, các dòng của cả hai website được ghi chung vào access.log toàn cục. Ban đầu có vẻ con bot này tấn công website thứ nhất, nhưng khi đối chiếu với log của từng website thì 22.390 trong số 22.391 yêu cầu là của website thứ hai.
4️⃣ Ba loại yêu cầu làm đầy worker: wp-admin của bot, 404 của máy quét, tải trước cache
Áp dụng cùng trình tự cho mọi thời điểm có cảnh báo, các yêu cầu làm đầy worker thuộc ba loại.
Loại thứ nhất là một bot liên tục yêu cầu địa chỉ quản trị. Một IP gửi 22.391 yêu cầu trong ba giờ; 9.944 yêu cầu trong số đó nhắm vào bốn đường dẫn, gồm cả /wp-admin/profile.php, và vì mỗi 302 khi chưa đăng nhập là một lần chạy PHP nên worker đã bị dùng 9.944 lần.
Loại thứ hai là một máy quét lỗ hổng yêu cầu các đường dẫn không tồn tại. Dù User-Agent ghi Googlebot, đó không phải Googlebot: IP gửi 14.672 yêu cầu mỗi ngày là một máy chủ ảo Google Cloud có DNS ngược kết thúc bằng googleusercontent.com, còn Googlebot kết thúc bằng googlebot.com. Cùng IP đó còn luân phiên dùng bingbot, Applebot, GPTBot và ClaudeBot.
Yêu cầu tệp như .env bị nginx chặn bằng 444 trong 0,04 giây, nhưng các đường dẫn thông thường không có trong quy tắc chặn như /userfiles hay /secrets.yml sẽ khiến WordPress tạo trang 404. Trang 404 này nặng 266KB, mất 1,4–2,9 giây, tốn như tạo một bài viết thường, và một IP đã yêu cầu nó 4.069 lần mỗi ngày.
Loại thứ ba là chính máy chủ. Khi lưu bài viết hoặc bản dịch, cache bị xóa và tính năng tải trước của WP Rocket yêu cầu lại mọi URL, 8.489 yêu cầu mỗi ngày, tối đa 60 yêu cầu mỗi phút. Việc kiểm tra trạng thái lập chỉ mục của Rank Math cũng dùng một worker 754 lần mỗi ngày thông qua Action Scheduler. Cả hai chỉ tập trung vào giờ làm việc và không gây lỗi cho khách truy cập, nên được loại khỏi danh sách cần xử lý.
Trong trình mô phỏng bên dưới, bạn có thể xem mỗi yêu cầu kết thúc ở lớp nào và có dùng worker PHP hay không.
| Yêu cầu | Dừng ở lớp | Phản hồi | Worker |
|---|---|---|---|
| wp-admin của bot | WordPress | 302 (đến trang đăng nhập) | Dùng 1 |
| Máy quét kiểu .env | Quy tắc chặn của nginx | 444 (0,04 giây) | Không dùng |
| Máy quét đường dẫn giả thông thường | WordPress | Trang 404 (266KB · 1,4–2,9 giây) | Dùng 1 |
| Tải trước cache | WordPress | 200 · lưu thành tệp cache | Dùng 1 |
| Truy cập bài đã cache | Tệp cache | 200 (nginx trả lời ngay) | Không dùng |
| Truy cập bài chưa cache | WordPress | 200 · lưu thành tệp cache | Dùng 1 |
5️⃣ Giới hạn tốc độ của nginx: giới hạn yêu cầu chưa đăng nhập và khôi phục IP thật
Giới hạn tốc độ được chọn thay cho việc chặn. Chặn đường dẫn thì máy quét đổi đường dẫn, chặn IP thì hôm sau một IP khác lại vào. Chỉ những yêu cầu chưa đăng nhập đi tới PHP mới bị giới hạn, ở mức 30 yêu cầu mỗi phút và 60 yêu cầu tức thời trên mỗi IP, phần vượt quá được trả về 429.
Với máy chủ đứng sau CDN, chỉ cấu hình này thì chưa đúng. IP mà nginx thấy là địa chỉ edge của CDN, nên mọi khách truy cập đi qua edge đó bị gộp thành một IP; cần khai báo danh sách IP edge bằng set_real_ip_from để khôi phục IP thật từ X-Forwarded-For trước. Yêu cầu có cookie đăng nhập được miễn, còn User-Agent thì không được dùng để miễn.
# ngữ cảnh http
real_ip_header X-Forwarded-For;
real_ip_recursive on;
set_real_ip_from 203.0.113.0/24; # danh sách edge CDN
map $http_cookie $php_anon_key {
default $binary_remote_addr;
~*wordpress_logged_in '';
}
limit_req_zone $php_anon_key zone=php_anon:10m rate=30r/m;
# location chuyển tới PHP
location @apache {
limit_req zone=php_anon burst=60 nodelay;
limit_req_status 429;
proxy_pass http://127.0.0.1:8080;
}Trang đã cache được try_files trả lời ngay trong nginx nên không tới location này, còn admin-ajax.php nằm ở location riêng nên không bị tính vào giới hạn.
Kiểm chứng bằng yêu cầu đồng thời. Yêu cầu tuần tự không vượt quá 30 yêu cầu mỗi phút vì mỗi 404 mất 1,5 giây, nên không bị giới hạn; gửi 100 yêu cầu, mỗi lần 20 yêu cầu đồng thời, sẽ được 67 lần 404 và 33 lần 429, xác nhận giới hạn đang hoạt động.
seq 1 100 | xargs -P 20 -I{} curl -s -o /dev/null -w '%{http_code}\n' https://example.com/zz-{} | sort | uniq -cNếu kết quả có đếm được 429 thì giới hạn đang hoạt động. Nếu đã miễn IP của chính máy chủ, hãy tạm bỏ miễn trừ khi chạy hoặc chạy từ một mạng khác.
6️⃣ So sánh trước và sau khi áp dụng
Giới hạn địa chỉ quản trị khi chưa đăng nhập được áp dụng vào tối 9 tháng 10, còn giới hạn yêu cầu tới PHP vào tối ngày 11. Số phản hồi 302 của địa chỉ quản trị giảm từ 9.996 vào ngày 9 xuống 11 vào ngày 10 và 1 vào ngày 11, còn IP từng gửi 22.391 yêu cầu trong ba giờ thì từ hôm sau không còn yêu cầu nào.
Với máy quét, giới hạn vừa mới áp dụng nên chưa có số liệu trọn một ngày; việc đánh giá sẽ dựa vào số trang 404 do WordPress tạo ra (ngày 9: 2.563, ngày 10: 2.612, ngày 11: 1.630) giảm xuống tới đâu trong đợt kiểm tra tuần sau. Lỗi 502·504 của khách truy cập đều là 0 cả trước và sau khi áp dụng.
Các con số này được đếm trực tiếp từ log của một máy chủ WordPress chạy Nginx, Apache và php-fpm, và việc đánh giá dựa trên lỗi của khách truy cập cùng số yêu cầu tới PHP chứ không dựa trên số cảnh báo.
🔢 FAQ & nội dung đề xuất
Cache
Cache website & cache phía server
Giải thích theo từng lớp cấu trúc giúp yêu cầu đã cache không phải đi qua PHP.
Bảo mật
Lỗ hổng bảo mật wp-config.php
Cách bảo vệ tệp cấu hình mà máy quét nhắm tới bằng quyền truy cập và quy tắc máy chủ.
Linux
Lệnh Linux: phân loại 15 nhóm
Bảng phân loại lệnh thành 15 nhóm, gồm cả grep và awk dùng để thống kê log.
ℹ️ Thông báo tiếp thị liên kết
Nội dung trên trang này có chứa liên kết affiliate (tiếp thị liên kết). Khi khách truy cập mua sản phẩm hoặc dịch vụ qua các liên kết này, trang sẽ nhận hoa hồng từ bên bán. Trong quá trình đó, số tiền người mua phải trả (sẽ giảm khi có chương trình khuyến mãi ↓) không hề tăng lên. Giá, khuyến mãi và tình trạng hàng được đăng theo thời điểm viết bài và có thể khác với thực tế, vì vậy hãy kiểm tra lại lần cuối với bên bán trước khi mua. Việc chọn và đánh giá sản phẩm dựa trên tiêu chí riêng của trang; việc có nhận hoa hồng hay không không ảnh hưởng đến thứ tự giới thiệu hay nội dung đánh giá.