انتشار/بیرون‌دهی (release) در یک سرویس جهانی

(ویرایش: یک سیستم نمونه در نوشتار دیگری ارائه شده که برخی مفاهیم نوشتار زیر را ملموس‌تر می‌کند.)

اگر یک سرویس نرم‌افزاری بالغ و باآبرو دارید یا قراره داشته باشید، فرایند انتشار (release) ناموسی می‌شه. منظور از بالغ، سیستمی‌ست بزرگ‌شده با کلی قابلیت و مولفه و همچنین سرعت (velocity) بالا در توسعه---یعنی تغییرات پیوسته در سیستم و انتشار مکرر نسخه‌ها که همه باید قابل اطمینان باشن---و منظور از باآبرو یعنی هر مشکل کوچکی می‌تونه برابر باشه با ضرر مالی و صد برابر غیرمالی به اعتبار محصول و brand.

چند رویه‌ی مربوط به فرایند انتشار که در سرویس‌های ما انجام می‌شه و سودمند بوده، یا انجام نمی‌شه و داریم ضربه‌اش رو می‌خوریم، در این نوشتار خلاصه می‌کنم.

انتشار یعنی چی؟ اصلا انتشارِ چه چیزی؟ نسخه‌ی کُد یا داده/dataset یا مدل‌های ML یا قابلیتِ پنهان‌شده پشتِ کلید/flag (که خواهیم دید چیه)؟ فعلا با انتشار کُد/binary جلو بریم و در ادامه به بقیه اشاره می‌کنیم. از لحظه‌ای که نوشتن کُد تموم می‌شه، تا لحظه‌ای که در محیط عملیاتی مستقر بشه، یک فرایند عریض و طویل در میونه که می‌شه خیلی سرانگشتی به دو بخش «تست و یک‌پارچه‌سازیِ پیوسته» (continuous integration یا CI) و «انتشار و استقرار» تقسیمش کرد. خیلی خلاصه:

  • بخش CI یعنی سرِ مخزن کُد، مثلا Git HEAD، همیشه تمیز بمونه. چه طوری؟ ابتدا: داشتن N سطح تست؛ واحد/یکپارچه‌سازی/الف-ب/بار/... (unit/integration/A-B/load/... test). سپس: سیستم CI (مثلا Jenkins) به مخزن کُد (مثلا Git) و سیستم بازبینی کد/code review (مثلا Gerrit) وصل می‌شه، و هر changelist/patch توسعه‌دهندگان رو پیش از اجازه‌ی commit به مخزن، در برابر همه‌ی تست‌های موجود می‌سنجه. جزییات بیشتر، خارج از موضوع این نوشتاره. نتیجه‌ی نهایی: سرِ مخزن کد همیشه تمیزه و آماده‌ی انتشار.
  • انتشار یک binary یعنی برداشتن یک بُرش (cut) از مخزن کد، یعنی معمولا آخرین changelistی که همه‌ی تست‌ها رو پاس کرده که ما اصطلاحا بهش می‌گیم «پلاتینی»/platinum یا طلایی (*)، و ساختن binary و استقرار یا push کردنِ گام به گامِ اون در محیط عملیاتی. این استقرارِ گام به گام، به کمک تست‌های قناری (canary) است - مثل قناری که معدن‌کاران می‌برن تا نشت گاز رو بفهمن. موضوع این نوشتار همینه.
  • (*) نکته‌ی کنکوری: آخرین changelist پلاتینی، طبق چیزی که گفته شد که مخزن همیشه تمیزه، باید بشه همون آخرین changelistی که اجازه‌ی commit گرفته و commit شده دیگه، پس پلاتینی و غیرپلاتینی دیگه چه صیغه‌ایه؟ برخی تست‌ها پیچیده‌تر از اون هستن که بشه روی همه‌ی changelistهای commit شونده اجرا کرد و به ناچار روی هر-چند-تا-یک-بار اجرا می‌شن. مثال: تست‌های نیازمندِ بالا آوردن یک محیط مبسوطِ تست (چندین job و ...)، یا خوروندنِ مقادیر زیادی ورودی به اون چیزِ زیرِ تست، یا اجرا کردن برای حداقل N‌ دقیقه، یا ... مثال: تست‌های نیازمندِ اجرا کردن یک خط لوله (pipeline) زمان‌بر و منبع‌خور.

پس محیط staging چی؟ عبارت staging سرریزِ معنایی شده. اگر منظور خارج از محیط عملیاتی باشه، در تقسیم‌بندیِ بالا جزو CI حساب می‌شه و اگر بخشی از محیط عملیاتی و زیرِ بارِ واقعی باشه، بخشی از قناری.

بدیهیات: کل سیستم رو نمی‌بریم پایین و دوباره بیاریم بالا. سیستم همیشه بالاست و در مقاطعی ترکیبی از نسخه‌ی جدید و قدیم دارن کنار هم کار می‌کنن (سازگاری رو به عقب / backward compatibility با دو نسخه‌ی قدیمی‌تر، از اصوله). این مساله فقط مربوط به انتشار نیست بلکه برای rollback که یکی از ساز و کارهای اصلی در پاسخ به حادثه‌ست هم ضروری‌ست.

انتشار داده/dataset (مثلا lookup tableهایی که توی حافظه‌ی سرورهاست) و مدل‌های ML و بقیه‌ی چیزهای push کردنی چه طور؟ شبیهه، خواهیم دید. انتشار قابلیتِ پشتِ کلید چی بود؟ binary و داده‌ی لازم منتشر شده ولی قابلیتِ مورد نظر ما هنوز غیرفعاله مگر این که یک کلیدی فعال باشه که ما به مرور فعال می‌کنیم مثلا اولش فقط روی ۰/۱٪ درخواست‌ها یا فقط برای ۰/۱٪ کاربران، سپس برای ۱٪ و ...

قناری‌گری رو طبقه‌بندی نمی‌کنم و چنین نگاه جامعی اصلا فراتر از بنده‌ست. تنها مثال‌هایی از چند محصول رو ببینیم برای پیشنهاددهی و پیشنهادگیری.

۱. محصول CDN

این محصول شامل دریایی از cache serverهاست که درخواست‌های کاربران رو پاسخ می‌دن، و نیز شامل دم و دستگاهی offline/نیمه-online خارج از مسیر بحرانی درخواست کاربر (critical query path) که «داده» آماده می‌کنه برای push کردن به سیستم و استفاده در مسیر بحرانی، مثلا یک lookup table برای نگاشت (mapping) از آدرس IP subnetهای دنیا به این که به کدوم سرورها (PoPها) باید برن.

نیمه‌ی اول - سرورهای درگیر با کاربر: binary جدید، که چنان‌که گفته شد همه‌ی تست‌های offline موجود رو از پیش پاس کرده، روی فقط یک یا چند سرور مستقر می‌شه تا برای چند ساعت کار کنه و دَم بکشه و جا بیفته (bake time)، سپس روی یک datacentre کامل مستقر می‌شه و جا می‌افته، سپس روی یک metro کامل---همیشه هم یک datacentre و metro یکسان---و نهایتا استقرارِ جهانی. در هر گام، پس از جا افتادنِ چند ساعته، شاخص(metric)های گوناگونی از سرور بررسی می‌شه تا ببینیم چیزی تغییر نکرده باشه و اگر کرده بررسی بشه، و سپس می‌ریم به گام بعد و باز جا افتادنِ چند ساعته و بررسی و به همین شکل. این تعریفِ کلاسیکِ قناری‌گری در یک سرویسه.

  • این فرایند بسیار مقدماتی و ناکاراست (البته شاید هم دیگه چنین نباشه چون آخرین اطلاع بنده از اون تیم مربوط به ۶ سال پیشه). ایرادها رو با مقایسه با سرویس «نمایش تبلیغات» در پایین خواهیم دید.

نیمه‌ی دوم - دم و دستگاهِ تولیدِ داده: این دم و دستگاه نیمه-online، شامل یک stack از jobهاست---یا یک خط لوله (pipeline)---که یک سروی داده‌های ورودی گردآوری کنه و داده‌ی خروجی تولید کنه برای push کردن به سیستم.

  • در یک چینش استاندارد N+2، این stack در واقع در سه stack موازی بالاست و اجرا می‌شه که رهبرگزینی (leader election) می‌کنن به کمک سرویسی معادل ZooKeeper، و یکی از سه تا می‌شه رییس و داده به سیستم push می‌کنه و دوتای دیگه می‌شن علی‌البدل و داده‌های تولیدیشون رو می‌ریزن دور؛ فقط زنده‌ان برای این که اگر رییس مُرد بلافاصله یکی حاضر و آماده جانشین بشه در حالی که همچنان یک علی‌البدل داره (N+1) تا زمانی که اون رییس قبلی برگرده.

قناری‌گری چنین سیستمی---یا عموما یک خط لوله‌ی خارج از مسیر بحرانی---ساده‌تر از سرویس‌های درگیر با کاربره: یک stack موازی بالا می‌آریم برای قناری، و داده‌های تولیدیش رو در کنارِ داده‌های تولیدیِ stackهای محیط عملیاتی بررسی می‌کنیم. البته همچنان بدیهی نیست، ولی دیگه پیش‌رَوی گام به گام و کنترل ریسک و کنترل دامنه‌ی انفجار (blast radius) هم لازم نیست. جزییات فراوانی رو حذف کردم ولی امیدوارم مفهوم منتقل شده باشه.

۲. محصولِ دستِ کاربر

محصول Nest رو تصور کنید، یا اصلا سیستم عامل Android - یعنی نیاز به push کردن به دستگاهِ دستِ کاربر و نه به سرورهای دستِ خودمون. تست‌های دستگاه، مثلا Nest، کمی متفاوت با تست یک سرویسه و با دخالت انسانیِ بیشتری درگیره، چون مثلا باید روی دستگاه یک دکمه فشار داده بشه بعد روی Mobile App یه کاری بشه بعد سیمِ دستگاه جدا بشه و ... البته این تست‌ها رو تیم‌های مهندسان نرم‌افزارِ ساعتی N دلاری انجام نمی‌دن بلکه تیم‌هایی از کارکنان قراردادیِ سطح پایین‌تر انجام می‌دن. خلاصه سیستم CI که در بالا گفته شد، در این دنیا فرق می‌کنه.

برای انتشار، imageی که این تست‌ها رو گذرونده، برای push شدن به دستگاه‌های ملت در دنیا فرایندی کم و بیش ساده و استاندارد طی می‌کنه: push شدن در چند گام مثلا به ۰/۱٪ کاربران و سپس ۱٪ و ۱۰٪ و الی آخر - البته پوشش خوبِ تست قناری (مثلا جغرافیایی) نیازمند اینه که انتخاب این نمونه‌های آماری درصدی، صرفا تصادفی محض نباشه.

اما یک گامِ امتیازی پیش از از این pushها، مرحله‌ایست که به نام «چشیدنِ غذای سگ» (dogfood)، یعنی push به دستگاه‌های شرکت که در دست صدها یا هزاران نفر از کارکنان خود شرکته. کارکنان، از هر تیمی با ربط یا بی‌ربط، می‌تونن ثبت نام کنن و دستگاه‌های رایگان برای استفاده‌ی شخصی دریافت کنن به این شرط که دریافت کننده‌ی imageهای dogfood باشه (در نتیجه کمی ناپایدارتر) و هر مشکلی هم دیدن bug گزارش بدن. گاهی پیش از dogood یک مرحله‌ی غیررسمی به نام fishfood هم مصطلحه که شامل قناری کردن روی دستگاه‌هاییه که دستِ اعضای خودِ تیمه - همون «کوزه‌گر از کوزه شکسته آب می‌خوره» که کوزه شکسته یعنی ناپایدارترین حالتِ دستگاه.

۳. سرویس نمایش تبلیغات

این محصول، به طور ساده شامل یک stack (در واقع درخت) از سرویس‌ها و jobهای مختلف است که همدیگر رو فرامی‌خونن. درخواستِ نمایش تبلیغ، از مرورگرِ کاربر به سرِ stack فرستاده می‌شه، می‌ره پایین و پردازش می‌شه و پاسخش برمی‌گرده بالا فرستاده می‌شه به مرورگرِ کاربر - حالت معمولِ هر سرویس آنلاین. این سیستم هم، هم شامل jobهاییست در مسیر بحرانی درخواست کاربر (critical query path) و هم jobهایی بیرون از مسیر بحرانی که اون کنار مشغول تهیه‌ی داده و یادگیریِ مدل و غیره هستن برای push کردن به و استفاده در سیستم.

برای انتشار هر یک از jobهای بحرانیِ درختِ بالا، نخست فرایندِ ابتدایی که در مثال CDN گفته شد رو تصور کنید: استقرارِ گام به گام روی {یک سرور، یک datacentre، دنیا} و بررسی شاخص‌ها (metricها) در هر مرحله. ایرادهای این فرایند چیه؟

  • استقرار روی یک سرور، عملا فقط یک «تست دود» (smoke test) است یعنی اگر چیزی به طرز افتضاحی ترکید ببینیم، ولی اطمینان چندانی از ایمن بودن نسخه‌ی جدید نمی‌ده چون فضای نمونه بسیار کوچک است و نویز بالا و معناداریِ آماری تعطیل. یعنی یک شاخصِ تحت نظر (مثلا تعداد کلیک) باید حداقل مثلا ۳۰٪ اُفت کنه تا دیده بشه، چون تا ۲۵٪ اُفت اصلا توی خودِ نویزِ مقایسه گُمه. ۳۰٪ آستانه (threshold) خیلی بزرگ و شُل و وِلی‌ست و مطلوب نیست.
  • استقرار روی یک datacentre هم، هر چند داده‌ی بزرگ‌تر می‌ده، ولی همچنان نویز بالا داره و معناداریِ آماری تعطیل، چون اون datacentre (که داره نسخه‌ی جدید رو اجرا می‌کنه) رو می‌خوایم با چی مقایسه کنیم؟ طبیعتا با یک datacentre نزدیک که داره نسخه‌ی پیش رو اجرا می‌کنه (نه با کل بقیه‌ی دنیا) - که دو گروه آزمایش و کنترل شبیهِ هم داشته باشیم برای مقایسه‌ی مثلا سیب سیب. ولی دو datacentre مجزا، هرچند نزدیک هم، باز مقایسه‌ی سیب و پرتقاله.
  • استقرار روی یک سرور یا یک datacentre، مطلقا پوشش کاملی به ما نمی‌ده چون بسیاری حادثه‌ها فقط در برخی جاها اتفاق می‌افتن - مثلا یک مشکل برای فقط کاربران فلان شهر یا فلان زبان یا فلان سخت‌افزار. یعنی قناری در یک جایی تست شده و به راحتی پاس می‌شه و می‌ره مستقرِ جهانی می‌شه، ولی در جای دیگه می‌ترکه.
  • برای انتشار job الف، ممکنه شاخص‌های سرورهای الف همه سالم باشن ولی پاسخی که سرورهای الف به سرورهای ب می‌دن و ب به پ، باعث مشکل در ب و پ بشن. با قناری‌های گفته شده نمی‌شه چنین اثراتی رو بررسی کرد، چون هر سرور در لایه‌ی الف با N سرور در لایه‌ی ب حرف می‌زنه و برعکس - و همین‌طور در لایه‌ی بین datacentreای. یعنی یک بُرش عمودی «فقط قناری» نداریم. چرا نداریم؟ چرا به سادگی یک stack/درخت موازی نمی‌آریم بالا برای قناری؟ چون یکی دوتا که نیست. ده‌ها job داریم و صدها data و model و قابلیت که هر یک باید مدام قناری بشن و انتشار. stack قناری رو به کدوم بدیم؟ ساده نیست.

راه‌حل‌های که ما استفاده می‌کنیم و سودمند بوده، یا استفاده نمی‌کنیم و دوست داریم کاش می‌شد، اینه:

  • قناری تک‌سروری در چندین datacentre به طور چرخشی و غیرثابت، طوری که هر بار پوشش جغرافیایی خوبی داشته باشیم.
  • قناری half datacentre، یعنی نپریدن از «یک سرور» به «یک datacentre» بلکه استقرار روی نصف سرورهای یک datacentre و در نتیجه مقایسه‌ی نصف نصف، یعنی واقعا مقایسه‌ی سیب سیب و نویز بسیار پایین. یعنی اگر یک شاخص مثلا ۱۰٪ اُفت کنه می‌تونیم شکارش کنیم. چرا تَنگ کردن این بازه مهمه؟ چون ۱۰٪ اُفت در شاخص الف (مثلا تعداد کلیک) اولا به خودی خود بَده و تمام، ولی دوما معمولا همگن نیست بلکه ناشی‌ست از اُفت ۱۰۰٪ی برای بُرشی از سیستم (که در شاخص تجمیع شده ۱۰٪ دیده می‌شه)، مثلا فقط کاربرانِ پشتِ فلان سیستم عامل یا کاربرانِ فلان منطقه یا فلان بخش محصول. بخشی از راه حل برای شکار چنین مشکلاتی، بُعد دادن به شاخص‌هاست که در نوشتار دیگری آمده، و بخش دیگر، میسّر کردنِ تنگ و تنگ‌تر شدن آستانه (threshold)ها.
  • ساز و کاری برای برچسب زدن به logهای درخواست‌ها، به طوری که در پایان معلوم بشه پاسخ به یک درخواست، از کدوم نسخه‌ی job الف و کدوم نسخه‌ی ب و کدوم نسخه‌ی داده‌ی پ و کدوم نسخه‌ی مدل ت رد شده و همچنین کدوم قابلیت‌های پشتِ کلیدِ ث و ج روش فعال بودن و نبودن، تا علی‌رغم قاطی بودن قناری و غیرقناری، با تجمیعِ logهای برچسب خورده در پایان بتونیم اثر الف و ب و... و ج رو در کل سیستم ببینیم.
  • ساز و کاری برای کنترل دامنه‌ی انفجار. اصطلاح «درخواست مرگ» (query of death) رو شاید شنیدید: درخواستی که مثل هر درخواست دیگری مثلا روی برگ (leaf)های یک زیرسیستم بازیابی اطلاعات پخش می‌شه ولی یک bug رو در اون‌ها فعال می‌کنه، یعنی یک درخواست به تنهایی کل سیستم رو می‌آره پایین. بعد هم retry می‌شه و ... (نتیجه‌ی فرعی: حتما برای سیستمتون به ساز و کاری برای query of-of-death filter فکر کنید). به همین صورت، یک تک‌سرور قناری در لایه‌ی الف می‌تونه درخواست/پاسخ خراب به لایه‌ی بعدی بده و کل سرویس ب یا پ رو بیاره پایین. در لایه‌ی اول سرورهای قناری از غیرقناری جداست، ولی از لایه‌ی بعدی به پایین، این جداسازی یا مستلزم دست‌کاریِ هوشمند در لایه‌ی RPC و الگوریتم load balancing است که سخته و فراموشش کن، یا مستلزم طراحی و بده‌بستان (tradeoff) برای بالا آوردن stack موازی ولی نه برای قناریِ هر چیزی.

انتشار داده و مدل‌های جدید (شامل انتشار jobهای غیربحرانی که اون‌ها رو تولید می‌کنن)، مشکل‌ها و راه‌حل‌هایی مشابه قناری jobهای بحرانی که در بالا گفته شد داره: داده‌ی جدید به زیرمجموعه‌ای از سرورها داده می‌شه و شاخص‌ها سنجیده می‌شه، و همین‌طور مشکل‌ها راه‌حل‌های قناری‌گری عمودی.

۴. جمع‌بندی

اشتباه نشه، اون‌چه گفته شد برای انتشار به محیط عملیاتی، ساز و کار اصلی برای «تست محصول» نیست، بلکه ساز و کار اصلی برای «استقرار» محصولِ پیش‌تر-کاملا-تست-شده است.

بیشترِ تمرکز این نوشتار بر انتشار در یک سرویس بود، و به انتشارِ محصول دست کاربر تنها اشاره‌ی کوتاهی شد. این به دلیل ساده بودن یکی و پیچیده بودن دیگری نیست بلکه تنها به دلیل درگیری بیشتر و کمتر بنده با تیم‌های این شکلی و اون شکلی بوده و معنی دیگری نداره.

بسیاری از پیچیدگی‌هایی که بحث شد، ربطی به جهانی بودن سرویس نداره بلکه در سرویسی مستقر در یک datacentre تنها هم وجود خواهد داشت، و بیشتر به پیچیدگی خودِ سرویس مربوط می‌شه.

مداخله‌ی انسانی در فرایندهایی که گفته شد، تنها در خط دوم دفاعی است یعنی مثلا مرور نتیجه‌ی تست‌ها/گام‌هایی که سبز نشدن. اگر بشه این حجم رو به نزدیک صفر رسوند، می‌شه رسیدن به کیمیای «push on green» - در این باره این مقاله‌ی خواندنی از گوگل رو ببینید.