تقنيات وتطبيق
هجرات قواعد البيانات بدون توقف عبر Alembic
١٥ أكتوبر ٢٠٢٦ · 8 دقائق قراءة
كابوس نشر التعديلات في منتصف الليل
الكثير من فرق التطوير تخشى هجرات قواعد البيانات (Database Migrations). والسيناريو المعتاد: إيقاف الموقع للصيانة، تشغيل alembic upgrade head، والانتظار مع الدعاء بألا يحجز التعديل قفل الجدول بالكامل (AccessExclusiveLock) ويتسبب في توقف جميع الاستعلامات الأخرى.
في الأنظمة الحديثة التي تعمل 24/7، لا وجود لما يسمى «نافذة صيانة مقبولة». يجب أن تُطبَّق الهجرات وقاعدة البيانات تحت الضغط الكامل بدون انقطاع أو فقدان لأي طلب.
1. الفخ الأكبر: حجز الأقفال وبناء الفهارس
عند إضافة فهرس عادي على جدول ضخم:
-- خطير جداً في بيئة الإنتاج: يحجز قفل القراءة والكتابة على الجدول بالكامل!
CREATE INDEX idx_users_email ON users(email);
إذا استغرق بناء الفهرس دقيقتين، فإن جميع طلبات تسجيل الدخول والبحث ستتوقف وتفشل لانتهاء المهلة (Timeout).
الحل في Alembic: بناء الفهرس بالتوازي (CONCURRENTLY)
يتطلب PostgreSQL عدم تشغيل أمر CONCURRENTLY داخل معاملة واحدة (transaction). لذلك نقوم بتعطيل المعاملة في ملف هجرة Alembic:
"""add concurrent index to users
Revision ID: 4a2b91c0e
"""
from alembic import op
# 1. إيقاف المعاملة الذرية التلقائية لهذه الهجرة
# non-atomic migration
commit_after_each_step = True
def upgrade() -> None:
# 2. بناء الفهرس بالتوازي دون تعطيل القراءة أو الكتابة
op.execute("COMMIT")
op.create_index(
"idx_users_email",
"users",
["email"],
postgresql_concurrently=True,
)
def downgrade() -> None:
op.execute("COMMIT")
op.drop_index("idx_users_email", table_name="users", postgresql_concurrently=True)
بهذا التعديل، يبني المحرك الفهرس في الخلفية بينما يواصل التطبيق خدمة المستخدمين بشكل طبيعي.
2. نمط التوسع والانكماش (Expand and Contract Pattern)
إذا أردت تغيير اسم عمود من full_name إلى display_name، أو تغيير نوعه: إياك أن تقوم بإعادة التسمية في خطوة واحدة! لأن النسخ القديمة من التطبيق ستفشل فوراً قبل اكتمال نشر الكود الجديد.
الحل عبر 3 مراحل متدرجة:
- المرحلة الأولى (Expand - التوسع):
- إضافة العمود الجديد
display_nameكعمود اختياري (Nullable). - كتابة البيانات إلى كلا العمودين في كود التطبيق الجديد، مع القراءة من القديم.
- إضافة العمود الجديد
- المرحلة الثانية (Migrate - الترحيل الخلفي):
- تشغيل سكربت خلفي على دفعات صغيرة لنقل البيانات القديمة إلى العمود الجديد دون تحميل قاعدة البيانات.
- المرحلة الثالثة (Contract - الانكماش):
- تحويل كود التطبيق للقراءة والكتابة من
display_nameفقط. - بعد التأكد من استقرار النظام، حذف العمود القديم
full_nameفي هجرة لاحقة.
- تحويل كود التطبيق للقراءة والكتابة من
3. إضافة أعمدة بقيم افتراضية (Safe Defaults)
في إصدارات PostgreSQL 11 وما بعدها، إضافة عمود بقيمة افتراضية ثابتة (DEFAULT 'active') لم يعد يعيد كتابة الجدول بالكامل، بل يضاف إلى البيانات الوصفية (Metadata) فقط في زمن أجزاء من الملي ثانية.
لكن انتبه: تجنب استخدام القيم الافتراضية الديناميكية الدالة (مثل DEFAULT clock_timestamp()) على جداول ضخمة لأنها تجبر المحرك على فحص وتحديث كل صف.
الخلاصة
الهجرة بدون توقف ليست مجرد أداة برمجية، بل هي انضباط معماري: تجنب الأقفال الحصرية عبر CONCURRENTLY، واستخدم نمط التوسع والانكماش للتغييرات الكبيرة، وتأكد من أن كودك وقاعدة بياناتك متوافقان مع خطوة سابقة وخطوة لاحقة في كل نشر.