Digisky
نرم‌افزار صنعتی و معدنی۱۴۰۵ — پنجرهٔ مهاجرت دو روزه

مهاجرت ۲۹ مخزن به CI/CD خودمیزبان

۲۹ مخزن و خط لوله‌هایشان روی زیرساختی که مشتری کنترلش می‌کند، به‌همراه دو حالت شکستی که بیشترین وقت را می‌گیرند.

مسئله

کل تاریخچهٔ کد، ساختار شاخه‌ها و خط لوله‌های ساخت یک تیم روی سرویسی میزبانی‌شده نشسته بود که از مسیر شبکه‌ای در دسترس بود که نه کنترلش می‌کردند و نه می‌شد به آن تکیه کرد. خواسته «پشتیبان‌گیری» نبود؛ خواسته این بود که آن سرویس اختیاری شود: هر مخزن، هر شاخه، هر تگ، و برای هرکدام خط لوله‌ای که واقعاً بسازد، روی سخت‌افزاری که خود تیم مالکش است.

۲۹مخزن منتقل‌شده
۲۹خط لولهٔ ترجمه‌شده
۲ of ۳فضای‌نام ref منتقل‌شده

خواسته «پشتیبان‌گیری از مخزن‌ها» نبود

پشتیبان‌گیری از مخزن با یک cron انجام می‌شود. خواستهٔ اینجا متفاوت و سخت‌تر بود: سرویس میزبانی‌شده باید اختیاری می‌شد. هر مخزن، هر شاخه، هر تگ، و خط لوله‌ای که واقعاً بسازد — همه روی سخت‌افزاری که تیم مالکش است و به آن دسترسی دارد.

۲۹ مخزن. مقصد: Gitea 1.23 خودمیزبان با act_runner برای یکپارچه‌سازی پیوسته، هر دو داخل Docker. دو روز.

بیشتر کار عادی بود. دو یافته عادی نبود، و همان دو دلیل وجود این پرونده‌اند.

یافتهٔ اول: push‌ای که هرگز شکست نمی‌خورد

راه پیش‌فرضِ فرستادن مخزن به سرور تازه، HTTPS با توکن است. همان چیزی که هر راهنما نشان می‌دهد.

روی این مسیر شبکه معلق ماند. نه خطا. نه timeout. نه انتقال ناقص با حالت قابل‌ازسرگیری. اتصالی که باز می‌شود، منتقل می‌کند و بعد فقط تمام نمی‌شود — روی مسیری که ترافیک را محدود و بازرسی می‌کند و بدنهٔ بزرگ POST دقیقاً همان شکل درخواستی است که گیر می‌کند.

«چرا مخزن اول چهار ساعت طول کشید و دومی هیچ‌وقت تمام نشد؟»

این حالت شکست دقیقاً به‌خاطر بی‌صدا بودنش گران است. push‌ای که خطا برگرداند بلافاصله اشکال‌زدایی می‌شود. push‌ای که معلق بماند منتظرش می‌مانند، بعد دوباره اجرا می‌شود، بعد دوباره منتظرش می‌مانند.

تغییر انتقال به SSH همان بایت‌ها را با قاب‌بندی دیگری فرستاد و کامل شد. همهٔ مخزن‌های بعدی روی SSH رفتند.

اگر تاریخچهٔ گیت را روی مسیر محدودشده، بازرسی‌شده یا کندشده جابه‌جا می‌کنید: پیش از آنکه یک روز صرف تنظیم بافرهای HTTP کنید، SSH را امتحان کنید.

یافتهٔ دوم: هرگز refs/pull/* را نفرستید

غریزهٔ آدم هنگام مهاجرت مخزن، کامل بودن است: همه‌چیز را آینه کن، چیزی گم نشود. این غریزه به push آینه‌ای می‌رسد، و push آینه‌ای refs/pull/* را هم با خودش می‌برد.

نکنید. سرور مقصد ref مربوط به pull request را خودش می‌سازد و مدیریت می‌کند، چون بخشی از تعریف خودش از «pull request چیست» است. فرستادن ref‌های سامانهٔ دیگر به آن فضای‌نام، دو نویسنده روی یک مجموعه ref می‌گذارد و وضعیتی روی مقصد باقی می‌گذارد که باز کردنش وقت‌گیر و برای هر کسی که بعداً نگاه کند گیج‌کننده است.

مشخصهٔ درست push باریک و عمدی است:

  1. refs/heads/* — هر شاخه. این تاریخچه است.
  2. refs/tags/* — هر تگ. این سند انتشار است.
  3. هیچ چیز دیگر. ref‌های pull، دفترداری سامانهٔ مبدأ دربارهٔ فرایند بازبینی خودش است و مقصد دفترداری خودش را دارد.

موتور

مهاجرت از یک مانیفست TSV هدایت می‌شود؛ یک سطر برای هر مخزن، حامل مبدأ، مقصد و وضعیت. هر مرحله قابل‌ازسرگیری است: اجرای دوبارهٔ موتور پس از شکست، دقیقاً باقی‌مانده را برمی‌دارد نه اینکه از اول شروع کند.

این طراحی پیچیدگی نیست، حساب و کتاب است. ۲۹ مخزن روی مسیر ناپایدار یعنی شکست جزئی حالت مورد انتظار است. اسکریپت یک‌بارمصرف هر شکست منفرد را به شروع دوباره از صفر تبدیل می‌کند و روی مسیر کند، شروع دوباره یعنی یک روز.

همان مانیفست، سند «چه چیزی منتقل شد و چه چیزی راستی‌آزمایی شد» هم هست — که مهم‌تر از آن است که به نظر می‌رسد، چون «آیا مخزن نوزدهم واقعاً تگ‌هایش را گرفت؟» پرسشی است که پایان روز دوم از حافظه جواب نمی‌گیرد.

سازمان، کاربرانش، تیم‌هایش و ثبت رانر همه با اسکریپت ساخته شدند نه از رابط وب. دسترسی‌های دستیِ ۲۹ مخزن نه قابل بازبینی است و نه قابل بازسازی.

این برای خریدار چه ارزشی دارد

دو چیز.

اول خودِ توانمندی: کنترل نسخه و یکپارچه‌سازی پیوسته را می‌شود در چند روز — نه چند فصل — به زیرساخت تحت کنترل خودتان منتقل کرد، با خط لوله‌هایی که ترجمه می‌شوند نه اینکه رها شوند.

دوم، و کاربردی‌تر: دو یافتهٔ بالا در مستندات مهاجرت هیچ فروشنده‌ای نیست، چون فقط روی مسیرهای شبکه‌ای ظاهر می‌شوند که آن مستندات فرضشان را نکرده‌اند. اگر روی یکی از آن مسیرها کار می‌کنید، به هر دو برمی‌خورید. حالا رایگان دارید‌شان.

معاملهٔ صادقانه

خودمیزبانی مسئلهٔ عملیاتی را حذف نمی‌کند، جابه‌جایش می‌کند. رانر ظرفیت می‌خواهد، سرور ارتقا می‌خواهد، و کل مجموعه یک تمرین بازیابی از پشتیبان می‌خواهد که واقعاً اجرا شده باشد. سکوی خودمیزبانی که هیچ‌کس تا حالا از پشتیبانش بازیابی‌اش نکرده، از همان سرویسی که جایگزینش شده بدتر است. این باید بخشی از تصمیم باشد، نه غافلگیریِ بعد از آن.

تصمیم‌ها

push روی SSH انجام شد، نه HTTPS.

گزینه‌های دیگر: گیت روی HTTPS با توکن

HTTPS پیش‌فرض است و هر راهنمایی همان را نشان می‌دهد؛ و روی این مسیر شبکه بی‌سروصدا کار نکرد. بدنه‌های بزرگ POST — که یک push تاریخچهٔ کامل دقیقاً همان است — بی‌نهایت معلق می‌ماندند. نه خطا، نه timeout، نه انتقال ناقص: اتصالی که باز می‌ماند و هرگز تمام نمی‌شود، روی مسیری که ترافیک را محدود و بازرسی می‌کند. SSH همان بایت‌ها را با قاب‌بندی دیگری منتقل کرد و کامل شد. این تک‌یافته بیشترین زمان را نجات داد و روی هر مسیر مشابهی ارزش دارد اول امتحان شود.

شکست بی‌صداست و همین گرانش می‌کند. push‌ای که خطا بدهد در چند دقیقه اشکال‌زدایی می‌شود؛ push‌ای که معلق بماند، منتظرش می‌مانند.

فقط refs/heads/* و refs/tags/* منتقل شد. هرگز refs/pull/*.

گزینه‌های دیگر: push --mirror یا push روی refs/*

push آینه‌ای گزینهٔ کامل به نظر می‌رسد و گزینهٔ اشتباه است. سرور مقصد ref مربوط به pull request را خودش به‌عنوان بخشی از مدل دادهٔ خودش می‌سازد و مدیریت می‌کند. فرستادن ref‌های سامانهٔ مبدأ به همان فضای‌نام یعنی دو نویسنده روی یک مجموعه ref، و وضعیتی روی مقصد که باز کردنش خسته‌کننده و برای همه گیج‌کننده است. شاخه‌ها و تگ‌ها تاریخچه‌اند؛ ref‌های pull، دفترداری آن سامانهٔ دیگر دربارهٔ فرایند بازبینی خودش.

مهاجرت از یک مانیفست هدایت شد و هر مرحله قابل‌ازسرگیری ساخته شد.

گزینه‌های دیگر: یک اسکریپت که روی فهرست مخزن‌ها حلقه بزند

۲۹ مخزن روی مسیر شبکهٔ ناپایدار یعنی شکست جزئی حالت مورد انتظار است، نه استثنا. اسکریپت یک‌بارمصرف هر شکست منفرد را به شروع دوباره از صفر تبدیل می‌کند و شروع دوبارهٔ کامل روی مسیر کند، ساعت‌ها هزینه دارد. یک مانیفست TSV با وضعیت به‌ازای مخزن یعنی اجرای دوباره دقیقاً از همان‌جایی که مانده ادامه می‌دهد — و همان فایل، سند این است که چه چیزی منتقل و چه چیزی راستی‌آزمایی شده.

سکو خودمیزبان شد، به‌جای انتقال به یک سرویس میزبانی‌شدهٔ دیگر.

گزینه‌های دیگر: یک ارائه‌دهندهٔ میزبانی‌شدهٔ دیگر برای گیت و CI

مسئله‌ای که حل می‌شد، وابستگی به مسیر شبکه‌ای بیرون از کنترل تیم بود. رفتن به ارائه‌دهندهٔ بیرونی دوم فقط فروشنده را عوض می‌کند و همان کلاس شکست را عیناً بازتولید می‌کند. خودمیزبانی، مسئلهٔ دسترس‌پذیری را به مسئلهٔ عملیات تبدیل می‌کند و این معامله‌ای است که مشتری واقعاً می‌تواند اداره‌اش کند.

سازمان، کاربران، تیم‌ها و رانر با اسکریپت راه‌اندازی شدند.

گزینه‌های دیگر: پیکربندی از طریق رابط وب

دسترسی‌های دستیِ ۲۹ مخزن نه قابل بازبینی است، نه قابل تکرار، و نه پس از بازیابی از پشتیبان قابل بازسازی. اسکریپتی که سازمان، کاربران، تیم‌ها و ثبت رانر را می‌سازد، مستنداتی است که اجرا می‌شود.

محدودیت‌ها

  • فقط ref شاخه‌ها و تگ‌ها منتقل شد. ref مربوط به pull request به مدل دادهٔ خود سرور مقصد تعلق دارد، پس تاریخچهٔ گفت‌وگوی pull request از سامانهٔ مبدأ همراه مخزن‌ها نیامد. آن تاریخچه همان‌جا که ساخته شده می‌ماند.
  • خط لولهٔ ترجمه‌شده بازنویسی است، نه انتقال. دو سامانه دربارهٔ تریگر، متغیر، اجراگر و کانتینر سرویس معنای مشترک ندارند و هیچ هم‌ارزی خودکاری میان تعریف اصلی و workflow جایگزین ادعا نمی‌شود.
  • خودمیزبانی بار را جابه‌جا می‌کند، نه اینکه حذفش کند. ظرفیت رانر، ارتقا، پشتیبان‌گیری و فضای دیسک حالا مسئولیت مشتری است؛ و سکوی خودمیزبانی که هرگز از پشتیبانش بازیابی تمرین نشده، وضعیتی بدتر از همان سرویس میزبانی‌شده است.
  • یافتهٔ SSH مختص یک مسیر شبکهٔ محدودشده است. این ادعا نیست که گیت روی HTTPS خراب است؛ روی شبکهٔ معمولی همان push بی‌مشکل کار می‌کند.
  • هیچ زمان‌سنجی، حجم انتقال یا مدت اجرای خط لوله به‌ازای مخزن در طول مهاجرت ثبت نشد. می‌توانیم بگوییم چه چیزی منتقل شد و چه تصمیم‌هایی هزینه داشت، نه اینکه با چه سرعتی منتقل شد.