Queue ใน Laravel คืออะไร และวิธีย้ายงานหนักไปทำเบื้องหลัง

Queue คือที่พักงานที่ไม่ต้องเสร็จก่อนตอบผู้ใช้ เช่น ส่งอีเมลหรือสร้าง PDF หน้าเว็บเก็บงานเป็น job ลงคิวแล้วตอบทันที จากนั้น worker ที่รันค้างไว้จะหยิบงานไปทำ โดยมี timeout, tries และ retry_after คุมว่างานช้าหรือพังแล้วจะเกิดอะไรขึ้น

เผยแพร่
เวลาอ่าน
3 นาที
แท็ก
#Laravel#Queue
ในโพสต์นี้ 6 หัวข้อ
  1. งานบางอย่างไม่ต้องเสร็จก่อนตอบผู้ใช้
  2. Job คืองานหนึ่งชิ้นที่เก็บลงคิวได้
  3. Connection บอกว่าคิวเก็บไว้ที่ไหน
  4. Worker คือ process ที่รันค้างไว้เพื่อหยิบงาน
  5. timeout, tries และ retry_after คุมงานที่ช้าหรือพัง
  6. Horizon ช่วยเมื่องานในคิวเยอะขึ้น

งานบางอย่างไม่ต้องเสร็จก่อนตอบผู้ใช้

ลองนึกถึงตอนลูกค้ากดสั่งซื้อในเว็บขายของออนไลน์ หลังบันทึกคำสั่งซื้อแล้ว ระบบยังต้องส่งอีเมลยืนยัน แจ้ง LINE ให้ร้าน สร้างใบเสร็จเป็น PDF และส่งข้อมูลไปให้ขนส่ง ถ้าทำทุกอย่างใน request เดียว ลูกค้าต้องนั่งรอหน้าจอหมุนจนครบทุกงาน และถ้า SMTP หรือ API ของขนส่งช้าสักตัว หน้าสั่งซื้อก็ช้าตาม หรือ error ทั้งที่คำสั่งซื้อบันทึกไปแล้ว

Queue แยกงานพวกนี้ออกไปทำเบื้องหลัง หน้าเว็บแค่จดว่ามีงานอะไรต้องทำ แล้วตอบลูกค้าทันที งานที่เหมาะกับคิวคืองานที่ช้า งานที่พึ่งระบบภายนอก และงานที่ลองใหม่ได้ถ้าพัง

request ได้คำตอบทันที ส่วนงานหนักไปรอในคิวให้ worker ทำ

หน้าเว็บเก็บงานลงคิวแล้วตอบทันที worker ที่รันอยู่อีกฝั่งค่อยหยิบงานไปทำ

Job คืองานหนึ่งชิ้นที่เก็บลงคิวได้

ใน Laravel งานแต่ละอย่างเขียนเป็นคลาส job สร้างด้วย php artisan make:job ใน handle() ใส่สิ่งที่ต้องทำ แล้วสั่งเข้าคิวจาก controller ด้วย dispatch()

class SendOrderConfirmation implements ShouldQueue
{
    use Queueable;

    public function __construct(public Order $order) {}

    public function handle(): void
    {
        Mail::to($this->order->email)->send(new OrderConfirmed($this->order));
    }
}

// ใน controller หลังบันทึกคำสั่งซื้อ
SendOrderConfirmation::dispatch($order);

ตอน dispatch Laravel แปลง job เป็นข้อความเก็บลงคิว model อย่าง $order เก็บแค่ id ไว้ แล้วโหลดใหม่จากฐานข้อมูลตอน worker หยิบไปทำ ถ้า dispatch ระหว่าง database transaction ให้ job รอ transaction commit ก่อน ไม่อย่างนั้น worker อาจหยิบงานไปทำก่อนข้อมูลจะบันทึกจริง

Connection บอกว่าคิวเก็บไว้ที่ไหน

QUEUE_CONNECTION ใน .env เลือกที่เก็บคิว โปรเจกต์ Laravel ใหม่ตั้งไว้เป็น database ซึ่งเก็บงานในตาราง jobs ของฐานข้อมูลที่ใช้อยู่แล้ว ไม่ต้องลงอะไรเพิ่ม

connection เก็บงานไว้ที่ เหมาะกับ
sync ไม่เข้าคิว รันทันทีใน request ตอนเขียนโค้ดหรือรัน test
database ตาราง jobs งานไม่เยอะ ไม่อยากเพิ่ม service
redis Redis งานเยอะ อยากได้ dashboard ของ Horizon

ใน connection เดียวกันยังแบ่งคิวย่อยตามชื่อได้ เช่น ให้งานชำระเงินอยู่คิว payment แล้วสั่ง worker ให้ทำคิวนี้ก่อน (--queue=payment,default) งานด่วนจะได้ไม่ต้องรอหลังงานส่งอีเมลเป็นร้อยฉบับ

Worker คือ process ที่รันค้างไว้เพื่อหยิบงาน

ถ้าไม่มี worker งานจะกองอยู่ในคิวเฉย ๆ worker เริ่มด้วย php artisan queue:work แล้วรันค้างไว้ หยิบงานทีละชิ้นมาทำ พอคิวว่างก็รอสักครู่แล้วเช็กใหม่

บน VPS เราให้ Supervisor หรือ systemd คุม worker ไว้ ถ้า process ตายก็เปิดใหม่ให้เอง บน shared hosting ที่รัน process ค้างไม่ได้ ทางที่ใช้กันมาตลอดคือตั้ง cron ทุกนาทีให้รัน queue:work --stop-when-empty ทำงานที่ค้างจนหมดแล้วออก งานจะช้ากว่าบ้างแต่ใช้ได้จริง

worker โหลดโค้ดเข้า memory ครั้งเดียวตอนเริ่ม แก้โค้ดแล้ว worker ไม่เห็นจนกว่าจะเริ่มใหม่ ทุกครั้งที่ deploy จึงต้องสั่ง php artisan queue:restart ให้ worker จบงานที่ทำอยู่แล้วออก ให้ Supervisor เปิดตัวใหม่ที่โหลดโค้ดใหม่ขึ้นมา

timeout, tries และ retry_after คุมงานที่ช้าหรือพัง

สามค่านี้ตั้งผิดแล้วอาการจะงงที่สุด เช่น งานรันซ้ำ หรือขึ้นว่าล้มเหลวทั้งที่ทำเสร็จแล้ว

  • timeout คือเวลาที่ job หนึ่งรันได้ ถ้าเกิน worker จะฆ่า process ทิ้ง ค่าเริ่มต้นของ queue:work คือ 60 วินาที ตั้งต่อ job ได้ด้วย public $timeout
  • tries คือจำนวนครั้งที่ลองได้ ค่าเริ่มต้นคือ 1 ครั้ง ถ้าเกินแล้วยังพัง งานจะไปอยู่ในตาราง failed_jobs ดูได้ด้วย queue:failed และสั่งลองใหม่ด้วย queue:retry ใส่ backoff ไว้ให้รอสักพักก่อนลองครั้งถัดไป
  • retry_after อยู่ใน config/queue.php ของแต่ละ connection (ค่าเริ่มต้น 90 วินาที) คือเวลาที่คิวยอมให้ worker ถืองานไว้ ถ้าเกินแล้วงานยังไม่เสร็จ คิวจะถือว่า worker ตายไปแล้ว และปล่อยงานกลับให้ worker ตัวอื่นหยิบ

กฎที่ docs ของ Laravel ย้ำไว้คือ timeout ต้องสั้นกว่า retry_after เสมอ ถ้า job รันนานกว่า retry_after แต่ยังไม่ถึง timeout คิวจะปล่อยงานเดียวกันให้ worker อีกตัวทำซ้อนกันทั้งที่ตัวแรกยังทำอยู่ ลูกค้าก็ได้อีเมลสองฉบับ

แม้ตั้งค่าถูกแล้ว งานก็ยังรันซ้ำได้ในบางกรณี เช่น worker ตายหลังส่งอีเมลไปแล้วแต่ก่อนบอกคิวว่าเสร็จ job ที่ส่งของออกไปข้างนอกจึงควรทำให้ส่งซ้ำได้โดยไม่เกิดผลซ้ำ

Horizon ช่วยเมื่องานในคิวเยอะขึ้น

พอใช้ Redis เป็นคิวแล้ว Laravel Horizon ช่วยคุม worker ให้ ตั้งจำนวน worker ต่อคิวไว้ใน config ไฟล์เดียว และมี dashboard ให้เห็นว่างานค้างกี่ชิ้น ใช้เวลาเท่าไหร่ และพังเพราะอะไร ส่วนวิธีตั้ง timeout ให้เป็นชั้น และเช็กก่อนเพิ่ม worker ดูต่อได้ในวิธีจูน Laravel Horizonครับ

อ่านต่อ

ถูกอ้างถึงใน

ล่าสุด

ดูทั้งหมด →