مقاله

Redis Queue و Laravel Horizon — راه‌اندازی production

Redis Queue و Laravel Horizon — راه‌اندازی production

تنظیم Redis به‌عنوان queue driver، نصب Horizon، dashboard، worker scaling و نکات deploy روی Ubuntu.

database queue برای شروع خوب است؛ وقتی حجم jobها بالا می‌رود یا چند نوع کار با اولویت متفاوت دارید، Redis + Horizon ترکیب رایج و رسمی Laravel برای اجرای صف است. Horizon dashboard زنده، retry، throughput و failed jobs — بدون حدس زدن «worker زنده است یا نه».

در این مقاله: Redis نقش ذخیره‌گاه صف را دارد و Horizon مدیر workerها و ابزار پایش آن است. تمرکز این راهنما روی اجرای روزمرهٔ صف Redis با Horizon است: پیکربندی supervisorها، جداسازی صف‌ها، خواندن داشبورد، تشخیص علت تأخیر و روال درست deploy. نحوهٔ dispatch کردن job در راهنمای dispatch و بازیابی jobهای شکست‌خورده در راهنمای failed jobs جدا آمده است.

چرا Redis برای Queue؟

  • سریع in-memory
  • atomic pop/push
  • همان Redis برای cache — دو نقش
  • Horizon فقط روی redis driver

تنظیم

REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
QUEUE_CONNECTION=redis
// config/queue.php — connection redis
'redis' => [
    'driver' => 'redis',
    'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
    'queue' => env('REDIS_QUEUE', 'default'),
    'retry_after' => 90,
],

نکتهٔ مهم: retry_after یعنی اگر jobی بعد از این تعداد ثانیه هنوز تمام نشده باشد، Redis آن را دوباره در دسترس workerها قرار می‌دهد. پس timeout هر job و supervisor باید چند ثانیه کمتر از این مقدار باشد؛ وگرنه یک job طولانی ممکن است هم‌زمان دو بار اجرا شود.

نصب Horizon

composer require laravel/horizon
php artisan horizon:install
php artisan migrate

مسیر dashboard: /horizon — با Gate محافظت کنید.

Gate::define('viewHorizon', fn (User $user) => $user->is_admin);

بعد از horizon:install، این تعریف در متد gate() فایل app/Providers/HorizonServiceProvider.php قرار می‌گیرد. در محیط local داشبورد بدون این شرط باز می‌شود؛ در محیط‌های دیگر فقط کاربرانی که از gate عبور کنند دسترسی دارند. چون نسخه‌ها و فایل‌های منتشرشده ممکن است تغییر کنند، قبل از نصب مستندات Horizon را برای نسخهٔ Laravel پروژه‌تان بررسی کنید.

Horizon چه چیزی به queue:work اضافه می‌کند؟

queue:work به‌تنهایی یک worker است که روی یک یا چند صف گوش می‌دهد؛ تعداد workerها، توزیع آن‌ها و پایششان با خودتان است. Horizon روی همان سیستم صف Laravel کار می‌کند و این‌ها را اضافه می‌کند:

  • پیکربندی در کد: تعداد و نوع workerها برای هر محیط در config/horizon.php تعریف می‌شود و همراه پروژه version control می‌شود
  • مدیریت پروسه‌ها: یک پروسهٔ اصلی، supervisorها و workerهای زیرمجموعه را بالا می‌آورد و در صورت نیاز تعدادشان را بین صف‌ها جابه‌جا می‌کند
  • داشبورد و متریک: زمان انتظار صف‌ها، throughput، زمان اجرای jobها، jobهای اخیر و شکست‌خورده
  • هشدار: اعلان وقتی زمان انتظار یک صف از آستانه‌ای که تعریف کرده‌اید بیشتر شود

Horizon فقط با connection از نوع redis کار می‌کند.

اجرای Horizon

php artisan horizon

جایگزین queue:work برای redis — خودش workerها را balance می‌کند.

دستورهای عملیاتی پرکاربرد:

php artisan horizon:status     # در حال اجرا، متوقف یا غیرفعال
php artisan horizon:pause      # توقف موقت پردازش
php artisan horizon:continue   # ادامهٔ پردازش
php artisan horizon:terminate  # خروج graceful بعد از اتمام jobهای جاری

Supervisor production

[program:horizon]
command=php /var/www/app/artisan horizon
autostart=true
autorestart=true
user=www-data

بعد از deploy: php artisan horizon:terminate — graceful restart.

در فایل Supervisor معمولاً stopwaitsecs را بیشتر از طولانی‌ترین زمان اجرای job تنظیم می‌کنند تا Supervisor پروسه را وسط کار نکشد، و خروجی را با stdout_logfile به یک فایل لاگ می‌فرستند. جزئیات راه‌اندازی سرور در راهنمای deploy روی Ubuntu آمده است.

چند queue

// config/horizon.php environments
'supervisor-1' => [
    'queue' => ['default', 'emails'],
    'balance' => 'auto',
    'maxProcesses' => 10,
],

job سنگین queue جدا — onQueue('videos') در Job.

نام صف‌ها و جداسازی بار کاری

عدد maxProcesses در مثال بالا فقط نمونه است؛ تعداد مناسب به حافظه و CPU سرور، مدت اجرای jobها و وابستگی‌های بیرونی بستگی دارد و باید با مشاهدهٔ متریک‌ها تنظیم شود. چند اصل برای جداسازی:

  • صف‌ها را بر اساس رفتار نام‌گذاری کنید، نه کلاس job: مثلاً notifications برای کارهای کوتاه و حساس به تأخیر، reports یا media برای کارهای طولانی
  • کارهای طولانی را در supervisor جدا با timeout مناسب خودشان اجرا کنید تا صف‌های کوتاه پشت آن‌ها گیر نکنند
  • ترتیب صف‌ها در آرایهٔ queue اولویت را تعیین می‌کند؛ در حالت balance با مقدار auto، Horizon تعداد پروسه‌ها را بر اساس حجم و زمان انتظار هر صف جابه‌جا می‌کند و minProcesses و maxProcesses حد پایین و بالای آن هستند
  • اگر jobی به سرویس بیرونی با محدودیت نرخ وابسته است، افزایش worker لزوماً سرعت را بیشتر نمی‌کند و ممکن است خطاها را بیشتر کند

مانیتورینگ

Horizon: wait time، jobs per minute، failed. هشدار اگر queue انباشته شد.

هر بخش داشبورد به یک سؤال جواب می‌دهد:

  • Dashboard: آیا Horizon فعال است، چند پروسه در حال کار است، و بیشترین زمان انتظار کدام صف است؟
  • Metrics: throughput و زمان اجرای هر job و هر صف در طول زمان — برای دیدن اینکه بعد از یک deploy، jobی کندتر شده یا نه. نمودارها از snapshotهای دوره‌ای ساخته می‌شوند، پس horizon:snapshot باید در scheduler اجرا شود:
    // routes/console.php
    Schedule::command('horizon:snapshot')->everyFiveMinutes();
  • Pending / Completed / Failed: jobهای در انتظار، تمام‌شده و شکست‌خورده با جزئیات payload و exception
  • Monitoring (tags): دنبال کردن jobهای مرتبط با یک تگ مشخص، مثلاً یک مدل خاص

برای هشدار، آستانهٔ زمان انتظار هر صف را در بخش waits فایل config/horizon.php تعیین کنید و مقصد اعلان (ایمیل، Slack یا SMS) را در HorizonServiceProvider تنظیم کنید.

jobها دیر اجرا می‌شوند — مشکل کجاست؟

«صف کند است» می‌تواند چهار مشکل کاملاً متفاوت باشد. داشبورد کمک می‌کند آن‌ها را از هم جدا کنید:

  • انباشت صف (backlog): jobها در Pending جمع می‌شوند و زمان انتظار بالا می‌رود، ولی workerها فعال‌اند و jobها تمام می‌شوند. یعنی سرعت ورود از سرعت پردازش بیشتر است — یا jobهای طولانی صف را اشغال کرده‌اند. جداسازی صف یا تنظیم ظرفیت را بررسی کنید.
  • worker متوقف یا غیرفعال: horizon:status وضعیت غیرفعال یا متوقف نشان می‌دهد، تعداد پروسه‌ها صفر است، یا Horizon بعد از horizon:pause هرگز ادامه داده نشده است. لاگ Supervisor و اتصال به Redis را بررسی کنید.
  • شکست job: jobها پردازش می‌شوند ولی در بخش Failed ظاهر می‌شوند. این دیگر مسئلهٔ ظرفیت نیست؛ بررسی exception و بازیابی کنترل‌شده در عیب‌یابی و retry در failed jobs آمده است.
  • dispatch نادرست: job اصلاً در Pending دیده نمی‌شود. معمولاً QUEUE_CONNECTION روی سرور redis نیست (مثلاً sync یا database)، نام صف با صف‌هایی که supervisorها گوش می‌دهند یکی نیست، یا job داخل transaction با afterCommit dispatch شده و transaction برگشت خورده است.

deploy و هماهنگی تنظیمات

پروسه‌های Horizon کد و config را هنگام شروع بارگذاری می‌کنند؛ پس بعد از هر deploy — و بعد از هر تغییر در config/horizon.php یا .env — باید با php artisan horizon:terminate اجازه دهید jobهای جاری تمام شوند و Supervisor پروسه را با کد جدید بالا بیاورد. اگر config:cache اجرا می‌کنید، آن را قبل از terminate انجام دهید.

چند مورد که باید بین محیط‌ها یکسان یا عمداً متفاوت باشد:

  • APP_ENV روی سرور باید با یکی از کلیدهای environments در config Horizon تطابق داشته باشد؛ در غیر این صورت ممکن است supervisor مورد انتظار بالا نیاید
  • اگر چند پروژه یا چند محیط از یک Redis مشترک استفاده می‌کنند، prefix کلیدها (مثلاً HORIZON_PREFIX و prefix Redis) باید متفاوت باشد تا صف‌ها و متریک‌ها قاطی نشوند
  • همهٔ سرورهایی که Horizon اجرا می‌کنند باید به همان Redis و همان نسخهٔ کد دسترسی داشته باشند

Horizon چه کاری را خودش حل نمی‌کند؟

  • کد کند یا کوئری سنگین داخل job را سریع نمی‌کند — فقط نشان می‌دهد کدام job کند است
  • job غیر idempotent را برای retry امن نمی‌کند
  • اگر Redis داده‌ها را از دست بدهد (مثلاً بدون persistence مناسب)، jobهای در صف را برنمی‌گرداند
  • جای پایش سرور (حافظه، CPU، دیسک) و پایش خطای برنامه را نمی‌گیرد
  • اشتباه در dispatch یا نام صف را خودکار اصلاح نمی‌کند

اشتباهات

  • Horizon و queue:work همزمان روی یک queue
  • فراموش horizon:terminate بعد از deploy
  • Redis بدون persistence در داده حیاتی — AOF/RDB
  • dashboard عمومی بدون auth
  • timeout supervisor بیشتر از retry_after connection
  • زمان‌بندی نکردن horizon:snapshot و خالی ماندن نمودارهای Metrics

جمع‌بندی

production queue = redis + horizon + supervisor. مقدمه: Queue. خطا: failed jobs.

Horizon را ابزار دیدن و مدیریت ظرفیت صف بدانید: صف‌ها را بر اساس رفتار jobها جدا کنید، با متریک‌ها ظرفیت را تنظیم کنید، قبل از تصمیم بین backlog، worker متوقف، شکست job و dispatch نادرست تفاوت بگذارید، و بعد از هر deploy روال terminate را فراموش نکنید.

Deploy queue

اشتراک‌گذاری