Laravel متعدد المستأجرين، الجزء الأول: خيارات الـ schema
افتتاح السلسلة. Schema-per-tenant مقابل row-per-tenant، مسار الـ migration، والـ queries اللي انكسرت.
جزء منmulti-tenant-laravelجاري تحميل المقال…
افتتاح السلسلة. Schema-per-tenant مقابل row-per-tenant، مسار الـ migration، والـ queries اللي انكسرت.
جزء منmulti-tenant-laravelجاري تحميل المقال…
هناك طريقتان لإضافة تعدّد المستأجرين إلى تطبيق Laravel، والاختيار الذي تتّخذه في الأسبوع الأوّل يُشكّل السنوات الثلاث القادمة. هذا هو الجزء الأوّل من أربعة عن تشغيل Laravel متعدّد المستأجرين بصدق. نبدأ بالـ schema لأنّ الـ schema يقرّر كلّ شيء آخر تقريباً: كيف تعمل الترحيلات، كيف تعمل النسخ الاحتياطيّة، كيف يقاس الأداء، كيف يُمنع التسرّب.
الأدبيّات تعطي إجابتَين، ومعظم الفرق تختار واحدة دون أن تدرك أنّها اختارت.
كلّ مستأجر يحصل على قاعدة بياناته الخاصّة (أو، في Postgres، schema خاصّ به داخل قاعدة بيانات مشتركة). النماذج تشير إلى connection يُختار وقت الطلب بناء على المستأجر النشط.
class TenantManager {
public function bootForTenant(Tenant $tenant): void {
config(['database.connections.tenant.database' => $tenant->database_name]);
DB::purge('tenant');
DB::reconnect('tenant');
}
}النتيجة: جدار صلب بين بيانات المستأجرين. أيّ استعلام في هذا الطلب يرى
صفوف ذلك المستأجر فقط. النسخ الاحتياطيّة لكلّ مستأجر. القياس لكلّ
مستأجر. لا يوجد عمود tenant_id على أيّ شيء.
كلّ المستأجرين يتشاركون قاعدة بيانات واحدة. كلّ جدول لديه عمود
tenant_id. كلّ استعلام مقصور بها. النماذج لديها global scope:
class TenantScope implements Scope {
public function apply(Builder $builder, Model $model): void {
$builder->where('tenant_id', auth()->user()->tenant_id);
}
}
abstract class TenantModel extends Model {
protected static function booted(): void {
static::addGlobalScope(new TenantScope());
static::creating(function ($model) {
$model->tenant_id ??= auth()->user()->tenant_id;
});
}
}النتيجة: قاعدة بيانات واحدة لإدارتها، لكنّ سلامة النظام تعتمد على مرور
كلّ استعلام عبر ذلك الـ scope. اتّصال واحد مفقود بـ withoutGlobalScopes()
ومستأجر يرى بيانات مستأجر آخر.
Schema لكلّ مستأجر له قصّة عزل نظيفة. لديه أيضاً مشكلة لا يحذّرك أحد منها: كلفة التشغيل تتراكم. إن كان لديك مئة مستأجر، لديك مئة قاعدة بيانات. كلّ منها يحتاج:
حين ينكسر schema لكلّ مستأجر، ينكسر لمستأجر واحد في كلّ مرّة، بينما يستمرّ الجميع. هذا جيّد. حين تنكسر الترحيلات، تنكسر لمستأجر واحد فقط، بينما تقضي اليوم التالي تُصالح. هذا سيّئ.
صفّ لكلّ مستأجر لا يوجد لديه تراكم تشغيليّ. هناك قاعدة بيانات واحدة،
مجموعة ترحيلات واحدة، سياسة نسخ احتياطيّ واحدة. المشكلة هي أنّ كلّ
استعلام يبعد scope مفقود واحد عن تسرّب، وعلى نطاق ينتهي tenant_id
في كلّ index، كلّ where، كلّ join.
لمنتج محاسبة يُشحَن للشركات الصغيرة والمتوسّطة، عدد المستأجرين بالمئات وينمو. عمليّات لكلّ مستأجر كانت ستأكل مهندساً بدوام كامل بحلول السنة الثانية. خطر التسرّب حقيقيّ لكنّه ميكانيكيّ، والمخاطر الميكانيكيّة يمكن تخفيفها ميكانيكيّاً:
TenantModel، و TenantModel
يضيف الـ global scope و hook الـ creating تلقائيّاً.TenantModel
ويستخدم جدولاً مرتبطاً بمستأجر. هذا الفحص هو الشيء الوحيد الذي
يلتقط بشكل ثابت النماذج الجديدة التي نسيت الفئة الأمّ.فحص التحليل الساكن هو الأكثر تقديراً ناقصاً من الثلاثة. مراجعة الكود ستفوّت هذا؛ الاختبارات ستفوّت هذا إن كان كاتب الاختبار قد ارتكب الخطأ أيضاً. الـ linter لا يفعل.
ثلاثة أعمدة، كلّ مرّة:
ALTER TABLE invoices
ADD COLUMN tenant_id BIGINT UNSIGNED NOT NULL,
ADD COLUMN created_by BIGINT UNSIGNED,
ADD COLUMN updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP;
CREATE INDEX invoices_tenant_id_idx ON invoices (tenant_id);
-- كلّ index من نوع "where هذا وذاك" يحصل على tenant_id كعمود قائد
CREATE INDEX invoices_tenant_status_idx ON invoices (tenant_id, status);
CREATE INDEX invoices_tenant_due_idx ON invoices (tenant_id, due_date);سياسة الـ index الثالثة هي التي تهمّ للأداء. index على status لا
يقود بـ tenant_id لا يمكن استخدامه من قِبَل استعلام مقصور بمستأجر
بدون خطوة فرز. أخطئ في هذا على جدول حارّ وستنهار خطط استعلامك حول
الشهر التاسع، حين لا يكون أيّ مستأجر فرديّ ضخماً لكنّ مجموع عدد
الصفوف ضخم.
ثلاثة أنماط ظللنا نصطدم بها:
إضافة عمود غير قابل للـ null إلى جدول مرتبط بمستأجر على قاعدة بيانات مشتركة يقفل الجدول طوال إعادة الكتابة. مع مئة مستأجر، يشعر بهذا القفل الجميع. الإصلاح هو رقصة expand-then-contract: شحن العمود قابل للـ null، backfill على دفعات، ثمّ إضافة القيد.
إعادة تسمية عمود على جدول مشترك هي إعادة هيكلة منسّقة عبر كلّ مستأجر في وقت واحد. النمط هو إضافة العمود الجديد، الكتابة المزدوجة من التطبيق لإصدار واحد، backfill، ثمّ إسقاط القديم. ثلاث عمليّات نشر، بدون downtime.
لا أحد يستخدمه على المستأجر ٤١ لأنّ المستأجر ٤١ صغير. المستأجر ٧ لديه استعلام لوحة معلومات يلمسه. الترحيل الذي يُسقطه يُسقط المستأجر ٧. الدرس: لا تنظّف «أعمدة غير مستخدمة» على schemas مشتركة، أبداً، دون البحث في سجلّ استعلامات كلّ مستأجر الأخير.
الجزء الثاني من هذه السلسلة عن الـ queues. تحديداً، ما يحدث حين يكون لمستأجر استيراد بمليون صفّ والـ ٩٩ الآخرون يريدون فقط أن تنتهي مزامنتهم اليوميّة في أقلّ من ثانية.
صفحة السلسلة تتابع البقيّة.