یک مثال ساختگی از یک سیستم نرمافزاری خواهیم دید تا هم نکاتی از دنیای واقعی معماری نرمافزار (فرای دورهی ماه عسل) را روی آن مرور کنیم و هم برخی مفاهیم سطح بالا و انتزاعی که در نوشتارهای پیشین رفت را به کمک آن ملموس کنیم - در مقولهی استقرار و پایش (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 سال دیگر هم به وصله اضافه کردن ادامه داده و نیازهای تجاری آینده را پاسخ دهیم. سالم نگه داشتن یعنی چه؟
«خراب کنیم و از نو بنویسیم» معمولا وسوسهی مهندسان کمتجربهتر است - و اتفاقا چنین تفکر انتقادی و به چالش کشیدن وضع موجود را ستایش میکنیم چه در هنگام جذب نیرو (نوشتار مربوطه) و چه ارزیابی نیرو (نوشتار مربوطه). ولی در بیشتر موارد رویکردی نادست است، زیرا هم هزینهای گزاف دارد و هم به زودی دوباره همین آش خواهد بود و همین کاسه. یعنی معمولا «بازسازی ماشین در حال حرکت»، هرچند سخت، رویکرد درستتر است.
بیشترِ پروژههایی که در تیمهای 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ها برایش تصمیم الف یا ب گرفتند را نیز در شاخصهایی میشماریم تا نمودار بکشیم. به چه هدفی؟
همچنین چندبُعدی کردن شاخصها اهمیت کلیدی دارد. مثلا فرض کنید bugی رخ داده و فلان field از دادهای که واسط Android app ما (و نه سایر واسطها) ارسال میکند و برای تشخیص علایق کاربر و پیشنهاددهی از «انبار» (و نه سایر مخزنها) لازم است، دیگر set نمیشود و خالی میماند. یا فرض کنید bugی در اِعمال مقررات رخ داده و کالاهای دستهی الف (و نه همهی دستهها) به «ائتلاف» (و نه به سایر خریداران) ارائه نمیشوند. یک اُفت مهیب ۵۰٪ای در چنین بُرشی از سیستم، در یک شاخص تجمیع شده و بیبُعد تنها یک اُفت ۲٪ای است و در نویز نمودارها گُم.
با این مقدمات، مطالبی گردِ پایش که در این گفتار پیشین بررسی شد احتمالا ملموستر و بازخوانی به علاقهمندان توصیه میشود.
۳.۴. تستها
افزون بر هزاران تست واحد (unit test) در کُدها و تستهای یکپارچهسازی (integration) در سطح مولفههای درون سرورها، سیستم بالا شامل تستهای یکپارچهسازی در سطح سرور و stack نیز خواهد بود. مثلا اگر کُدی در یک مولفه از سرور «میاندار» نوشتیم، باید یک سرور کامل میاندار بالا آمده و آزموده شود، مثلا:
با این مقدمات، بازخوانی مسالههای مربوط به تزلزل (flakiness) در تستها که در این نوشتار پیشین مرور شد به علاقهمندان توصیه میشود.
۳.۵. انتشار/بیروندهی (release)
(وابسته زیرفصلهای بالا) فرض کنیم هر روز یا هر هفته binary جدیدی برای «میاندار» منتشر میشود، یعنی از روی آخرین نسخهی commit شده به مخزن کد--که به لطف سیستم CI همیشه تمیز است و همهی N لایه تست را پاس کرده و آمادهی انتشار است (اینجا)--binary ساخته میشود و برای استقرار در محیط عملیاتی، وارد فرایند استقرار گام به گام «قناری» (canary) میشود.
نکاتی که در این نوشتار پیشین گِردِ انتشار (release) مرور شد را به طور خلاصه روی مثال کنونی ببینیم:
اگر مسالهها ملموس شد، با تجسم مثال فروشگاه اینترنتیِ بالا و استقرار سرورها و ارتباطات بین سروری و شاخصهای بُعددار و غیره، بازخوانی این نوشتار پیشین دربارهی انتشار (release) به علاقهمندان توصیه میشود.