Với PHP-FPM, mỗi request tới một site Laravel đều bắt đầu từ con số không: nạp autoload, đọc config, đăng ký hàng chục service provider, rồi mới tới code xử lý thật. Request xong, mọi thứ bị vứt bỏ. Với site có lượng truy cập lớn, phần khởi động lặp lại này chiếm một tỉ lệ đáng kể thời gian phản hồi. Laravel Octane đổi cách chơi: ứng dụng khởi động một lần, nằm sẵn trong bộ nhớ và phục vụ nhiều request liên tiếp. Tài liệu này giải thích Laravel Octane hoạt động thế nào, vì sao chọn FrankenPHP, cách đặt nó sau nginx trên Cloud VPS, những thay đổi bắt buộc trong cách viết code và quy trình deploy.
1. Laravel Octane là gì, khác PHP-FPM ở đâu
PHP-FPM theo mô hình "share nothing": một worker nhận request, chạy toàn bộ vòng đời ứng dụng, trả kết quả rồi dọn sạch bộ nhớ. Mô hình này rất an toàn, vì lỗi của request trước không thể rò sang request sau. Cái giá là bootstrap Laravel bị lặp lại ở mọi request. OPcache giúp không phải biên dịch lại file PHP, nhưng vẫn phải thực thi lại toàn bộ phần khởi tạo.
Theo tài liệu chính thức của Laravel Octane, Octane khởi động ứng dụng một lần, giữ nó trong bộ nhớ và đưa từng request vào ứng dụng đã khởi động sẵn đó. Octane chạy trên một trong ba app server: FrankenPHP, Swoole hoặc RoadRunner.
| Tiêu chí | PHP-FPM | Laravel Octane |
|---|---|---|
| Vòng đời ứng dụng | Khởi động và huỷ theo từng request | Khởi động một lần, sống qua nhiều request |
| Giao tiếp với nginx | FastCGI qua unix socket | HTTP qua cổng TCP nội bộ |
| State giữa các request | Không có, luôn sạch | Có, nếu code không cẩn thận |
exit(), header() thuần PHP | Bình thường | Gây lỗi hoặc giết worker |
| Deploy code mới | Có hiệu lực ngay (hoặc sau reload FPM) | Phải octane:reload hoặc restart |
| Rò bộ nhớ | Tự hết khi request kết thúc | Tích luỹ, cần --max-requests |
2. Vì sao chạy Laravel Octane trên FrankenPHP
Swoole và RoadRunner đều là lựa chọn tốt, nhưng FrankenPHP có hai lợi thế cho hạ tầng Cloud VPS. Thứ nhất, không cần build extension PHP riêng như Swoole: FrankenPHP là app server viết bằng Go, nhúng sẵn PHP, và Octane tự tải binary FrankenPHP khi cài với --server=frankenphp. Thứ hai, Laravel Octane hỗ trợ FrankenPHP chính thức từ cuối năm 2023, không phải tự vá gì thêm. Site cần chạy PHP 8.x theo yêu cầu của Octane.
Nên dành riêng một nhóm minion cho các site Laravel Octane, tách khỏi minion PHP-FPM, để không phải trộn hai mô hình chạy và hai cách tính RAM trên cùng một máy.
3. Đặt Laravel Octane sau nginx trên Cloud VPS
Luồng request không đổi so với mô hình trong bài Cloud VPS: mô hình Balancer – Minion – Database. Balancer vẫn giữ SSL và chuyển HTTP về minion. Điểm khác nằm ở minion: thay vì fastcgi_pass tới socket PHP-FPM, nginx proxy_pass tới FrankenPHP nghe trên 127.0.0.1, mỗi site một cổng riêng:
Internet ─HTTPS─▶ balancer (SSL, rate limit)
│ HTTP :80
▼
minion Octane: nginx (10.0.0.24:80)
├─ file tĩnh: nginx đọc thẳng từ public/
└─ request động ─▶ 127.0.0.1:8001 FrankenPHP (site-a)
─▶ 127.0.0.1:8002 FrankenPHP (site-b)File cấu hình site trên minion (đã lược bớt):
server {
listen 10.0.0.24:80;
server_name site-a.example;
root /var/www/site-a.example/public/;
# minion chỉ nói HTTP, không để nginx tự sinh redirect http:// tuyệt đối
absolute_redirect off;
location / {
client_max_body_size 128M;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https; # SSL đã kết thúc ở balancer
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 300;
proxy_pass http://127.0.0.1:8001;
}
location ~ \.php$ { deny all; } # không file .php nào được gọi trực tiếp
location ~* \.(?:css|js|webp|woff2|png|jpg|jpeg|gif|ico|svg|mp4)$ {
expires 30d;
access_log off;
add_header Cache-Control "public";
}
}X-Forwarded-Proto https đóng vai trò tương tự fastcgi_param HTTPS on ở site PHP-FPM. Laravel chỉ tin header này khi request đến từ proxy được tin cậy, nên trong TrustProxies cần đặt $proxies = ['127.0.0.1']. Thiếu bước này, Laravel Octane sinh link CSS/JS dạng http:// và trình duyệt chặn vì mixed content.
Chạy Laravel Octane bằng supervisor
Mỗi site Laravel Octane là một tiến trình chạy dài, cần được giám sát và tự khởi động lại. Có thể dùng supervisor như cho queue worker, mỗi site một program:
[program:site-a-octane]
command=/usr/bin/php8.3 artisan octane:start --server=frankenphp
--host=127.0.0.1 --port=8001 --admin-port=9001
--workers=1 --max-requests=500
directory=/var/www/site-a.example
user=www-data
autostart=true
autorestart=true
environment=PATH="/usr/local/bin:/usr/bin:/bin",
CADDY_ADMIN="127.0.0.1:9001",
PHP_INI_SCAN_DIR="/etc/php/8.3/cli/conf.d:/etc/php-octane/site-a"Ba điểm cần nhớ khi vận hành:
- Không được trùng cổng, cả
--portlẫn--admin-port. FrankenPHP nhúng Caddy, và Caddy có cổng admin riêng; hai site cùng admin port thì site khởi động sau sẽ lỗi. Hãy giữ một bảng đăng ký cổng cho từng minion (ví dụ site-a dùng 8001 và 9001, site-b dùng 8002 và 9002). - FrankenPHP không đọc php.ini của PHP-FPM. Lỗi hay gặp nhất là upload file lớn bị chặn dù
upload_max_filesizecủa FPM đã đủ. Tạo file ini riêng cho site rồi chỉPHP_INI_SCAN_DIRtới thư mục chứa nó (đặt ngoàivendor/đểcomposer installkhông xoá). - Đổi cấu hình phải nạp lại đúng program:
supervisorctl reread,supervisorctl update, rồisupervisorctl restart site-a-octane.
# /etc/php-octane/site-a/custom.ini
post_max_size = 128M
upload_max_filesize = 128M
memory_limit = 256M4. Viết code cho Laravel Octane: ứng dụng không còn "chết đi sống lại"
Đây là phần tốn công nhất. Code chạy đúng trên PHP-FPM có thể sai trên Laravel Octane, vì nó ngầm giả định mọi thứ được làm mới sau mỗi request. Các lỗi hay gặp khi đưa một CMS Laravel lên Octane rơi vào năm nhóm.
State rò giữa các request
Ví dụ một service theme lưu danh sách CSS/JS trong mảng thuộc tính. Trên PHP-FPM, mảng được tạo mới mỗi request. Trên Octane, service là singleton sống suốt đời worker, nên mỗi request lại cộng thêm vài thẻ <link> vào mảng cũ: sau nhiều request, trang HTML chứa cùng một file CSS lặp lại nhiều lần. Cách sửa: chống trùng khi thêm, và một middleware gọi flush() ở đầu mỗi request.
Singleton và Facade
Lỗi ngược lại: helper sitemap, menu được đăng ký bằng bind thay vì singleton. Giữa các request, Octane xoá cache của Facade, Facade tạo instance mới rỗng, dữ liệu đã đăng ký lúc boot biến mất, sitemap trả về rỗng. Quy tắc: thứ gì được nạp dữ liệu lúc boot và cần sống suốt worker thì phải là singleton; thứ gì phụ thuộc request (user, locale, giỏ hàng) thì không được giữ trong singleton.
Thuộc tính static làm cache
Kiểu tối ưu private static $options để "khỏi query lại trong cùng request" là quả bom hẹn giờ trên Octane: giá trị static sống suốt worker, admin sửa cấu hình xong nhưng site vẫn hiện dữ liệu cũ tới khi worker restart. Hãy bỏ static cache, chuyển sang Cache::remember() để thời hạn và việc xoá cache nằm ở một nơi.
exit() và header() thuần PHP
Trên PHP-FPM, exit() chỉ kết thúc request; trên Octane, nó kết thúc cả worker. Tương tự, header("Content-Type: text/xml"); echo $xml; không có tác dụng vì Octane chỉ đọc header từ đối tượng Response, nên trang RSS hiện trắng. Luôn trả về Response của Laravel:
// Trước: chạy được trên PHP-FPM
header("Content-Type:text/xml");
echo $rss->getRSSContent();
// Sau: đúng trên cả PHP-FPM lẫn Laravel Octane
return response($rss->getRSSContent(), 200)->header('Content-Type', 'text/xml');
// exit() để chặn truy cập đổi thành
abort(403);Logic chỉ chạy một lần lúc boot
Kiểm tra license, áp cấu hình mail từ database thường đặt trong booted() của service provider. Trên Octane, booted() chỉ chạy khi worker khởi động, nên admin đổi cấu hình mail qua giao diện thì worker vẫn dùng cấu hình cũ. Chuyển phần này sang listener chạy theo request, kèm cơ chế chỉ áp lại khi cấu hình thật sự thay đổi.
--max-requests: lưới an toàn cho rò bộ nhớ
Ứng dụng Laravel Octane lớn với nhiều package bên thứ ba khó tránh hoàn toàn rò bộ nhớ nhỏ. --max-requests bảo Octane khởi động lại worker sau một số request, trả bộ nhớ về mức ban đầu. Đặt quá cao thì worker phình to, quá thấp thì mất bớt lợi ích của việc boot một lần. Có thể bắt đầu ở mức vài trăm rồi theo dõi RAM của tiến trình frankenphp để điều chỉnh. Site ít truy cập có thể chạy --workers=1 để minion còn chỗ cho nhiều site Octane.
5. Deploy site Laravel Octane: đừng quên reload
Với PHP-FPM, git pull xong là code mới chạy (khi OPcache kiểm tra timestamp). Với Laravel Octane, code cũ vẫn nằm trong bộ nhớ worker. Quy trình deploy tối thiểu:
cd /var/www/site-a.example
git pull
composer install --no-dev --optimize-autoloader
php artisan optimize:clear
php artisan octane:reload # nạp lại worker, không cắt kết nối đang xử lýBật hay tắt plugin trong CMS cũng thuộc dạng này: service provider của plugin chỉ được nạp lúc worker khởi động, nên sau thao tác đó cần reload worker. Nên đẩy việc reload sang một job trong hàng đợi, vì gọi reload đồng bộ từ chính request đang chạy trong worker sẽ tự khoá lẫn nhau. Hệ quả: site Octane cần có queue:work chạy kèm.
6. Khi nào nên và không nên dùng Laravel Octane
| Nên dùng | Không nên dùng |
|---|---|
| Site có lượng request động lớn, nhiều trang không cache được | Site nhỏ, ít truy cập: PHP-FPM với cache trang là đủ |
| Code do đội mình kiểm soát, rà soát được singleton và static | Code kế thừa hoặc package cũ đầy exit(), biến global |
| Chạy PHP 8.x, Laravel bản mới | Site còn kẹt ở PHP 7.x |
| Đội vận hành theo dõi được RAM từng tiến trình | Minion RAM thấp đã chạy sát giới hạn |
Laravel Octane không phải nút "tăng tốc" bật là xong. Nó chuyển một phần trách nhiệm từ runtime sang người viết code: PHP-FPM dọn dẹp giúp bạn sau mỗi request, còn Octane thì không. Với site phù hợp, phần thưởng là bỏ được chi phí bootstrap lặp lại; với site không phù hợp, PHP-FPM vẫn là lựa chọn ít rủi ro hơn.
Bài tiếp theo
Chuẩn hoá bảo mật Nginx: security snippet và giới hạn kết nối. Gom luật chặn đường dẫn nhạy cảm, bot xấu và giới hạn request vào cấu hình dùng chung, rồi triển khai đồng loạt lên các balancer mà không làm sập site nào.
