یک انتشار مجدد ناموفق هرگز بکاِند سالم را حذف نمیکند
در ۲۵ سپتامبر، یک انتشار مجدد ناموفق بکاِندِ یک اپ را حذف کرد؛ بکاِندی که حدود دو ساعت و نیم زنده و سالم کار میکرد. کدی که انتشار ناموفق را برمیگرداند، هر اپ سروری را که انتشار لمس کرده بود حذف میکرد، از جمله اپهایی که پیش از شروع انتشار هم در حال سرویسدهی بودند. حالا فقط اپهایی که همان انتشار ساخته، حذف میشوند؛ اپی که از قبل وجود داشته، دقیقاً روی همان نسخهای که پیش از این اجرا میکرد برگردانده میشود. دو اصلاح بعدی همان روز تضمین کردند که این بازگردانی هنگام خرابی ابزار دیپلوی خود پلتفرم میزبانی هم اجرا شود، و اینکه یک اپ سالم فقط به این دلیل که خواب بوده، ناموفق اعلام نشود.
بکاِند چگونه منتشر میشود
وقتی اپ Mythex شما بکاِند دارد، یعنی API یا سرور و نه فقط فایلهای ایستا، انتشار آن را از روی Dockerfile (دستورالعمل بستهبندی اپ در یک image) میسازد و آن image را روی ماشینهای پلتفرم میزبانی ما مستقر میکند. سپس یک health check منتظر میماند تا نسخه جدید واقعاً بالا بیاید و پایدار بماند، و بعد انتشار زنده اعلام میشود.
اگر در این مسیر چیزی خراب شود، یک rollback برای برگرداندن وضعیت اجرا میشود. درست بودن این rollback تقریباً از هر بخش دیگر انتشار مهمتر است، چون دقیقاً وقتی اجرا میشود که از قبل چیزی اشتباه پیش رفته است.
مشکل ۱: rollback چیزی را حذف میکرد که باید بازمیگرداند
rollback فهرستی از اپهای سروری که انتشار روی آنها مستقر شده بود نگه میداشت و در صورت خرابی، همه آنها را نابود میکرد. برای انتشار اول این کار درست است: اپ همین لحظه ساخته شده، چیزی به آن وابسته نیست و حذفش وضعیت را به حالت قبل برمیگرداند.
اما برای انتشار مجدد دقیقاً اشتباه است. اپ از قبل وجود داشته و به بازدیدکنندگان واقعی سرویس میداده است. در ۲۵ سپتامبر، API یک اپ که حدود دو ساعت و نیم زنده بود، با یک انتشار مجدد ناموفق حذف شد.
یک حالت خرابی دوم هم در همین کد پنهان بود. وقتی حذف انجام نمیشد، image جدید خراب روی ماشینها باقی میماند، در حالی که به صاحب اپ گفته بودیم سایت فعلیاش هنوز زنده است. تا زمانی که health check بتواند شکست بخورد، image جدید از قبل روی همه ماشینها مستقر شده است. انجام ندادن کاری به معنی برگرداندن نیست.
راهحل: فقط آنچه را ساختهای حذف کن، بقیه را بازگردان
حالا هر انتشار، برای هر اپ سروری که لمس میکند، این دو چیز را ثبت میکند:
- اینکه همین انتشار آن را ساخته یا نه،
- و اگر نساخته، کدام image را درست پیش از آن اجرا میکرده است.
هنگام خرابی، هر اپ یکی از سه رفتار را میگیرد:
- توسط همین انتشار ساخته شده: مثل قبل حذف شود.
- از قبل وجود داشته و image قبلی مشخص است: همان image قبلی را با همان پیکربندی دوباره مستقر کن. بیلدی در کار نیست، فقط image برمیگردد.
- از قبل وجود داشته و image قبلی مشخص نیست: دست نزن. اپ موجود هرگز حذف نمیشود، حتی اگر نتوانیم بازش گردانیم.
این قوانین یک تابع کوچک و خالص با تستهای خودش هستند، پس تصمیمگیری نمیتواند دوباره به «همه چیز را نابود کن» برگردد بیآنکه یک تست شکست بخورد.
مشکل ۲: ابزار دیپلوی ممکن بود پیش از ثبت روش برگشت، شکست بخورد
بعد از آن اصلاح، شکاف دیگری پیدا کردیم. ابزار دیپلوی پلتفرم میزبانی تستهای سلامت خودش را اجرا میکند: اگر ماشینهای جدید کرش کنند، با خطا خارج میشود. اما این کار را بعد از آن انجام میدهد که image جدید از قبل روی ماشینها قرار گرفته است.
کد ما اپ را فقط وقتی برای rollback ثبت میکرد که ابزار با موفقیت برمیگشت. وقتی خود ابزار دیپلوی را شکست میداد، اپ هنوز در فهرست rollback نبود و چیزی بازگردانده نمیشد. در محیط staging ما، یک انتشار مجدد خراب باعث شد image خراب همچنان سرویس بدهد.
راهحل: قبل از شروع، روش برگشت را ثبت کن
حالا اپ بهمحض آماده شدن و پیش از مستقر شدن هر چیزی روی آن، با این اطلاعات برای rollback ثبت میشود: اینکه ساخته شده یا نه و چه imageای اجرا میکرده است. هر چه بعداً خراب شود، rollback میداند چه چیزی را برگرداند. ما این را محلی آزمایش کردیم: یک انتشار مجدد خراب در تستهای سلامت خود ابزار دیپلوی شکست خورد و نسخه قبلی همچنان پاسخ میداد.
مشکل ۳: یک اپ خوابیده بهعنوان «هرگز شروع نشد» ناموفق اعلام شد
اپهای منتشرشده در Mythex برای صرفهجویی در اعتبار شما، وقتی بیکار باشند میخوابند. انتشار مجدد یک ماشین خوابیده را متوقف نگه میدارد، پس health check باید آن را بیدار کند. قبلاً درخواست شروع ماشین را یک بار به پلتفرم میفرستاد و پاسخش را نادیده میگرفت.
در ۲۵ سپتامبر، همان یک درخواست شروع به جایی نرسید. یک API سالم برای کل پنج دقیقهی health check متوقف ماند و با پیام «۱ ماشین هرگز شروع نشد (آخرین وضعیت: stopped)» ناموفق اعلام شد.
پلتفرم گاهی شروع را رد میکند. هنگام بازتولید این مشکل، بدون اینکه ما عمداً کاری کرده باشیم، یک خطای «lease currently held» برگرداند، و در حین بهروزرسانی یک ماشین هم شروعها را نمیپذیرد.
راهحل: تکرار درخواست و کوبیدن به در
حالا health check:
- در حین انتظار، هر ۲۰ ثانیه دوباره از پلتفرم میخواهد ماشین متوقفشده را شروع کند،
- هر پاسخی را که پلتفرم در رد کردن شروع میدهد ثبت میکند،
- و یک درخواست عادی، همانطور که یک بازدیدکننده میفرستد، به اپ میفرستد. پراکسی پلتفرم با رسیدن درخواست، ماشین خوابیده را بیدار میکند.
ما این را روی پلتفرم واقعی و با یک اپ آزمایشی بازتولید کردیم که شروع اولش رد میشد. پیش از اصلاح، انتشار بعد از ۹۱ ثانیه شکست میخورد. بعد از اصلاح، در ۲۰ ثانیه منتشر شد. اپی که واقعاً هنگام بوت کرش میکند همچنان شکست میخورد، همانطور که باید.
آنچه از این ماجرا یاد گرفتیم
- Rollback باید تفاوت «جدید» و «موجود» را بداند. «برگرداندن» چیزی که خودت ساختهای یعنی حذف. «برگرداندن» چیزی که از قبل وجود داشته یعنی بازگردانی. یکی گرفتن این دو، همان راهی است که یک مکانیزم ایمنی، اپ سالم را حذف میکند.
- پیش از اقدام، نحوه برگشت را یادداشت کن. اگر روش برگشت فقط بعد از موفقیت یک مرحله ثبت شود، مرحلهای که نصفهراه شکست بخورد چیزی برای برگشتن باقی نمیگذارد.
- پاسخ درخواستی را که به آن وابستهای هرگز نادیده نگیر. یک «نه» که نادیده گرفته شد، یک اپ سالم را به یک انتشار ناموفق تبدیل کرد.
یک اصلاح مرتبط از همان روز: گرفتن اپهایی که درست بعد از انتشار کرش میکنند. اگر بخواهید بدانید وقتی انتشار در سمت شما شکست میخورد چه اتفاقی میافتد، عیبیابی انتشار را در مستندات ببینید.