Laravel queue với Supervisor là mô hình phổ biến để chạy các job nền như gửi email, xử lý ảnh, đồng bộ dữ liệu hoặc tạo báo cáo mà không làm request web phải chờ quá lâu. Tuy nhiên, chạy `php artisan queue:work` trong một SSH session không phải cách vận hành production ổn định: session có thể đóng, worker có thể gặp lỗi, deploy có thể để lại process dùng code cũ và không ai biết queue đang bị dừng.
Bài viết này trình bày quy trình thực tế trên VPS Ubuntu: chuẩn bị worker, tạo Supervisor program, phân tách queue theo ưu tiên, chọn `numprocs`, `tries`, `timeout`, `stopwaitsecs`, xử lý deploy bằng `queue:restart`, kiểm tra failed jobs và rollback. Supervisor giúp giữ tiến trình sống, nhưng không thay thế cơ chế idempotent, retry và giám sát lỗi của chính ứng dụng Laravel [1].
Nguyên tắc production: Supervisor chỉ nên chịu trách nhiệm quản lý vòng đời process. Laravel vẫn phải chịu trách nhiệm về retry, timeout, failed job, tính idempotent và tính đúng của nghiệp vụ.
1. Queue worker khác gì với request web?
Một request HTTP thường có vòng đời ngắn: Nginx nhận request, PHP-FPM thực thi controller rồi trả response. Queue worker thì khác; nó là process dài hạn, liên tục lấy job từ backend như database, Redis hoặc SQS. Đưa tác vụ nặng vào queue giúp web phản hồi nhanh hơn nhưng tạo thêm một lớp vận hành cần theo dõi. Worker có thể bị kill bởi memory limit, timeout, deploy hoặc lỗi dependency.
Laravel phân biệt connection và queue. Connection mô tả backend, còn queue là luồng công việc bên trong connection. Bạn có thể cho email vào queue `emails`, tác vụ nặng vào `heavy` và để worker ưu tiên bằng `--queue=high,default`. Tài liệu Laravel mô tả chính worker là nơi quyết định queue nào được xử lý và theo thứ tự ưu tiên nào [1].
2. Kiểm tra môi trường trước khi cấu hình Supervisor
Trước khi tạo service, hãy xác định đúng thư mục release, user chạy ứng dụng, PHP CLI, Composer dependency và connection queue. Nếu server chạy nhiều website, tuyệt đối không dùng `php` hoặc `.env` của website khác. Lưu lại kết quả kiểm tra để khi worker không chạy có thể phân biệt lỗi Supervisor với lỗi Laravel.
cd /var/www/example.com/current
php -v
php artisan --version
php artisan queue:failed
php artisan about | sed -n '1,120p'
command -v php
readlink -f "$(command -v php)"
ls -la storage bootstrap/cache
sudo -u www-data php artisan queue:work --help | sed -n '1,120pNếu dùng Redis, kiểm tra `QUEUE_CONNECTION=redis`, Redis service và quyền truy cập. Nếu dùng database, migration cho bảng jobs và failed_jobs phải tồn tại. Với Laravel mới, cách cấu hình có thể khác theo phiên bản; hãy đọc `config/queue.php`, `.env` và lệnh `queue:work --help` của đúng codebase thay vì sao chép nguyên file từ dự án khác.
3. Chọn tham số worker an toàn
| Tham số | Ý nghĩa | Cách suy nghĩ trong production |
|---|---|---|
| --queue=high,default | Thứ tự ưu tiên queue | Đặt queue khẩn cấp trước queue thường |
| --tries=3 | Số lần thử lại job | Chọn theo tính idempotent và lỗi tạm thời |
| --timeout=90 | Thời gian tối đa một job | Phải thấp hơn retry_after phù hợp |
| --sleep=3 | Thời gian nghỉ khi queue rỗng | Giảm polling nhưng không làm trễ job |
| --max-jobs=500 | Số job trước khi worker tự thoát | Giúp giải phóng memory tích lũy |
| --max-time=3600 | Thời gian sống tối đa | Tạo chu kỳ recycle có kiểm soát |
| --memory=256 | Ngưỡng memory Laravel theo MB | Không thay thế giới hạn RAM của hệ điều hành |
Không có một bộ số đúng cho mọi server. Job gửi email thường ngắn; job xử lý video hoặc import CSV cần timeout và memory khác. `timeout` phải được đối chiếu với `retry_after` để tránh một job bị worker khác nhận lại trong khi worker đầu tiên vẫn đang xử lý. Đối với job có side effect, hãy dùng unique key hoặc kiểm tra trạng thái trước khi ghi dữ liệu để retry không tạo bản ghi trùng.
4. Tạo Supervisor configuration cho Laravel
Trên Ubuntu, Supervisor thường đọc các program file trong `/etc/supervisor/conf.d/`. Ví dụ dưới đây chạy worker bằng user ứng dụng, dùng đường dẫn tuyệt đối và ghi log riêng. Thay `/var/www/example.com/current` bằng release thực tế; nếu dùng symlink current trong deploy atomic, cần hiểu worker đang giữ code của release nào.
[program:example-laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/example.com/current/artisan queue:work redis --queue=high,default --sleep=3 --tries=3 --timeout=90 --max-jobs=500 --max-time=3600
directory=/var/www/example.com/current
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/example.com/shared/storage/logs/worker.log
stopwaitsecs=3600`numprocs=2` chỉ là baseline. Mỗi worker có thể tạo memory riêng, vì vậy tăng process đồng nghĩa tăng áp lực lên RAM, CPU và database. `stopasgroup`/`killasgroup` giúp xử lý process con nhất quán hơn. `stopwaitsecs` nên đủ dài cho job hợp lệ dài nhất; nếu đặt quá thấp, Supervisor có thể kill job đang ghi dữ liệu.
5. Nạp cấu hình và kiểm tra trạng thái
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl status example-laravel-worker:*
sudo supervisorctl tail -f example-laravel-worker:example-laravel-worker_00
ps -ef | grep '[q]ueue:work'
php artisan queue:failedKết quả mong đợi là các process ở trạng thái RUNNING và PID thay đổi khi bạn chủ động restart. Nếu process chuyển sang FATAL, chạy command bằng đúng user ở foreground để xem lỗi thật. Các lỗi thường gặp gồm sai path `artisan`, thiếu quyền `storage`, `.env` không đọc được, PHP extension thiếu, Redis không truy cập được hoặc Supervisor dùng PHP binary khác với web.
6. Deploy Laravel mà không để worker dùng code cũ
Worker là process dài hạn nên không tự đọc code mới sau mỗi lần deploy. Sau khi release mới đã có dependency, migrate và cache config đúng thứ tự, hãy yêu cầu worker thoát sau job hiện tại bằng `php artisan queue:restart`. Laravel ghi tín hiệu restart vào cache; worker đang chạy sẽ kết thúc ở thời điểm an toàn và Supervisor tự khởi động process mới [1].
cd /var/www/example.com/current
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan queue:restart
sudo supervisorctl status example-laravel-worker:*
# sau đó dispatch một job kiểm tra release/build mớiNếu deploy dùng symlink `current`, hãy kiểm tra `directory` và đường dẫn log. Một cách an toàn là chạy queue command qua path `current` nhưng luôn restart sau khi symlink đổi. Đừng dùng `supervisorctl restart` một cách mù quáng trước khi job dài đã có chiến lược retry; restart cưỡng bức có thể làm job dở dang được nhận lại.
Khi queue worker chạy cùng Nginx, PHP-FPM và nhiều phiên bản PHP, hãy đối chiếu socket và runtime trong bài chạy nhiều phiên bản PHP với Nginx và PHP-FPM để tránh Supervisor dùng nhầm binary.
7. Failed jobs, retry và idempotency
Supervisor không biến một job lỗi thành job thành công. Khi job hết số lần thử, Laravel ghi failed job nếu ứng dụng đã cấu hình cơ chế này. Quy trình vận hành phải gồm đọc danh sách, xem exception, sửa nguyên nhân rồi retry có chọn lọc. Không nên chạy `queue:retry all` ngay sau một lỗi diện rộng nếu job có thể tạo email, thanh toán hoặc bản ghi trùng.
php artisan queue:failed
php artisan queue:retry 42
php artisan queue:forget 42
php artisan queue:flush
sudo supervisorctl restart example-laravel-worker:*Các job có side effect nên có idempotency key, unique constraint hoặc kiểm tra trạng thái trước khi thực hiện. Với API ngoài, lưu request ID và trạng thái phản hồi. Với import, lưu offset hoặc batch ID. Đây là phần logic quan trọng hơn việc tăng `numprocs`; nhiều worker chỉ làm lỗi lặp nhanh hơn nếu job không an toàn khi retry.
8. Checklist giám sát worker trên VPS
- Kiểm tra Supervisor program có đang RUNNING và số process đúng với `numprocs`.
- Theo dõi `storage/logs/laravel.log`, worker log, CPU, RAM, swap và database connections.
- Đếm job tồn đọng theo queue; queue high không được bị queue default làm nghẽn.
- Kiểm tra failed jobs sau mỗi deploy và sau các đợt traffic cao.
- Đặt cảnh báo khi process chuyển FATAL, restart liên tục hoặc memory tăng bất thường.
- Ghi lại lệnh rollback, phiên bản release và thời điểm chạy `queue:restart`.
9. Các lỗi thường gặp và cách khoanh vùng
Supervisor báo RUNNING nhưng queue không xử lý
Kiểm tra connection trong `.env`, cache config và backend Redis/database. Chạy cùng command dưới đúng user ở foreground, dispatch một job nhỏ rồi xem log. Nếu queue name sai, worker có thể sống nhưng không đọc đúng luồng.
Worker bị restart liên tục
Đọc stderr/stdout, kiểm tra PHP extension, quyền file và memory. Thử giảm `numprocs`, đặt `--max-jobs` thấp hơn để recycle có kiểm soát và sửa job gây crash trước khi tăng tài nguyên.
Deploy xong nhưng job vẫn chạy code cũ
Chạy `php artisan queue:restart`, xác nhận PID đổi và dispatch job có dấu release mới. Nếu dùng config cache, đảm bảo release mới đã chạy `config:cache` trước khi restart worker.
10. Kết luận
Chạy Laravel queue ổn định trên VPS là sự kết hợp giữa queue backend, worker parameters, Supervisor, deploy lifecycle và observability. Hãy bắt đầu với một hoặc hai process, đo queue latency và memory, sau đó mới tăng concurrency. Supervisor giúp worker sống lại, còn Laravel job design, retry và idempotency quyết định hệ thống có xử lý đúng hay không.
Nguồn tham khảo
[1] Laravel Documentation — Queues; [2] Supervisor Documentation — Configuration File.