أساسيات التكامل المستمر

سرعة خط الأنابيب على مستوى شركات التقنية الكبرى

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

سرعة خط الأنابيب على مستوى شركات التقنية الكبرى

تُشغّل شركات مثل Google وMeta وStripe آلاف عمليات تنفيذ خطوط أنابيب CI كل ساعة. وعلى هذا الحجم، يُترجَم تحسينٌ بمقدار دقيقتين لكل تشغيل إلى سنوات هندسية من وقت المطورين موفَّرًا سنويًا. لكن السرعة لا تتعلق بالحوسبة الخام فقط، بل بالبنية الذكية: معرفة ما يجب تخزينه مؤقتًا، وأي الاختبارات يجب تشغيلها، وكيفية تسلسل العمل حتى لا ينتظر البشر الآلات أبدًا.

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

الرافعة الأولى — تخزين التبعيات مؤقتًا

المصدر الأكبر لضياع وقت CI في معظم المؤسسات هو إعادة تنزيل وإعادة تصريف التبعيات التي لم تتغير. قد يستغرق تنفيذ npm install بارد في مستودع ضخم من 4 إلى 8 دقائق. أما عند وجود ضربة مخزن مؤقت، فينخفض الأمر إلى 15 ثانية.

تعرض كل منصة CI رئيسية مخزنًا مؤقتًا بمفتاح-قيمة. المفتاح هو تجزئة (hash) لملف القفل (package-lock.json، Gemfile.lock، go.sum، Cargo.lock). عندما لا يتغير ملف القفل — وهو الحال في الغالبية العظمى من عمليات الإيداع — يُستعاد المخزن المؤقت كما هو ويُتجاوَز تثبيت التبعيات بالكامل.

# GitHub Actions — تخزين تبعيات Node.js مؤقتًا بشكل متين - name: Cache node_modules uses: actions/cache@v4 with: path: | ~/.npm node_modules key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} restore-keys: | npm-${{ runner.os }}- - name: Install dependencies (skipped on cache hit) run: npm ci --prefer-offline

مفتاح الاستعادة الاحتياطي restore-keys بالغ الأهمية: إذا تغير ملف القفل — كإضافة حزمة جديدة — فلن يتطابق المفتاح الدقيق. يجد المفتاح الاحتياطي بالبادئة أحدث مخزن مؤقت يشترك في نفس نظام التشغيل، مما يمنحك ضربة جزئية تتجنب التثبيت البارد الكامل. وتُجلب فقط الحزمة المضافة حديثًا من السجل.

طبّق مخازنك المؤقتة بطبقات. خزّن مخرجات البناء (TypeScript المصرَّف، chunks الخاصة بـ Webpack) بمفتاح يُجزّئ ملفات المصدر بالإضافة إلى ملف القفل. بهذه الطريقة، تُنتج الحزم غير المتغيرة والمصدر غير المتغير صفرًا من عمل إعادة البناء. تُفيد فرق Shopify بمعدلات ضرب تصل إلى 60–70% في الإنتاج، مما يقلص متوسط وقت خط الأنابيب إلى النصف تقريبًا.

الرافعة الثانية — البناء التدريجي

تخزين التبعيات يُزيل فئة واحدة فقط من الهدر. الفئة التالية هي إعادة تصريف كود المصدر الذي لم يتغير. يعتمد البناء التدريجي على رسم بياني للبناء: رسم بياني موجَّه غير دوري لكل ملف مصدر ومخرجاته المصرَّفة. يُعيد نظام البناء الصحيح تنفيذ العقد التي تغيرت مدخلاتها أو غابت مخرجاتها فقط.

Nx (لجافاسكريبت/تايبسكريبت) وBazel (متعدد اللغات) هما الأداتان الأكثر شيوعًا على مستوى شركات التقنية الكبرى. يستخدم Nx مخزنًا مؤقتًا للحوسبة مُفهرسًا بتجزئة ملفات المصدر والإعدادات؛ أما Bazel فيستخدم بيئات عزل محكمة وتخزينًا بعناوين محتوى حتى تُنتج المدخلات المتطابقة مخرجات متطابقة دومًا بصرف النظر عن حالة الجهاز.

# Nx — تشغيل المشاريع المتأثرة فقط منذ آخر دمج في main npx nx affected --target=build --base=origin/main --head=HEAD # Nx Cloud تشارك المخزن المؤقت البعيد عبر جميع المشغّلين والمطورين npx nx affected --target=test --base=origin/main \ --head=HEAD \ --parallel=4 \ --configuration=ci

أمر affected هو العنصر الجوهري. يبني Nx رسمًا بيانيًا للتبعيات لمساحة عملك ويحدد المشاريع المتأثرة بشكل انتقالي بالفارق بين HEAD وأساس الدمج. إذا غيّرت مكتبة مساعدة مشتركة، تُعلَّم كل تطبيق نهائي على أنه متأثر. إذا غيّرت خدمة ورقية فحسب، يُعاد بناء تلك الخدمة فقط واختبارها. في Meta، تطبّق الأدوات الداخلية (Buck2) المبدأ نفسه عبر مستودع يضم مئات الآلاف من الأهداف.

الحتمية (Hermeticity) هي الشرط المسبق. تبقى البناءات التدريجية آمنة فقط إذا كان بناؤك محكمًا — أي أن المدخلات المتطابقة تُنتج مخرجات متطابقة في كل مرة. إذا قرأ بناؤك ساعة النظام أو بذرة عشوائية أو مكتبة نظام غير مرقَّمة، فإن المخرج غير حتمي وتخزينه يُنتج نتائج متقلبة. يُطبّق Bazel الحتمية عبر العزل؛ أما Nx فيعتمد على تكوين المدخلات بشكل صحيح من طرفك.

الرافعة الثالثة — اختيار الاختبارات

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

  • تحليل التبعية الساكن: تعيين كل ملف اختبار إلى ملفات المصدر التي يستوردها. إذا أثّر إيداع X في src/billing/invoice.ts، شُغِّلت فقط الاختبارات التي تستورد هذا الملف بشكل مباشر أو غير مباشر. تفعل ذلك أدوات مثل jest --changedSince وNx affected:test وتصفية اختبارات Bazel.
  • الترتيب القائم على التعلم الآلي: يستخدم نظام TAP من Google بيانات فشل الاختبارات التاريخية لترتيب الاختبارات بحسب احتمال اكتشافها للفارق الحالي. تُشغَّل الاختبارات عالية الاحتمال في الموجة الأولى؛ أما الاختبارات منخفضة الاحتمال فتُشغَّل في شُعبة لاحقة أو تُرجأ إلى ليلية.
# Jest — تشغيل الاختبارات المتعلقة بالملفات المتغيرة فقط (يستخدم git diff داخليًا) jest --changedSince=origin/main --passWithNoTests # Pytest — تشغيل الاختبارات التي تغطي سطور المصدر المتغيرة pytest --co -q # جمع فقط، طباعة معرفات الاختبارات المتأثرة pytest tests/ -k "invoice or billing" # تصفية يدوية أثناء الفرز
لا تتخطَّ المجموعة الكاملة على الفروع المحمية. اختيار الاختبارات آمن على فروع الميزات وطلبات السحب. قبل الدمج في main، شغّل المجموعة الكاملة — ويُفضَّل أن يكون ذلك داخل طابور دمج (انظر أدناه) حتى تعمل الاختبارات قبل الدمج على الحالة بعد الدمج لا على حالة الفرع. تجاوز المجموعة الكاملة على الجذع هو الكيفية التي تتراكم بها بنى معطلة متقلبة بصمت.

الرافعة الرابعة — طوابير الدمج

تحلّ طوابير الدمج مشكلة التعارض الدلالي: طلبا سحب يجتازان CI بشكل منفرد قد يتعارضان دلاليًا عند جمعهما. دون طابور دمج، يُوافَق على كليهما، ويرث الثاني منهما فرعًا رئيسيًا معطلًا، ويُستدعى مهندس المناوبة.

يُسلسل طابور الدمج عمليات الدمج (أو يُجمّعها بتفاؤل). عند الموافقة على طلب سحب وإضافته إلى الطابور، يُنشئ النظام فرع "مرشح للدمج" مؤقتًا: يُكدّس طلب السحب فوق ما هو موجود في الطابور حاليًا، ويُشغّل CI على تلك الحالة المجمّعة، ولا يُجري الدمج الفعلي إلا إذا نجح CI. في حال الفشل، يُطرد المرشح وحده؛ وتظل طلبات السحب الأمامية في الطابور غير متأثرة.

Merge Queue flow diagram Merge Queue — Optimistic Batching main PR-441 PR-442 PR-443 PRs approved, added to queue Merge Queue Candidate A = main + PR-441 | Candidate B = A + PR-442 | Candidate C = B + PR-443 CI: Candidate A ✓ pass → merge CI: Candidate B ✗ fail → eject PR-442 CI: Candidate C ✓ pass → merge A merged C merged PR-442 ejected; author notified; does not affect A or C
يُشغّل طابور الدمج CI على فروع "مرشحة" متكدسة. المرشحون الناجحون يُدمجون في main بالترتيب؛ المرشحون الفاشلون يُطردون دون إعاقة الآخرين.

يُطبّق كلٌّ من طابور الدمج الأصلي في GitHub (المتاح منذ 2023) وMergify والأدوات الداخلية في Google (Submit Queue) وStripe هذا النمط. في Stripe، يعالج طابور الدمج آلاف عمليات الدمج يوميًا، مُجمِّعًا طلبات السحب المتوافقة في مجموعات لمضاعفة إنتاجية CI دون التضحية بالصحة.

# GitHub Actions — تشغيل CI عند حدث merge_group (مطلوب لطوابير الدمج) on: push: branches: [main] pull_request: branches: [main] merge_group: # <-- يُشغَّل عند دخول PR إلى الطابور jobs: ci: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Build & Test run: make ci
حدث merge_group إلزامي. إذا كانت فحوصات الحالة المطلوبة تستمع فقط إلى pull_request، لن يتمكن طابور الدمج من الحصول على نتيجة CI لفرع المرشح وسيتوقف. أضف دائمًا merge_group إلى مُشغّلات سير عملك عند تفعيل طابور الدمج في GitHub.

الجمع بين الرافعات — خط أنابيب مُحسَّن للسرعة

يبدو خط الأنابيب الذي يطبّق الرافعات الأربع هكذا: استعادة مخزن التبعيات المؤقت أولًا (اجعل كل خطوة لاحقة تعتمد على مفتاح المخزن المؤقت)، ثم تشغيل البناء التدريجي عبر Nx أو Bazel، ثم تشغيل الاختبارات المتأثرة فقط على طلبات السحب، ثم تشغيل المجموعة الكاملة داخل مرشح طابور الدمج، وأخيرًا دفع إدخال مخزن مؤقت جديد عند غياب ضربة مخزن. الأوقات المتوسطة الملحوظة على نطاق واسع: تشغيل بارد تحت 8 دقائق، تشغيل دافئ تحت 90 ثانية.

الانضباط الذي يميز CI في شركات التقنية الكبرى عن CI المتوسط هو معاملة وقت خط الأنابيب كمقياس منتج. كل تباطؤ يُسجَّل كخطأ. معدلات ضرب المخزن المؤقت تُعرض على لوحات البيانات. مدد الاختبارات تُتتبَّع لكل ملف بمرور الوقت. عندما يبدأ اختبار في أخذ 30 ثانية بدلًا من 3، يُبلَّغ عنه للتحسين قبل تضخمه عبر آلاف التشغيلات اليومية.