تعبئة البيانات القديمة (Backfill) دون تعطيل قاعدة بيانات الإنتاج
لقد قمت للتو بنشر ترحيل جديد (migration) يضيف عمود last_login_at إلى جدول المستخدمين. تم التغيير في المخطط (schema) بسلاسة. لكن الآن عندما تنظر إلى البيانات، تجد أن كل مستخدم موجود مسبقًا لديه قيمة فارغة (null) في هذا العمود. سجل تسجيل الدخول الخاص بهم من الأسبوع الماضي، الشهر الماضي، العام الماضي — كل ذلك غير مرئي للحقل الجديد.
هذه هي اللحظة التي تحتاج فيها إلى تعبئة البيانات القديمة (backfill).
ما معنى التعبئة (Backfill) فعليًا
التعبئة (Backfill) هي عملية ملء البيانات التي كانت موجودة قبل تطبيق الترحيل. الأمر لا يتعلق بنقل البيانات إلى هيكل جديد — فهذا ما تفعله نصوص الترحيل (migration scripts). التعبئة تتعلق بتحديث البيانات القديمة لتتوافق مع القواعد الجديدة التي يتبعها نظامك الآن.
المواقف التي تصبح فيها التعبئة ضرورية شائعة:
- أضفت عمودًا جديدًا، لكن الصفوف الموجودة لا تحتوي على قيم له.
- غيرت طريقة تخزين العناوين، من حقل نصي واحد إلى أعمدة منفصلة للشارع والمدينة والرمز البريدي.
- أدخلت عملية حسابية جديدة، مثل درجة المخاطرة للمعاملات، لكن المعاملات السابقة لم يتم تقييمها أبدًا.
في كل حالة، البيانات موجودة، صالحة ولكنها غير مكتملة. النظام يعرف ماذا يفعل بالبيانات الجديدة الواردة، لكن البيانات القديمة عالقة في التنسيق السابق.
لماذا لا يمكنك معالجة كل شيء دفعة واحدة
النهج الساذج هو تشغيل استعلام واحد يقوم بتحديث جميع الصفوف مرة واحدة. بالنسبة لجدول صغير يحتوي على بضع مئات من الصفوف، هذا يعمل بشكل جيد. بالنسبة لجدول بملايين الصفوف، فهذه كارثة تنتظر الحدوث.
تحديث ضخم واحد يؤمن الصفوف (row locks)، ويستهلك سجلات المعاملات (transaction logs)، ويبطئ كل استعلام آخر يصل إلى نفس الجدول. إذا كان تطبيقك يخدم المستخدمين أثناء التعبئة، فسيواجه هؤلاء المستخدمون انتهاء المهلة (timeouts)، واستجابات بطيئة، أو طلبات فاشلة. قد تنفد ذاكرة قاعدة البيانات أو مساحة القرص أثناء محاولة التعامل مع العملية.
الحل هو معالجة البيانات في أجزاء صغيرة ومحكومة.
المعالجة المجزأة (Batch Processing): التقنية الأساسية
بدلاً من تحديث مليون صف دفعة واحدة، تقوم بتحديث عشرة آلاف صف في كل مرة، ثم تتوقف مؤقتًا، ثم تعالج الدفعة التالية. هذا يسمى المعالجة المجزأة (batch processing)، وهو أساس التعبئة الآمنة.
إليك كيف تعمل عمليًا:
المخطط الانسيابي التالي يوضح حلقة التعبئة الكاملة، بما في ذلك فحوصات التماثل (idempotency) والحد من التحميل (throttling):
-- معالجة دفعة واحدة من الصفوف التي لا تزال بحاجة إلى التعبئة
UPDATE users
SET last_login_at = (
SELECT MAX(login_time)
FROM login_history
WHERE login_history.user_id = users.id
)
WHERE last_login_at IS NULL
LIMIT 10000;
بعد تشغيل هذا، تتحقق من عدد الصفوف المتأثرة. إذا كان مطابقًا لحجم الدفعة، تنتظر بضع ثوانٍ وتشغله مرة أخرى. إذا أعاد عددًا أقل من الصفوف، فإن التعبئة تقترب من الاكتمال.
اختيار حجم الدفعة المناسب
لا يوجد حجم دفعة عالمي يناسب كل قاعدة بيانات. الحجم المناسب يعتمد على:
- مدى قوة خادم قاعدة البيانات لديك.
- مقدار الحمل الذي يضعه التطبيق على قاعدة البيانات.
- مدى تعقيد منطق التحديث.
- مقدار مساحة سجل المعاملات المتاحة.
ابدأ بحجم متحفظ، مثل 5000 صف. قم بتشغيل بضع دفعات وراقب مقاييس قاعدة البيانات: استخدام وحدة المعالجة المركزية (CPU)، الإدخال/الإخراج للقرص (disk I/O)، زمن استجابة الاستعلام من جانب التطبيق. إذا تعاملت قاعدة البيانات مع الأمر بسهولة، قم بمضاعفة حجم الدفعة. إذا رأيت ارتفاعات في زمن الاستجابة أو تنافسًا على الأقفال (lock contention)، قم بتقليل الحجم إلى النصف.
الهدف هو إيجاد حجم دفعة يكتمل في بضع ثوانٍ دون التسبب في تأثير ملحوظ على الاستعلامات الأخرى. الدفعة التي تستغرق ثلاثين ثانية من المحتمل أن تكون كبيرة جدًا لنظام إنتاج تحت الحمل العادي.
الحد من التحميل (Throttling): إعطاء قاعدة البيانات مساحة للتنفس
حجم الدفعة يتحكم في مقدار العمل الذي يحدث في وحدة واحدة. الحد من التحميل يتحكم في مقدار الوقت الذي يمر بين الوحدات.
بعد اكتمال كل دفعة، أضف توقفًا متعمدًا قبل بدء الدفعة التالية. هذا التوقف يسمح لقاعدة البيانات بتفريغ الكتابات المعلقة، وتحرير الأقفال، وخدمة الاستعلامات الأخرى دون منافسة من عملية التعبئة.
قد يكون حد التحميل النموذجي من ثانيتين إلى خمس ثوانٍ بين الدفعات. خلال ساعات الذروة، قد تزيده إلى عشر أو خمس عشرة ثانية. خلال نوافذ الصيانة، قد تقلله إلى ثانية واحدة أو تزيله تمامًا.
الحد من التحميل هو صمام الأمان الخاص بك. إذا حدث خطأ ما — ارتفاع مفاجئ في حركة مرور التطبيق، استعلام بطيء من فريق آخر، تحذير من تأخير النسخ المتماثل (replication lag) — يمكنك زيادة فترة التوقف والسماح للنظام بالاستقرار قبل الاستمرار.
جعل التعبئة متماثلة (Idempotent)
يجب أن يكون نص التعبئة آمنًا للتشغيل عدة مرات. إذا فشلت دفعة في منتصف الطريق، أو إذا احتجت إلى إعادة تشغيل العملية بأكملها، يجب ألا يؤدي تشغيل نفس النص مرة أخرى إلى إنتاج بيانات مكررة أو أخطاء.
التماثل (idempotency) للتعبئة يعني عادةً أحد أمرين:
- التحقق قبل الكتابة: قم بتحديث الصفوف التي لا تزال تحتوي على قيم فارغة أو قيم قديمة فقط.
- استخدام منطق الإدراج أو التحديث (upsert): قم بالإدراج أو التحديث بناءً على ما إذا كان الصف يحتوي بالفعل على البيانات الجديدة.
بالنسبة لمثال last_login_at، الاستعلام أعلاه متماثل بالفعل لأنه يستهدف فقط الصفوف التي يكون العمود فيها فارغًا. إذا فشلت دفعة بعد تحديث 5000 صف، فإن التشغيل التالي سيتخطى تلك الصفوف ويستمر مع الباقي.
للتعبئات الأكثر تعقيدًا، مثل إعادة حساب قيمة مشتقة، قد تضيف عمود طابع زمني processed_at. يتحقق نص التعبئة مما إذا كان processed_at فارغًا قبل معالجة كل صف. بمجرد المعالجة، يتم تعيين الطابع الزمني، وتتخطى عمليات التشغيل اللاحقة هذا الصف.
التسجيل (Logging): التفاصيل التي لا يفكر فيها أحد حتى تتعطل
عندما تستمر التعبئة لساعات، تحتاج إلى معرفة أين هي وما إذا كانت لا تزال تعمل بشكل صحيح. سجل كل دفعة:
- رقم الدفعة ونطاق الوقت.
- عدد الصفوف المعالجة.
- مدة الدفعة.
- أي أخطاء تمت مواجهتها.
- التقدم الحالي كنسبة مئوية أو عدد صفوف.
هذا السجل يخدم غرضين. أولاً، إذا توقفت التعبئة بشكل غير متوقع، يمكنك الاستئناف من آخر دفعة مكتملة بدلاً من البدء من جديد. ثانيًا، عندما تنتهي التعبئة، يكون لديك سجل دقيق لما حدث بالضبط، مما يساعد في التصحيح والتدقيق.
قد يبدو إدخال السجل البسيط كما يلي:
2025-03-15 14:32:01 | الدفعة 47 | تمت معالجة 10,000 صف | المدة 3.2 ثانية | لا توجد أخطاء
2025-03-15 14:32:06 | الدفعة 48 | تمت معالجة 10,000 صف | المدة 3.1 ثانية | لا توجد أخطاء
2025-03-15 14:32:11 | الدفعة 49 | تمت معالجة 10,000 صف | المدة 3.5 ثانية | لا توجد أخطاء
قائمة التحقق العملية للتعبئة
قبل تشغيل التعبئة في الإنتاج، راجع هذه القائمة:
- تم اختبار حجم الدفعة على بيئة اختبار (staging) بحجم بيانات مشابه.
- تم تكوين فترة الحد من التحميل (throttle interval) ويمكن تعديلها دون تغيير الكود.
- النص متماثل (idempotent) — تشغيله مرتين ينتج نفس النتيجة.
- التسجيل يلتقط تقدم الدفعة والأخطاء والتوقيت.
- توجد خطة تراجع (rollback): يمكنك عكس التعبئة إذا حدث خطأ ما.
- المراقبة (monitoring) موجودة لاكتشاف تدهور أداء قاعدة البيانات.
- تم تنفيذ تشغيل تجريبي (dry run) على نسخة من بيانات الإنتاج.
الخلاصة
التعبئة (Backfill) ليست نصًا لمرة واحدة تكتبه ثم تنساه. إنها عملية محكومة تحترم حقيقة أن قاعدة البيانات الخاصة بك تخدم المستخدمين بينما تقوم أنت بتغيير بياناتهم. المعالجة المجزأة (batch processing) والحد من التحميل (throttling) ليستا تحسينات — إنهما الحد الأدنى من المتطلبات للقيام بهذا العمل بأمان. بدونهما، أنت على بعد استعلام كبير واحد من حادثة إنتاجية.