งานตามเวลาที่ระบบส่วนใหญ่ต้องมี
แทบทุกระบบที่ใช้งานจริงมีงานที่ไม่มีใครกดปุ่มสั่ง แต่ต้องเกิดตามเวลา
- ส่งสรุปยอดขายเมื่อวานเข้า LINE กลุ่มผู้บริหารทุกเช้า
- ยกเลิกคำสั่งซื้อที่ไม่โอนเงินภายใน 24 ชั่วโมง
- ส่งไฟล์โอนเงินให้ธนาคารตามรอบ ซึ่งต้องรู้วันทำการของธนาคารด้วย
- สำรองฐานข้อมูลตอนตีสอง และลบไฟล์สำรองที่เก่ากว่า 30 วัน
ทางที่ใช้กันมาตลอดคือตั้ง cron บนเซิร์ฟเวอร์ทีละงาน หนึ่งบรรทัดต่อหนึ่งงาน ปัญหาคือรายการงานไปอยู่นอกโค้ด ไม่มีใน git ย้ายเซิร์ฟเวอร์ทีก็ต้องจำมาตั้งใหม่ และไม่มีใครรู้ว่าบนเครื่องตั้งอะไรไว้บ้างจนกว่าจะ ssh เข้าไปดู
cron บรรทัดเดียว แล้ว Laravel ตัดสินใจเอง
cron คือตัวตั้งเวลาของ Linux ที่รันคำสั่งตามรูปแบบเวลา เช่น 0 8 * * * คือทุกวันตอน 8:00 Laravel ใช้ cron แค่บรรทัดเดียวให้เรียกตัวเองทุกนาที
* * * * * cd /var/www/shop && php artisan schedule:run >> /dev/null 2>&1
ทุกนาที schedule:run จะโหลดรายการงานจากโค้ด เช็คว่างานไหนถึงเวลาในนาทีนั้น แล้วรันเฉพาะงานนั้น นาทีที่ไม่มีงานก็จบไปเฉย ๆ รายการงานจริงจึงอยู่ใน git ตรวจใน pull request ได้ และย้ายเซิร์ฟเวอร์ก็แค่ตั้ง cron บรรทัดเดิม
ใครที่ใช้ shared hosting กับ cPanel หรือ DirectAdmin ก็ใช้ได้เหมือนกัน เข้าเมนู Cron Jobs แล้วใส่คำสั่งเดียวกัน โดยใช้ path เต็มของ PHP ตามที่โฮสต์บอก บางโฮสต์ให้ตั้งถี่สุดแค่ทุก 5 หรือ 15 นาที งานที่ตั้งไว้ระหว่างนั้นจะไม่ได้รัน เพราะ schedule:run เช็คเฉพาะนาทีที่ cron เรียกเท่านั้น ตอนพัฒนาบนเครื่องตัวเองไม่ต้องตั้ง cron ใช้ php artisan schedule:work ค้างไว้ใน terminal แทนได้
ประกาศงานไว้ใน routes/console.php
ตั้งแต่ Laravel 11 รายการงานอยู่ใน routes/console.php ส่วนเวอร์ชันก่อนหน้าอยู่ในเมธอด schedule() ของ app/Console/Kernel.php
use Illuminate\Support\Facades\Schedule;
Schedule::command('reports:daily-sales')->dailyAt('08:00');
Schedule::command('orders:cancel-unpaid')->hourly();
Schedule::command('backup:run')->dailyAt('02:00')->withoutOverlapping();
นอกจาก command() ยังสั่ง job() เพื่อโยน job เข้า queue หรือ call() กับ closure ได้ เวลามีให้เลือกเยอะ ตั้งแต่ everyMinute(), everyFiveMinutes(), weekdays() ไปจนถึง cron('0 8 * * 1-5') ถ้าอยากเขียนเอง
ดูว่ามีงานอะไรบ้างและจะรันครั้งถัดไปเมื่อไหร่ได้ด้วย php artisan schedule:list ซึ่งช่วยมากตอนรับระบบต่อจากคนอื่น
ตัวช่วยที่ควรรู้จัก
| ตัวช่วย | ใช้ตอนไหน |
|---|---|
withoutOverlapping() |
งานยาวที่ห้ามมีสองรอบรันพร้อมกัน เช่น นำเข้าข้อมูลทุก 5 นาทีแต่บางรอบใช้ 7 นาที |
onOneServer() |
มีเว็บหลายเครื่องที่ตั้ง cron เหมือนกัน แต่อยากให้งานรันแค่เครื่องเดียว ต้องใช้ cache กลางอย่าง Redis หรือ database |
runInBackground() |
งานยาวที่ไม่อยากให้งานอื่นในนาทีเดียวกันต้องรอ ปกติ Laravel รันงานในนาทีเดียวกันทีละตัว |
timezone('Asia/Bangkok') |
งานที่ต้องอิงเวลาไทย ถ้าแอปตั้ง timezone เป็น UTC ไว้ |
when() / skip() |
รันเฉพาะเมื่อเงื่อนไขผ่าน เช่น ข้ามวันหยุดของบริษัท |
เรื่อง timezone สำคัญกว่าที่คิด ถ้า config/app.php ยังเป็น UTC ตามค่าเริ่มต้น dailyAt('08:00') จะรันตอน 15:00 เวลาไทย ตั้งที่ APP_TIMEZONE ทั้งแอป หรือใส่ timezone() เฉพาะงานก็ได้
ส่วนงานที่ใช้เวลานานหรือเรียก API ภายนอก ให้ scheduler แค่โยน job เข้า queue แล้วให้ worker รันต่อ งานจะได้ retry ได้และไม่ทำให้งานอื่นในนาทีนั้นรอ
สิ่งที่ scheduler ไม่ได้ทำให้
scheduler รันงานได้เฉพาะนาทีที่ cron เรียก ถ้าเซิร์ฟเวอร์ล่ม รีบูตนาน หรือ cron หายไปหลังย้ายเครื่อง งานในช่วงนั้นจะหายไปเงียบ ๆ Laravel ไม่ย้อนกลับไปรันให้ตอนเครื่องกลับมา รายงานเช้าวันนั้นจึงไม่ออก และอาจไม่มีใครรู้จนผู้บริหารถาม
ทางป้องกันมีสองชั้น ชั้นแรกคือรู้ตัวให้เร็ว ตั้งให้งานสำคัญ ping บริการเฝ้าระวังอย่าง Healthchecks.io ทุกครั้งที่รันเสร็จ ถ้าไม่มี ping เข้ามาตามเวลาก็แจ้งเตือน ชั้นที่สองคือออกแบบให้งานรันย้อนหลังได้ เช่นคำสั่งรับวันที่เข้ามา และรันซ้ำแล้วไม่เกิดผลซ้ำ (idempotent) ถ้าอยากให้ระบบรันงานที่พลาดไปให้อัตโนมัติ มีแพ็กเกจที่ทำเรื่องนี้ ลองอ่านต่อที่โพสต์เรื่องรันงานที่พลาดไปตอนเซิร์ฟเวอร์ล่มได้ครับ
รายละเอียดทุกตัวเลือกอยู่ในเอกสาร Task Scheduling ของ Laravel






