Chuyển các site trên minion từ pool PHP-FPM www dùng chung sang mỗi site một pool riêng là bước nên làm để chấm dứt lỗi max_children. Lý do và cách làm có trong bài Mỗi website một PHP-FPM pool: chấm dứt lỗi max_children. Tài liệu này nói về phần dễ bị bỏ qua: sau khi migrate PHP-FPM, thường lộ ra ba lỗi về đường dẫn log, giới hạn upload và múi giờ. Điểm chung của cả ba: php-fpm -t, nginx -t và cả health check HTTP đều báo xanh. Không lỗi nào làm site sập, nhưng cả ba đều làm site chạy sai một cách im lặng, và thường lộ ra lần lượt trong vài ngày đầu.

Bối cảnh: migrate PHP-FPM bằng script tự động

Việc tách pool thường được tự động hoá: với mỗi site trên minion, script sinh một file pool mới từ một danh sách directive, đổi fastcgi_pass của nginx sang socket riêng, kiểm tra cú pháp, reload PHP-FPM rồi gọi thử trang chủ. Cách làm này nhanh và lặp lại được, nhưng chất lượng của pool mới phụ thuộc hoàn toàn vào danh sách directive mà người viết script nghĩ tới.

Triệu chứng sau migrate PHP-FPM

  1. Log lỗi PHP nằm sai chỗ. Pool mới ghi lỗi PHP vào chính file error log mà nginx dùng cho site. Hai nguồn log trộn trong một file, và việc PHP có ghi được hay không phụ thuộc quyền của thư mục /var/log/nginx/, vốn thuộc root:adm.
  2. Upload thất bại im lặng. File lớn hơn 2 MB không lên được, dù nginx trên balancer và minion đều cho phép body lớn hơn nhiều. Không có 413, không có 5xx.
  3. Giờ lệch và script bị cắt sớm. Site chạy theo giờ UTC, lệch 7 tiếng so với giờ Việt Nam. Script nặng vốn chạy được trong 60 giây nay có thể báo hết giờ ở mốc 30 giây.

Điều tra lỗi sau migrate PHP-FPM

Vì sao health check không bắt được lỗi sau migrate PHP-FPM

Health check điển hình gọi trang chủ từng site qua IP minion, kèm header Host, và coi mọi mã 5xx hoặc timeout là lỗi. Nó bắt tốt các lỗi về user chạy pool hay chủ socket, vì khi đó trang chủ trả 502. Ba lỗi sau migrate PHP-FPM thì khác: trang chủ vẫn trả 200. Log ghi sai chỗ không đổi mã trả về. Giới hạn upload chỉ có tác dụng khi có người upload. Múi giờ chỉ lộ ra khi có người đọc giờ trên trang. Health check chỉ trả lời "site có sống không", không trả lời "site có chạy như trước không".

So sánh giá trị, không so sánh file

Câu hỏi đúng sau một lần migrate PHP-FPM là: pool mới khác pool cũ ở directive nào? Lệnh dưới đây liệt kê mọi dòng php_value, php_admin_value của hai file cạnh nhau:

POOLD=/etc/php/7.4/fpm/pool.d
grep -hE '^\s*php_(admin_)?(value|flag)' $POOLD/www.conf | sort > /tmp/www.txt
grep -hE '^\s*php_(admin_)?(value|flag)' $POOLD/site-a-example.conf | sort > /tmp/site-a.txt
diff /tmp/www.txt /tmp/site-a.txt

Kết quả hay gặp: www.conf có php_admin_value[date.timezone] = Asia/Ho_Chi_Minh và php_admin_value[max_execution_time] = 60, còn pool mới không có. Pool mới cũng không khai upload_max_filesize và post_max_size.

Mỗi pool là một thế giới riêng

Theo tài liệu cấu hình PHP-FPM, mỗi khối [pool] độc lập: directive nào pool không khai thì lấy theo php.ini, không lấy theo pool khác. Khi migrate PHP-FPM, tức tách site ra khỏi www.conf, mọi giá trị từng được đặt riêng trong www.conf rơi về mặc định của php.ini:

DirectiveTrong www.confPool mới thiếu khai báoHệ quả
date.timezoneAsia/Ho_Chi_MinhPHP dùng UTCLệch 7 tiếng
max_execution_time60Mặc định 30Script nặng bị dừng sớm hơn
upload_max_filesize / post_max_sizeGiá trị riêng của siteMặc định 2M / 8MUpload lớn hơn 2 MB thất bại

Có hai chi tiết làm mức ảnh hưởng khó đoán. Thứ nhất, site Laravel tự đặt múi giờ theo config('app.timezone') khi khởi động, nên lệch giờ chủ yếu ảnh hưởng site PHP thuần và những đoạn code dựa vào múi giờ mặc định của PHP. Thứ hai, theo tài liệu PHP về max_execution_time, trên Linux thời gian này không tính thời gian chờ truy vấn database, gọi mạng hay gọi hệ thống. Script chậm vì chờ I/O có thể không chạm giới hạn 30 giây, còn script nặng về CPU thì có.

Đường dẫn log: lỗi của chính bản thiết kế

Lỗi đường dẫn log không đến từ php.ini, mà từ lựa chọn ghi lỗi PHP vào /var/log/nginx/<domain>.error. Tiến trình PHP-FPM chạy bằng user của pool, trong khi thư mục log của nginx thuộc root:adm. User đó có ghi được hay không tuỳ quyền của từng file, và khi file bị xoay vòng thì quyền có thể đổi. Thêm vào đó, đọc một file trộn lẫn log của nginx và PHP làm việc điều tra chậm đi đúng lúc cần nhanh.

Nguyên nhân gốc

  • Pool mới được sinh từ danh sách cứng, không kế thừa tường minh từ www.conf. Mọi giá trị riêng của www.conf không có trong danh sách đều bị đánh rơi khi migrate PHP-FPM.
  • Health check chỉ kiểm tra "sống hay chết" ở trang chủ. Các thay đổi hành vi (upload, giờ, log) không đổi mã trả về nên lọt qua mọi bước kiểm tra.
  • Không có bước so sánh cấu hình hiệu lực giữa trước và sau khi tách, nên lỗi chỉ lộ ra khi người dùng gặp phải.

Khắc phục

Pool mẫu đầy đủ khi migrate PHP-FPM

Pool mới nên được sinh với đủ các dòng sau. Lưu ý post_max_size phải lớn hơn hoặc bằng upload_max_filesize, theo tài liệu php.ini. Đặt ngược thì file nằm giữa hai giá trị vẫn không lên được:

request_terminate_timeout = 60s
php_admin_value[memory_limit]        = 256M
php_admin_value[upload_max_filesize] = 64M
php_admin_value[post_max_size]       = 72M
php_admin_value[error_log]           = /var/log/php/site-a.example.error
php_admin_value[max_execution_time]  = 60
php_admin_value[date.timezone]       = Asia/Ho_Chi_Minh

Vá ngược cho pool đã migrate PHP-FPM trước đó

Với pool đã tách trước khi có bản vá, chỉ thêm dòng nào còn thiếu, không ghi đè giá trị mà một site đã tự chỉnh riêng:

POOL=/etc/php/7.4/fpm/pool.d/site-a-example.conf
grep -qE '^[;[:space:]]*php_admin_value\[date\.timezone\]' "$POOL" \
  || echo 'php_admin_value[date.timezone] = Asia/Ho_Chi_Minh' >> "$POOL"
grep -qE '^[;[:space:]]*php_admin_value\[max_execution_time\]' "$POOL" \
  || echo 'php_admin_value[max_execution_time] = 60' >> "$POOL"

# thư mục log riêng, thuộc đúng user chạy PHP-FPM
mkdir -p /var/log/php && chown www-data:www-data /var/log/php && chmod 755 /var/log/php

# pool nào vẫn thiếu giới hạn upload?
grep -L 'upload_max_filesize' /etc/php/*/fpm/pool.d/*.conf | grep -v www.conf

php-fpm7.4 -t && systemctl reload php7.4-fpm

Đồng thời sửa luôn quy trình tạo site mới theo mô hình pool riêng, để site tạo sau không lặp lại ba lỗi này.

Cải tiến quy trình migrate PHP-FPM

  • Kế thừa thay vì liệt kê. Khi migrate PHP-FPM, chép mọi dòng php_value, php_admin_value của www.conf sang pool mới trước, rồi mới ghi đè những giá trị cần đổi. Danh sách cứng sẽ luôn thiếu thứ mà người viết script không biết là có.
  • Thống nhất chuỗi timeout. Thường có ba lớp: max_execution_time và request_terminate_timeout ở PHP-FPM, fastcgi_read_timeout ở nginx của minion, và timeout của balancer. Lớp trong cùng quyết định, các lớp ngoài chỉ nên dài hơn một chút.
  • Health check theo chức năng. Ngoài trang chủ, gọi thêm một endpoint kiểm tra trả về date_default_timezone_get() và các giới hạn ini_get() chính của pool, so với giá trị mong đợi.
<?php // health-ini.php, chỉ cho phép gọi từ mạng nội bộ
header('Content-Type: application/json');
echo json_encode([
  'tz'     => date_default_timezone_get(),
  'upload' => ini_get('upload_max_filesize'),
  'post'   => ini_get('post_max_size'),
  'met'    => ini_get('max_execution_time'),
]);

Checklist cho lần migrate PHP-FPM sau

  • Trước khi migrate PHP-FPM: liệt kê mọi directive đang khác mặc định trong www.conf và php.ini của minion.
  • Sau khi tách: chạy lại phép so sánh cho từng pool, kết quả phải rỗng hoặc chỉ gồm những khác biệt có chủ đích.
  • Thử một lần upload file lớn và một lần in giờ hiện tại trên một site mẫu của mỗi minion.
  • Đặt log của PHP và log của nginx ở hai thư mục riêng, thuộc đúng user ghi chúng.
  • Migrate PHP-FPM theo từng minion, ghi ngày tách vào sổ thay đổi, để khi có báo lỗi biết ngay site nào vừa đổi cấu hình.
  • Sau khi tách, theo dõi log reached pm.max_children để xác nhận mục tiêu chính của đợt chuyển đổi đã đạt.

Bài học

  • Đổi cấu trúc cấu hình là đổi giá trị mặc định. Migrate PHP-FPM là ví dụ điển hình: tách một khối cấu hình ra không chỉ đổi chỗ nó nằm, mà còn đổi nơi nó lấy giá trị khi thiếu.
  • "Xanh" chỉ nghĩa là đã kiểm tra những gì mình nghĩ tới. Health check HTTP bắt được lỗi sống chết, không bắt được lỗi hành vi.
  • Vá thì phải vá ngược. Bản vá trong script chỉ có ích cho pool tạo sau đó. Bước bổ sung cho pool cũ mới thực sự đóng lỗi trên những site đã migrate PHP-FPM.
  • Lỗi lộ ra lần lượt là dấu hiệu của một lỗ hổng quy trình, không phải nhiều sự cố riêng lẻ. Ở đây, lỗ hổng đó là thiếu bước so sánh trước và sau.

Bài tiếp theo

Right-sizing Kubernetes: đặt request CPU, RAM theo mức dùng thật. Chuyển sang cụm Kubernetes của Website AI: khi tổng request lớn gấp nhiều lần mức dùng thật, và mỗi báo cáo cho ra một bộ số khác nhau.