خلاصة سريعة. يعرض
rdc repo diffالفرق على مستوى الملفات بين مستودعَين متفرّعَين بنحوgit status --short(أي A/M/D/R)، دون فك تشفير أي منهما.
- يقارن صورتَي LUKS على مستوى الكتل باستخدام استدعاء FIEMAP ioctl، الذي يقرأ بيانات خريطة الامتدادات فحسب. لا يُحمَّل أي مفتاح، ولا يُقرأ أي نص صريح.
- aes-xts يُبقي الطول وفق ما كان ويشفّر كل قطاع 512 بايت باستقلالية، فالقطاع المشفّر المتغير هو قطاع ciphertext متغير في نفس الإزاحة (مع مراعاة الإزاحة البالغة 16 ميغابايت لبيانات LUKS). اطرح الإزاحة، وحوّل النطاقات إلى أسماء ملفات عبر خريطة امتدادات ext4، وستحصل على قائمة الملفات.
- التكلفة تتتبع عدد الكتل المتغيرة، لا حجم المستودع. تفريع بحجم 1 جيجابايت وآخر بحجم 100 جيجابايت تتم مقارنتهما في ميلي ثوانٍ متساوية، لأن المقارنة تعتمد على البيانات الوصفية فحسب.
إذن، التفريع في Rediacc هو cp --reflink=always لصورة LUKS الخاصة بالمستودع. فوري، ولا يأبه بالحجم. مستودع بحجم 100 جيجابايت يُفرَّع بسرعة مستودع بحجم 1 جيجابايت. أعلم أن ذلك يبدو كلاماً تسويقياً، لكنه ببساطة هو كيف تعمل الروابط المرجعية: يُنسخ btrfs خريطة الامتدادات ويشارك الكتل الأساسية. نعتمد على ذلك اعتماداً كبيراً. التفريعات هي صندوق اختبار، والفرع المؤقت، ونسخة التجهيز التي تتخلص منها عند الانتهاء.
ما كان ينقصنا هو إجابة سريعة على السؤال البديهي التالي: ما الذي غيّره هذا التفريع فعلاً. الطريق الساذجة: تحميل التفريع، وفتح حاوية LUKS، والمشي عبر ext4 الداخلي، وتجزئة كل ملف مقارنةً بالأصل. يتوسع ذلك مع حجم المستودع في كلٍّ من القراءات وعمليات فك التشفير. ويحتاج المفاتيح حية في مسار المقارنة. ويُهدر الشيء الوحيد الذي تعرفه طبقة التخزين مجاناً: أي الكتل تباعدت. rdc repo diff يسلك الطريق الأخرى. يتوسع مع الكتل المتغيرة. لا يُحمِّل أي مفتاح. ويحصل على قائمة الملفات بمقارنة الصورتين المشفرتين.
الطبقات التي تقارنها
دعني أكون دقيقاً حول ما يعنيه “مستودعان” على القرص. تعتمد الحيلة كلها على ذلك. من الأسفل إلى الأعلى: قرص SSD، وتخزين المضيف، وتجمع btrfs. فوق ذلك، ملف صورة LUKS2 واحد لكل مستودع. افتح تشفيره وستحصل على جهاز dm-crypt. في داخله يعيش نظام الملفات ext4 الذي تستخدمه الحاويات. مستودع واحد هو ملف واحد على تجمع btrfs.
التفريع هو رابط reflink لذلك الملف. بعد التفريع مباشرةً، يكون ملفَّا الصورة متطابقَين بايت بايت. يتشاركان كل كتلة فيزيائية. الأصل والتفريع ليسا نسختين من البيانات. إنهما خريطتان للامتدادات تشيران إلى الكتل نفسها. حين تكتب داخل التفريع، تُخصص طبقة التخزين كتلة جديدة للمنطقة المتغيرة. فقط خريطة امتدادات ذلك التفريع تُعاد كتابتها. تبقى كتل الأصل دون تغيير.
إذن يختزل “مقارنة مستودعَين” إلى “مقارنة ملفَّين يتشاركان معظم امتداداتهما.” النواة تستطيع الإجابة على ذلك. لا أحد بحاجة إلى قراءة بايت واحد من أي منهما.
FIEMAP: سؤال النواة عمّا تغيّر دون قراءته
يُعيد استدعاء FIEMAP ioctl خريطة امتدادات الملف: قائمة من صفوف (الإزاحة المنطقية، الإزاحة الفيزيائية، الطول). يقول كل صف أين يعيش جزء من الملف على القرص. إنها بيانات وصفية خالصة لنظام الملفات. لا تقرأ بيانات الملف. للصورة المشفرة لا تحتاج إلى مفتاح. النص المشفر مجرد بايتات لا تحتاج النواة إلى تفسيرها.
قارن خريطتَي الامتداد. أي نطاق منطقي تشير فيه كلتا التفريعتان إلى الكتلة الفيزيائية نفسها يكون مشتركاً. المشترك يعني متطابقاً، لأنه حرفياً الكتلة نفسها على الجهاز. النطاقات التي تمتلك فيها التفريعة كتلتها الفيزيائية الخاصة هي عمليات الكتابة. تلك هي الكتل المتغيرة. حصلنا عليها من البيانات الوصفية التي تحتفظ بها طبقة التخزين على أي حال.
من هنا تأتي قصة التكلفة. تقرأ مقارنة FIEMAP سجلات الامتدادات، لا البيانات. عملها يتوسع مع عدد الامتدادات المتغيرة، لا حجم المستودع. تُعيد التفريعة بحجم 1 جيجابايت والتفريعة بحجم 100 جيجابايت القائمة نفسها القصيرة من الامتدادات الخاصة. نفس الميلي ثوانٍ، إن غيّرتا الملفات نفسها. تحذير صريح: وقت المشي عبر الامتدادات يتوسع مع تجزئة الصورة، لا حجمها. صورة copy-on-write تحت عمليات كتابة عشوائية مكثفة تتراكم فيها الامتدادات. استغرق المشي الكامل لـ filefrag 3.19 ثانية على أكثر صور الإنتاج تجزئةً التي قِستُها. راجع منشور اختبار قياس الأداء للتجزئة. هذا هو السقف على جانب البيانات الوصفية. إنه فحص خلفي، لا قراءة بيانات.
من كتلة متغيرة إلى اسم ملف، عبر طبقتَي تشفير
قائمة نطاقات البايتات المتغيرة في الصورة المشفرة ليست مفيدة بعد. النطاقات هي مواضع في النص المشفر. الأسماء التي تريدها تعيش طبقتين فوقك، في ext4 الداخلي. الجسر بينهما هو العمليات الحسابية على العناوين، لا فك التشفير.
يشفّر LUKS بـ aes-xts. إنه يُبقي الطول ويشفّر كل قطاع 512 بايت باستقلالية. القطاع الصريح المتغير يُنتج قطاع ciphertext متغيراً في نفس الإزاحة. الإزاحة الوحيدة هي إزاحة بيانات LUKS. ذلك هو الـ 16 ميغابايت من الترويسة ومفاتيح الجلسة أمام الحمولة المشفرة. اطرح تلك الإزاحة من كل نطاق صورة متغير. ستحصل الآن على النطاق المطابق على جهاز dm-crypt. هذا هو جهاز الكتل الذي يستند إليه ext4 الداخلي. لم يُستخدم أي مفتاح. إنه طرح.
الآن حوّل نطاقات الجهاز إلى ملفات. يحتفظ ext4 بخريطة امتدادات لكل inode أيضاً. نفس بنية (المنطقية، الفيزيائية، الطول). تصل إليها عبر FIEMAP على نظام الملفات الداخلي المحمول. امشِ عبر inodes مرة واحدة لبناء فهرس كتلة-إلى-ملف. ثم ابحث عن كل نطاق جهاز متغير في ذلك الفهرس. النطاق الذي يتداخل مع امتدادات بيانات inode 1234 ينتمي إلى مسار ذلك inode. ذلك المسار هو الملف الذي تغيّر.
دعني أقول صراحةً ما لا تفعله هذه العملية أبداً. لا تشتق نصاً صريحاً من الصورة المتغيرة. تقرأ بنية نظام الملفات عند إزاحات معروفة. تفعل ذلك على كلٍّ من الجانب المشفر والجانب المفكوك. ثم تربط الاثنين بالعنوان. مرشح الكتل يقول أي مناطق الجهاز تحركت. خريطة امتدادات ext4 تقول أي ملف يملك كل منطقة. لا تفحص أيٌّ من الخطوتين محتويات كتلة متغيرة لتقرر أنها تغيّرت.
الإضافات والحذف وإعادة التسمية: مشي هوية inode
التعديلات تنبع مباشرةً من مقارنة الكتل. الإضافات والحذف وإعادة التسمية تحتاج ملاحظة واحدة إضافية. يعطيها لنا الرابط المرجعي مجاناً: التفريع يُبقي أرقام inodes. ربط الصورة كلها بنسخة reflink ينسخ نظام الملفات الداخلي بأكمله بايت بايت قبل أي تباعد. إذن inode كان موجوداً في الأصل له نفس الرقم في التفريع.
هذا يجعل الهوية مقارنة مجموعات. inode موجود على الجانبين بمسار مختلف هو إعادة تسمية. inode موجود فقط على الجانب الجديد هو إضافة. inode موجود فقط على الجانب القديم هو حذف. تُؤكَّد إعادة التسمية بتداخل امتدادات الجهاز. تجلس كتل بيانات الملف المُعاد تسميته عند نفس إزاحات الجهاز في كلتا التفريعتين. التفريعتان تتشاركان إحداثيات واحدة. يستبعد هذا التداخل أيضاً احتمال إعادة استخدام رقم inode لبيانات غير ذات صلة. إعادة التسمية الخالصة تظهر إذن مع كتل بيانات الملف دون تغيير. فقط إدخال الدليل تحرك.
إليك شكل حالة الاسم الافتراضية، نفس نحو A/M/D/R الذي تقرأه من git status --short:
$ rdc repo diff --name test-1gb:fork1 -m hostinger
M hello.txt
1 file changed: 0 added, 1 modified, 0 deleted, 0 renamed
ملف واحد معدَّل في مستودع بحجم 1 جيجابايت. مُبلَّغ عنه من مقارنة كتل لم تقرأ أي بيانات ملف. لم يُفتح أي تشفير.
يفعل الوضع الافتراضي شيئاً إضافياً للصحة. مرشح الكتل هو مجموعة شاملة. امتداد btrfs يمكن أن يغطي أكثر من البايتات التي تغيّرت فعلاً. لذا فالكتابة على ملف واحد يمكن أن تُبلّغ عن جار يشارك امتداداً. لتجنب الإبلاغ عن ملف لم يتغير، يؤكد الوضع الافتراضي كل مرشّح مُبلَّغ عنه بالكتل. يُجزئ فقط ذلك الملف على الجانبين. يُجزئ المرشحين، لا المستودع. لذا تتتبع تكلفة التأكيد مجموعة التغيير. --fast يثق بمرشح الكتل ويتخطى التأكيد. استخدمه حين تريد الإجابة سريعاً وتتسامح مع إيجابيات كاذبة متفرقة.
لماذا يحتاج وكيل الذكاء الاصطناعي إلى ذلك
السبب في وجود هذا الأمر أصلاً هو سير عمل الوكيل. ظللتُ أرى وكلاء يفرّعون الإنتاج، يُجرون تغييرات، ثم لا يجدون طريقة نظيفة للإبلاغ عمّا لمسوه فعلاً. وكيل الذكاء الاصطناعي يستطيع تفريع الإنتاج فورياً. يُشغّل تغييراً محفوفاً بالمخاطر داخل التفريع المعزول. ثم يحتاج إلى معرفة ما الذي لمسه تحديداً قبل ترقية أي شيء. التفريع هو الفرع. المقارنة هي المراجعة.
الوكيل لا يقرأ حالة الاسم، بل يقرأ --json:
$ rdc repo diff --name prod:experiment --json -m hostinger
يعطي المخرج المنظَّم الوكيلَ مجموعة تغيير دقيقة. المسارات التي عدّلها، أنشأها، حذفها. مع --stat، حجم التغيير لكل ملف بالبايتات والكتل. الوكيل الذي يرى مقارنته قبل الترقية هو الذي يمكنك السماح له بالاقتراب من الإنتاج. نطاق الانفجار قابل للفحص، لا مجرد التأكيد عليه. أوضاع أخرى تخدم حلقة المراجعة نفسها. --name-only لقائمة مسارات خام. --content <path> للفرق النصي الموحد لملف واحد (نصي فقط؛ الملف الثنائي يُبلَّغ عنه بـ Binary files differ). --stat حين يحتاج الوكيل معرفة ما تغيّر وبقدر كم.
لماذا يحتاج اختبار DR إلى ذلك
المبدأ نفسه يجيب على سؤال DR كان من المحرج طرحه بدون مخاطر. فرّع الإنتاج. استعد نسخة احتياطية في التفريع. قارن التفريع بالإنتاج. يخبرك الفرق إن كانت الاستعادة أعادت إنتاج مجموعة الملفات المتوقعة. يفعل ذلك دون إيقاف الإنتاج. ودون فك تشفير أي شيء في مسار المقارنة.
هذا تدريب يمكنك إجراؤه بجدول زمني. الاستعادة تصل في تفريع معزول. يُبلّغ الفرق عن الدلتا بنحو git. التدريب النظيف: مجموعة التغيير تطابق ما كانت النسخة الاحتياطية يُفترض أن تحتويه. أنت تتحقق من الاسترداد في مواجهة الإنتاج الحي. النسخة لا تكلف شيئاً للصنع ولا شيئاً للتخلص منها.
الحدود الصريحة
فرق المحتوى نصي فحسب. --content يُنتج فرقاً موحداً للملفات النصية. لكل شيء آخر يُبلّغ بـ Binary files differ، كما يفعل git. الفرق الموجّه بالأسطر للكتلة المشفرة ثم المضغوطة هو ضجيج.
يقارن التفريعات ذات الصلة، لا المستودعات التعسفية. تستند الآلية كلها إلى نظام إحداثيات مشترك. الامتدادات المشتركة تُثبت التطابق. أرقام inodes المحفوظة ترسّخ الهوية. إزاحة بيانات مشتركة تربط كل شيء. مستودعان لم يُفرَّعا أبداً من سلف مشترك لا يتشاركان أياً من ذلك. لا مقارنة رخيصة بينهما. هذه ميزة، لا خلل. بنفس المنطق لا معنى لـ git diff بين تاريخَين غير مترابطَين.
الكشف عن إعادة التسمية مبني على inode. إنه دقيق بالنسبة لإعادة التسميات التي يُسجلها نظام الملفات فعلاً. حذف-ثم-إنشاء محتوى متطابق تحت اسم جديد؟ عمليتان على جدول inode. فيُبلَّغ عنهما كحذف وإضافة، لا إعادة تسمية. مرشّح تشابه المحتوى في git قد يسمّيها إعادة تسمية. مشي inode لن يفعل. تلك هي الإجابة الصحيحة حول ما فعله نظام الملفات. حتى لو لم تكن الإجابة حول ما قصده إنسان.
ومشي البيانات الوصفية يتوسع مع التجزئة. على صورة مجزأة بشدة يستغرق تعداد الامتدادات ثوانٍ، لا ميلي ثوانٍ. لا يزال مستقلاً عن حجم المستودع. لا يزال خالياً من أي قراءة بيانات. لكنه ليس حرفياً فورياً على الصور الأكثر تجزئة.
الخلاصة
يضع rdc repo diff وظائف التحكم في الإصدارات على البنية التحتية المشفرة والجارية. الواجهة مقصود أن تكون git. A/M/D/R، وفروق موحدة، و--stat. لا شيء جديد لتتعلمه. إن كنت تستطيع قراءة git status --short، تستطيع قراءة فرق بين صورتَي LUKS. الهندسة الكامنة هي الجزء الذي يستحق الاهتمام. تختزل في رفضَين. لا تفك التشفير أبداً: aes-xts يتيح لمقارنة FIEMAP على مستوى الكتل تحديد كل قطاع متغير بالعنوان. ولا تدفع أبداً مقابل بيانات لم تتغير: طبقة التخزين سجلت بالفعل أي الكتل تباعدت. التفريع هو الفرع. المقارنة هي المراجعة. تكلفة المراجعة تتتبع تكلفة التغيير، لا وزن المستودع.