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

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

aliyosef.online

الاستوديو

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

البوابة

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

مصادر

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

النشرة

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

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

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

Feature flags لكل tenant بدون spaghetti

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

٣٠ آذار ٢٠٢٦·9 دقائق·بقلم علي يوسف

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

شاركXLinkedInRSS
استلم المقال التالي في بريدك.مقال جديد كل أحد. هندسة بلغة واضحة. مجاني، يمكنك إلغاء الاشتراك في أي وقت.اشترك→
AY
علي هيثم يوسف

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

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

اقرأ بعدها

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

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

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

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

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

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

    8 دقائق
  • AL
    ١٤ آذار ٢٠٢٦·تعدد المستأجرين

    Audit logs اللي بتتوسّع فعلياً

    Append-only، partitioned، queryable. الـ schema اللي وقفنا عندها بعد ما خسرنا أسبوعين بالإنتاج.

    11 دقيقة
←العودة إلى كل المقالات

feature flag هو سطر تفرّع منطقيّ احتاجه شخص ما مرّة. حين يكون لديك أربعون منها عبر codebase يخدم مئة مستأجر، «احتاجه مرّة» قد تراكم إلى شيء لا أحد يفهمه بالكامل. هذا المقال عن البنية التي منعت ذلك التراكم لنا، والبنية التي لم تفعل.

لماذا الـ feature flags فعلاً#

ثلاثة استخدامات صادقة، بترتيب صحّتها:

  1. طرح تدريجيّ. ميزة جديدة تُشحَن خلف flag، تُفعَّل لمستأجر واحد، ثمّ عشرة، ثمّ الجميع. الـ flag هو kill switch إن ساء شيء.
  2. قدرات لكلّ فئة. مستأجرو الفئة Pro يحصلون على bulk-export. مستأجرو الفئة المجّانيّة لا يحصلون. الـ flag هو إذن، لا صمّام إصدار.
  3. سلوك خاصّ بمستأجر. عميل معيّن يحتاج تعديلاً لا يريده الباقون. الـ flag ينحت زاوية.

أيّ شيء آخر، غالباً لا تحتاج flag. اختبار A/B مشكلته الخاصّة بأدواته الخاصّة. flags طويلة الأجل لا يجرؤ أحد على إزالتها هي المشكلة التي يدور حولها هذا المقال.

الشكل الذي اتّسع#

استقررنا على جدول واحد:

CREATE TABLE feature_flags (
    id              BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    key             VARCHAR(120) NOT NULL,           -- 'bulk_export'
    description     TEXT NOT NULL,                   -- السبب
    owner_team      VARCHAR(60) NOT NULL,            -- من يزيله
    rollout_kind    ENUM('global', 'plan', 'tenant', 'percent') NOT NULL,
    rollout_value   JSON NOT NULL,                   -- حمولة لكلّ نوع
    sunset_date     DATE NOT NULL,                   -- حكم الإعدام
    created_at      TIMESTAMP NOT NULL,
    UNIQUE KEY (key)
);

ثلاث تفاصيل تكسب قيمتها:

owner_team مطلوب#

flag بدون مالك هو flag لا يُزيله أحد. النسخة الأولى من هذا الجدول تركته قابلاً للـ null؛ انتهى بنا الأمر بثلاثين flag وبدون وضوح حول من يستطيع إيقاف أيّ منها. جعله NOT NULL وزرع «platform» كافتراضيّ أجبر المحادثة.

sunset_date مطلوب#

كلّ flag، حتّى الدائمة، لديه تاريخ يجب إعادة النظر فيه عنده. «إعادة النظر» يمكن أن تعني الإزالة، يمكن أن تعني التأكيد، يمكن أن تعني التمديد. المهمّ أنّ الجدول ليس للكتابة فقط. نشغّل وظيفة مجدولة تُرسل بريداً إلى المالكين قبل ١٤ يوماً من الـ sunset؛ إن مرّ flag بدون مراجعة، يحصل على tag STALE ويظهر باللون الأحمر على لوحة المنصّة.

rollout_kind و rollout_value مُطبَّعان#

كلّ flag هو أحد أربعة أشكال:

  • global — value: {"enabled": true}. boolean للجميع.
  • plan — value: {"plans": ["pro", "enterprise"]}. بوّابة فئة.
  • tenant — value: {"tenant_ids": [42, 113, 7]}. قائمة صريحة.
  • percent — value: {"percent": 25}. hash مستقرّ حسب tenant id.

بإجبار كلّ flag على أحد أربعة أشكال، حصلنا على شيئَين: دالّة تقييم واحدة بدلاً من أربعين، والقدرة على سرد «كلّ الـ flags المفعّلة حالياً للمستأجر ٤٢» باستعلام واحد.

دالّة التقييم#

namespace App\Features;

class Features {
    public function isOn(string $key, ?Tenant $tenant = null): bool {
        $flag = $this->cache->remember(
            "flag:{$key}",
            60,
            fn () => FeatureFlag::where('key', $key)->first()
        );
        if (!$flag) {
            return false;  // flag نسيناه يكون مُغلقاً
        }

        $value = $flag->rollout_value;
        $tenant ??= app(TenantContext::class)->current();

        return match ($flag->rollout_kind) {
            'global' => (bool) ($value['enabled'] ?? false),
            'plan'   => $tenant && in_array($tenant->plan, $value['plans'] ?? [], true),
            'tenant' => $tenant && in_array($tenant->id, $value['tenant_ids'] ?? [], true),
            'percent' => $tenant
                && (crc32("{$key}:{$tenant->id}") % 100) < ((int) ($value['percent'] ?? 0)),
        };
    }
}

ثلاثة أمور للملاحظة:

  1. الافتراضيّ مغلق. flag مفقود يعيد false. هذا يبدو بديهيّاً حتّى تدرك أنّ البديل هو «الافتراضيّ مفتوح كي لا نخفي ميزة في dev عن طريق الخطأ.» هذا إصدار في انتظار الحدوث.
  2. hash مستقرّ للطرح بالنسبة المئويّة. crc32("{key}:{tenant_id}") حتميّ لكلّ (flag، tenant). نفس المستأجر يحصل على نفس الإجابة كلّ مرّة، حتّى بعد إعادة التشغيل. هكذا تقوم بطرح تدريجيّ بدون قلب المستخدمين داخلاً وخارجاً.
  3. ذاكرة مؤقّتة لـ ٦٠ ثانية. تغييرات الـ flag تنتشر خلال دقيقة. إن احتجت انتشاراً فوريّاً (لا يجب، تقريباً أبداً)، تحتاج شيئاً آخر. cache miss في كلّ نداء يكلّف أكثر من دقيقة من البياتة.
الفحص هو الـ API

كود التطبيق ينادي Features::isOn('bulk_export') فقط. لا يعرف ما إذا كان هذا flag عالميّ، flag فئة، أو طرح بالنسبة المئويّة. فصل الفحص عن شكل الطرح هو ما يدعك تغيّر flag من «المستأجر ٤٢ فقط» إلى «كلّ خطط pro» دون لمس كود التطبيق.

الأنماط التي لم تتّسع#

جرّبنا بنيتَين انكسرتا حول العشرين flag.

env vars لكلّ ميزة#

FEATURE_BULK_EXPORT=true، FEATURE_DARK_MODE=enterprise، إلخ. عمل لفترة. فشل حين احتجنا بوّابة لكلّ مستأجر: env vars لا يستطيعون قول «المستأجر ٤٢ نعم، المستأجر ١١٣ لا» بدون مصفوفة، والمصفوفة أصبحت غير قابلة للقراءة. أيضاً، كلّ تغيير تطلّب نشراً.

blob إعدادات لكلّ مستأجر#

عمود config_overrides JSON على جدول tenants. عمل عند خمسة flags. عند خمسة عشر، كان العمود جداراً من {"feature_x": true, "feature_y": false, ...} لكلّ مستأجر، بدون مكان مركزيّ للسؤال «ماذا تعني feature_x» أو «من يملكها». البنية وضعت البيانات على المستأجر، لكنّ الـ flags تنتمي إلى الميزات، لا إلى المستأجرين.

طقس مساءلة صغير#

كلّ صباح جمعة، بوت Slack ينشر قائمة الـ flags التي يقع تاريخ sunset الخاصّ بها في الـ ٣٠ يوماً القادمة، مع مالكيها. المالك لديه أسبوع إمّا: تأكيد وتمديد، إزالة الـ flag (احذف الصفّ في الجدول مع نداءات Features::isOn)، أو تحويله إلى قدرة دائمة (أضِف إلى جدول الفئات).

في الشهر الأوّل الذي فعلنا فيه هذا، أزلنا تسعة flags. ثلاثة منها كانت مفعّلة لأكثر من سنة بدون اعتراضات. نصف هؤلاء كانوا ميزات «تجريبيّة» شُحنت منذ زمن طويل للجميع.

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

بعد ثمانية عشر شهراً على البنية الجديدة، كان لدينا ستّة وأربعون flag نشط. لوحة المعلومات تتسع لشاشة واحدة. flags جديدة تستغرق خمس دقائق للإضافة (إدخال + مرجع). flags قديمة تتقاعد حسب الجدول. لم يحتج أيّ مهندس للسؤال «ماذا تعني الميزة X» منذ أكثر من سنة.

الطقس أهمّ من schema الجدول. الـ schema مجرّد السطح الذي يجعل الطقس ممكناً.

الجزء التالي من سلسلة تعدّد المستأجرين عن صادرات بيانات المستأجرين بدون كسر قاعدة البيانات. صفحة السلسلة تتابع البقيّة.