سرعة خط الأنابيب على مستوى شركات التقنية الكبرى
سرعة خط الأنابيب على مستوى شركات التقنية الكبرى
تُشغّل شركات مثل Google وMeta وStripe آلاف عمليات تنفيذ خطوط أنابيب CI كل ساعة. وعلى هذا الحجم، يُترجَم تحسينٌ بمقدار دقيقتين لكل تشغيل إلى سنوات هندسية من وقت المطورين موفَّرًا سنويًا. لكن السرعة لا تتعلق بالحوسبة الخام فقط، بل بالبنية الذكية: معرفة ما يجب تخزينه مؤقتًا، وأي الاختبارات يجب تشغيلها، وكيفية تسلسل العمل حتى لا ينتظر البشر الآلات أبدًا.
يُشرّح هذا الدرس الرافعات الأربع التي تستخدمها فرق CI في شركات التقنية الكبرى للإبقاء على خطوط الأنابيب تحت عشر دقائق حتى مع نمو قواعد الأكواد إلى الملايين من السطور: تخزين التبعيات مؤقتًا، والبناء التدريجي، واختيار الاختبارات، وطوابير الدمج.
الرافعة الأولى — تخزين التبعيات مؤقتًا
المصدر الأكبر لضياع وقت CI في معظم المؤسسات هو إعادة تنزيل وإعادة تصريف التبعيات التي لم تتغير. قد يستغرق تنفيذ npm install بارد في مستودع ضخم من 4 إلى 8 دقائق. أما عند وجود ضربة مخزن مؤقت، فينخفض الأمر إلى 15 ثانية.
تعرض كل منصة CI رئيسية مخزنًا مؤقتًا بمفتاح-قيمة. المفتاح هو تجزئة (hash) لملف القفل (package-lock.json، Gemfile.lock، go.sum، Cargo.lock). عندما لا يتغير ملف القفل — وهو الحال في الغالبية العظمى من عمليات الإيداع — يُستعاد المخزن المؤقت كما هو ويُتجاوَز تثبيت التبعيات بالكامل.
مفتاح الاستعادة الاحتياطي restore-keys بالغ الأهمية: إذا تغير ملف القفل — كإضافة حزمة جديدة — فلن يتطابق المفتاح الدقيق. يجد المفتاح الاحتياطي بالبادئة أحدث مخزن مؤقت يشترك في نفس نظام التشغيل، مما يمنحك ضربة جزئية تتجنب التثبيت البارد الكامل. وتُجلب فقط الحزمة المضافة حديثًا من السجل.
الرافعة الثانية — البناء التدريجي
تخزين التبعيات يُزيل فئة واحدة فقط من الهدر. الفئة التالية هي إعادة تصريف كود المصدر الذي لم يتغير. يعتمد البناء التدريجي على رسم بياني للبناء: رسم بياني موجَّه غير دوري لكل ملف مصدر ومخرجاته المصرَّفة. يُعيد نظام البناء الصحيح تنفيذ العقد التي تغيرت مدخلاتها أو غابت مخرجاتها فقط.
Nx (لجافاسكريبت/تايبسكريبت) وBazel (متعدد اللغات) هما الأداتان الأكثر شيوعًا على مستوى شركات التقنية الكبرى. يستخدم Nx مخزنًا مؤقتًا للحوسبة مُفهرسًا بتجزئة ملفات المصدر والإعدادات؛ أما Bazel فيستخدم بيئات عزل محكمة وتخزينًا بعناوين محتوى حتى تُنتج المدخلات المتطابقة مخرجات متطابقة دومًا بصرف النظر عن حالة الجهاز.
أمر affected هو العنصر الجوهري. يبني Nx رسمًا بيانيًا للتبعيات لمساحة عملك ويحدد المشاريع المتأثرة بشكل انتقالي بالفارق بين HEAD وأساس الدمج. إذا غيّرت مكتبة مساعدة مشتركة، تُعلَّم كل تطبيق نهائي على أنه متأثر. إذا غيّرت خدمة ورقية فحسب، يُعاد بناء تلك الخدمة فقط واختبارها. في Meta، تطبّق الأدوات الداخلية (Buck2) المبدأ نفسه عبر مستودع يضم مئات الآلاف من الأهداف.
الرافعة الثالثة — اختيار الاختبارات
تشغيل مجموعة الاختبارات الكاملة في كل عملية إيداع مكلف وبطيء. يُضيّق اختيار الاختبارات مجموعة الاختبارات المُنفَّذة لتشمل تلك المرجَّح أن تتأثر بتغيير الكود. هناك نهجان سائدان:
- تحليل التبعية الساكن: تعيين كل ملف اختبار إلى ملفات المصدر التي يستوردها. إذا أثّر إيداع X في
src/billing/invoice.ts، شُغِّلت فقط الاختبارات التي تستورد هذا الملف بشكل مباشر أو غير مباشر. تفعل ذلك أدوات مثلjest --changedSinceوNxaffected:testوتصفية اختبارات Bazel. - الترتيب القائم على التعلم الآلي: يستخدم نظام TAP من Google بيانات فشل الاختبارات التاريخية لترتيب الاختبارات بحسب احتمال اكتشافها للفارق الحالي. تُشغَّل الاختبارات عالية الاحتمال في الموجة الأولى؛ أما الاختبارات منخفضة الاحتمال فتُشغَّل في شُعبة لاحقة أو تُرجأ إلى ليلية.
main، شغّل المجموعة الكاملة — ويُفضَّل أن يكون ذلك داخل طابور دمج (انظر أدناه) حتى تعمل الاختبارات قبل الدمج على الحالة بعد الدمج لا على حالة الفرع. تجاوز المجموعة الكاملة على الجذع هو الكيفية التي تتراكم بها بنى معطلة متقلبة بصمت.
الرافعة الرابعة — طوابير الدمج
تحلّ طوابير الدمج مشكلة التعارض الدلالي: طلبا سحب يجتازان CI بشكل منفرد قد يتعارضان دلاليًا عند جمعهما. دون طابور دمج، يُوافَق على كليهما، ويرث الثاني منهما فرعًا رئيسيًا معطلًا، ويُستدعى مهندس المناوبة.
يُسلسل طابور الدمج عمليات الدمج (أو يُجمّعها بتفاؤل). عند الموافقة على طلب سحب وإضافته إلى الطابور، يُنشئ النظام فرع "مرشح للدمج" مؤقتًا: يُكدّس طلب السحب فوق ما هو موجود في الطابور حاليًا، ويُشغّل CI على تلك الحالة المجمّعة، ولا يُجري الدمج الفعلي إلا إذا نجح CI. في حال الفشل، يُطرد المرشح وحده؛ وتظل طلبات السحب الأمامية في الطابور غير متأثرة.
يُطبّق كلٌّ من طابور الدمج الأصلي في GitHub (المتاح منذ 2023) وMergify والأدوات الداخلية في Google (Submit Queue) وStripe هذا النمط. في Stripe، يعالج طابور الدمج آلاف عمليات الدمج يوميًا، مُجمِّعًا طلبات السحب المتوافقة في مجموعات لمضاعفة إنتاجية CI دون التضحية بالصحة.
merge_group إلزامي. إذا كانت فحوصات الحالة المطلوبة تستمع فقط إلى pull_request، لن يتمكن طابور الدمج من الحصول على نتيجة CI لفرع المرشح وسيتوقف. أضف دائمًا merge_group إلى مُشغّلات سير عملك عند تفعيل طابور الدمج في GitHub.
الجمع بين الرافعات — خط أنابيب مُحسَّن للسرعة
يبدو خط الأنابيب الذي يطبّق الرافعات الأربع هكذا: استعادة مخزن التبعيات المؤقت أولًا (اجعل كل خطوة لاحقة تعتمد على مفتاح المخزن المؤقت)، ثم تشغيل البناء التدريجي عبر Nx أو Bazel، ثم تشغيل الاختبارات المتأثرة فقط على طلبات السحب، ثم تشغيل المجموعة الكاملة داخل مرشح طابور الدمج، وأخيرًا دفع إدخال مخزن مؤقت جديد عند غياب ضربة مخزن. الأوقات المتوسطة الملحوظة على نطاق واسع: تشغيل بارد تحت 8 دقائق، تشغيل دافئ تحت 90 ثانية.
الانضباط الذي يميز CI في شركات التقنية الكبرى عن CI المتوسط هو معاملة وقت خط الأنابيب كمقياس منتج. كل تباطؤ يُسجَّل كخطأ. معدلات ضرب المخزن المؤقت تُعرض على لوحات البيانات. مدد الاختبارات تُتتبَّع لكل ملف بمرور الوقت. عندما يبدأ اختبار في أخذ 30 ثانية بدلًا من 3، يُبلَّغ عنه للتحسين قبل تضخمه عبر آلاف التشغيلات اليومية.