صف (Queue) و Job در لاراول؛ از ارسال تا اجرا

یاسین
1405/6/29
9 دقیقه
25 بازدید
صف (Queue) و Job در لاراول؛ از ارسال تا اجرا

صف (Queue) در لاراول چیست و چه زمانی به آن نیاز داریم؟

هر درخواستی که به یک برنامه لاراولی می‌رسد، در یک چرخه کوتاه باید پاسخ بگیرد. اگر همان درخواست مجبور باشد منتظر ارسال ایمیل، ساخت تصویر بندانگشتی، تماس با یک API بیرونی یا تولید گزارش سنگین بماند، کاربر پشت صفحه می‌ماند و سرور هم در همان لحظه منابعش را قفل می‌کند. صف لاراول دقیقاً برای بیرون کشیدن این کارها از مسیر درخواست ساخته شده است: کار سنگین را به یک صف می‌سپارید، پاسخ را فوری به کاربر برمی‌گردانید، و یک فرایند جداگانه در پس‌زمینه آن کار را انجام می‌دهد.

مثال ملموس: در یک فروشگاه اینترنتی، پس از ثبت سفارش سه کار باید انجام شود — ارسال ایمیل تأیید، اطلاع به انبار، و تولید فاکتور PDF. اگر هر سه در همان درخواست اجرا شوند، کاربر چند ثانیه منتظر می‌ماند و اگر سرویس ایمیل کند باشد، ممکن است درخواست حتی با خطای تایم‌اوت بخورد. با صف، سفارش ثبت می‌شود، پاسخ بلافاصله برمی‌گردد و هر سه کار در پس‌زمینه و با امکان تلاش مجدد انجام می‌شوند.

نمای واقعی از کارهای در صف و پردازش پس‌زمینه در لاراول
صف یعنی سپردن کار سنگین به فرایندی جداگانه؛ مثل بسته‌هایی که روی نوار به مقصد می‌رسند، jobها هم در پس‌زمینه پردازش می‌شوند.

تفاوت کار هم‌زمان و کار پس‌زمینه

لاراول دو حالت ساده دارد. در حالت هم‌زمان (sync)، job همان لحظه و داخل همان درخواست اجرا می‌شود؛ برای توسعه و تست خوب است ولی هیچ سودی برای کاربر ندارد. در حالت پس‌زمینه، job در یک درایور صف ذخیره می‌شود و یک فرایند جداگانه به نام کارگر صف آن را برمی‌دارد و اجرا می‌کند. اگر پیش‌تر با آموزش جامع لاراول از مبتدی تا پیشرفته با ساختار لاراول آشنا شده‌اید، صف پله بعدی همان مسیر است.

چه کارهایی را نباید به صف سپرد

  • کارهایی که کاربر باید نتیجه‌شان را ببیند و بدون نتیجه نمی‌تواند ادامه دهد (مثل اعتبارسنجی پرداخت).
  • کارهای بسیار کوتاه که هزینه سریالایز کردن و بردن به صف بیشتر از اجرای مستقیم آن‌هاست.
  • کارهایی که داخل یک تراکنش دیتابیس اجرا می‌شوند؛ job ممکن است پیش از تکمیل تراکنش برداشته شود و به رکوردی دست بزند که هنوز ثبت نشده است.

پیش‌نیاز: درایور صف را انتخاب کنید

لاراول صف را روی چند درایور می‌تواند اجرا کند: database، redis، sqs، beanstalkd و sync. انتخاب درست، به محیط اجرای پروژه بستگی دارد، نه به سلیقه.

درایورمناسب برایمحدودیت
databaseبازه‌ای از پروژه‌های واقعی، هاست اشتراکی و VPS معمولیسرعت کمتر از redis در حجم بالا؛ فشار روی دیتابیس
redisترافیک بالا، صف‌های زیاد، Horizonنیاز به نصب و مدیریت سرویس redis روی سرور
syncتوسعه و تستکار را همان لحظه و در همان درخواست اجرا می‌کند

برای بیشتر پروژه‌ها و به‌ویژه روی هاست‌هایی که redis ندارند، درایور database انتخاب واقع‌بینانه است. جدول‌هایش با مایگریشن ساخته می‌شوند و هیچ سرویس اضافه‌ای لازم نیست.

ساخت جدول‌های صف

php artisan queue:table      # جدول jobs
php artisan queue:failed-table   # جدول jobهای ناموفق
php artisan migrate

اگر با مایگریشن و مدیریت تغییرات دیتابیس آشنا نیستید، راهنمای کامل مایگریشن‌ها در لاراول را اول ببینید؛ تغییر ساختار جدول‌ها در ادامه به آن نیاز پیدا می‌کنید.

سپس در فایل .env صف را روی دیتابیس بگذارید:

QUEUE_CONNECTION=database

این یک خط، دلیل شمارهٔ یک «job من اجرا نمی‌شود» در پروژه‌های ایرانی است. اگر مقدار روی sync بماند، هیچ jobی به صف نمی‌رود و همه چیز همزمان اجرا می‌شود — یعنی صف دارید ولی صف کار نمی‌کند.

ساخت Job و ارسال آن به صف

Job یک کلاس است که کار مشخصی را در متد handle انجام می‌دهد. با آرتیزان بسازیدش:

php artisan make:job SendOrderConfirmation
<?php

namespace App\Jobs;

use App\Models\Order;
use App\Mail\OrderConfirmed;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Mail;

class SendOrderConfirmation implements ShouldQueue
{
    use Queueable;

    public int $tries = 3;
    public int $timeout = 60;

    public function __construct(public Order $order)
    {
        //
    }

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

دو نکته مهم در همین کلاس: پیاده‌سازی ShouldQueue باعث می‌شود لاراول job را به صف بفرستد نه اینکه همزمان اجرا کند، و tries تعیین می‌کند در صورت خطا چند بار تلاش شود. بدون tries، یک خطای موقت شبکه در سرویس ایمیل یعنی ایمیل هیچ‌وقت ارسال نمی‌شود.

ارسال job به صف

use App\Jobs\SendOrderConfirmation;

// ساده‌ترین شکل
SendOrderConfirmation::dispatch($order);

// اجرا با تأخیر پنج دقیقه‌ای
SendOrderConfirmation::dispatch($order)->delay(now()->addMinutes(5));

// روی یک صف مشخص (برای جدا کردن کار سنگین از سبک)
SendOrderConfirmation::dispatch($order)->onQueue('emails');

// فقط اگر شرط برقرار بود
SendOrderConfirmation::dispatchIf($order->user->email_verified, $order);

// پس از ارسال پاسخ به کاربر، نه قبل از آن
SendOrderConfirmation::dispatchAfterResponse($order);

وقتی job با یک مدل کار می‌کند — مثل همین $order — لاراول فقط شناسه مدل را در صف ذخیره می‌کند و هنگام اجرا آن را از دیتابیس می‌خواند. برای کار با مدل‌ها به‌صورت حرفه‌ای‌تر، آموزش کار با Eloquent ORM مسیر روابط و کوئری‌ها را پوشش می‌دهد.

اجرای کارگر صف (Queue Worker)

تا اینجا job فقط یک ردیف در جدول jobs است. برای اجرا شدنش، کارگر صف لازم است:

php artisan queue:work

کارگر یک فرایند مستقل است که صف را می‌خواند و jobها را یکی‌یکی اجرا می‌کند. تفاوتش با queue:listen این است که queue:listen کد را در هر اجرا از نو بارگذاری می‌کند (برای توسعه راحت‌تر، برای تولید کندتر) و queue:work کد را در حافظه نگه می‌دارد.

تنظیم تعداد تلاش، زمان و اولویت

# حداکثر سه تلاش، تایم‌اوت ۶۰ ثانیه، فقط صف emails
php artisan queue:work --tries=3 --timeout=60 --queue=emails,default

# کارگر تا وقتی صف خالی شود کار می‌کند (مناسب اجرای دوره‌ای با کرون)
php artisan queue:work --stop-when-empty --max-time=300

ترتیب --queue=emails,default یعنی اول صف ایمیل‌ها پردازش شود؛ این کار ترتیب اولویت را مشخص می‌کند. اگر صف‌های متعددی دارید و می‌خواهید جفت‌های کاری همزمان اجرا شوند، می‌توانید چند کارگر با --queue متفاوت بالا بیاورید.

اجرای دائمی با supervisor در سرور اختصاصی

در سرورهای اختصاصی، کارگر باید همیشه زنده بماند و پس از ری‌استارت سرور یا خطا خودش بالا بیاید. ابزار متعارف این کار supervisor است:

[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/app/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
numprocs=2
user=www-data
redirect_stderr=true
stdout_logfile=/var/www/app/storage/logs/worker.log

بعد از هر تغییر در این فایل، supervisor باید بازخوانی شود (supervisorctl reread و سپس supervisorctl update).

سرور میزبان کارگر صف لاراول و تنظیم کرون در هاست
روی هاست بدون supervisor، کارگر صف با کرون هر دقیقه اجرا و بسته می‌شود.

اجرای صف روی هاست ایرانی با کرون (cPanel)

بخش بزرگی از پروژه‌های فارسی روی هاست اشتراکی یا VPS بدون دسترسی ریشه اجرا می‌شوند؛ جایی که نه supervisor نصب می‌شود و نه سرویس دائمی مجاز است. در این محیط، کارگر صف را با کرون زنده نگه می‌دارند: کرون هر دقیقه یک کارگر کوتاه‌عمر اجرا می‌کند که صف را تا خالی شدن پردازش می‌کند و بعد خودش می‌بندد.

# در cPanel → Cron Jobs، هر دقیقه:
/usr/local/bin/php /home/USER/public_html/artisan queue:work --stop-when-empty --max-time=280 --tries=3 >> /dev/null 2>&1

سه نکته که این دستور را از یک دستور شانسی جدا می‌کند:

  • مسیر PHP باید مسیر واقعی سرور باشد؛ در cPanel معمولاً /usr/local/bin/php یا مسیر نسخه‌ای مثل /opt/cpanel/ea-php82/root/usr/bin/php است.
  • --stop-when-empty باعث می‌شود کارگر بیکار نماند و کرون بعدی دوباره اجرایش کند؛ بدون آن، هر دقیقه یک کارگر جدید روی کارگر قبلی باز می‌شود.
  • --max-time کمتر از فاصله کرون باشد تا دو کارگر روی هم نیفتند.

زمان‌بندی تسک‌ها با همان کرون

اگر تسک زمان‌بندی‌شده دارید (مثل پاک کردن لاگ‌ها یا ارسال گزارش روزانه)، یک ردیف کرون دیگر لازم است:

/usr/local/bin/php /home/USER/public_html/artisan schedule:run >> /dev/null 2>&1

تسک‌ها در فایل routes/console.php یا bootstrap/app.php تعریف می‌شوند:

use Illuminate\Support\Facades\Schedule;

Schedule::command('sitemap:generate')->dailyAt('03:00');
Schedule::job(new App\Jobs\SyncProducts)->hourly()->withoutOverlapping();

withoutOverlapping() جلوگیری می‌کند که اجرای قبلی تمام نشده، اجرای بعدی شروع شود — همان چیزی که در کرون خام باید دستی با قفل فایل مدیریت شود.

خطاهای رایج و رفع آن‌ها

job به صف می‌رود ولی هیچ اتفاقی نمی‌افتد

این پرتکرارترین مشکل است و تقریباً همیشه یکی از این چهار دلیل را دارد:

  1. کارگر صف اجرا نشده است. جدول jobs را ببینید؛ اگر ردیف‌ها بالا می‌روند و هیچ‌وقت کم نمی‌شوند، کارگری در حال اجرا نیست.
  2. QUEUE_CONNECTION در .env با محیط اجرای واقعی یکی نیست؛ پس از هر تغییر، php artisan config:clear لازم است.
  3. مسیر PHP در کرون اشتباه است؛ یک‌بار همان دستور را دستی در ترمینال اجرا کنید تا خطا را ببینید.
  4. job در صف دیگری است. اگر با onQueue('emails') فرستاده‌اید، کارگر هم باید با --queue=emails اجرا شود، وگرنه آن ردیف هرگز برداشته نمی‌شود.

job ناموفق: دیدن، تلاش مجدد و پاک کردن

php artisan queue:failed            # فهرست خطاها
php artisan queue:retry all         # تلاش مجدد همه
php artisan queue:retry 9f2c1d      # تلاش مجدد یک job
php artisan queue:forget 9f2c1d     # حذف یک job از فهرست ناموفق‌ها
php artisan queue:flush             # پاک کردن کل فهرست ناموفق‌ها
php artisan queue:prune-failed --hours=168   # پاکسازی قدیمی‌تر از یک هفته

اگر jobها پشت سر هم ناموفق می‌شوند، جدول failed_jobs را باز کنید؛ ستون exception پیام دقیق خطا را دارد. یکی از رایج‌ترین‌ها خطای حافظه است، چون کارگر queue:work کد را در حافظه نگه می‌دارد و در پردازش طولانی‌مدت ممکن است پر شود.

job تکراری اجرا می‌شود

دو دلیل متعارف دارد. اول، --tries بالا همراه با کاری که ذاتاً تکرارپذیر نیست (مثل کسر موجودی انبار)؛ اینجا مقدار tries را پایین بیاورید و اجرای job را idempotent بسازید — یعنی اگر دو بار اجرا شد، اثر دوباره‌ای نداشته باشد:

public function handle(): void
{
    // اگر قبلاً ارسال شده، دوباره انجام نده
    if ($this->order->confirmation_sent_at) {
        return;
    }

    Mail::to($this->order->user->email)->send(new OrderConfirmed($this->order));

    $this->order->forceFill(['confirmation_sent_at' => now()])->save();
}

دلیل دوم، اجرای همزمان دو کارگر روی یک صف بدون قفل است. برای کارهای حساس، میانی‌ور WithoutOverlapping روی خود job بگذارید.

بعد از هر انتشار (deploy) کارگر را بازخوانی کنید

کارگر صف کد را در حافظه دارد؛ اگر کد را به‌روزرسانی کنید ولی کارگر همچنان نسخه قدیمی را اجرا کند، خطاهای عجیب و غیرقابل بازتولید می‌بینید. در اسکریپت انتشار، این خط را داشته باشید:

php artisan queue:restart

این دستور کارگرها را بعد از پایان job جاری می‌بندد تا فرایند مدیریتی (supervisor یا کرون) آن‌ها را با کد جدید بالا بیاورد.

پایش صف بدون ابزار پولی

لاراول خودش دو ابزار ساده دارد. اول queue:monitor که وقتی حجم صف از حد مشخصی بگذرد هشدار می‌دهد:

php artisan queue:monitor emails:100,default:500

دوم، ثبت هشدار در زمان‌بندی، تا اگر صف از حد گذشت ایمیل یا پیام بفرستد:

Schedule::command('queue:monitor default:500')->everyFiveMinutes();

ابزار کامل‌تر Horizon است که داشبورد و آمار دقیق می‌دهد، ولی به redis نیاز دارد؛ اگر درایور شما database است، همان دو دستور بالا با یک نگاه روزانه به جدول failed_jobs کفایت می‌کند.

پایش روزانهٔ صف لاراول و بررسی jobهای ناموفق
پایش روزانهٔ صف و جدول jobهای ناموفق، جلوی خرابی بی‌صدا را می‌گیرد.

نکات عملی

  • job را کوچک نگه دارید. یک job = یک کار. چند کار در یک job یعنی یک خطا همه را متوقف می‌کند.
  • job را با Queue::fake تست کنید و در تست جداگانه خود job را اجرا کنید تا مطمئن شوید متد handle واقعاً کار می‌کند.
  • دیتای حساس را در job نگذارید. وقتی مدل پاس می‌دهید، لاراول شناسه را ذخیره می‌کند؛ اما اگر آرایه یا فایل پاس می‌دهید، دقت کنید که در جدول صف سریالایز و ذخیره می‌شود.
  • پیش از تولید انبوه، در محیط واقعی یک job آزمایشی بفرستید تا مسیر کرون، مسیر PHP و درایور را یک‌بار از ابتدا تا انتها ببینید.
  • اگر تازه در حال راه‌اندازی پروژه هستید، اول نصب لاراول روی ویندوز را انجام دهید و بعد صف را اضافه کنید؛ ترتیب کار خطاهای محیطی را کم می‌کند.

جمع‌بندی

صف لاراول در عمل سه تکه است: یک درایور که jobها را نگه می‌دارد، یک کلاس job که کار را انجام می‌دهد، و یک کارگر که آن را اجرا می‌کند. اگر هر سه تکه سر جای خودشان باشند، صف روی هاست اشتراکی هم بی‌نقص کار می‌کند؛ اگر یکی نباشد، jobها بی‌صدا در جدول jobs جمع می‌شوند و هیچ خطایی هم دیده نمی‌شود. اول درایور و مسیر کرون را قطعی کنید، بعد سراغ بهینه‌سازی بروید.

اشتراک‌گذاری مقاله:

سوالات متداول

نظرات کاربران

ارسال نظر جدید

حداقل ۱۰ و حداکثر ۱۰۰۰ کاراکتر

هنوز نظری ثبت نشده است

اولین نفری باشید که دیدگاه خود را می‌نویسد!

مطالب مرتبط

مقالات مشابه که ممکن است برایتان جالب باشد