انتقل للمحتوى
علي هيثم·TECH·يردّ خلال أقل من ساعة
  • الرئيسيةابدأ من هنا
  • الأعمالدراسات حالة، مشاريع
  • الخدماتما الذي سأبنيه لك
  • المدونةمقالات، سلاسل
  • التدريبدورات، مختبرات، دورات
  • عنّيالمهندس وراء هذا الموقع
تسجيل الدخولابدأ مشروع
علي هيثم · TECH
  • الرئيسية↗
  • الأعمال↗
  • الخدمات↗
  • المدونة↗
  • التدريب↗
  • عنّي↗
تسجيل الدخولابدأ مشروع
متاح
علي هيثم·TECH

استوديو هندسي. دمشق، GMT+3.

aliyosef.online

الاستوديو

  • الأعمال
  • المدونة
  • التدريب
  • عنّي
  • تواصل معي

البوابة

  • تسجيل الدخول
  • فتح تذكرة دعم
  • متابعة مشروع
  • حسابي

مصادر

  • الوثائق
  • حالة الأنظمة
  • سجلّ التغييرات
  • حزمة العلامة
  • الخصوصية
  • الشروط

النشرة

ملاحظات تقنية وأخبار، أسبوعياً. بدون حشو.

مجاني. إلغاء الاشتراك متى ما حبيت.

تابعني على
© 2026 علي هيثم يوسف. جميع الحقوق محفوظة.مبنيّ يدويًا بـ React 19.آخر نشر · 2026-05-08كل الأنظمة تعمل
دورةLaravel متعدّد المستأجرين↗
  1. الرئيسية/
  2. المدوّنة/
  3. تعدد المستأجرين
تعدد المستأجرين

Laravel متعدد المستأجرين، الجزء الثاني: queues لكل tenant

كيف Horizon بيتعامل مع عدالة الـ tenants لما واحد بحاول يستحوذ على الـ workers.

٢٠ تشرين الثاني ٢٠٢٥·11 دقيقة·بقلم علي يوسف
جزء منmulti-tenant-laravel→

جاري تحميل المقال…

شاركXLinkedInRSS
في هذه السلسلةالجزء 1 من 2
←نظرة عامة على السلسلةالتاليLaravel متعدد المستأجرين، الجزء الأول: خيارات الـ schema→
استلم المقال التالي في بريدك.مقال جديد كل أحد. هندسة بلغة واضحة. مجاني، يمكنك إلغاء الاشتراك في أي وقت.اشترك→
AY
علي هيثم يوسف

مهندس وكاتب. أبني منصات متعدّدة المستأجرين، محرّكات مزامنة، وأنظمة موثوقة بهدوء. أكثر من ٦٠٠ مشروع مُسلَّم منذ ٢٠١٥.

اشتركابدأ مشروعاً→

اقرأ بعدها

  • MT
    Series
    ٨ نيسان ٢٠٢٦·تعدد المستأجرين

    Laravel متعدد المستأجرين، الجزء الأول: خيارات الـ schema

    افتتاح السلسلة. Schema-per-tenant مقابل row-per-tenant، مسار الـ migration، والـ queries اللي انكسرت.

    12 دقيقة
  • FF
    ٣٠ آذار ٢٠٢٦·تعدد المستأجرين

    Feature flags لكل tenant بدون spaghetti

    ثلاث سنين تشغيل flags. البنية اللي قبلت توسّع. والبنية اللي ما قبلت.

    9 دقائق
  • TD
    ٢٢ آذار ٢٠٢٦·تعدد المستأجرين

    تصدير بيانات المستأجر بدون كسر الـ DB

    تصدير بمستوى GDPR على بنية مشتركة. الـ query plan، الـ throttle، والإيصال.

    8 دقائق
←العودة إلى كل المقالات

مستأجر واحد يرفع استيراداً من ٢٠٠٠٠٠ صفّ. الـ ٩٩ الآخرون يفتحون التطبيق وينتظرون انتهاء مزامنتهم اليوميّة. مع queue عالميّة واحدة، الـ ٩٩ ينتظرون. مع queues لكلّ مستأجر، المستأجر الصاخب يُبطئ نفسه فقط.

هذا هو الجزء الثاني من سلسلة Laravel متعدّد المستأجرين، عمّا يحدث للـ queues حين يكون لديك عملاء، لا مجرّد مستخدمين.

إعداد Horizon الافتراضيّ غير عادل بالتصميم#

من علبة الكرتون، يعطيك Horizon queue لكلّ اسم (default، emails، sync) و pool عمّال لكلّ queue. داخل queue، الوظائف FIFO. إن أضاف المستأجر A ٥٠٠٠٠ وظيفة مزامنة في ٠٩:٠٠ والمستأجر B ٢٠٠ في ٠٩:٠١، تجلس وظائف B خلف وظائف A. العمّال يطحنون عبر A أوّلاً.

هذا عادل إن فكّرت في الوظائف كوحدة العمل. غير عادل إن فكّرت في المستأجرين كوحدة العمل، وهو ما يجب أن تفعل، لأنّ عملاءك يفعلون.

ماذا يعني «العدل» هنا#

نريد ضماناً مثل:

كلّ مستأجر نشط يحقّق على الأقلّ N من وظائف التقدّم في الدقيقة، بغضّ النظر عن عمق queue أيّ مستأجر آخر.

round-robin عبر المستأجرين النشطين يأخذك معظم الطريق، مع تعديلَين:

  1. تخطّي المستأجرين الفارغين كي لا نضيّع دورات على مستأجرين بدون عمل في الـ queue.
  2. تحديد العمل لكلّ دورة كي لا يُحتجَز مستأجر لديه وظيفة واحدة في انتظار مستأجر يطحن حالياً عبر ٢٠٠.

شكل الـ dispatcher#

تحديداً، نقسّم الـ queue الأساسيّة حسب tenant id ونوجّه dispatcher مخصّصاً عبرها:

namespace App\Queue;

use Illuminate\Contracts\Queue\Job;
use Illuminate\Queue\Worker;

class TenantFairWorker extends Worker {
    private array $rotation = [];

    protected function getNextJob($connection, $queue): ?Job {
        $tenantQueues = $this->activeTenantQueues();
        if (empty($tenantQueues)) {
            return null;
        }

        // round-robin: انزع الرأس، انظر إليه، ادفعه إلى الذيل.
        $tenantQueue = array_shift($tenantQueues);
        $tenantQueues[] = $tenantQueue;
        $this->rotation = $tenantQueues;

        return $connection->pop($tenantQueue);
    }

    private function activeTenantQueues(): array {
        // عدّ فقط queues لها وظيفة واحدة على الأقلّ. مُخزَّن لـ ٢٥٠ مللي ثانية.
        return Cache::remember('tenant-queues:active', 0.25, function () {
            return Redis::keys('queues:tenant-*')
                ->filter(fn ($k) => Redis::llen($k) > 0)
                ->values()
                ->toArray();
        });
    }
}

بضع قرارات تستحقّ إخراجها:

اسم queue واحد لكلّ مستأجر، لا اتّصال واحد#

نسمّي الـ queues بـ tenant-{id} (مثل tenant-42). كلّها تعيش على نفس instance Redis تحت نفس الاتّصال. حدود المستأجر هي اسم الـ queue بحتاً. هذا يعني أنّنا لا نزال نحصل على مجموعة مقاييس واحدة، مجموعة لوحات واحدة، مكان واحد للنظر.

round-robin في العامل، لا في المنتج#

النسخ السابقة حاولت توزيع الوظائف عبر الـ queues بالتناسب مع «من يحتاج العمل أكثر». ذلك المنطق انتهى بتكرار عمل العامل. القطع الأنظف: المنتجون يدفعون دائماً إلى queue مستأجرهم، والعامل يقرّر من يُخدَم تالياً.

تخزين المجموعة النشطة مؤقّتاً#

تعداد مفاتيح Redis في كلّ pop وظيفة جيّد لعشرة queues، مؤلم لألفَين. ذاكرة مؤقّتة بـ ٢٥٠ مللي ثانية على المجموعة النشطة تمتصّ الحمل دون أن تجعل round-robin يبدو غير متساوٍ. (بمعدّل وظيفة ١٠٠/ث، تخدم ٢٥ وظيفة من نفس اللقطة قبل التحديث. هذا جيّد.)

ما هذا ليس

هذا ليس priority queue. لا ندَع المستأجرين «المهمّين» يقفزون في الصفّ عمداً. إن دفع مستأجر لـ throughput أعلى، يحصل على عمليّة Horizon منفصلة مع pool عمّال خاصّ بها. عزل الفئة المتميّزة قرار نشر، لا قرار تشغيل.

قاعدة «الحدّ لكلّ دورة»#

round-robin وحده لديه مرض دقيق: إن كانت وظيفة مستأجر سريعة (ميكروثوانٍ) وأخرى بطيئة (ثوانٍ)، البطيء يجوع. الحدّ يصلح هذا. بعد خدمة K وظائف من نفس المستأجر في دوران واحد، يفرض العامل دوراناً بغضّ النظر عمّا إذا كان الـ queue فارغاً:

protected int $maxConsecutiveJobsPerTenant = 5;

private string $lastTenantQueue = '';
private int $consecutive = 0;

protected function shouldForceRotate(string $tenantQueue): bool {
    if ($tenantQueue === $this->lastTenantQueue) {
        $this->consecutive++;
        return $this->consecutive >= $this->maxConsecutiveJobsPerTenant;
    }
    $this->lastTenantQueue = $tenantQueue;
    $this->consecutive = 1;
    return false;
}

K = ٥ كانت النقطة المثاليّة لنا. K = ١ تخبّط لأنّ pop Redis ليس مجّاناً، و K = ٥٠ جوّع المستأجرين الصغار حين يكون كبير في منتصف وظيفة.

أوضاع فشل لا يذكرها الكتاب#

مستأجر تفشل وظائفه دائماً#

بدون تدخّل، مستأجر تفشل وظائفه كلّها وتُعاد محاولتها سيهيمن على المجموعة النشطة: في كلّ دورة، queue الخاصّ به لديه عمل، فيستمرّ round-robin بتضمينه، والوظيفة الفاشلة تُعاد محاولتها للأبد.

الإصلاح: آليّة حجر صحّيّ لكلّ مستأجر. إن فشلت أكثر من X وظيفة من مستأجر في Y دقيقة، ينتقل queue الخاصّ به إلى «مسار بطيء» يُؤخذ منه عيّنة بمعدّل ١/١٠. العملاء الذين يبلّغون عن مشاكل يصبحون فجأة هم من يُخدَم، بينما يتوقّف مستأجرو حلقات الفشل الهادئة عن الاحتكار.

مستأجر بوظيفة واحدة كبيرة جدّاً#

استيراد ٢٠٠٠٠٠ صفّ يعمل كوظيفة واحدة يحجب العامل طوال الاستيراد. حتّى مع cap-per-cycle، «وظيفة واحدة» تعني «الحدّ لا يُفعَّل». الإصلاح في جانب المنتج: الاستيرادات الكبيرة يجب تقسيمها إلى وظائف أصغر كثيرة لحظة الإرسال. نفرض هذا بـ wrapper:

class TenantImport implements ShouldQueue {
    public int $maxRowsPerJob = 500;

    public function handle() {
        if ($this->rows->count() > $this->maxRowsPerJob) {
            // قطّع نفسك إلى نسخ أصغر.
            foreach ($this->rows->chunk($this->maxRowsPerJob) as $chunk) {
                static::dispatch($chunk)->onQueue("tenant-{$this->tenantId}");
            }
            return;
        }
        $this->processChunk($this->rows);
    }
}

لوحات معلومات تكذب عليك#

مقاييس Horizon تجمع الوظائف عبر كلّ الـ queues. مع queues لكلّ مستأجر، ذلك المجموع يخفي التوزيع. أضفنا عرضاً مستأجراً بمستأجر: الوظائف الجارية، معدّل الفشل، p99 وقت الانتظار. بدونه، «الـ queue سليم» يمكن أن يكون صحيحاً في المتوسّط وخاطئاً للعميل المحدّد الذي يتّصل.

ما اشتراه لنا هذا#

مستأجر «جار صاخب» لم يعد يؤثّر على الجميع. التصعيد الذي كان يأتي كـ «التطبيق كلّه بطيء» يأتي الآن كـ «نحن بطيئون اليوم، كلّ مستأجر آخر بخير» — وهو تشخيص أسرع بكثير.

الكلفة كانت قرابة أسبوع من الهندسة بالإضافة إلى الانضباط التشغيليّ لتقطيع الوظائف المُرسَلة. يستحقّ من اليوم الأوّل.

الجزء التالي عن feature flags لكلّ مستأجر، البنية التي تتسع لأكثر من أربعين flag والبنية التي لا تفعل.

صفحة السلسلة تتابع البقيّة.