
Vì sao website "gục ngã" giờ cao điểm — và cách tính config PHP-FPM, Nginx, OPcache cho đúng
Table of Contents21 sections
Đa số sự cố 5xx lúc traffic dồn về không phải vì "server yếu", mà vì các tham số mặc định chưa bao giờ được tính lại theo đúng hệ thống. Bài viết này chỉ ra chính xác con số nào quyết định, công thức tính ra sao, và cách dùng OPcache để tăng tốc gần như miễn phí.
19h tối. Chiến dịch flash sale vừa mở. Traffic tăng gấp 5 lần và dashboard bắt đầu nhuộm đỏ: 502, 503, 504 xen kẽ nhau. Reload thì lúc được lúc không. Điều khó chịu nhất: CPU mới chỉ 40%, RAM còn trống, log MySQL sạch sẽ — mà người dùng vẫn nhận lỗi.
Nghịch lý này gặp ở rất nhiều hệ thống PHP, và gần như luôn quy về một nguyên nhân: tài nguyên còn dư, nhưng số "khe" xử lý đồng thời thì đã cạn. Server không nghẽn vì hết sức — nó nghẽn vì bị giới hạn bởi một vài con số trong file cấu hình mà từ lúc cài đặt đến giờ chưa ai động vào.
Để chỉnh đúng những con số đó, trước hết phải nhìn rõ một request thực sự đi qua đâu.
01. Một request đi qua đâu trước khi trả lỗi?
Trong stack PHP điển hình, Nginx không tự chạy code PHP. Nó nhận request, rồi chuyển phần PHP qua giao thức FastCGI cho một tiến trình riêng là PHP-FPM (FastCGI Process Manager). PHP-FPM duy trì một "pool" gồm nhiều tiến trình con (worker), mỗi worker chỉ xử lý đúng một request tại một thời điểm. Hiểu được điều này là đã giải quyết được một nửa vấn đề.

Điểm mấu chốt: nếu pool có pm.max_children = 10 thì hệ thống chỉ chạy được tối đa 10 request PHP cùng lúc, bất kể bạn còn 8GB RAM hay 16 nhân CPU. Request thứ 11 phải xếp hàng; xếp hàng quá lâu sẽ thành 504; hàng đợi tràn sẽ thành 502. Server "rảnh" mà vẫn lỗi là vì vậy.
02. Đọc vị lỗi 4xx/5xx: mỗi mã "tố cáo" một điều
Thay vì hoảng loạn khi thấy lỗi, hãy đọc chúng như chẩn đoán. Mỗi mã chỉ thẳng tới một nút trong sơ đồ trên.
Mã lỗi | Nghĩa thực tế khi quá tải | Nghi phạm hàng đầu |
|---|---|---|
502 | Nginx kết nối được PHP-FPM nhưng không nhận được phản hồi hợp lệ. | Pool cạn worker + hàng đợi tràn, hoặc PHP-FPM crash / fatal error. |
503 | Dịch vụ tạm thời không phục vụ được. | Quá tải, hoặc bị chặn bởi |
504 | PHP-FPM nhận request nhưng trả lời quá chậm. | Query chậm, vòng lặp nặng, gọi API ngoài treo — vượt |
429 | Quá nhiều request trong thời gian ngắn. | Rate limit chủ động (đôi khi là tốt — nó đang bảo vệ pool). |
499 | Client đóng kết nối trước khi server trả lời (mã riêng của Nginx). | Người dùng bỏ cuộc vì chờ lâu — hệ quả gián tiếp của 504. |
💡 Dấu hiệu vàng trong log. Khi pool cạn, PHP-FPM ghi rõ trong log của nó. Tìm đúng dòng này trước khi đoán bất cứ điều gì:
[pool www] server reached pm.max_children setting (10), consider raising itThấy dòng này nghĩa là vấn đề không nằm ở MySQL hay code — mà ở số khe xử lý. Chuyển thẳng sang phần 03.
03. PHP-FPM: nút thắt cổ chai thật sự
Đây là nơi 80% các vụ 5xx giờ cao điểm được sinh ra và cũng là nơi được sửa. Trước hết là cách pool tự quản lý số tiến trình.
Chọn chế độ pm: static, dynamic hay ondemand
Chế độ | Hành vi | Hợp với |
|---|---|---|
| Tạo sẵn đúng | Server chuyên dụng chạy PHP, traffic ổn định cao. Độ trễ thấp nhất. |
| Co giãn số tiến trình trong khoảng spare đã đặt. | Lựa chọn mặc định an toàn cho phần lớn web app. |
| Không tạo tiến trình nào tới khi có request; tắt khi rảnh. | Server ít RAM, nhiều site, traffic thấp/thưa. |
pm.max_children — tham số quan trọng nhất của toàn bộ stack
Số request PHP tối đa được xử lý đồng thời. Đặt thấp → 502/504 dù tài nguyên còn dư. Đặt cao → các tiến trình ăn hết RAM, kernel kích hoạt OOM-killer và giết tiến trình ngẫu nhiên (thường là MySQL). Phải tính, không đoán.
Công thức tính pm.max_children
Ý tưởng đơn giản: lấy phần RAM thực sự dành cho PHP, chia cho lượng RAM trung bình một tiến trình PHP tiêu thụ.
# Bước 1 — RAM còn lại cho PHP sau khi chừa cho OS và DB
RAM_cho_PHP = RAM_tổng − RAM_cho_OS − RAM_cho_MySQL/Redis
# Bước 2 — chia cho mức tiêu thụ trung bình mỗi tiến trình
pm.max_children = RAM_cho_PHP / RAM_trung_bình_mỗi_processĐừng đoán "RAM trung bình mỗi process". Đo trực tiếp trên chính server đang chạy:
# Trả về: số tiến trình hiện có và RAM trung bình (MB) mỗi tiến trình
ps --no-headers -o "rss,cmd" -C php-fpm8.2 \
| awk '{ sum+=$1 } END { printf("%d process, trung binh %.0f MB\n", NR, sum/NR/1024) }'Cách RAM được "chia khẩu phần" trên một server 4GB:

⚠️ Bẫy thường gặp. Đừng tính
max_childrentrên RAM tổng. MySQL, Redis, cron, hệ điều hành đều cần phần của mình. Nếu PHP "ăn" lấn sang, OOM-killer sẽ ra tay và thường giết đúng MySQL — biến một sự cố nhỏ thành sập toàn hệ thống.
Các tham số đi kèm (chế độ dynamic)
Bốn tham số còn lại điều khiển cách pool co giãn. Ràng buộc bắt buộc: min_spare ≤ start_servers ≤ max_spare ≤ max_children.
; /etc/php/8.2/fpm/pool.d/www.conf
pm = dynamic
pm.max_children = 40 ; trần tuyệt đối — tính từ RAM ở trên
pm.start_servers = 10 ; số tiến trình lúc khởi động
pm.min_spare_servers = 8 ; luôn giữ tối thiểu bấy nhiêu tiến trình rảnh
pm.max_spare_servers = 20 ; dọn bớt khi rảnh vượt mức này
pm.max_requests = 500 ; restart worker sau 500 request → chống rò rỉ bộ nhớ💡 Vì sao cần
max_requests. Một số extension hoặc code rò rỉ bộ nhớ chậm theo thời gian.pm.max_requestscho worker "tự làm mới" định kỳ, giải phóng RAM mà không cần can thiệp tay. Đặt 300–1000 là hợp lý cho phần lớn app.
04. Nginx: người gác cổng ít được để ý
Nginx hiếm khi là thủ phạm chính, nhưng cấu hình mặc định có vài chỗ âm thầm bóp băng thông và tạo ra 504 không đáng có.
Số kết nối tối đa cùng lúc
max_connections = worker_processes × worker_connections
# Nginx làm reverse proxy: mỗi client dùng 2 kết nối (client ↔ nginx ↔ upstream)
# → số client thực tế ≈ max_connections / 2worker_processes auto; — Đặt auto để Nginx tạo đúng một worker cho mỗi nhân CPU, không hơn. Nhiều worker hơn số nhân chỉ tạo thêm chuyển ngữ cảnh, không tăng thông lượng.
Hai tham số ít người chỉnh nhưng đáng giá
keepalive tới upstream (PHP-FPM). Mặc định, Nginx mở rồi đóng kết nối tới PHP-FPM cho mỗi request. Bật keepalive trong khối upstream để tái sử dụng kết nối — giảm độ trễ rõ rệt khi traffic cao. Đây là một trong những thay đổi "một dòng" cho hiệu quả lớn nhất.
# /etc/nginx/nginx.conf
upstream php_fpm {
server unix:/run/php/php8.2-fpm.sock;
keepalive 32; # số kết nối rảnh giữ lại để tái dùng
}
location ~ \.php$ {
fastcgi_pass php_fpm;
fastcgi_keep_conn on; # bắt buộc để keepalive ở trên có hiệu lực
include fastcgi_params;
}worker_rlimit_nofile & somaxconn. Mỗi kết nối tốn một file descriptor. Nếu giới hạn FD thấp hơn số kết nối, Nginx báo too many open files và rớt request. Đặt worker_rlimit_nofile lớn hơn worker_connections. Đồng thời nâng net.core.somaxconn của kernel để hàng đợi kết nối đủ sâu, tránh rớt ngay ở tầng TCP lúc cao điểm.
Timeout & buffer — bộ đôi trị 504
504 xảy ra khi PHP trả lời chậm hơn fastcgi_read_timeout. Nâng timeout chỉ là vá triệu chứng — gốc rễ thường là query chậm. Nhưng đặt một giá trị tỉnh táo vẫn cần thiết, kèm buffer đủ lớn để Nginx không phải ghi tạm ra đĩa với response lớn.
# khối http hoặc server
fastcgi_connect_timeout 5s;
fastcgi_send_timeout 60s;
fastcgi_read_timeout 60s; # 504 bung ra khi vượt mốc này
fastcgi_buffer_size 16k;
fastcgi_buffers 16 16k; # tổng buffer cho mỗi response🚫 Đừng làm thế này. Đặt
fastcgi_read_timeout 600sđể "hết 504" là cái bẫy chết người. Nó không sửa gì cả — chỉ giữ worker bận lâu gấp 10 lần, khiến pool cạn nhanh hơn và 502 thay thế 504. Hãy đi tìm query chậm.
05. OPcache: tăng tốc gần như miễn phí
Mặc định, mỗi request PHP đều phải đọc file .php, phân tích (lexing), dựng cây cú pháp (parse) rồi biên dịch (compile) thành opcode — trước khi chạy được dòng lệnh đầu tiên. OPcache lưu phần opcode đã biên dịch vào bộ nhớ chia sẻ, nên từ lần thứ hai trở đi, mọi bước nặng nề đó bị bỏ qua hoàn toàn.

Cấu hình OPcache cho production
; /etc/php/8.2/fpm/conf.d/10-opcache.ini
opcache.enable = 1
opcache.memory_consumption = 192 ; MB bộ nhớ chia sẻ cho opcode
opcache.interned_strings_buffer = 16 ; MB cho chuỗi dùng lại
opcache.max_accelerated_files = 20000 ; phải ≥ tổng số file .php
opcache.validate_timestamps = 0 ; PROD: không kiểm tra mtime mỗi request
opcache.revalidate_freq = 0
opcache.save_comments = 1 ; giữ lại nếu dùng annotation (Doctrine…)Tính max_accelerated_files cho đúng
Tham số này phải lớn hơn tổng số file PHP của ứng dụng (kể cả vendor). Nếu nhỏ hơn, OPcache không cache hết và một phần code vẫn bị biên dịch lại mỗi request. Đếm số file thật:
find /var/www/app -type f -name "*.php" | wc -l
# Ví dụ trả về 11542 → đặt max_accelerated_files = 20000 cho thoải mái
# Lưu ý: PHP tự làm tròn lên số nguyên tố gần nhất (vd 16229, 21701…)🚫 Đánh đổi của
validate_timestamps = 0. Đặt= 0cho hiệu năng tốt nhất, nhưng OPcache sẽ không nhận ra code mới sau khi deploy. Bắt buộc nạp lại cache mỗi lần deploy, nếu không người dùng vẫn chạy code cũ:>
sudo systemctl reload php8.2-fpm(hoặc gọiopcache_reset())> Trên môi trường dev thì ngược lại: đặt
validate_timestamps = 1để sửa file là thấy ngay.
JIT — không phải "viên đạn bạc"
PHP 8 thêm JIT. Nó tăng tốc rõ rệt với code nặng CPU (xử lý ảnh, tính toán, mã hoá), nhưng với web app điển hình — vốn nghẽn ở I/O và database — lợi ích thường nhỏ. Bật có chọn lọc và đo lường, đừng bật theo phong trào.
opcache.jit = tracing
opcache.jit_buffer_size = 64MKiểm tra OPcache có thực sự hiệu quả không
Đừng tin "cấu hình xong là ổn". Hãy đo hit rate — tỷ lệ request lấy được opcode từ cache. Mục tiêu > 99%.
<?php
$s = opcache_get_status();
// hit_rate nên > 99%. Nếu thấp hoặc num_cached_keys chạm trần
// max_accelerated_files → cần tăng giới hạn / memory_consumption.
printf("Hit rate: %.2f%%\n", $s['opcache_statistics']['opcache_hit_rate']);
printf("Bo nho: %d / %d MB\n",
$s['memory_usage']['used_memory'] / 1048576,
($s['memory_usage']['used_memory'] + $s['memory_usage']['free_memory']) / 1048576);💡 Bonus ít người biết: realpath cache. Với framework nhiều file (Laravel, Symfony), PHP liên tục phân giải đường dẫn file. Nâng
realpath_cache_size = 4096kvàrealpath_cache_ttl = 600giúp giảm số lần truy cập đĩa — một cải thiện nhỏ nhưng cộng dồn đáng kể ở traffic cao.
06. Ráp lại: một ví dụ end-to-end
Tổng hợp tất cả vào một tình huống cụ thể để thấy các con số ăn khớp với nhau.
Đầu bài: Server 2 vCPU · 4GB RAM, Ubuntu, chạy đồng thời Nginx + PHP-FPM 8.2 + MySQL trên một máy. Ứng dụng Laravel, đo được mỗi tiến trình PHP ≈ 55MB, có ~11.500 file PHP.
Bước tính
# PHP-FPM
RAM_cho_PHP = 4096 − 512 (OS) − 1024 (MySQL) = 2560 MB
pm.max_children = 2560 / 55 ≈ 46 → đặt an toàn 40
# Nginx (2 vCPU)
max_connections = 2 × 2048 = 4096 → ~2048 client thực tế
# OPcache
số file = 11500 → max_accelerated_files = 20000Cấu hình cuối
# ── PHP-FPM ─────────────────────────────
pm = dynamic
pm.max_children = 40
pm.start_servers = 10
pm.min_spare_servers = 8
pm.max_spare_servers = 20
pm.max_requests = 500
# ── Nginx ───────────────────────────────
worker_processes auto;
worker_rlimit_nofile 8192;
events { worker_connections 2048; }
# upstream php_fpm { server ...; keepalive 32; }
fastcgi_read_timeout 60s;
# ── OPcache ─────────────────────────────
opcache.enable = 1
opcache.memory_consumption = 192
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 0
★ Checklist khi website 5xx giờ cao điểm
In ra dán cạnh màn hình. Theo thứ tự, từ chẩn đoán đến chỉnh sửa.
[ ] Mở log PHP-FPM, tìm dòng
server reached pm.max_children setting— bằng chứng số một.[ ] Phân loại lỗi: 504 → đi tìm query/đoạn code chậm; 502 → pool cạn hoặc PHP crash; 429/503 → rate limit hoặc quá tải.
[ ] Đo RAM thực tế mỗi tiến trình PHP, rồi tính lại
pm.max_childrentheo công thức — không lấy con số mặc định.[ ] Kiểm tra
pm.max_childrenđã chừa đủ RAM cho MySQL/Redis/OS, tránh OOM-killer.[ ] Bật
keepalivetới upstream PHP-FPM trong Nginx — thay đổi một dòng, lợi ích lớn.[ ] Đảm bảo
worker_rlimit_nofilevànet.core.somaxconnđủ cao cho lượng kết nối cao điểm.[ ] Bật OPcache, đặt
max_accelerated_files> tổng số file PHP thực tế.[ ] Production để
validate_timestamps = 0và nhớreload php-fpmmỗi lần deploy.[ ] Đo
opcache_hit_rate— phải > 99%; nếu thấp thì tăng bộ nhớ OPcache.[ ] Thêm
pm.max_requestsđể worker tự làm mới, chống rò rỉ bộ nhớ chậm.
→ Tóm lại
Phần lớn sự cố 5xx giờ cao điểm không cần nâng cấp phần cứng — chúng cần một phép tính. PHP-FPM quyết định bao nhiêu request chạy song song, và con số đó phải được suy ra từ RAM thực tế, không phải để mặc định. Nginx cần keepalive cùng giới hạn kết nối hợp lý để không tự bóp nghẽn mình. OPcache là khoản tăng tốc gần như miễn phí, miễn là bạn đặt đúng giới hạn và nhớ nạp lại cache khi deploy.
Khi ba mảnh ghép này được tính toán ăn khớp, dashboard giờ cao điểm sẽ chuyển từ đỏ rực sang một màu xanh yên ả — và bạn biết chính xác vì sao nó xanh.
Các con số trong bài là điểm khởi đầu để tính toán, không phải giá trị áp dụng máy móc. Hãy luôn đo trên chính hệ thống của bạn (RAM mỗi process, số file PHP, đặc tính traffic) rồi điều chỉnh dần và quan sát log.