استراتيجيات النشر والتسليم التدريجي

تغييرات قاعدة البيانات وأسلوب التوسع والتقليص

18 دقيقة الدرس 8 من 28

تغييرات قاعدة البيانات وأسلوب التوسع والتقليص

كل تقنية في التسليم التدريجي — إصدارات الكناري، النشر الأزرق-الأخضر، أعلام الميزات — تفترض أن كود التطبيق يمكن استبداله باستقلالية عن مخطط قاعدة البيانات. في الواقع، قاعدة البيانات هي مورد مشترك وذو حالة تتلقى القراءة والكتابة في آنٍ واحد من الإصدار القديم والجديد لتطبيقك خلال أي عملية نشر متدحرج أو كناري. أي تغيير على المخطط غير متوافق مع الإصدار قيد التشغيل سيُلحق الفساد بالبيانات أو يُعطل الحاويات فور تنفيذه. هذا السبب الجذري لحوادث إنتاج أكثر مما يدرك معظم المهندسين.

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

لماذا يُعطِّل الترحيل التقليدي النشر المتدحرج

تخيّل إعادة تسمية عمود user_name إلى full_name بأمر ALTER TABLE RENAME COLUMN واحد. لحظة تنفيذ هذا الترحيل على قاعدة بيانات الإنتاج، تبدأ كل حاوية لا تزال تشغّل الإصدار القديم — الذي يقرأ user_name — في إلقاء الأخطاء. مع 100 حاوية وتحديث متدحرج يستغرق 5 دقائق، أنت مضمون انقطاعاً جزئياً لـ 5 دقائق. ينطبق نفس الخطر على DROP COLUMN وإضافات NOT NULL وتغييرات قيود المفتاح الخارجي.

المبدأ الجوهري: الترحيل الذي يعمل قبل نشر الكود يجب أن يكون متوافقاً مع الإصدار القديم. الترحيل الذي يعمل بعد نشر الكود يجب أن يكون متوافقاً مع الإصدار الجديد. يُرسِّخ نمط التوسع والتقليص هذا المبدأ بالتصميم.

الأطوار الثلاثة لنمط التوسع والتقليص

Expand-Contract migration phases Phase 1: Expand Add new column (nullable) Write to BOTH columns Read from OLD column Old code: still works New code: also works Phase 2: Migrate Backfill old rows in batches Both columns consistent Add NOT NULL + index All pods on new code Old code fully retired Phase 3: Contract Drop old column Clean up app writes Migration complete Schema is lean again No downtime taken نشر v1 + ترحيل نشر v2 + نقل البيانات نشر v3 + تقليص كل طور = عملية نشر منفصلة. الكود القديم والجديد يتعايشان بأمان في كل خطوة.
الأطوار الثلاثة لنمط التوسع والتقليص: كل طور يُشحن كعملية نشر مستقلة، مع ضمان توافق المخطط مع جميع إصدارات التطبيق قيد التشغيل.

الطور الأول — التوسع: الإضافة دون كسر

يُضيف الترحيل الأول بنية جديدة مع إبقاء البنية القديمة سليمة. في حالة إعادة تسمية عمود، يعني ذلك إضافة العمود الجديد بقيمة nullable. يكتب إصدار التطبيق الجديد في كلا العمودين ويقرأ من العمود القديم. الإصدار القديم يتجاهل العمود الجديد كلياً. لا يُكسر أي كود.

-- الترحيل في الطور الأول (يعمل قبل نشر كود التطبيق الجديد) -- إعادة تسمية user_name -> full_name بأمان ALTER TABLE users ADD COLUMN full_name VARCHAR(255) NULL; -- نسخ البيانات الحالية مباشرة للجداول الصغيرة (<1 مليون صف) UPDATE users SET full_name = user_name WHERE full_name IS NULL; -- كود التطبيق في هذه المرحلة يكتب في كلا العمودين: -- INSERT INTO users (user_name, full_name, ...) VALUES (?, ?, ...) -- UPDATE users SET user_name = ?, full_name = ? WHERE id = ? -- SELECT user_name FROM users WHERE id = ? -- لا يزال يقرأ العمود القديم

الطور الثاني — الترحيل: نقل البيانات على دفعات

للجداول الكبيرة، سيُقفِّل أمر UPDATE users SET full_name = user_name الواحد الجدول لدقائق ويتسبب في حادثة إنتاجية. النمط الآمن هو النقل على دفعات — تحديث الصفوف في مجموعات من 1,000 إلى 10,000 مع توقف قصير بين كل دفعة لتفادي تأخر النسخ المتماثل وإرهاق القرص. يعمل هذا كمهمة خلفية أو سكريبت منفصل، لا كجزء من ملف الترحيل نفسه.

#!/bin/bash # batch-backfill.sh — نقل بيانات آمن على دفعات (يعمل على قاعدة بيانات الإنتاج المباشرة) # اضبط BATCH_SIZE و SLEEP_MS وفق حجم جدولك وتحمّلك لتأخر النسخ المتماثل BATCH_SIZE=5000 SLEEP_MS=200 # 200ms بين كل دفعة LAST_ID=0 while true; do ROWS=$(mysql -u root -p"${DB_PASSWORD}" esb1995 -sN -e " UPDATE users SET full_name = user_name WHERE id > ${LAST_ID} AND full_name IS NULL ORDER BY id LIMIT ${BATCH_SIZE}; SELECT ROW_COUNT(); ") echo "تم نقل ${ROWS} صفاً من id > ${LAST_ID}" if [ "${ROWS}" -eq 0 ]; then echo "اكتمل النقل." break fi LAST_ID=$(mysql -u root -p"${DB_PASSWORD}" esb1995 -sN -e " SELECT MAX(id) FROM users WHERE full_name IS NOT NULL; ") sleep "0.${SLEEP_MS}" done -- بعد اكتمال النقل، أضف قيد NOT NULL والفهرس في ترحيل منفصل: ALTER TABLE users MODIFY COLUMN full_name VARCHAR(255) NOT NULL, ADD INDEX idx_users_full_name (full_name);
استخدم gh-ost أو pt-online-schema-change للجداول الكبيرة. للجداول التي تتجاوز 10 ملايين صف، استخدم gh-ost (من GitHub) أو pt-online-schema-change (من Percona Toolkit). تُطبِّق هذه الأدوات تغييرات المخطط عبر الإنترنت بإنشاء جدول ظل، وإعادة تشغيل أحداث binlog، وإجراء تبديل ذري للجدول — دون توقف حتى على جداول تحتوي على مليار صف. مُستخدَمة في الإنتاج من قِبل GitHub وShopify وAirbnb.

الطور الثالث — التقليص: إزالة البنية القديمة

فقط بعد التحقق من أن كل حاوية من حاويات التطبيق تعمل على الإصدار الجديد — بالتأكيد من أن لا حاوية لا تزال تستعلم عن user_name — تُشغِّل ترحيل التقليص. هذا يحذف العمود القديم. من هذه النقطة فصاعداً، المخطط نظيف والترحيل مكتمل.

-- ترحيل الطور الثالث (يعمل بعد انتقال 100% من الحاويات للكود الجديد) -- تحقق أولاً: ابحث في قاعدة الكود وسجلات التطبيق عن أي إشارة إلى user_name -- فحص سجل الاستعلامات البطيئة قبل الحذف: SELECT * FROM performance_schema.events_statements_summary_by_digest WHERE DIGEST_TEXT LIKE '%user_name%' AND LAST_SEEN > NOW() - INTERVAL 7 DAY; -- إذا لم تظهر نتائج، يمكنك الحذف بأمان: ALTER TABLE users DROP COLUMN user_name; -- اختبر الحذف دائماً على نسخة staging أولاً: -- mysqldump prod_users | mysql staging_users -- ALTER TABLE staging_users.users DROP COLUMN user_name;

تطبيق التوسع والتقليص على أنواع أخرى من تغييرات المخطط

نفس منطق الأطوار الثلاثة ينطبق على كل عملية DDL خطيرة:

  • إضافة عمود NOT NULL — التوسع: أضفه nullable مع قيمة افتراضية. الترحيل: أصلح كل القيم الفارغة. التقليص: أضف قيد NOT NULL.
  • تقسيم عمود (مثلاً address إلى city + country) — التوسع: أضف العمودين الجديدين. الترحيل: حلِّل البيانات وانسخها. التقليص: احذف العمود القديم.
  • إضافة قيد مفتاح خارجي — التوسع: أضف العمود بدون قيد. الترحيل: أصلح الصفوف اليتيمة. التقليص: أضف القيد مع VALIDATE CONSTRAINT.
  • تغيير نوع عمود (مثلاً INT إلى BIGINT) — أضف عمود ظل بالنوع الجديد؛ اكتب للاثنين؛ بدِّل القراءات؛ احذف العمود القديم.
لا تُضف عموداً NOT NULL بدون قيمة افتراضية في ترحيل واحد على جدول مباشر. سيُعيد PostgreSQL كتابة كامل الجدول صفاً بصف ويحتجز قفلاً حصرياً طوال المدة — دقائق على جدول كبير. يُجري MySQL/MariaDB بمحرك InnoDB تعديل DDL عبر الإنترنت لبعض التغييرات لكن ليس جميعها. تحقق دائماً من توافق ALGORITHM=INPLACE, LOCK=NONE في وثائق MySQL قبل افتراض أن التغيير خالٍ من القفل.

إدارة الترحيلات في أنابيب CI/CD

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

# نمط Kubernetes Job — تشغيل الترحيل قبل نشر حاويات التطبيق الجديدة # migration-job.yaml apiVersion: batch/v1 kind: Job metadata: name: migrate-v2-3-0 namespace: production labels: app: myapp version: "2.3.0" spec: backoffLimit: 0 # فشل سريع؛ لا إعادة محاولة للترحيل المعطوب ttlSecondsAfterFinished: 3600 template: spec: restartPolicy: Never containers: - name: migrate image: myapp:2.3.0 command: ["php", "artisan", "migrate", "--force"] env: - name: DB_PASSWORD valueFrom: secretKeyRef: name: db-credentials key: password resources: requests: cpu: "100m" memory: "256Mi" limits: cpu: "500m" memory: "512Mi" # في أنابيب GitOps (ArgoCD / Flux): # 1. طبِّق Job وانتظر اكتماله قبل مزامنة Deployment. # 2. إذا فشل Job، تُلغى المزامنة — الحاويات على الكود القديم تستمر في الخدمة. # 3. عند النجاح، تزامن ArgoCD الـ Deployment للتحديث المتدحرج.
استخدم قفل الترحيل. إذا أمكن لعدة عمليات CI أو حاويات البدء في آنٍ واحد، قد تحاول عمليتا ترحيل تشغيل نفس الترحيل في وقت واحد. تستخدم أدوات مثل Flyway وLiquibase قفل استشاري على قاعدة البيانات لتسلسل تشغيل الترحيلات. يستخدم Laravel عمود القفل في جدول migrations لنفس الغرض. لا تبني منفِّذ ترحيل خاصاً بك دون قفل موزَّع.

إمكانية الرصد خلال الترحيل المباشر

راقب دائماً المؤشرات التالية خلال نشر الترحيل وبعده: زمن استجابة الاستعلامات على الجدول المتأثر (راقب انتظارات القفل)، وتأخر النسخ المتماثل على النسخ للقراءة (الكتابات الجماعية قد تسبب ارتفاعات في التأخر)، ومعدلات الأخطاء من التطبيق. أنشئ لوحة Grafana مُعدَّة مسبقاً لكل ترحيل رئيسي. إذا تجاوز تأخر النسخ المتماثل حد SLA المحدد لك، أوقف مهمة النقل الجماعي فوراً.