Laravel Scheduler với cron: kiểm tra lịch chạy và chống chạy trùng

Cấu hình Laravel Scheduler với cron trong production, kiểm tra schedule:list, chống chạy trùng bằng mutex và xử lý nhiều server, timezone.

NHT
· 7 phút đọc
Sơ đồ Laravel Scheduler chống chạy trùng task production bằng cron và mutex

Laravel Scheduler với cron giúp gom lịch chạy của ứng dụng vào code thay vì tạo nhiều crontab rời rạc trên server. Mô hình này phù hợp cho các tác vụ như gửi báo cáo, dọn dữ liệu cũ, đồng bộ API, dispatch queued job hoặc chạy command định kỳ. Nhưng một cron đúng cú pháp chưa đủ: production còn phải kiểm soát timezone, mutex, task chạy trùng, server nhiều node, log và cách xác minh lịch thực tế.

Bài viết này đi từ cron entry tối thiểu đến Laravel Scheduler: cách định nghĩa schedule, chạy `schedule:run`, đọc `schedule:list`, dùng `withoutOverlapping()` và `onOneServer()`, xử lý task dài, phân biệt scheduler với queue worker và khoanh vùng khi lịch không chạy. Tài liệu Laravel khuyến nghị chỉ cần một cron gọi scheduler mỗi phút trong môi trường production thông thường [1].

Nguyên tắc chống chạy trùng: cron chỉ là tín hiệu đánh thức scheduler. Việc chống overlap phải được thiết kế ở Laravel bằng mutex, lock hoặc idempotency; đừng giả định hai lần gọi cron sẽ tự động an toàn.

1. Scheduler, cron và queue khác nhau thế nào?

Cron là bộ lập lịch của hệ điều hành; nó gọi một command theo thời gian. Laravel Scheduler là lớp định nghĩa lịch trong application, còn queue worker là process xử lý job nền. Một task scheduled có thể chạy trực tiếp bằng command hoặc chỉ dispatch job vào queue. Nếu task nặng, nên để scheduler dispatch job và để Supervisor xử lý queue; không nên giữ một lệnh `schedule:run` lâu vì một API chậm.

LớpNhiệm vụLỗi thường gặp
CronGọi scheduler theo phútSai user, sai path PHP hoặc sai timezone server
Laravel SchedulerQuyết định task nào đến hạnKhông thấy schedule do cache/code cũ
Queue workerXử lý job đã dispatchWorker dừng, queue sai hoặc job failed
Mutex/lockNgăn overlap hoặc chạy nhiều nodeCache driver không chia sẻ hoặc lock bị kẹt

2. Cron production tối thiểu

Với ứng dụng Laravel, tạo một cron chạy mỗi phút bằng user có quyền đọc source, ghi log và truy cập `.env`. Dùng đường dẫn tuyệt đối cho PHP và Artisan để tránh khác biệt PATH giữa SSH và cron. Thay `/var/www/example.com/current` bằng path thật của bạn.

* * * * * cd /var/www/example.com/current && php artisan schedule:run >> /dev/null 2>&1

Nếu cần chẩn đoán, tạm thời chuyển output vào log riêng thay vì `/dev/null`, nhưng phải đặt rotation để log không đầy ổ đĩa. Kiểm tra crontab của đúng user bằng `crontab -l`, log hệ thống bằng `journalctl` hoặc `/var/log/syslog` tùy distro. Cron chạy bằng user khác có thể không đọc được `.env`, không thấy SSH key hoặc không có quyền ghi `storage`.

3. Định nghĩa task trong Laravel

Tùy phiên bản Laravel, lịch có thể được định nghĩa trong `routes/console.php` hoặc lớp Console Kernel. Hãy kiểm tra cấu trúc dự án trước khi thêm code. Ví dụ sau dùng Facade Schedule để minh họa một command hằng ngày, một job mỗi năm phút và một tác vụ dọn dẹp theo timezone rõ ràng.

<?php

use Illuminate\Support\Facades\Schedule;

Schedule::command('reports:daily')
    ->dailyAt('02:30')
    ->timezone('Asia/Ho_Chi_Minh')
    ->withoutOverlapping(120);

Schedule::job(new \App\Jobs\SyncInventory)
    ->everyFiveMinutes()
    ->withoutOverlapping();

Schedule::command('logs:prune')
    ->weeklyOn(0, '03:00')
    ->onOneServer();

Tên command nên thể hiện nghiệp vụ và có thể chạy thủ công. Mỗi command cần log kết quả, exit code rõ ràng và không phụ thuộc vào trạng thái terminal. Nếu schedule chỉ dispatch job, cần xác nhận queue worker đang chạy; scheduler chạy thành công không có nghĩa job đã hoàn tất.

4. Kiểm tra toàn bộ lịch bằng schedule:list

cd /var/www/example.com/current
php artisan schedule:list
php artisan schedule:run -v
php artisan about | sed -n '1,120p'
php artisan tinker --execute="dump(config('app.timezone'));"

`schedule:list` là điểm kiểm tra đầu tiên để biết Laravel có đọc được task, tần suất kế tiếp và timezone hay không. Nếu task không xuất hiện, xem lại file định nghĩa schedule, service provider, branch release và config cache. Nếu task xuất hiện nhưng không chạy, kiểm tra thời gian hệ thống, cron daemon và quyền user.

5. Chống task chạy trùng bằng withoutOverlapping

`withoutOverlapping()` tạo cơ chế mutex để task chưa kết thúc không được chạy thêm instance mới. Tham số số phút có thể dùng để giới hạn thời gian khóa theo tình huống. Chọn TTL dài hơn thời gian chạy thông thường nhưng có đường xử lý khi process bị kill. Nếu TTL quá ngắn, task dài có thể overlap; nếu quá dài, lock lỗi có thể khiến task bị bỏ qua lâu.

Schedule::command('billing:sync')
    ->everyTenMinutes()
    ->withoutOverlapping(30)
    ->onFailure(function () {
        logger()->error('billing:sync failed');
    });

Mutex phụ thuộc cache driver. Nếu cache không hoạt động hoặc nhiều app dùng chung key prefix, hành vi lock có thể khó đoán. Sau khi sửa lock, không nên xóa toàn bộ cache production một cách tùy tiện; hãy xác định cache store và key của ứng dụng, rồi dùng runbook có kiểm soát.

6. Nhiều server và onOneServer

Trong mô hình nhiều node, nếu mỗi node chạy cùng một cron thì task có thể được kích hoạt nhiều lần. `onOneServer()` yêu cầu các node dùng cache backend chia sẻ để Laravel tạo lock chung. Redis hoặc database cache thường phù hợp hơn file cache cục bộ; file cache trên từng node không thể đảm bảo một lock toàn cụm [1].

Schedule::command('reports:generate')
    ->dailyAt('01:00')
    ->timezone('Asia/Ho_Chi_Minh')
    ->onOneServer()
    ->withoutOverlapping(180);

`onOneServer()` không thay thế `withoutOverlapping()`: một cái chọn node chạy task, cái kia ngăn task mới chồng lên task đang chạy. Với nghiệp vụ quan trọng, vẫn nên có unique constraint hoặc idempotency key trong database để bảo vệ ở tầng dữ liệu.

7. Timezone và daylight saving

Có ba đồng hồ cần phân biệt: timezone của OS, timezone PHP/Laravel và timezone trong định nghĩa task. Một task `dailyAt('02:30')` có thể chạy khác thời điểm nếu app dùng UTC nhưng người vận hành đọc theo GMT+7. Hãy khai báo timezone trong code cho task nhạy giờ, đồng bộ NTP và ghi timestamp ISO 8601 vào log.

date -Is
timedatectl status
php -r "echo date('c'), PHP_EOL;"
php artisan schedule:list

Không nên chỉ sửa giờ hệ điều hành để chữa một task; điều đó có thể ảnh hưởng log, database và chứng chỉ. Tốt hơn là xác định timezone của nghiệp vụ trong schedule, test trước trên staging và ghi rõ quy đổi UTC/GMT+7 trong runbook.

8. Task dài, background và queue

Nếu command mất nhiều phút, có thể dùng `runInBackground()` cho command phù hợp, nhưng cần hiểu process con và log. Với tác vụ nặng hoặc cần retry, chiến lược thường dễ quan sát hơn là scheduler dispatch một job vào queue rồi Supervisor xử lý. Khi đó cần kiểm tra cả hai đường: scheduler có dispatch và queue worker có consume.

Schedule::command('reports:dispatch')
    ->hourly()
    ->withoutOverlapping();

// reports:dispatch chỉ dispatch job, không xử lý toàn bộ báo cáo trong cron.
// Supervisor đảm nhận queue worker và retry.

Tách scheduler và worker cũng giúp deploy rõ hơn: restart scheduler không đồng nghĩa restart worker. Bài [Laravel queue với Supervisor] trình bày phần process monitor cho queue worker.

9. Quy trình debug khi scheduler không chạy

Triệu chứngKiểm traHướng xử lý
Không thấy task trong schedule:listFile định nghĩa, branch, config cacheSửa code đúng release rồi cache/config lại
Cron không gọi Artisancrontab -l, service cron, path PHPDùng path tuyệt đối và log tạm thời
Task chạy sai giờdate, timedatectl, app timezoneKhai báo timezone rõ trong schedule
Task bị bỏ quawithoutOverlapping lock hoặc TTLKiểm tra cache store và lock kẹt
Task dispatch nhưng không hoàn tấtqueue worker, failed jobs, queue nameKiểm tra Supervisor và backend queue
Chạy nhiều lần trên nhiều nodecache không chia sẻDùng onOneServer với shared cache
crontab -l
sudo systemctl status cron 2>/dev/null || sudo systemctl status crond
php artisan schedule:list
php artisan schedule:run -vvv
tail -n 100 storage/logs/laravel.log
php artisan queue:failed

10. Checklist chống chạy trùng trước khi đưa production

  • Xác nhận chỉ có một cron entry cho mỗi application hoặc hiểu rõ vì sao nhiều node cùng chạy.
  • Chạy `schedule:list` và lưu output vào runbook release.
  • Khai báo timezone nghiệp vụ thay vì dựa vào timezone ngầm của server.
  • Dùng `withoutOverlapping()` cho task không được chạy đồng thời.
  • Dùng `onOneServer()` khi nhiều node dùng chung scheduler và cache backend.
  • Tách task nặng thành queued job; kiểm tra cả scheduler lẫn Supervisor worker.
  • Thiết kế idempotency, unique constraint hoặc lock nghiệp vụ cho dữ liệu quan trọng.
  • Theo dõi log, exit code, task duration và alert khi lịch bị trễ.

11. Kết luận

Laravel Scheduler không chỉ là một dòng cron. Một hệ thống chạy ổn định cần cron đánh thức đúng lúc, application đọc đúng schedule, timezone được xác định, mutex chống overlap, shared cache khi có nhiều node và queue worker xử lý phần việc nặng. Hãy kiểm tra bằng `schedule:list`, chạy thử có verbose, ghi log và bảo vệ nghiệp vụ bằng idempotency thay vì phụ thuộc một lớp khóa duy nhất.

Nguồn tham khảo

[1] Laravel Documentation — Task Scheduling; [2] Laravel Documentation — Queues.