قواعد البيانات في الإنتاج

ترحيل المخطط دون توقف

22 دقيقة الدرس 5 من 30

ترحيل المخطط دون توقف

كل مخطط قاعدة بيانات إنتاجي يحتاج في النهاية إلى تغيير. إضافة عمود، إعادة تسمية جدول، حذف مؤشر، أو تقسيم حقل — كلها عمليات روتينية خلال تطوير المنتج. النهج الساذج هو إيقاف التطبيق، تطبيق الترحيل، ثم إعادة التشغيل. هذا مقبول لمشروع شخصي بلا مستخدمين. أما على نطاق التقنية الكبرى، حيث تتطلب SLOs توافراً بنسبة 99.9% وأكثر وحركة مرور تبلغ الملايين من الطلبات في الدقيقة، فإن أي تغيير في المخطط يُقفل جدولاً لثوانٍ يُحوَّل إلى حادثة تمس العملاء مباشرة.

ترحيل المخطط دون توقف ليس أداةً واحدة — بل هو منهجية مبنية على ثلاثة أفكار متشابكة: نمط التوسع والتقليص، وDDL الآني (دعم محرك قاعدة البيانات للتغييرات الهيكلية غير المُعيقة)، وأدوات الترحيل المدمجة في خطوط CI/CD. اتقن الثلاثة وستتمكن من تطوير مخطط قاعدة البيانات باستمرار وأمان، دون الحاجة إلى تنسيق نوافذ صيانة.

لماذا تقتل عمليات الترحيل الساذجة التوافر؟

تأخذ كل من MySQL وPostgreSQL أقفالاً على مستوى بيانات الجدول أثناء عمليات DDL. قد يمسك أمر ALTER TABLE orders ADD COLUMN loyalty_points INT على جدول بـ 500 مليون صف قفلاً حصرياً لدقائق. كل استعلام يصل إلى هذا الجدول — قراءات وكتابات — يُصطف خلف القفل. تمتلئ مجموعة الاتصالات. يُعيد التطبيق 503. يُنبَّه المهندس المناوب في الساعتين صباحاً.

حتى DDL "السريع" له مزالق خفية. إضافة عمود بقيمة DEFAULT في MySQL القديمة (قبل 8.0) تُعيد كتابة الجدول بأكمله. إعادة تسمية عمود تُعطّل أي كود تطبيق لا يزال يستخدم الاسم القديم — وفي النشر المتدرج، تعمل الـ pods القديمة والجديدة في آنٍ واحد. هذه هي المشكلة الجوهرية التي يحلها نمط التوسع والتقليص.

فخ النشر المتدرج: أثناء التحديث المتدرج في Kubernetes، تعمل pods التطبيق القديمة والجديدة بشكل متزامن لمدة 30–120 ثانية. إذا أعاد ترحيلك تسمية عمود من user_id إلى account_id، تكتب الـ pods القديمة إلى user_id (الذي لم يعد موجوداً)، وتكتب الجديدة إلى account_id. تفشل كلتا مجموعتي الكتابة. تحدث أخطاء وفقدان بيانات حتى لو بدت كل قطعة صحيحة منفردةً.

نمط التوسع والتقليص

يُفكّك نمط التوسع والتقليص (المعروف أيضاً بالتغيير الموازي أو ترحيل المخطط الأزرق-الأخضر) كلَّ تغيير مخطط كاسر إلى ثلاث مراحل نشر على الأقل، كل منها متوافقة مع رجوعية كود التطبيق الحي:

  1. التوسع: أضف البنية الجديدة بجانب القديمة. أضف account_id كعمود قابل للقيمة الفارغة. انشر كوداً يكتب في كلا العمودين ويقرأ من القديم. العمود القديم لا يزال هو المرجع؛ لا يُفقد أي بيانات.
  2. ترحيل البيانات: امأل account_id من user_id على دُفعات (لا تستخدم أبداً أمر UPDATE واحداً على الجدول كله). بعد الملء، انشر كوداً يقرأ من account_id. أصبح كلا العمودين الآن متزامنَين.
  3. التقليص: بعد تأكد 100% من الـ pods أنها على الكود الجديد وتأكيد المقاييس عدم قراءة العمود القديم، احذف user_id. هذا الآن DDL آمن وسريع لأنه لا يوجد كود حي يستخدمه.
نمط التوسع والتقليص عبر ثلاث مراحل نشر Phase 1: Expand Phase 2: Migrate Phase 3: Contract DB Schema user_id | account_id (NULL) App Code كتابة → كلا العمودين قراءة → user_id DB Schema user_id | account_id (مملوء) App Code كتابة → كلا العمودين قراءة → account_id Batch Backfill Job ينقل user_id → account_id DB Schema account_id (user_id محذوف) App Code كتابة → account_id فقط قراءة → account_id فقط
نمط التوسع والتقليص: كل مرحلة قابلة للنشر بشكل مستقل ومتوافقة مع الإصدار السابق من التطبيق.

الفكرة الجوهرية هي أن مخطط قاعدة البيانات وإصدار كود التطبيق مفصولان. لا حاجة أبداً لنشرهما في آنٍ واحد. كل مرحلة قابلة للتراجع بشكل مستقل، ولأن إصدارات التطبيق القديمة والجديدة دائماً متوافقة مع المخطط الحالي، فإن التراجع في أي مرحلة آمن.

ابدأ دائماً التوسع بعمود قابل للقيمة الفارغة، لا بـ NOT NULL مع قيمة افتراضية. عمود NOT NULL DEFAULT 0 في MySQL قبل 8.0 يُجري إعادة كتابة كاملة للجدول. في PostgreSQL 11+، NOT NULL DEFAULT بقيمة ثابتة يكون فورياً (بلا إعادة كتابة). اعرف إصدار محركك قبل اختيار صيغة DDL.

DDL الآني: التغييرات غير المعيقة على مستوى المحرك

تمتلك محركات قواعد البيانات الحديثة آليات مدمجة لإجراء التغييرات الهيكلية دون تعطيل القراءات والكتابات المتزامنة. فهم هذه الآليات ضروري للتنبؤ بما إذا كان الترحيل آمناً أم سيُسبب انقطاعاً.

MySQL / InnoDB Online DDL (MySQL 5.6+، 8.0): معظم عمليات ALTER TABLE تدعم ALGORITHM=INPLACE, LOCK=NONE، التي تُطبق التغييرات في مكانها مع الاستمرار في خدمة حركة المرور. ليست كل العمليات تدعم ذلك — حذف المفتاح الأساسي أو تغيير مجموعة أحرف العمود لا يزال يتطلب نسخة كاملة. دائماً اختبر باستخدام EXPLAIN FORMAT=TREE ALTER TABLE ... أو راجع مصفوفة توثيق MySQL.

pt-online-schema-change (pt-osc) وgh-ost أدوات تُطبق DDL الآني على مستوى طبقة التطبيق للحالات التي لا يستطيع فيها المحرك القيام بذلك نيتاً، أو حيث تحتاج إلى مزيد من التحكم (التقييد، الترحيل القابل للإيقاف المؤقت). gh-ost (أداة GitHub) هو المعيار الصناعي للجداول الكبيرة في MySQL — يستخدم دفق السجل الثنائي لإعادة تطبيق الكتابات على جدول ظل دون محفزات، ثم يُعيد تسمية الجداول ذرياً عند التحويل.

PostgreSQL لديه المُعدِّل CONCURRENTLY لعمليات المؤشر: CREATE INDEX CONCURRENTLY يبني المؤشر دون قفل حصري، بتكلفة وقت أطول. PostgreSQL 12+ أضاف CREATE MATERIALIZED VIEW CONCURRENTLY. لكن ALTER TABLE ADD COLUMN NOT NULL العادي بلا قيمة افتراضية من جانب الخادم لا يزال يتطلب مسح الجدول كاملاً في الإصدارات القديمة.

-- MySQL: تحقق من استخدام INPLACE أو COPY قبل النشر في الإنتاج ALTER TABLE orders ADD COLUMN loyalty_points INT NULL DEFAULT 0, ALGORITHM=INPLACE, LOCK=NONE; -- إذا فشل الأمر أعلاه بـ "ALGORITHM=INPLACE not supported"، استخدم gh-ost: gh-ost \ --user="admin" \ --password="$DB_PASS" \ --host="primary.prod.internal" \ --database="orders" \ --table="orders" \ --alter="ADD COLUMN loyalty_points INT NULL DEFAULT 0" \ --allow-on-master \ --chunk-size=1000 \ --max-lag-millis=1500 \ --throttle-control-replicas="replica1.prod.internal,replica2.prod.internal" \ --execute -- PostgreSQL: إضافة مؤشر دون قفل CREATE INDEX CONCURRENTLY idx_orders_user_id ON orders (user_id) WHERE status = 'active'; -- PostgreSQL: إضافة عمود قابل للقيمة الفارغة بأمان (فوري في PG 11+) ALTER TABLE orders ADD COLUMN loyalty_points INT; UPDATE orders SET loyalty_points = 0 WHERE loyalty_points IS NULL; -- تُنفَّذ خارجياً على دُفعات ALTER TABLE orders ALTER COLUMN loyalty_points SET NOT NULL;

الملء على دُفعات: الطريقة الأكثر أماناً لنقل البيانات

خلال مرحلة التوسع قد تحتاج إلى تعبئة عمود جديد من بيانات موجودة. لا تُشغّل أبداً UPDATE orders SET account_id = user_id — على جدول كبير هذا يأخذ قفل صف حصري على كل صف في آنٍ واحد، يُشبع تأخير النسخ المتماثل، ويستمر لساعات. استخدم حلقة ملء دُفعي بدلاً من ذلك:

#!/usr/bin/env bash # backfill_account_id.sh — يعمل كـ Kubernetes Job أو cron منفرد # يعالج 1000 صف لكل تكرار مع نوم قصير للتقييد BATCH=1000 SLEEP=0.1 MAX_ID=$(mysql -u admin -p"$DB_PASS" -sNe "SELECT MAX(id) FROM orders" orders_db) CURSOR=0 while [ "$CURSOR" -lt "$MAX_ID" ]; do NEXT=$((CURSOR + BATCH)) mysql -u admin -p"$DB_PASS" orders_db <<SQL UPDATE orders SET account_id = user_id WHERE id > $CURSOR AND id <= $NEXT AND account_id IS NULL; SQL echo "Backfilled rows $CURSOR to $NEXT" CURSOR=$NEXT sleep "$SLEEP" done echo "Backfill complete."
راقب تأخير النسخ المتماثل أثناء الملء. حتى أوامر UPDATE المُقسَّمة على دُفعات تُنسخ إلى النسخ المتماثلة للقراءة وقد تُسبب ارتفاع التأخير. اضبط حد تأخير (مثل الإيقاف المؤقت إذا تجاوز التأخير 2 ثانية) باستخدام SHOW SLAVE STATUS أو pt-heartbeat. gh-ost لديه هذا مدمجاً عبر --max-lag-millis.

أدوات الترحيل في خطوط CI/CD

عمليات الترحيل المنفصلة التي تُشغَّل يدوياً هي مصدر للانجراف والأخطاء والحالة غير الموثقة. المعيار الصناعي هو إصدار التحكم في كل ترحيل وتشغيله تلقائياً عبر خط الأنابيب، مع ضوابط تُفرز السلامة في كل مرحلة.

أدوات الترحيل الشائعة حسب البيئة:

  • Flyway (Java، SQL) — ترحيلات إصدارية مبنية على الملفات (V1__add_column.sql). يتكامل مع Maven وGradle وGitHub Actions. يمتلك وضع dryRun للمراجعة المسبقة.
  • Liquibase (Java، XML/YAML/SQL) — أغنى من Flyway؛ يدعم سكريبتات التراجع والشروط المسبقة وتصنيف مجموعات التغيير لاستهداف البيئات.
  • Atlas (Go، HCL) — المخطط ككود؛ يولد خطة الترحيل من الفارق بين المخططات، يتكامل مع Terraform، ولديه واجهة برمجية سحابية للتدقيق.
  • golang-migrate — CLI + مكتبة Go؛ طبيعي لخدمات Go المصغرة مع ملفات ترحيل صاعدة/نازلة.
  • Alembic (Python) — أداة ترحيل SQLAlchemy؛ تُولّد الترحيلات تلقائياً بمقارنة نماذج ORM بالمخطط الحي.

يتبع خط أنابيب CI جاهز للإنتاج لترحيل قواعد البيانات هذا التسلسل:

# .github/workflows/deploy.yml (مقتطف) # يُشغّل هذا الخط الترحيلات قبل نشر pods التطبيق الجديدة # (مرحلة التوسع). التحديث المتدرج للتطبيق يأتي بعده. jobs: migrate: runs-on: ubuntu-latest environment: production steps: - uses: actions/checkout@v4 # 1. التدقيق: رفض العمليات الخطرة قبل تشغيلها - name: Atlas migration lint run: | atlas migrate lint \ --dev-url "mysql://root:password@localhost:3306/dev" \ --dir "file://migrations" \ --format "{{ range .Files }}{{ .Name }}: {{ range .Reports }}{{ .Text }}{{ end }}{{ end }}" # 2. تشغيل تجريبي على نسخة مستنسخة من الإنتاج - name: Flyway dry-run on prod clone run: | flyway \ -url="jdbc:mysql://$PROD_CLONE_HOST:3306/orders" \ -user="$DB_USER" \ -password="$DB_PASS" \ -dryRunOutput="/tmp/migration-preview.sql" \ migrate cat /tmp/migration-preview.sql # 3. تطبيق على الإنتاج (قبل نشر التطبيق) - name: Apply migration run: | flyway \ -url="jdbc:mysql://$PROD_DB_HOST:3306/orders" \ -user="$DB_USER" \ -password="$DB_PASS" \ migrate deploy-app: needs: migrate # نشر التطبيق يبدأ فقط بعد نجاح الترحيل runs-on: ubuntu-latest steps: - name: Rolling deploy run: kubectl rollout restart deployment/orders-service -n production
لا تُطبّق أبداً ترحيلات هدامة تلقائياً. أوامر DROP TABLE وDROP COLUMN وTRUNCATE في خطوط الأنابيب الآلية عالية الخطورة. اربطها بخطوة موافقة يدوية في GitHub Actions (عبر environment: production مع مُراجعين مطلوبين)، أو انشرها في PR منفصل يُطلق تشغيل خط أنابيب منفصل بموافقة يدوية.

تدقيق المخطط وقائمة التحقق من السلامة

قبل أن يصل أي ترحيل إلى الإنتاج، يجب أن يُشير المدقق تلقائياً إلى الأنماط المعروفة بتسبب حوادث. أوامر migrate lint في Atlas وSquawk (خاص بـ PostgreSQL) وskeema diff من GitHub يمكنها جميعاً التشغيل في CI لحجب SQL الخطير. الأنماط التي يجب رفضها تلقائياً:

  • إضافة عمود NOT NULL بلا قيمة افتراضية من جانب الخادم (إعادة كتابة كاملة للجدول أو تتطلب ملء البيانات الموجودة أولاً).
  • إنشاء مؤشر بلا CONCURRENTLY (PostgreSQL) أو بدون التحقق من ALGORITHM=INPLACE (MySQL).
  • ALTER TABLE RENAME COLUMN بدون مرحلة توسع سابقة (ستُعطّل الـ pods القديمة أثناء النشر المتدرج).
  • أي ترحيل يُعدّل جدولاً أكبر من N غيغابايت بلا تصنيف موافقة صريح.
  • غياب ترحيل نازل (V2__undo.sql) — كل ترحيل يجب أن يكون قابلاً للعكس، وإلا فإن خطة التراجع تتطلب تجميد الكود.

ترحيل المخطط دون توقف هو من أكثر المهارات التي تُقلَّل قيمتها في هندسة الإنتاج. الأنماط هنا — التوسع والتقليص، DDL الآني، الملء على دُفعات، والتدقيق المدمج في خط الأنابيب — هي كيف تشحن الفرق في Google وStripe وGitHub تغييرات قواعد البيانات عشرات المرات يومياً دون نافذة صيانة. استوعب دورة حياة التوسع والتقليص أولاً؛ كل تقنية أخرى تنبع منها بشكل طبيعي.