Trong cùng một cụm Cloud VPS, thường có website cũ chỉ chạy được trên PHP 7.2, site Laravel mới cần PHP 8.x, và nhiều site nằm ở giữa với PHP 7.4. Nâng cấp tất cả lên một phiên bản PHP là không thực tế, nên tầng minion phải chạy được nhiều phiên bản song song mà không giẫm lên nhau. Tài liệu này hướng dẫn cài và tổ chức một minion: nguồn gói, socket PHP-FPM theo phiên bản, nginx local, thư mục site, quyền user, supervisor, opcache, các lỗi hay gặp và checklist kiểm tra.

1. Vì sao gói PHP mặc định của Debian không đủ

Mỗi bản Debian chỉ đóng gói đúng một phiên bản PHP: Debian 10 có PHP 7.3, Debian 11 có PHP 7.4. Muốn có PHP 7.2 hay 8.1 trên Debian 11, cần một nguồn gói khác. Lựa chọn phổ biến là repo của Ondřej Surý (deb.sury.org), người cũng duy trì gói PHP chính thức trong Debian. Repo này cung cấp nhiều phiên bản PHP cùng lúc, mỗi phiên bản là một bộ gói có tiền tố riêng (php7.2-*, php7.4-*, php8.1-*), cài cạnh nhau không xung đột.

apt-get install -y apt-transport-https lsb-release ca-certificates wget
wget -O /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg
echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" \
    > /etc/apt/sources.list.d/php.list
apt-get update

# cài một phiên bản PHP cùng các extension Laravel cần
V=7.4
apt-get install -y php$V-fpm php$V-cli php$V-mysql php$V-mbstring php$V-xml \
    php$V-curl php$V-zip php$V-gd php$V-bcmath php$V-intl

Sau khi cài, mỗi phiên bản PHP có bộ thư mục và dịch vụ riêng:

Thành phầnPHP 7.2PHP 7.4
Cấu hình FPM/etc/php/7.2/fpm//etc/php/7.4/fpm/
Cấu hình CLI/etc/php/7.2/cli//etc/php/7.4/cli/
Dịch vụ systemdphp7.2-fpmphp7.4-fpm
Socket mặc định/run/php/php7.2-fpm.sock/run/php/php7.4-fpm.sock
Log FPM/var/log/php7.2-fpm.log/var/log/php7.4-fpm.log
Lệnh CLI/usr/bin/php7.2/usr/bin/php7.4

Lệnh php trơn chỉ là một liên kết do update-alternatives quản lý, trỏ tới một trong các phiên bản trên. Chi tiết nhỏ này là nguồn gốc của khá nhiều lỗi, sẽ nói ở phần cuối.

2. Tách minion theo phiên bản PHP

Cài được nhiều phiên bản PHP trên một máy không có nghĩa là nên chạy production như vậy. Cách dễ vận hành hơn là chia minion thành nhóm, mỗi nhóm một phiên bản chính (ví dụ app-php72-*, app-php74-*, app-php82-*). Lý do:

  • Mỗi máy chỉ phải vá, theo dõi và tinh chỉnh một bộ PHP-FPM.
  • Tên máy nói luôn phiên bản, người deploy không phải đoán.
  • Nâng phiên bản PHP cho một site là chuyển site sang nhóm minion mới, rồi đổi một dòng upstream trên balancer chạy Nginx reverse proxy. Site khác trên máy cũ không bị ảnh hưởng.

Khả năng cài song song vẫn hữu ích: trước khi chuyển một site sang nhóm minion mới, có thể cài thêm phiên bản đích để chạy composer install và test của site đó ngay trên máy đang có code.

3. PHP-FPM: pool chung, chạy cùng user với nginx

Cấu hình đơn giản nhất là mỗi phiên bản PHP dùng một pool chung tên www cho mọi site, chạy bằng cùng user với nginx và là chủ sở hữu code (trên Debian thường là www-data):

; /etc/php/7.4/fpm/pool.d/www.conf (trích)
[www]
user = www-data
group = www-data
listen = /run/php/php7.4-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

pm = dynamic
pm.max_children = 40
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 500

pm.max_children là số request PHP xử lý đồng thời tối đa của pool. Cách ước lượng: lấy RAM còn lại sau khi trừ hệ điều hành và nginx, chia cho RAM trung bình một worker (Laravel thường vài chục MB mỗi worker). pm.max_requests cho mỗi worker tự khởi động lại sau một số request, hạn chế rò rỉ bộ nhớ của thư viện cũ. Các con số trong ví dụ chỉ là điểm khởi đầu, cần đo trên máy thật. Ý nghĩa từng tham số có trong tài liệu cấu hình PHP-FPM trên php.net.

Pool chung đơn giản nhưng có điểm yếu: một site chậm có thể giữ hết worker và kéo cả máy. Nếu chọn cách này, cần theo dõi sát cảnh báo max_children trong log FPM; khi số site tăng, nên chuyển sang mỗi site một pool riêng.

4. Nginx local và thư mục site

Mỗi minion chạy nginx riêng, chỉ lắng nghe trên IP private, nhận request từ balancer và chuyển cho đúng socket của phiên bản PHP:

server {
    listen 10.0.0.21:80;
    server_name example.com www.example.com;
    root /var/www/example.com/public;
    index index.php;
    client_max_body_size 64M;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass unix:/run/php/php7.4-fpm.sock;   # đúng phiên bản của minion
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param HTTPS on;
        fastcgi_read_timeout 180;
    }
}

Code mọi site nên nằm ở một đường dẫn thống nhất, ví dụ /var/www/<domain>/, chủ sở hữu là user chạy web. Laravel chỉ cần ghi vào storage/ và bootstrap/cache/:

cd /var/www/example.com
chown -R www-data:www-data .
find . -type d -exec chmod 755 {} \;
find . -type f -exec chmod 644 {} \;
chmod -R ug+rwX storage bootstrap/cache

Vì cùng một đường dẫn trên mọi minion, script deploy, backup hay đọc log chỉ cần tên domain, bất kể site đang chạy phiên bản PHP nào.

5. Supervisor và cron: luôn ghi rõ phiên bản PHP

Như trong bài tổng quan mô hình Cloud VPS, supervisor trên minion có thể đọc thêm file supervisord.ini trong thư mục từng site:

# /etc/supervisor/supervisord.conf
[include]
files = /etc/supervisor/conf.d/*.conf /var/www/*/supervisord.ini

Điểm cần nhớ khi máy có hơn một phiên bản PHP: lệnh trong supervisor và cron phải gọi đường dẫn đầy đủ của phiên bản, không gọi php trơn.

# /var/www/example.com/supervisord.ini
[program:example-com-queue]
command=/usr/bin/php7.4 /var/www/example.com/artisan queue:work --sleep=2 --tries=2 --timeout=300
user=www-data
autostart=true
autorestart=true
stdout_logfile=/var/www/example.com/storage/logs/queue.log

# /etc/cron.d/example-com: Laravel scheduler
* * * * * www-data /usr/bin/php7.4 /var/www/example.com/artisan schedule:run >> /dev/null 2>&1

Nếu ai đó cài thêm một phiên bản PHP mới, update-alternatives có thể chuyển php sang phiên bản đó. Web vẫn chạy bình thường vì nginx gọi đúng socket, nhưng queue worker và scheduler lặng lẽ chạy bằng phiên bản khác.

6. php.ini của CLI và FPM là hai file khác nhau

Mỗi phiên bản PHP có hai file php.ini: /etc/php/7.4/fpm/php.ini cho web và /etc/php/7.4/cli/php.ini cho dòng lệnh. Bản CLI của Debian để memory_limit = -1 và không giới hạn thời gian chạy, hợp với lệnh artisan chạy lâu. Bản FPM thì có giới hạn. Hệ quả:

  • Sửa upload_max_filesize trong bản CLI thì web không thay đổi gì.
  • Một đoạn code chạy được bằng php artisan vẫn có thể lỗi Allowed memory size exhausted khi gọi qua web.
  • php --ini chỉ cho biết file của CLI. Muốn biết FPM đang đọc gì, xem phpinfo() hoặc đọc thẳng thư mục fpm/.

Khi đã cài gói php7.4-opcache, opcache được bật cho FPM. Với mặc định opcache.validate_timestamps=1, PHP định kỳ kiểm tra file đã đổi chưa, nên code mới có hiệu lực sau vài giây. Nếu tắt kiểm tra này để tăng tốc, mỗi lần deploy bắt buộc phải systemctl reload php7.4-fpm, nếu không site tiếp tục chạy code cũ trong bộ nhớ.

7. Swap nhỏ cho minion RAM thấp

Minion cấu hình nhỏ dễ gặp đỉnh RAM ngắn hạn, ví dụ khi composer install chạy cùng lúc với traffic. Một swapfile nhỏ với swappiness thấp giúp máy không bị OOM killer xử lý ngay ở những đỉnh này, trong khi bình thường kernel vẫn ưu tiên RAM:

fallocate -l 1G /swapfile && chmod 600 /swapfile
mkswap /swapfile && swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
sysctl -w vm.swappiness=10

Swap không thay được RAM. Nếu swap luôn đầy, đó là tín hiệu cần giảm pm.max_children hoặc tách bớt site.

8. Lỗi thường gặp trên minion nhiều phiên bản PHP

Triệu chứngDấu hiệu trong logCách xử lý
502, nginx minion báo không thấy socketconnect() to unix:/run/php/php7.2-fpm.sock failed (2: No such file or directory)Site trỏ socket của phiên bản không có trên máy, hoặc FPM phiên bản đó chưa chạy
502, socket có nhưng bị từ chối(13: Permission denied) while connecting to upstreamlisten.owner/listen.group của pool không khớp user nginx
502 trên mọi site của máyConnection refused hoặc socket biến mấtPHP-FPM chết: systemctl status php7.4-fpm, xem log FPM và dmesg tìm OOM
500, Laravel không ghi được logThe stream or file ... could not be opened: Permission deniedstorage/ không thuộc user chạy web, thường do chạy lệnh bằng root
Trang trắng sau composer installComposer detected issues in your platformChạy composer update bằng CLI phiên bản mới hơn FPM nên composer chọn thư viện đòi PHP mới. Gọi đúng /usr/bin/php7.4 /usr/local/bin/composer
Chậm hoặc 502 khi đông ngườiserver reached pm.max_children settingPool chung cạn worker. Tìm request chậm trước khi nghĩ tới tăng max_children

Lỗi "Permission denied" ở storage/ phổ biến đến mức nên thành quy tắc: không chạy artisan bằng root trên minion. Lệnh nào cần chạy tay thì dùng sudo -u www-data, để file cache, file log mới tạo ra vẫn thuộc đúng user.

9. Checklist cho minion nhiều phiên bản PHP

  • Cài PHP từ một nguồn gói thống nhất, đủ extension Laravel cần cho từng phiên bản.
  • Mỗi nhóm minion một phiên bản PHP chính, tên máy ghi rõ phiên bản.
  • fastcgi_pass trỏ đúng socket; quyền socket khớp user nginx.
  • Supervisor, cron và composer gọi đường dẫn PHP đầy đủ, không dùng php trơn.
  • Sửa php.ini ở đúng thư mục fpm/ hoặc cli/, reload FPM sau khi sửa.
  • Không chạy artisan bằng root; storage/ và bootstrap/cache/ ghi được.
  • Có swap nhỏ, theo dõi cảnh báo max_children trong log FPM.

Chạy nhiều phiên bản PHP song song cho phép site cũ tiếp tục sống trong khi site mới dùng phiên bản hiện đại, và việc nâng cấp từng site trở thành thao tác chuyển máy có thể hoàn tác. Cái giá là số nhóm minion tăng theo số phiên bản PHP, và các phiên bản cũ dần hết hỗ trợ bảo mật. Chuyện này sẽ được bàn kỹ trong bài về PHP 7.4 hết hỗ trợ, ngay sau bài về tầng database.

Bài tiếp theo

Tầng Database: MySQL đa tenant trên VPS cấu hình nhỏ. Buffer pool theo RAM, giới hạn kết nối, đánh đổi khi tắt binlog, phân quyền theo database và cách kiểm tra nhanh một máy MySQL đang chậm.