مثالی از حیات/استقرار/پایش/تست/انتشار/... یک سرویس نرم‌افزاری

یک مثال ساختگی از یک سیستم نرم‌افزاری خواهیم دید تا هم نکاتی از دنیای واقعی معماری نرم‌افزار (فرای دوره‌ی ماه عسل) را روی آن مرور کنیم و هم برخی مفاهیم سطح بالا و انتزاعی که در نوشتارهای پیشین رفت را به کمک آن ملموس کنیم - در مقوله‌ی استقرار و پایش (monitoring) و تست و انتشار (release) یک سرویس جهانی.۱. سیستم نمونهفرض کنید یک فروشگاه اینترنتی داریم که بالغ و بالغ‌تر شده و خواهد شد. مثلا در سمت فروش، علاوه بر فروش از انبار خود، دو محصول هم برای فروشنده‌های بیرونی ارائه می‌دهیم: یک محصول مناسب نیازهای تولیدکننده‌های عمده با قابلیت‌های مربوط به آن فضا ارائه داده‌ایم و دیگری را کمی بعدتر برای خرده‌فروش‌ها و آن فضا - نامشان را مثلا بگذاریم «چارسوق» و «سِداِسمال». احتمالا یک قابلیت نیز برای بالا آوردن و ترویج (promotion) کالاها در نتایج جستجو به تولیدکننده‌ها ارائه می‌دهیم. در سمت خریدار، واسط app و web داریم. همچنین واسطی داریم برای بده‌بستان با دو سه فروشگاهِ هم‌پیمان که هم خریدار و هم فروشنده‌ی یکدیگر باشیم (مثلا با دریافت کمیسیون) - نامش را بگذاریم «ائتلاف». این صرفا یک مثال سرهمی و من‌درآوردی‌ست برای مرور نکات نرم‌افزاری و نه تجاری. سیستم‌های جانبی گوناگونی نیز گِرد این ساختار اصلی وجود دارند، مانند قیمت‌گذاری پویا (dynamic) و مدیریت انبار و پیشنهاددهی بر اساس علایق کاربر.ساختار نمونه برای سرویسیک معماری ساده برای این مجموعه، شکل بالاست: چند سیستم برای بازیابی کالا از مخازن، چند سیستم برای جستجو و خرید، یک stack بازیابی در میان، و سیستم‌های جانبی مانند تنظیمات و مقررات و ردگیری کاربر («فضول ملت» در شکل). ده‌ها پرسش گردِ این سیستم و این ساختار، بی‌پاسخ مانده ولی همین نما برای منظور کنونی کافی‌ست. همچنین منظور از stack چه در این نوشتار و چه در ۹۸/۲٪ مکالمات مهندسان نرم‌افزار در واقع یک درخت یا DAG است!این نمای سطح بالا، اسکلت مشترک و رایج در بسیاری سیستم‌های آنلاین است، از موتور جستجو تا فروشگاه اینترنتی و platform محتوا و غیره: یک سو محصول‌های سمت «درخواست کاربر» و یک سو محصول‌های سمت «داده» یا «ارائه» و یک کوه از منطق تجاری (business logic) در میان این دو برای تطبیق (matching) و فیلتر کردن و رتبه‌بندی و اِعمال تنظیمات و سیاست‌ها و ... که معمولا پخش شده‌اند روی stack یا درختی از سرویس‌ها که همدیگر را فرامی‌خوانند.بخش رایج دیگری که در شکل بالا بهش پرداخته نشده--کوه دیگری از منطق تجاری--سیستم‌های عریض و طویل تهیه‌ی داده است، مثلا از «واسط چارسوق» تا مخزن مربوطه که توسط «چارسوقگرد» فراخوانده می‌شود و در شکل بالا تنها یک خط کشیده شده، در دنیای واقعی معمولا یک خط لوله (pipeline) مبسوط برای پردازش و آماده‌سازی داده است - گردآوری، تمیزکاری، اِعمال فلان و فلان، index کردن و الی ماشاالله، که بیرون از موضوع این نوشتار است. خط لوله‌ی دیگر، احتمالا تجمیع logهاست و مدیریت پرداخت و ...۲. ماه عسل و زندگی پس از آناگر به تکامل ساختار یک سرویس نرم‌افزاری علاقه‌ای ندارید می‌توانید از این فصل رد شوید - مستقل از فصل بعد است.شکل بالا زیادی شیک و مجلسی‌ست. یعنی مربوط به زمانی‌ست که اجازه داریم سیستم را از صفر طراحی کنیم. ولی معمولا چنین نیست. نخست یک ماشین سواری مورد نظر را در حد لامبورگینی طراحی می‌کنیم و سپس به مرور تبدیل به اتوبوس می‌شود. مشکلی نیست، فقط باید یک اتوبوس تر و تمیز داشته باشیم و نه با وصله و پینه با تُف و آدامس چسبیده.یعنی مثلا ابتدا تنها یک stack ساده داشتیم که فقط کاربر از فقط انبار کالا بگیرد. سپس به دلایل تجاری لازم شد تا محصول «چارسوق» ارائه شود، سیستمی که منطق تجاری (business logic)اش تفاوت‌های زیادی با stack موجود داشت. طبیعتا N ماه زمان برای بازطراحی و ادغام بهینه‌ی این دو نداشتیم بلکه یک نسخه‌ی جدید از stack را برای آن سفارشی‌سازی (customize) کردیم و بالا آوردیم؛ مثلا سرِ دو stack را به هم دوختیم یا از وسطِ stack اول فراخوانی به سرِ stack دوم زدیم یا هر معماری میان‌مدتی، تا نیاز پاسخ داده شود. سپس محصول «سِداسمال» در کنار «چارسوق»، شاید اصلا به دست دو تیم متفاوت، توسعه داده شد. سپس نیاز تجاری به سرویس ائتلاف رخ داد و سپس سیستم قیمت‌گذاری پویا (dynamic) و سیستم ترویج (promotion) هنگام بازیابی و غیره.هر کدام از این تولدها یعنی یک وصله، یعنی روابط پیچیده و ناهمگن و پر از حالت خاص بین مولفه‌ها، یعنی همان دلیلی که معماری هر سیستم بالغ چندساله‌ای ابتدا توی ذوق می‌زند، یعنی هزاران انشعابِ سفارشی‌سازی و حالت خاص در کُدها. زندگی پس از ماه عسل (که در عنوان آمده) یعنی سالم نگه داشتن این سیستم از N سال پیش تا کنون تا بتوانیم تا N سال دیگر هم به وصله اضافه کردن ادامه داده و نیازهای تجاری آینده را پاسخ دهیم. سالم نگه داشتن یعنی چه؟هنگام افزودن وصله: حفظ سرعت (velocity) با متحمل شدن بدهی فنی (technical debt) قابل مدیریت.به طور پیوسته: بازطراحی و ادغام/جداسازی وصله‌ها و تمیز شدن معماری. یعنی بیشتر پروژه‌های backendی در یک سیستم بالغ.«خراب کنیم و از نو بنویسیم» معمولا وسوسه‌ی مهندسان کم‌تجربه‌تر است - و اتفاقا چنین تفکر انتقادی و به چالش کشیدن وضع موجود را ستایش می‌کنیم چه در هنگام جذب نیرو (نوشتار مربوطه) و چه ارزیابی نیرو (نوشتار مربوطه). ولی در بیشتر موارد رویکردی نادست است، زیرا هم هزینه‌ای گزاف دارد و هم به زودی دوباره همین آش خواهد بود و همین کاسه. یعنی معمولا «بازسازی ماشین در حال حرکت»، هرچند سخت، رویکرد درست‌تر است.بیشترِ پروژه‌هایی که در تیم‌های backendی محصول‌های بزرگ انجام می‌شود از این جنس‌اند: بازسازی پشت صحنه - تعمدا از اصطلاح refactoring استفاده نمی‌کنم. در زیرسازمان ما، یک پروژه‌ی ۳ ساله با چندده نفر از چندین تیم درگیر، فقط یک نمونه بازسازی بود که دو تا stack مشابه ادغام بشن به جای این hack که تهِ این یکی سرِ آن یکی را فرابخواند. کاری‌ست پیچیده‌تر از معماری آغازین و نیازمند طراحی‌های پیچیده‌ی چندفازی. سیستم در طول این ۳ سال بالاست و کارش را می‌کند.این فصل طولانی شد و وارد خودِ مقوله‌ی بازسازی نمی‌شویم، ولی امیدوارم صحنه‌ی ملموسی چیده شده باشد برای دید دادن به هم‌کارانِ تازه‌کارتر با محصولاتِ رو به بلوغ: این شُتری‌ست که روی همه می‌خوابد!۳. نکاتی از توسعه و نگه‌داری یک سرویس جهانی و همیشه بالادر چند نوشتار قبلی، نکاتی از توسعه و نگه‌داری یک سرویس رفت که شاید جز برای هم‌کارانی که از نزدیک با مساله‌های مشابه درگیرند، قابل استفاده نبوده باشند. در این فصل، برخی را روی سیستم نمونه‌ی بالا تصویر می‌کنم برای انتقال بهتر و با ارجاع به نوشتار مربوطه برای جزییات بیشتر. زیرفصل‌های زیر مستقل از یکدیگرند مگر گفته شود، و قابل خوانش جداگانه و انتخابی.۳.۱. استقرار سیستم بالامی‌توانیم فرض کنیم جعبه‌های شکل بالا، jobهای مجزایند که یک‌دیگر را فرامی‌خوانند، مثلا با پروتکل gRPC. منظور از job، خرواری از N سرور است که یک binary (یا container) یکسان اجرا می‌کنند و روی یک یا چند datacentre پخش شده‌اند. منظور از سرور هم یک نمونه/replica/instance از job است و نه یک سرور فیزیکی.یک چینش رایج برای این سرویس، بالا آوردن N نمونه/instance از stack در Nتا datacentre است. یک نمونه از stack یعنی مثلا ۲۰۰تا جلودار و ۳۰۰تا میان‌دار و الی آخر؛ نمونه‌ی دیگر در datacentre دیگر شاید ۵۰تا جلودار و ۸۰تا میان‌دار و به همین شکل.۳.۲. توزیع بار / load balancing(وابسته به زیرفصل ۳.۱) سرورهای جلودار در datacentre الف ممکن است تنها با سرورهای میان‌دار در datacentre الف در ارتباط باشند یا با سرورهای میان‌دار در datacentreهای دیگر نیز--و به همین شکل بین سرورهای مثلا میان‌دار و تنظیمات‌چی یا بین عقب‌دار و انبارگرد و الی آخر--بسته به پیکربندی سیستم که کدام backendها محلی (محدود به datacentre) باشند و کدام‌ها جهانی؛ سود و زیان‌های این انتخاب نیز آموزنده ولی فراتر از این نوشتار است و از خواننده دعوت می‌شود در آن درنگ کرده و نظرش را در پایین بنویسد.در هر صورت، یک سرور میان‌دار با N سرور عقب‌دار در ارتباط است و درخواست‌ها را روی آن‌ها پخش می‌کند. با این مقدمات، نکاتی از توزیع بار در یک سرویس جهانی که در این گفتار پیشین بررسی شد شاید اکنون ملموس‌تر و بازخوانی به علاقه‌مندان توصیه می‌شود - به ویژه مطالب زیر عنوان «پردازشی».۳.۳. پایش (monitoring) سطح بالامنظور تنها پایش نمودارِ شاخص(metric)های نهایی مانند حجم فروش و تعداد کلیک بر ساعت نیست بلکه پایش جزییات درون سیستم به مانند یک جعبه‌ی شفاف (white box) است. مثلا اگر بخشی از منطق «میان‌دار» این است که نتایج همانند بین «انبار» و «چارسوق» را تکراری‌زدایی کند، شاخصی خواهیم داشت که تعداد موارد تکراری‌زدایی‌شده را بشمرد تا از روی آن نمودار بکشیم؛ اگر این منطق شامل چند if-else است برای دسته‌های مختلف کالا، تعدادی که هر یک از if-elseها برایش تصمیم الف یا ب گرفتند را نیز در شاخص‌هایی می‌شماریم تا نمودار بکشیم. به چه هدفی؟وقتی یک شاخص نهایی مانند «حجم فروش کالاهای دسته‌ی الف» اُفت کرد و مهندسان گوش‌به‌زنگ (oncall) هشدار دریافت کردند، با استیصال نپرسند حالا چه کنیم بلکه با زنجیره‌ای از شاخص‌ها بتوانند مشکل را ردگیری کنند. مثلا دریابند نمودار «تعداد حضور کالای الف بر ثانیه» در شاخص‌های سرویس «انباردار» و «عقب‌دار» تغییری نداشته ولی از «میان‌دار» به بعد یک اُفت واضح داشته هماهنگ با افت نهایی در «حجم فروش»؛ سپس با بررسی نمودارهای مربوط به مولفه‌های درونِ میان‌دار، برسند به مولفه‌ی تکراری‌زدایی، و آمرزش بفرستند به رفتگانِ آن مهندسِ فهیمی که این شاخص‌ها را تعبیه کرده. بدون داشتن چنین دیدی (visibility)، هنگام حادثه تنها گزینه‌ی ما «چک کردن زمانی» است: آیا انتشار (release) یا تغییر دیگری در لحظه‌ی آغاز حادثه داشتیم که rollback کنیم یا نه (و دعا که «توروخدا آره!»)، ولی گاهی آری و گاهی نه.خودِ شاخص‌های مولفه‌ی تکراری‌زدایی--و عموما شاخص‌های نزدیک به کُد و نه شاخص‌های نهایی در پایانِ سیستم--منبع بهتری برای شلیک هشدارهای عمل‌پذیر (actionable) هستند.مهم‌ترین معیار برای پاسخ به «آیا نسخه‌ی جدید باعث تغییری در سیستم می‌شود» که هنگام انتشار (release) و استقرار گام به گامِ قناری (canary) می‌پرسیم، مقایسه‌ی همین شاخص‌های جعبه-شفاف است.همچنین چندبُعدی کردن شاخص‌ها اهمیت کلیدی دارد. مثلا فرض کنید bugی رخ داده و فلان field از داده‌ای که واسط Android app ما (و نه سایر واسط‌ها) ارسال می‌کند و برای تشخیص علایق کاربر و پیشنهاددهی از «انبار» (و نه سایر مخزن‌ها) لازم است، دیگر set نمی‌شود و خالی می‌ماند. یا فرض کنید bugی در اِعمال مقررات رخ داده و کالاهای دسته‌ی الف (و نه همه‌ی دسته‌ها) به «ائتلاف» (و نه به سایر خریداران) ارائه نمی‌شوند. یک اُفت مهیب ۵۰٪ای در چنین بُرشی از سیستم، در یک شاخص تجمیع شده و بی‌بُعد تنها یک اُفت ۲٪ای است و در نویز نمودارها گُم.با این مقدمات، مطالبی گردِ پایش که در این گفتار پیشین بررسی شد احتمالا ملموس‌تر و بازخوانی به علاقه‌مندان توصیه می‌شود.۳.۴. تست‌هاافزون بر هزاران تست واحد (unit test) در کُدها و تست‌های یکپارچه‌سازی (integration) در سطح مولفه‌های درون سرورها، سیستم بالا شامل تست‌های یک‌پارچه‌سازی در سطح سرور و stack نیز خواهد بود. مثلا اگر کُدی در یک مولفه از سرور «میان‌دار» نوشتیم، باید یک سرور کامل میان‌دار بالا آمده و آزموده شود، مثلا:تست‌های در انزوا (isolation) با محیط ساختگی (backendها و data storeهای mock)، چنان که مثلا اگر ورودی الف به میان‌دار داده شود، backendهای عقب‌دار و تنظیمات‌چی و ... انتظار ورودی ب و پ و ... از میان‌دارِ زیرِ تست دارند و نیز از مجموعه‌ی میان‌دار انتظار خروجی ت داریم.تست‌های با محیط تستی، چنان که یک نمونه‌ی کوچک از stack در محیط تست بالا آورده و به میان‌دار (یا اصلا به جلودار در سرِ stack) ورودی از پیش تعیین شده می‌دهیم و انتظار خروجی از پیش تعیین شده داریم. نکته‌ی فرعی: شبیه نگه داشتن این محیط با محیط عملیاتی، از طرفی بسیار مهم است و از طرفی کار حضرت فیل.تست‌های با محیط واقعی، چنان که نمونه‌ی تستی از میان‌دار، با یک کپی از درخواست‌های ورودی واقعی و نیز با اتصال به داده‌های زنده و واقعی و backendهای واقعی آزموده می‌شود. عبارت staging که سرریز معنایی شده، گاهی برای چنین صحنه‌آرایی‌هایی (که پیش از مرحله‌ی قناری‌گری/canarying است) به کار می‌رود و گاهی برای بخشی از قناری‌گری. در چنین صحنه‌آرایی، ورودی و خروجیِ از پیش تعیین شده، شدنی نیست، بنابراین تست‌های از این جنس عموما به شکل الف/ب یا در برابر انتظارات آماری سنجیده می‌شوند و «عدم تعیّن» (non-determinism) در چندین لایه، در ذات آن‌هاست. چنین تست‌هایی حیاتی‌اند تا کمتر بپرسیم «این رفتار که توی تست پاس شده بود پس چرا تو محیط واقعی ترکید».به جز درستی‌سنجی خروجی، رفتار سرور نیز در هر یک از صورت‌های بالا (از ساختگی به واقعی) نیازمند سنجش است: از ظرفیت پردازشی (مثلا تعداد درخواست پردازش شده تقسیم بر cpu مصرف شده) تا تاخیر در درخواست‌ها و چکش‌خواری زیر overload و الی آخر. نکته‌ی فرعی: در این گونه تست‌ها، همسانی میان‌دارِ محیط تست و میان‌دارِ محیط واقعی، بَل حساس‌تر از تست‌های درستی‌سنجی‌ست.با این مقدمات، بازخوانی مساله‌های مربوط به تزلزل (flakiness) در تست‌ها که در این نوشتار پیشین مرور شد به علاقه‌مندان توصیه می‌شود.۳.۵. انتشار/بیرون‌دهی (release)(وابسته زیرفصل‌های بالا) فرض کنیم هر روز یا هر هفته binary جدیدی برای «میان‌دار» منتشر می‌شود، یعنی از روی آخرین نسخه‌ی commit شده به مخزن کد--که به لطف سیستم CI همیشه تمیز است و همه‌ی N لایه تست را پاس کرده و آماده‌ی انتشار است (اینجا)--binary ساخته می‌شود و برای استقرار در محیط عملیاتی، وارد فرایند استقرار گام به گام «قناری» (canary) می‌شود.نکاتی که در این نوشتار پیشین گِردِ انتشار (release) مرور شد را به طور خلاصه روی مثال کنونی ببینیم:نخستین گام قناری کجا مستقر شود؟ روی چند تک‌سرور در یک یا چند datacentre گوناگون که پوشش خوب جغرافیای به دست دهد. هدف؟ جا افتادن برای دست کم چند ساعت و سپس مقایسه‌ی شاخص‌ها (metricها). گام بعدی؟ گسترش به نیمی از سرورها در آن datacentreها. هدف؟ مقایسه با معناداری آماری. گام بعدی؟ گسترش به همه‌ی سرورها. هدف؟ دیدن تاثیراتی که فقط در مقیاس «همه‌ی datacentre» دیده می‌شود. گام بعدی؟ استقرار جهانی روی سایر datacentreها.در هر گام، مثلا گام نخست، چه چیزی سنجیده شود؟ شاخص‌های خودِ «میان‌دار». اگر نسخه‌ی جدید میان‌دار باعث خطا نه در خود میان‌دار بلکه در جلودار شد چه؟ اگر درخواست‌هایی که به به پایین فرستاد، ماشه‌ی یک bug را در دو لایه پایین‌تر کشید و کُشنده بود چه؟برای انتشار داده‌ها چه طور؟ مثلا datasetای که «تنظیمات‌چی» با آن کار می‌کند یا datasetای در «مقررات‌چی» که کالاها را فیلتر می‌کند و مرتب انتشار (release) دارند، شاید bug داشته باشد یا bugی را در سایر سیستم فعال کند.اگر مساله‌ها ملموس شد، با تجسم مثال فروشگاه اینترنتیِ بالا و استقرار سرورها و ارتباطات بین سروری و شاخص‌های بُعددار و غیره، بازخوانی این نوشتار پیشین درباره‌ی انتشار (release) به علاقه‌مندان توصیه می‌شود.