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 باafterCommitdispatch شده و 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_afterconnection - زمانبندی نکردن
horizon:snapshotو خالی ماندن نمودارهای Metrics
جمعبندی
production queue = redis + horizon + supervisor. مقدمه: Queue. خطا: failed jobs.
Horizon را ابزار دیدن و مدیریت ظرفیت صف بدانید: صفها را بر اساس رفتار jobها جدا کنید، با متریکها ظرفیت را تنظیم کنید، قبل از تصمیم بین backlog، worker متوقف، شکست job و dispatch نادرست تفاوت بگذارید، و بعد از هر deploy روال terminate را فراموش نکنید.