Skip to content

فصل ۱۵ — رویهٔ اصلی: قطعات کوچک و ساده

سیستم موفق تمایل دارد با گذشت زمان رشد کند. افراد بیشتری از آن استفاده می‌کنند، افراد بیشتری روی آن کار می‌کنند، چیزهای بیشتری به آن اضافه می‌شود. با رشد سیستم، تغییرات پرریسک‌تر و پیچیده‌تر می‌شوند. این اغلب به فرایندهای پیچیده‌تر و وقت‌گیرتر برای مدیریت تغییرات منجر می‌شود. سربار تغییر دادن، رفع و بهبود سیستم را سخت‌تر می‌کند و بدهی فنی را رشد می‌دهد و کیفیت سیستم را فرسایش می‌دهد.

این نسخه منفی چرخه‌ای است که سرعت تغییر، کیفیت بهتر را هدایت می‌کند و کیفیت بهتر سرعت تغییر را ممکن می‌سازد — همان‌طور که در فصل ۱ توضیح داده شد.

اعمال سه رویه اصلی Infrastructure as Code — تعریف همه چیز به‌صورت کد، تست و تحویل مداوم، و ساخت قطعات کوچک («Three Core Practices for Infrastructure as Code» در صفحه ۹) — نسخه مثبت این چرخه را ممکن می‌سازد.

این فصل روی رویه سوم متمرکز است: ترکیب سیستم از قطعات کوچک‌تر تا بتوانید با رشد سیستم نرخ تغییر سریع‌تر را حفظ کنید و همزمان کیفیت را بهبود دهید. بیشتر ابزارها و زبان‌های کدنویسی زیرساخت ویژگی‌هایی برای پشتیبانی از ماژول‌ها، کتابخانه‌ها و انواع دیگر اجزا دارند. اما تفکر و practice طراحی زیرساخت هنوز به سطح بلوغ طراحی نرم‌افزار نرسیده است.

پس این فصل از اصول طراحی modularity که از دهه‌ها طراحی نرم‌افزار آموخته شده بهره می‌گیرد و آن‌ها را از دیدگاه زیرساخت مبتنی بر کد در نظر می‌گیرد. سپس انواع مختلف اجزا در سیستم زیرساخت را با نگاه به leverage کردن آن‌ها برای modularity بهتر بررسی می‌کند. بر این اساس می‌توانیم ملاحظات مختلف برای کشیدن مرزها بین اجزای زیرساخت را ببینیم.

طراحی برای modularity

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

راه سوم آسان‌تر و ایمن‌تر کردن تغییرات، طراحی سیستم طوری است که بتوانید جزء کوچک‌تری را تغییر دهید بدون نیاز به تغییر بخش‌های دیگر سیستم. قطعات کوچک‌تر از قطعات بزرگ‌تر آسان‌تر، ایمن‌تر و سریع‌تر برای تغییرند.

بیشتر قوانین طراحی برای modularity تنش دارند. اگر بی‌احتیاط دنبال شوند، می‌توانند سیستم را شکننده‌تر و سخت‌تر برای تغییر کنند. چهار معیار کلیدی از «The Four Key Metrics» در صفحه ۹ عدسی مفید برای سنجش اثربخشی modularize کردن سیستم شماست.

ویژگی‌های اجزای طراحی‌شده خوب

طراحی اجزا هنر تصمیم‌گیری درباره کدام عناصر سیستم را با هم گروه‌بندی کنید و کدام را جدا کنید است. انجام درست این کار درک روابط و وابستگی‌ها بین عناصر را می‌خواهد. دو ویژگی طراحی مهم یک جزء coupling و cohesion هستند. هدف طراحی خوب ایجاد coupling پایین و cohesion بالاست.

coupling توصیف می‌کند چقدر تغییر در یک جزء نیاز به تغییر در جزء دیگر دارد. coupling صفر برای دو بخش سیستم هدف واقع‌بینانه‌ای نیست. coupling صفر احتمالاً یعنی اصلاً بخشی از یک سیستم نیستند. در عوض هدف coupling پایین یا loose است.

stack و image سرور coupled هستند، چون ممکن است هنگام upgrade نرم‌افزار روی سرور نیاز به افزایش حافظه تخصیص‌یافته به نمونه سرور در stack داشته باشید. اما نباید هر بار image سرور را به‌روز می‌کنید کد stack را تغییر دهید. coupling پایین تغییر جزء را با ریسک کم شکستن بخش‌های دیگر سیستم آسان‌تر می‌کند.

cohesion رابطه بین عناصر درون یک جزء را توصیف می‌کند. مانند coupling، مفهوم cohesion به الگوهای تغییر مربوط است. تغییرات در منبعی که در stack با cohesion پایین تعریف شده اغلب به منابع دیگر در stack مرتبط نیست.

stack زیرساختی که ساختارهای networking جدا برای سرورهای provision‌شده توسط دو stack دیگر تعریف می‌کند cohesion پایین دارد. اجزای با cohesion بالا آسان‌تر برای تغییرند چون کوچک‌تر، ساده‌تر و تمیزترند، با blast radius («Blast Radius» در صفحه ۵۸) کمتر از اجزایی که mash چیزهای loosely related را شامل می‌شوند.

چهار قانون طراحی ساده

Kent Beck، خالق XP و TDD، اغلب چهار قانون برای ساده کردن طراحی یک جزء ذکر می‌کند. طبق قوانین او، کد ساده باید:

  • تست‌هایش را pass کند (کاری که باید بکند را انجام دهد)
  • قصدش را آشکار کند (واضح و قابل فهم باشد)
  • تکرار نداشته باشد
  • کمترین عناصر را شامل شود

قوانین طراحی اجزا

معماری و طراحی نرم‌افزار اصول و راهنماهای زیادی برای طراحی اجزا با coupling پایین و cohesion بالا دارد.

از تکرار اجتناب کنید

اصل DRY (Don't Repeat Yourself) می‌گوید: «هر قطعه دانش باید یک نمایش واحد، بدون ابهام و مرجع در سیستم داشته باشد.»¹ تکرار مجبور می‌کند مردم در چند مکان تغییر دهند.

مثلاً همه stackهای ShopSpinner از حساب کاربری provisioner برای اعمال پیکربندی به نمونه‌های سرور استفاده می‌کنند. در ابتدا جزئیات login حساب در هر stack و همچنین کدی که image سرور پایه آن‌ها را می‌سازد مشخص شده. وقتی کسی باید جزئیات login حساب را تغییر دهد، باید همه این مکان‌ها را در codebase پیدا و تغییر دهد. پس تیم جزئیات login را به مکان مرکزی منتقل می‌کند و هر stack به‌علاوه server image builder به آن مکان ارجاع می‌دهد.

تکرار مفید تلقی می‌شود

اصل DRY از تکرار پیاده‌سازی یک مفهوم نهی می‌کند، که همان تکرار خطوط تحت‌اللفظی کد نیست. وابسته کردن چند جزء به کد مشترک می‌تواند coupling تنگ ایجاد کند که تغییر را سخت کند.

تیم‌هایی دیده‌ام که اصرار دارند هر کدی که شبیه به نظر می‌رسد متمرکز شود؛ مثلاً همه سرورهای مجازی با یک ماژول ساخته شوند. در عمل، سرورهای ساخته‌شده برای اهداف مختلف — مثل application server، web server و build server — معمولاً باید متفاوت تعریف شوند. ماژولی که باید همه این انواع سرور را بسازد می‌تواند بیش از حد پیچیده شود.

وقتی در نظر می‌گیرید آیا کد تکراری است و باید متمرکز شود، در نظر بگیرید آیا کد واقعاً همان مفهوم را نمایندگی می‌کند. آیا تغییر یک نمونه کد همیشه یعنی نمونه دیگر هم باید تغییر کند؟

همچنین در نظر بگیرید آیا قفل کردن دو نمونه کد در یک چرخه تغییر ایده خوبی است. مجبور کردن هر application server در سازمان به upgrade همزمان ممکن است غیرواقع‌بینانه باشد.

reuse coupling را افزایش می‌دهد. پس قانون سرانگشتی خوب برای reuse این است: درون یک جزء DRY باشید، و بین اجزا wet باشید.

قانون composition

برای سیستم composable، قطعات مستقل بسازید. جایگزین کردن یک طرف رابطه وابستگی بدون اختلال در طرف دیگر باید آسان باشد.²

تیم ShopSpinner با یک image application server Linux شروع می‌کند که از stackهای مختلف provision می‌کند. بعد image application server Windows اضافه می‌کند. کد provision کردن imageهای سرور از هر stack را طوری طراحی می‌کنند که بتوانند به‌راحتی بین این دو image سرور برای برنامه داده‌شده جابه‌جا شوند.

اصل تک‌مسئولیتی (Single responsibility principle)

اصل تک‌مسئولیتی (SRP) می‌گوید هر جزء داده‌شده باید مسئولیت یک چیز را داشته باشد. ایده نگه داشتن هر جزء متمرکز است تا محتوایش cohesive باشد.³

یک جزء زیرساخت — چه سرور، کتابخانه پیکربندی، جزء stack یا stack — باید با یک هدف واحد سازمان‌دهی شود. آن هدف ممکن است لایه‌ای باشد. فراهم کردن زیرساخت برای برنامه هدف واحدی است که یک stack زیرساخت می‌تواند برآورده کند. می‌توانید آن هدف را به routing ترافیک امن برای برنامه — پیاده‌سازی‌شده در کتابخانه stack؛ application server — پیاده‌سازی‌شده با image سرور؛ و نمونه پایگاه داده — پیاده‌سازی‌شده با ماژول stack — بشکنید. هر جزء در هر سطح هدف واحد و به‌راحتی قابل فهم دارد.

اجزا را حول مفاهیم دامنه بسازید، نه فنی

مردم اغلب وسوسه می‌شوند اجزا را حول مفاهیم فنی بسازند. مثلاً ممکن است ساخت جزئی برای تعریف سرور و reuse آن در هر stackی که سرور نیاز دارد خوب به نظر برسد. در عمل، هر جزء مشترک همه کدی که از آن استفاده می‌کند را couple می‌کند.

رویکرد بهتر ساخت اجزا حول مفهوم دامنه است. application server مفهوم دامنه‌ای است که ممکن است برای چند برنامه reuse کنید. build server مفهوم دامنه دیگری است که ممکن است برای دادن نمونه جدا به تیم‌های مختلف reuse کنید. پس این‌ها جزء بهتری از سرورها هستند که احتمالاً به روش‌های مختلف استفاده می‌شوند.

قانون Demeter

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

تیم ShopSpinner در ابتدا با داشتن stackی که cluster application server تعریف می‌کند و stack networking مشترکی که load balancer و قوانین firewall برای آن cluster تعریف می‌کند این قانون را نقض می‌کند. stack networking مشترک دانش جزئی زیادی از stack application server دارد.

ارائه‌دهندگان و مصرف‌کنندگان

در رابطه وابستگی بین اجزا، جزء ارائه‌دهنده (provider) منبعی می‌سازد یا تعریف می‌کند که جزء مصرف‌کننده (consumer) استفاده می‌کند.

stack networking مشترک ممکن است ارائه‌دهنده باشد و بلوک‌های آدرس networking مثل subnet بسازد. stack زیرساخت برنامه ممکن است مصرف‌کننده stack networking مشترک باشد و سرورها و load balancerها را در subnetهای مدیریت‌شده توسط ارائه‌دهنده provision کند.

موضوع کلیدی این فصل تعریف و پیاده‌سازی رابط‌ها بین اجزای زیرساخت است.

بدون وابستگی دایره‌ای

وقتی روابط را از جزئی که منابع ارائه می‌دهد به مصرف‌کنندگان trace می‌کنید، نباید حلقه (یا cycle) پیدا کنید. به عبارت دیگر، جزء ارائه‌دهنده نباید از منابع یکی از مصرف‌کنندگان مستقیم یا غیرمستقیم خودش consume کند.

مثال ShopSpinner stack شبکه مشترک وابستگی دایره‌ای دارد. stack application server سرورهای cluster خود را به ساختارهای شبکه در stack networking مشترک تخصیص می‌دهد. stack networking مشترک load balancer و قوانین firewall برای clusterهای سرور خاص در stack application server می‌سازد.

تیم ShopSpinner می‌تواند وابستگی‌های دایره‌ای را برطرف کند و دانش stack networking از اجزای دیگر را کاهش دهد با انتقال عناصر networking مختص stack application server به همان stack. این هم cohesion و coupling را بهبود می‌دهد، چون stack networking دیگر عناصری ندارد که بیشتر به عناصر stack دیگر مرتبط‌اند.

استفاده از تست برای هدایت تصمیم‌های طراحی

فصل‌های ۸ و ۹ practiceهای تست مداوم کد زیرساخت هنگام کار روی تغییرات را توصیف می‌کنند. این تمرکز سنگین بر تست، testability را ملاحظه طراحی ضروری برای اجزای زیرساخت می‌کند.

سیستم تحویل تغییر شما باید بتواند کد زیرساخت را در هر سطح — از ماژول پیکربندی سرور که agent monitoring نصب می‌کند تا کد stack که cluster کانتینر می‌سازد — ایجاد و تست کند. مراحل pipeline باید بتوانند به‌سرعت نمونه هر جزء را به‌صورت ایزوله ایجاد کنند. این سطح تست با codebase اسپاگتی با وابستگی‌های درهم‌تافته یا اجزای بزرگی که نیم ساعت provision می‌گیرند غیرممکن است.

این چالش‌ها بسیاری ابتکارها برای معرفی regime تست خودکار مؤثر برای کد زیرساخت را منحرف می‌کند. نوشتن و اجرای تست خودکار برای سیستم طراحی‌شده ضعیف سخت است.

و این فایده پنهان تست خودکار است: طراحی بهتر را هدایت می‌کند. تنها راه تست و تحویل مداوم کد، پیاده‌سازی و نگه‌داری طراحی‌های سیستم تمیز با coupling شل و cohesion بالاست.

پیاده‌سازی تست خودکار برای ماژول‌های پیکربندی سرور loosely coupled آسان‌تر است. ساخت و استفاده از mock برای ماژول با رابط‌های تمیزتر و ساده‌تر آسان‌تر است («Using Test Fixtures to Handle Dependencies» در صفحه ۱۳۷). می‌توانید stack کوچک و تعریف‌شده‌خوب را در pipeline سریع‌تر provision و تست کنید.

modularize کردن زیرساخت

سیستم زیرساخت انواع مختلف اجزا دارد، همان‌طور که در فصل ۳ توضیح داده شد، هر کدام می‌توانند از قطعات مختلف تشکیل شوند. نمونه سرور ممکن است از image ساخته شود، با نقش پیکربندی سرور که به مجموعه‌ای از ماژول‌های پیکربندی سرور ارجاع می‌دهد، که خود ممکن است کتابخانه‌های کد import کنند. stack زیرساخت ممکن است از نمونه‌های سرور تشکیل شود و از ماژول‌ها یا کتابخانه‌های کد stack استفاده کند. و چند stack ممکن است ترکیب شوند تا محیط یا estate بزرگ‌تری بسازند.

اجزای stack در برابر stack به‌عنوان جزء

stack زیرساخت، همان‌طور که در فصل ۵ تعریف شد، واحد deployable اصلی زیرساخت است. stack نمونه‌ای از Architectural Quantum است که Ford، Parsons و Kua تعریف می‌کنند: «جزئی deployable مستقل با cohesion عملکردی بالا که همه عناصر ساختاری لازم برای عملکرد صحیح سیستم را شامل می‌شود.»⁴ به عبارت دیگر، stack جزئی است که می‌توانید به‌تنهایی به production push کنید.

همان‌طور که قبلاً گفته شد، stack می‌تواند از اجزا تشکیل شود و stack خودش ممکن است جزء باشد. سرورها یکی از اجزای بالقوه stack هستند که به‌اندازه کافی پیچیده‌اند که بعداً در این فصل عمیق‌تر بررسی شوند. بیشتر ابزارهای مدیریت stack هم پشتیبانی از قرار دادن کد stack در ماژول‌ها یا استفاده از کتابخانه‌ها برای تولید عناصر stack را دارند.

شکل ۱۵-۱ دو stack — با نام‌های خلاقانه StackA و StackB — را نشان می‌دهد که از ماژول کد مشترکی استفاده می‌کنند که ساختار networking تعریف می‌کند.

شکل ۱۵-۱. ماژول کد مشترک استفاده‌شده توسط دو stack

فصل ۱۶ الگوها و ضدالگوهایی برای استفاده از ماژول‌ها و کتابخانه‌های کد stack توصیف می‌کند.

ماژول‌ها و کتابخانه‌های stack برای reuse کد مفیدند. با این حال، برای آسان‌تر کردن تغییر stackها کمتر کمک می‌کنند. تیم‌هایی دیده‌ام که سعی کردند stack یکپارچه («Antipattern: Monolithic Stack» در صفحه ۵۶) را با شکستن کد به ماژول‌ها بهبود دهند. در حالی که ماژول‌ها کد را قابل‌پیگیری‌تر کردند، هر نمونه stack به‌اندازه قبل بزرگ و پیچیده بود.

شکل ۱۵-۲ نشان می‌دهد کد جدا شده در ماژول‌های جدا در نمونه stack ترکیب می‌شود.

شکل ۱۵-۲. ماژول‌های stack پیچیدگی به نمونه stack اضافه می‌کنند

علاوه بر افزودن به عناصر هر نمونه stack، ماژولی که توسط stackهای دیگر هم استفاده می‌شود coupling بین آن stackها ایجاد می‌کند. تغییر ماژول برای برآورده کردن الزام در یک stack ممکن است stackهای دیگری که از ماژول استفاده می‌کنند تحت تأثیر قرار دهد. این coupling ممکن است اصطکاک برای تغییرات اضافه کند.

رویکرد مثمرتر برای manageable کردن stack بزرگ، شکستن آن به چند stack است که هر کدام می‌تواند مستقل از دیگران provision، مدیریت و تغییر یابد. «Patterns and Antipatterns for Structuring Stacks» در صفحه ۵۶ چند الگو برای در نظر گرفتن اندازه و محتوای stack فهرست می‌کند. فصل ۱۷ جزئیات بیشتری درباره مدیریت وابستگی‌ها بین stackها می‌دهد.

استفاده از سرور در stack

سرورها نوع رایج جزء stack هستند. فصل ۱۱ اجزای مختلف سرور و چرخه عمر آن را توضیح داد. کد stack معمولاً سرورها را از ترکیبی از imageهای سرور (فصل ۱۳ را ببینید) و ماژول‌های پیکربندی سرور («Server Configuration Code» در صفحه ۱۷۲) — اغلب با مشخص کردن نقش («Server Roles» در صفحه ۱۷۵) — می‌گنجاند.

codebase تیم ShopSpinner نمونه‌ای از استفاده از image سرور به‌عنوان جزء stack دارد. stackی به نام cluster_of_host_nodes دارد که cluster سرورها را برای عمل به‌عنوان nodeهای host کانتینر می‌سازد، همان‌طور که در شکل ۱۵-۳ نشان داده شده.

شکل ۱۵-۳. image سرور به‌عنوان ارائه‌دهنده به stack

کد تعریف server cluster نام image سرور را مشخص می‌کند:

yaml
server_cluster:
  name: "cluster_of_host_nodes"
  min_size: 1
  max_size: 3
  each_server_node:
    source_image: host_node_image
    memory: 8GB

تیم از pipeline برای ساخت و تست تغییرات image سرور استفاده می‌کند. pipeline دیگری تغییرات cluster_of_host_nodes را تست می‌کند و آن را با آخرین نسخه host_node_image که تست‌هایش را pass کرده یکپارچه می‌کند (شکل ۱۵-۴ را ببینید).

شکل ۱۵-۴. pipeline یکپارچه‌سازی image سرور و stack مصرف‌کننده آن

«Infrastructure Delivery Pipelines» در صفحه ۱۱۹ توضیح داد pipelineها چگونه برای زیرساخت کار می‌کنند.

این مثال یک مسئله جزئی دارد. مرحله اول pipeline برای stack cluster_of_host_nodes از host_node_image استفاده نمی‌کند. اما مثال کد stack نام image سرور را شامل می‌شود، پس نمی‌تواند به‌عنوان مرحله تست آنلاین («Online Testing Stages for Stacks» در صفحه ۱۳۴) اجرا شود. تست کد stack بدون image ممکن است مفید باشد تا تیم مشکلات کد stack را بدون provision کامل سرورهای host node که سنگین‌اند پیدا کند.

تیم ShopSpinner مسئله را با استخراج host_node_image hardcode از کد stack و استفاده از پارامتر stack (فصل ۷) برطرف می‌کند. این کد testableتر است:

yaml
server_cluster:
  name: "cluster_of_host_nodes"
  min_size: 1
  max_size: 3
  each_server_node:
    source_image: ${HOST_NODE_SERVER_IMAGE}
    memory: 8GB

مرحله تست آنلاین برای stack cluster_of_host_nodes می‌تواند پارامتر HOST_NODE_SERVER_IMAGE را با شناسه image سرور stripped-down تنظیم کند. تیم می‌تواند در این مرحله تست کند server cluster درست کار می‌کند — scale up و down و بازیابی نمونه‌های fail شده. image سرور stripped-down نمونه test double است («Using Test Fixtures to Handle Dependencies» در صفحه ۱۳۷).

تغییر ساده جایگزین کردن ارجاع hardcode به image سرور با پارامتر coupling را کاهش می‌دهد. همچنین قانون composition را دنبال می‌کند («Rule of composition» در صفحه ۲۵۴). تیم می‌تواند به‌راحتی نمونه‌های cluster_of_host_nodes با image سرور متفاوت بسازد که اگر افراد تیم بخواهند سیستم‌عامل متفاوتی را برای clusterهایشان تست و به‌صورت تدریجی rollout کنند مفید است.

زیرساخت shared-nothing

در حوزه محاسبات توزیع‌شده، معماری shared-nothing با اطمینان از اینکه nodeهای جدید بدون افزودن contention برای منابع خارج از خود node به سیستم اضافه شوند، scaling را ممکن می‌سازد.

مثال متضاد معمول سیستمی است که پردازنده‌ها یک دیسک مشترک دارند. contention برای دیسک مشترک مقیاس‌پذیری سیستم هنگام افزودن پردازنده‌های بیشتر را محدود می‌کند. حذف دیسک مشترک از طراحی یعنی سیستم می‌تواند با افزودن پردازنده‌ها نزدیک‌تر به خطی scale کند.

طراحی shared-nothing با کد زیرساخت منابع را از stack مشترک به هر stackی که به آن‌ها نیاز دارد منتقل می‌کند و رابطه provider-consumer را حذف می‌کند. مثلاً تیم ShopSpinner می‌تواند application-infrastructure-stack و shared-network-stack را در یک stack ترکیب کند.

هر نمونه زیرساخت برنامه مجموعه کامل ساختارهای networking خود را دارد. این ساختارهای networking را duplicate می‌کند، اما هر نمونه برنامه را مستقل از دیگران نگه می‌دارد. مانند معماری سیستم توزیع‌شده، این محدودیت‌های scaling را حذف می‌کند. مثلاً تیم ShopSpinner می‌تواند به‌اندازه نیاز نمونه زیرساخت برنامه اضافه کند بدون پر کردن فضای آدرس تخصیص‌یافته با یک stack networking مشترک.

اما محرک رایج‌تر برای طراحی کد زیرساخت shared-nothing آسان‌تر کردن تغییر، rebuild و بازیابی منابع networking برای stack برنامه است. طراحی stack networking مشترک blast radius و سربار مدیریت کار با networking را افزایش می‌دهد.

زیرساخت shared-nothing هم مدل امنیتی zero-trust را پشتیبانی می‌کند («Zero-Trust Security Model with SDN» در صفحه ۳۱)، چون هر stack می‌تواند جدا امن شود.

طراحی shared-nothing لزوماً همه چیز را در یک نمونه stack نمی‌خواهد. برای تیم ShopSpinner، جایگزین ترکیب networking و زیرساخت برنامه در یک stack، تعریف networking و زیرساخت برنامه در stackهای مختلف — همان‌طور که قبل — اما ایجاد نمونه جدا از stack networking برای هر نمونه stack برنامه (شکل ۱۵-۵ را ببینید).

با این رویکرد، نمونه‌های stack برنامه networking را با stackهای دیگر share نمی‌کنند، اما دو بخش می‌توانند مستقل مدیریت شوند. محدودیت این کار این است که همه نمونه‌های stack شبکه هنوز با همان کد تعریف می‌شوند، پس هر تغییر در کد نیاز به کار اضافی دارد تا مطمئن شوید هیچ نمونه‌ای را نمی‌شکند.

شکل ۱۵-۵. چند stack با مدل deploy shared-nothing

کشیدن مرزها بین اجزا

برای تقسیم زیرساخت، مانند هر سیستم، باید به دنبال seam بگردید. seam جایی است که می‌توانید رفتار را در سیستم بدون ویرایش در همان جا تغییر دهید.⁵ ایده یافتن مکان‌های طبیعی برای کشیدن مرز بین بخش‌های سیستم است، جایی که می‌توانید نقاط یکپارچه‌سازی ساده و تمیز بسازید.

هر کدام از استراتژی‌های زیر عناصر زیرساخت را بر اساس نگرانی خاصی گروه‌بندی می‌کند: الگوهای تغییر، ساختارهای سازمانی، امنیت و حاکمیت، و تاب‌آوری و scaling. این استراتژی‌ها، مانند بیشتر اصول و practiceهای معماری، به بهینه‌سازی برای تغییر می‌رسند. جستجوی طراحی اجزا طوری که بتوانید تغییرات را در سیستم آسان‌تر، ایمن‌تر و سریع‌تر بدهید.

هم‌راستا کردن مرزها با الگوهای تغییر طبیعی

پایه‌ای‌ترین رویکرد بهینه‌سازی مرزهای جزء برای تغییر، درک الگوهای تغییر طبیعی آن‌هاست. این ایده پشت یافتن seam است — seam مرز طبیعی است.

با سیستم موجود، ممکن است با بررسی تغییرات تاریخی بفهمید چه چیزهایی معمولاً با هم تغییر می‌کنند. تغییرات ریزدانه‌تر مثل commitهای کد بینش مفیدترین را می‌دهند. مؤثرترین تیم‌ها برای commitهای مکرر بهینه می‌کنند و هر کدام را کاملاً یکپارچه و تست می‌کنند. با درک اینکه کدام اجزا تمایل دارند به‌عنوان بخشی از یک commit یا commitهای مرتبط نزدیک در اجزای مختلف با هم تغییر کنند، الگوهایی پیدا می‌کنید که refactor کد برای cohesion بیشتر و coupling کمتر را پیشنهاد می‌دهد.

بررسی سطوح بالاتر کار — مثل ticket، story یا پروژه — می‌تواند کمک کند بفهمید کدام بخش‌های سیستم اغلب در مجموعه تغییرات درگیرند. اما باید برای تغییرات کوچک و مکرر بهینه کنید. پس حتماً drill down کنید تا بفهمید چه تغییراتی می‌توانند مستقل از یکدیگر انجام شوند تا تغییرات تدریجی در context ابتکارات تغییر بزرگ‌تر ممکن شود.

هم‌راستا کردن مرزها با چرخه عمر اجزا

بخش‌های مختلف زیرساخت ممکن است چرخه عمر متفاوتی داشته باشند. مثلاً سرورها در cluster («Compute Resources» در صفحه ۲۸) به‌صورت پویا ایجاد و نابود می‌شوند، شاید چند بار در روز. volume ذخیره‌سازی پایگاه داده کمتر تغییر می‌کند.

سازمان‌دهی منابع زیرساخت در اجزای deployable، به‌ویژه stackهای زیرساخت، بر اساس چرخه عمرشان مدیریت را ساده می‌کند. stack زیرساخت application server ShopSpinner را در نظر بگیرید که از مسیرهای networking، server cluster و نمونه پایگاه داده تشکیل شده.

سرورهای این stack حداقل هر هفته به‌روز می‌شوند، با rebuild با imageهای سرور جدید با آخرین patchهای سیستم‌عامل (همان‌طور که در فصل ۱۳ بحث شد). دستگاه ذخیره‌سازی پایگاه داده به‌ندرت تغییر می‌کند، اگرچه نمونه‌های جدید ممکن است برای بازیابی یا replicate نمونه‌های برنامه ساخته شوند. تیم گاهی networking در stackهای دیگر را تغییر می‌دهد که به‌روزرسانی routing مختص برنامه در این stack نیاز دارد.

تعریف این عناصر در یک stack می‌تواند ریسکی ایجاد کند. به‌روزرسانی image application server ممکن است fail شود. رفع مسئله ممکن است rebuild کل stack — از جمله دستگاه ذخیره‌سازی پایگاه داده — بخواهد که به‌نوبه خود نیاز به backup داده برای restore به نمونه جدید دارد («Data Continuity in a Changing System» در صفحه ۳۸۲). اگرچه مدیریت این در یک stack ممکن است، ساده‌تر بود اگر ذخیره‌سازی پایگاه داده در stack جدا تعریف می‌شد، همان‌طور که در شکل ۱۵-۶ نشان داده شده.

شکل ۱۵-۶. stackهای مختلف چرخه عمر متفاوتی دارند

تغییرات در هر کدام از این micro stackها («Pattern: Micro Stack» در صفحه ۶۲) بدون تأثیر مستقیم بر دیگران انجام می‌شود. این رویکرد می‌تواند رویدادهای مدیریت مختص stack را ممکن کند. مثلاً هر تغییر در stack ذخیره‌سازی پایگاه داده ممکن است backup داده trigger کند که احتمالاً برای هر تغییر در عناصر دیگر stack ترکیبی اول خیلی پرهزینه بود.

بهینه‌سازی مرزهای stack برای چرخه عمر به‌ویژه برای تست خودکار در pipeline مفید است. مراحل pipeline اغلب چند بار در روز هنگام کار روی تغییرات زیرساخت اجرا می‌شوند، پس باید برای feedback سریع و ریتم کاری خوب بهینه شوند. سازمان‌دهی عناصر زیرساخت در stackهای جدا بر اساس چرخه عمر می‌تواند زمان اعمال تغییرات برای تست را کاهش دهد.

مثلاً هنگام کار روی کد زیرساخت application serverها، بعضی مراحل pipeline ممکن است هر بار stack را rebuild کنند («Pattern: Ephemeral Test Stack» در صفحه ۱۴۳). rebuild ساختارهای networking یا دستگاه‌های ذخیره‌سازی بزرگ می‌تواند کند باشد و برای بسیاری تغییرات درگیر در کار لازم نباشد. در این صورت طراحی micro-stack نشان‌داده‌شده قبلاً (شکل ۱۵-۶) فرایند تست و تحویل را ساده می‌کند.

use case سوم جداسازی stackها بر اساس چرخه عمر مدیریت هزینه است. خاموش یا نابود و rebuild زیرساختی که در دوره‌های آرام لازم نیست راه رایج مدیریت هزینه cloud عمومی است. اما بعضی عناصر — مثل ذخیره‌سازی داده — rebuild سخت‌تری دارند. می‌توانید آن‌ها را در stackهای خودشان جدا کنید و در حالی که stackهای دیگر برای کاهش هزینه نابود می‌شوند روشن بمانند.

هم‌راستا کردن مرزها با ساختارهای سازمانی

قانون Conway می‌گوید سیستم‌ها تمایل دارند ساختار سازمانی که آن‌ها را می‌سازد منعکس کنند.⁶ یک تیم معمولاً یکپارچه‌سازی نرم‌افزار و زیرساختی که کاملاً مالک آن است را آسان‌تر می‌یابد و به‌طور طبیعی مرزهای سخت‌تری با بخش‌های سیستم متعلق به تیم‌های دیگر ایجاد می‌کند.

قانون Conway دو پیامد کلی برای طراحی سیستم‌هایی که زیرساخت شامل می‌شود دارد. یکی اجتناب از طراحی اجزایی است که چند تیم باید تغییر دهند. دیگری در نظر گرفتن ساختار تیم‌ها برای منعکس کردن مرزهای معماری مطلوب است، طبق «Inverse Conway Maneuver».

سیلوهای legacy زیرساخت ناهمگون می‌سازند

ساختارهای سازمانی legacy که build و run را در تیم‌های جدا قرار می‌دهند اغلب زیرساخت ناسازگار در مسیر به production ایجاد می‌کنند و زمان، هزینه و ریسک تحویل تغییرات را افزایش می‌دهند. در یک سازمانی که با آن کار کردم، تیم توسعه برنامه و تیم عملیات از ابزارهای پیکربندی مختلف برای ساخت سرور استفاده می‌کردند. برای هر release نرم‌افزار چند هفته هدر رفت تا برنامه‌ها در محیط‌های production درست deploy و اجرا شوند.

برای زیرساخت به‌ویژه، ارزش دارد در نظر بگیرید چگونه طراحی را با ساختار تیم‌هایی که از زیرساخت استفاده می‌کنند هم‌راستا کنید. در بیشتر سازمان‌ها این‌ها خطوط محصول یا سرویس و برنامه‌ها هستند. حتی با زیرساختی که چند تیم استفاده می‌کنند — مثل سرویس DBaaS («Storage Resources» در صفحه ۲۹) — ممکن است زیرساخت را طوری طراحی کنید که نمونه‌های جدا برای هر تیم مدیریت کنید.

هم‌راستا کردن نمونه‌های زیرساخت با تیم‌هایی که از آن‌ها استفاده می‌کنند تغییرات را کم‌مزاحم‌تر می‌کند. به‌جای مذاکره یک پنجره تغییر واحد با همه تیم‌هایی که از نمونه مشترک استفاده می‌کنند، می‌توانید پنجره‌های جدا برای هر تیم مذاکره کنید.

ایجاد مرزهایی که تاب‌آوری را پشتیبانی کنند

وقتی چیزی در سیستم fail می‌شود، می‌توانید جزء deployable مستقل مثل stack زیرساخت را rebuild کنید. می‌توانید عناصر درون stack را دستی تعمیر یا rebuild کنید و جراحی زیرساخت انجام دهید. جراحی زیرساخت به کسی با درک عمیق زیرساخت برای مداخله دقیق نیاز دارد. یک اشتباه ساده می‌تواند وضعیت را خیلی بدتر کند.

برخی به جراحی زیرساخت افتخار می‌کنند، اما fallbackی است برای جبران شکاف در سیستم‌های مدیریت زیرساخت شما.

جایگزین جراحی زیرساخت، rebuild اجزا با فرایندها و ابزارهای تعریف‌شده‌خوب است. باید بتوانید هر نمونه stack را با trigger همان فرایند خودکاری که برای اعمال تغییرات و به‌روزرسانی‌ها استفاده می‌کنید rebuild کنید. اگر بتوانید این کار را بکنید، نیازی نیست در نیمه‌شب بهترین جراح زیرساخت خود را بیدار کنید. در بسیاری موارد می‌توانید بازیابی را به‌طور خودکار trigger کنید.

اجزای زیرساخت باید طوری طراحی شوند که rebuild و بازیابی سریع ممکن باشد. اگر منابع را در اجزا بر اساس چرخه عمرشان سازمان‌دهی کنید («Align Boundaries with Component Life Cycles» در صفحه ۲۶۳)، می‌توانید use caseهای rebuild و بازیابی را هم در نظر بگیرید.

مثال قبلی تقسیم زیرساختی که داده پایدار شامل می‌شود (شکل ۱۵-۶) این کار را می‌کند. فرایند rebuild ذخیره‌سازی داده باید گام‌هایی شامل ذخیره و بارگذاری خودکار داده داشته باشد که در سناریوی disaster recovery مفید است. برای بیشتر «Data Continuity in a Changing System» در صفحه ۳۸۲ را ببینید.

تقسیم زیرساخت به اجزا بر اساس فرایند rebuild آن‌ها بازیابی را ساده و بهینه می‌کند. رویکرد دیگر برای تاب‌آوری اجرای چند نمونه از بخش‌هایی از زیرساخت است. استراتژی‌های redundancy می‌توانند در scaling هم کمک کنند.

ایجاد مرزهایی که scaling را پشتیبانی کنند

استراتژی رایج scaling سیستم‌ها ایجاد نمونه‌های اضافی از بعضی اجزای آن است. ممکن است در دوره‌های تقاضای بالاتر نمونه اضافه کنید و شاید نمونه‌ها را در مناطق جغرافیایی مختلف deploy کنید.

بیشتر مردم می‌دانند بیشتر پلتفرم‌های cloud می‌توانند server clusterها («Compute Resources» در صفحه ۲۸) را با تغییر بار به‌صورت خودکار scale up و down کنند. مزیت کلیدی FaaS serverless («Infrastructure for FaaS Serverless» در صفحه ۲۴۶) این است که فقط وقتی لازم است نمونه‌های کد را اجرا می‌کند.

با این حال، عناصر دیگر زیرساخت — مثل پایگاه داده‌ها، message queueها و دستگاه‌های ذخیره‌سازی — وقتی compute scale up می‌شود می‌توانند گلوگاه شوند. و بخش‌های مختلف سیستم نرم‌افزار شما — حتی جدا از زیرساخت — ممکن است گلوگاه شوند.

مثلاً تیم ShopSpinner می‌تواند چند نمونه stack سرویس مرور محصول deploy کند تا با بار بالاتر کنار بیاید، چون بیشتر ترافیک کاربر در اوج‌ها به آن بخش سیستم می‌خورد. تیم یک نمونه stack routing ترافیک frontend و یک نمونه stack پایگاه داده که نمونه‌های application server به آن متصل می‌شوند نگه می‌دارد (شکل ۱۵-۷ را ببینید).

شکل ۱۵-۷. scaling تعداد نمونه‌های stackهای مختلف

بخش‌های دیگر سیستم — مثل checkout سفارش و سرویس‌های مدیریت پروفایل مشتری — احتمالاً نیازی به scale همزمان با سرویس مرور محصول ندارند. جدا کردن آن سرویس‌ها در stackهای مختلف به تیم کمک می‌کند سریع‌تر scale کند. اتلاف replicate کردن همه چیز را کاهش می‌دهد.

ترجیح گروه‌بندی عمودی بر گروه‌بندی افقی

به‌طور سنتی بسیاری معماران سیستم‌ها را به‌صورت عملکردی سازمان‌دهی می‌کردند. چیزهای networking با هم، چیزهای پایگاه داده با هم و چیزهای سیستم‌عامل با هم. این اغلب نتیجه طراحی سازمان است، همان‌طور که قانون Conway پیش‌بینی می‌کند — وقتی تیم‌ها حول تخصص‌های فنی و عملکردی این‌طور سازمان‌دهی می‌شوند، زیرساخت را بر اساس آنچه مدیریت می‌کنند تقسیم می‌کنند.

گودال این رویکرد این است که سرویسی که به کاربران ارائه می‌شود از بسیاری عملکردها عبور می‌کند. این اغلب به‌صورت سرویس عمودی که لایه‌های عملکردی افقی را قطع می‌کند نشان داده می‌شود، همان‌طور که در شکل ۱۵-۸ است.

سازمان‌دهی عناصر سیستم در stackهای زیرساخت عملکردی cross-cutting دو عیب دارد. یکی اینکه تغییر زیرساخت برای یک سرویس ممکن است شامل تغییر در چند stack باشد. این تغییرات باید با دقت orchestrate شوند تا وابستگی در stack مصرف‌کننده قبل از ظاهر شدن در stack ارائه‌دهنده معرفی نشود («Providers and Consumers» در صفحه ۲۵۵).

پخش مالکیت زیرساخت یک سرویس در چند تیم عملکردی سربار ارتباطی و فرایند قابل‌توجهی برای تغییرات هر سرویس اضافه می‌کند.

عیب دوم stackهای عملکردی وقتی ظاهر می‌شود که در سرویس‌ها share شوند. در شکل ۱۵-۹، تیم سرور یک نمونه stack واحد با سرورها برای چند سرویس مدیریت می‌کند.

وقتی تیم سرور یکی از سرویس‌ها را تغییر می‌دهد، ریسک شکستن سرویس‌های دیگر هست، چون مرز stack همان blast radius برای تغییر است که در «Antipattern: Monolithic Stack» در صفحه ۵۶ توضیح داده شد.

شکل ۱۵-۸. زیرساخت هر سرویس در چند stack تقسیم شده

شکل ۱۵-۹. تغییر لایه افقی روی چند سرویس user-facing اثر می‌گذارد

هم‌راستا کردن مرزها با نگرانی‌های امنیت و حاکمیت

امنیت، انطباق و حاکمیت از داده، تراکنش‌ها و دسترس‌پذیری سرویس محافظت می‌کنند. بخش‌های مختلف سیستم قوانین متفاوتی دارند. مثلاً استاندارد امنیتی PCI الزاماتی روی بخش‌هایی که شماره کارت اعتباری یا پردازش پرداخت را handle می‌کنند تحمیل می‌کند. داده شخصی مشتریان و کارمندان اغلب باید با کنترل‌های سخت‌گیرانه‌تر handle شود.

بسیاری سازمان‌ها زیرساخت را بر اساس مقررات و سیاست‌هایی که به سرویس‌ها و داده‌ای که host می‌کند اعمال می‌شود تقسیم می‌کنند. این وضوح برای ارزیابی چه اقداماتی با جزء زیرساخت داده‌شده لازم است ایجاد می‌کند. فرایند تحویل تغییرات می‌تواند متناسب با الزامات حاکمیت تنظیم شود. مثلاً فرایند می‌تواند بررسی‌ها و تأییدها را enforce و ثبت کند و گزارش‌های تغییر تولید کند که auditing را ساده می‌کند.

مرزهای شبکه مرز stack زیرساخت نیستند

مردم اغلب زیرساخت را به zoneهای امنیتی networking تقسیم می‌کنند. سیستم‌های zone frontend مستقیماً از اینترنت عمومی در دسترس‌اند، با firewall و مکانیزم‌های دیگر محافظت می‌شوند. zoneهای دیگر — مثلاً برای میزبانی برنامه و پایگاه داده — فقط از zoneهای خاص دیگر با لایه‌های امنیتی اضافی در دسترس‌اند.

اگرچه این مرزها برای محافظت در برابر حملات مبتنی بر شبکه مهم‌اند، معمولاً برای سازمان‌دهی کد زیرساخت در واحدهای deployable مناسب نیستند. قرار دادن کد web server و load balancer در stack «frontend» لایه دفاع در برابر تغییرات مخرب کد application server یا پایگاه داده ایجاد نمی‌کند. threat model بهره‌برداری از کد و ابزار زیرساخت با threat model حملات شبکه متفاوت است.

حتماً از کد زیرساخت برای ایجاد مرزهای networking لایه‌ای استفاده کنید.⁷ اما فرض نکنید اعمال مدل امنیتی networking به ساختار کد زیرساخت ایده خوبی است.

جمع‌بندی

این فصل گفت‌وگوی مدیریت زیرساخت بزرگ‌تر و پیچیده‌تر تعریف‌شده به‌صورت کد با شکستن آن به قطعات کوچک‌تر را آغاز کرد. بر محتوای قبلی کتاب — از جمله واحدهای سازمانی زیرساخت (stackها و سرورها) و دو رویه اصلی دیگر تعریف چیزها به‌صورت کد و تست مداوم — بنا شد.


¹ اصل DRY و دیگران در The Pragmatic Programmer: From Journeyman to Master اثر Andrew Hunt و David Thomas (Addison-Wesley) یافت می‌شود.

² قانون composition یکی از قوانین پایه فلسفه Unix است.

³ همکارم James Lewis SRP را روی سؤال اندازه microservice اعمال می‌کند. توصیه او این است که یک جزء باید conceptually در سر شما جا شود — که برای زیرساخت و اجزای نرم‌افزار اعمال می‌شود.

⁴ فصل ۴ Building Evolutionary Architectures اثر Neal Ford، Rebecca Parsons و Patrick Kua (O'Reilly) را ببینید.

⁵ Michael Feathers اصطلاح seam را در کتاب Working Effectively with Legacy Code (Addison-Wesley) معرفی کرد. اطلاعات بیشتر آنلاین هم موجود است.

⁶ تعریف کامل: «هر سازمانی که سیستمی (به‌معنای گسترده) طراحی می‌کند، طراحی‌ای تولید می‌کند که ساختار آن کپی ساختار ارتباطی سازمان است.»

⁷ اگرچه باید مدل zero-trust را بر مدل‌های امنیتی صرفاً perimeter-based ترجیح دهید، همان‌طور که در «Zero-Trust Security Model with SDN» در صفحه ۳۱ ذکر شد.