الگوهای معماری میکروسرویس بخش دوم : الگوهای جداسازی

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

https://aparat.design/microservice-patterns-part-1-xfktsz2zal3q

الگوهای جداسازی

الگوهای مورد استفاده برای جداسازی میکروسرویس‌ها یا به زبان دیگر، شکستن یک مونولیت به چندین میکروسرویس، عموماً براساس نوع تصمیم‌گیری دسته‌بندی می‌شوند. الگوهای زیر جزو موارد پرکاربرد در این زمینه هستند:

  • جداسازی بر اساس قابلیت های کسب و کاری
  • جداسازی بر اساس زیر دامنه
  • جداسازی بر اساس تراکنش
  • جداسازی به روش Strangler

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

مثلث موفقیت
مثلث موفقیت


معماری میکروسرویس این کار را به دو روش انجام می‌دهد:

  1. تست‌کردن بخش‌های مختلف، کد را ساده  و در نتیجه امکان توسعه اجزای مختلف را به‌صورت مستقل فراهم می‌کند.
  2. تیم‌های مهندسی را ساختاربندی می‌کند یعنی تیم به صورت مجموعه‌ای از تیم‌های کوچک ۶ تا ۱۰ نفره‌ی مستقل، که هر کدام مسئولیت توسعه و نگهداری یکی از سرویس‌ها هستند، تقسیم می‌شوند.

توجه: این مزایا به صرف تصمیم‌گیری برای استفاده از میکروسرویس به وجود نمی‌آیند و این امر نیازمند جداسازی نرم‌افزار به سرویس‌های کوچک‌تر وظیفه‌محور است، یک سرویس، باید آن‌قدر کوچک باشد که یک تیم کوچک برای توسعه و تست آن کفایت کند.

یک راهبرد مفید در این زمینه استفاده از اصل مسئولیت واحد(Single Responsibility) از موضوع کلی‌تر Object Oriented Design است، در این اصل، گفته می‌شود:

  1. رای رسیدن به یک طراحی خوب، نیاز داریم که هر کلاس تنها و تنها یک هدف و وظیفه را در برنامه ایفا کند. کار تمام روش‌های(Methods) کلاس باید در راستای رسیدن به آن هدف باشد و استفاده از هر روشی با وظایف منحرف از هدف اصلی اشتباه است؛ این امر باعث درهم‌تنیدگی وظایف در برنامه و بنابراین پیچیدگی کد و در نتیجه کم‌کردن Reusability یا قابلیت استفاده مجدد از کدها می‌شود، همچنین هر روش کلاس باید تنها و تنها یک وظیفه را ایفا کند.
  2. جداسازی سرویس‌ها نیز باید به گونه‌ای باشد که تغییرات مورد نیاز تنها یک سرویس را تحت تاثیر قرار دهد. چون پیاده‌سازی امکاناتی که بتواند چند سرویس را تحت تاثیر قرار دهد، نیازمند هماهنگی بین چند تیم است که باعث کند شدن روال توسعه می‌شود.

یکی از دیگر اصول مفید OOD برای جداسازی میکروسرویس ها اصل Common Closure Principle است. این اصل بیان میکند:

کلاس‌هایی که به دلایل مشابهی نیاز به تغییر دارند باید در یک پیکچ قرار گیرند.

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

این دیدگاه در زمان طراحی سرویس‌ها خیلی کمک‌کننده است؛ چون به ما کمک می‌کند مطمئن شویم هر تغییری تا جای ممکن فقط یک سرویس را تحت‌تاثیر قرار دهد.

صورت مساله؛ چگونه یک نرم‌افزار را به چند سرویس کوچک‌تر تقسیم کنیم؟چگونه یک نرم افزار را به چند سرویس کوچکتر تقسیم کنیم ؟

نیازمندی ها :

  1. معماری نهایی باید پایدار باشد.
  2. سرویس‌ها باید به هم پیوسته باشند. به عبارت دیگر هر سرویس باید مجموعه‌ای از توابع به‌شدت مرتبط باشد.
  3. هر سرویس باید از اصل CCP (Common Closure Principle) تبعیت کند، یعنی بخش‌هایی که باید باهم تغییر کنند داخل یک سرویس قرار بگیرند تا در صورت نیاز به تغییر، تنها یک سرویس تحت‌تاثیر قرار گیرد.
  4. سرویس‌ها باید کمترین وابستگی را به هم داشته باشند. هر سرویس به عنوان یک API پیاده‌سازی می‌شود و هرگونه تغییر در پیاده‌سازی داخلی نباید سرویس‌گیرندگان را تحت‌تاثیر خود قرار دهد.
  5. هر سرویس باید قابل تست باشد.
  6. هر سرویس باید به اندازه‌ای کوچک باشد که توسط یک تیم "دو پیتزایی" قابل پیاده‌سازی و نگهداری باشد.
  7. هر تیمی که مسئولیت یک یا چند سرویس را برعهده دارد باید خودکفا باشد. تیم باید با کمترین نیاز به هماهنگی با دیگر تیم‌ها، توانایی توسعه و پیاده‌سازی سرویس خود را داشته باشد.

راهکار اول : جداسازی بر اساس قابلیت های کسب و کاری

یکی از پایه‌ای‌ترین راه‌ها برای طراحی سرویس‌ها، تعریف‌کردن آن‌ها براساس قابلیت‌های کسب و کاری سیستم است.

  • تعریف قابلیت‌های کسب‌و‌کاری معمولاً بر پایه یک هدف انجام می‌شود. برای مثال:

۱. سیستم مدیریت سفارشات، مسئولیت سفارش‌های کاربران را برعهده دارد.

۲. سیستم مدیریت مشتریان، مسئولیت مدیریت کاربران را برعهده دارد.

  • قابلیت‌های کسب و کاری اغلب به صورت وراثتی، چندلایه دسته‌بندی می‌شوند.

در مثال یک نرم‌افزار سازمانی enterprise:

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

  • توسعه محصول
  • تحویل محصول
  • خلق نیاز

یا در مثال فروشگاه آنلاین:

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

  • مدیریت محصولات
  • مدیریت انبار
  • مدیریت سفارشات
  • مدیریت تحویل کالا
  • و ...

در این نمونه معماری میکروسرویس‌ها نیز می‌تواند معادل هر یک از این قابلیت‌ها باشد.

جداسازی سرویس ها بر اساس قابلیت های کسب و کار
جداسازی سرویس ها بر اساس قابلیت های کسب و کار

مزایای این الگو :

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

مشکلات این الگو :

  • طراحی نرم‌افزار کاملاً به مدل کسب و کار وابسته است.
  • این روش نیازمند شناخت عمیق از قابلیت‌های کسب و کار است و انجام این کار، امری مشکل خواهد‌بود.

راهکار دوم، الگوی جداسازی براساس زیردامنه‌ها

سرویس‌های خود را با استفاده از زیردامنه‌های الگوی Domain-Driven-Design(DDD) جدا کنید. در اینجا DDD به حوزه و دامنه‌ای اصلی و فرعی فعالیت نرم افزار اشاره می‌کند، به‌گونه‌ای که دامنه، متشکل از چندین زیردامنه است و هرکدام از این زیر دامنه‌ها معادل یکی از بخش‌های آن کسب و کار هستند.زیردامنه‌ها می‌توانند به صورت زیر دسته‌بندی شوند:

  • هسته: وجه تمایز کسب و کار، از دیگر کسب و کارها به‌شمار می‌رود که به نوعی با‌ارزش‌ترین بخش آن نرم‌افزار است.
  • پشتیبان‌ها: بخش‌هایی که برای سرویس‌دهی هسته مورد نیاز هستند، اما بخش مختص به کسب و کار شما نیستند (مثلاً بخش Automatioc Marketing)‌. این بخش‌ها می‌توانند به صورت داخلی پیاده‌سازی و یا برون‌سپاری شوند.
  • بخش‌های عمومی: این بخش‌ها مخصوص کسب و کار نیستند و به‌ صورت ایده‌آل توسط نرم‌افزارها یا سرویس‌های آماده پیاده‌سازی می‌شوند، مانند ارسال پیامک.

مثالی از یک سرویس ارایه محتوای ویدیويی:

برای مثال به صورت خلاصه زیردامنه‌های یک سرویس UGC مبتنی‌بر ویدیو، مانند آپارات را در نظر بگیرید که می‌تواند شامل بخش‌های زیر باشد:

  • زیردامنه کاتالوگ محتوا
  • زیردامنه مدیریت محتوا (آپلود، نظارت و انتشار)
  • زیردامنه کاربران
  • زیردامنه ریکامندیشن (recommendation)
  • زیردامنه جستجو
  • زیردامنه نظرات کاربران
نمونه دامنه های یک سرویس ویدیوئی ( ارتباطات واقعی متفاوت از تصویر فوق هستند )
نمونه دامنه های یک سرویس ویدیوئی ( ارتباطات واقعی متفاوت از تصویر فوق هستند )

در این مثال زیردامنه مدیریت محتوای ویدیویی بسته به نیاز به دو میکرسرویس کوچک‌تر برای آپلود ویدیو و مدیریت و ممیزی محتوا، تقسیم‌بندی شده است.

مزایای این الگو:

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

مشکلات این الگو:

  • این الگو اتعداد میکروسرویس‌ها افزایش می‌دهد و در نتیجه عملیات service discovery (کشف خدمات) و پکپارچه‌سازی سیستم مشکل‌تر می‌شود.
  • شناخت زیردامنه‌ها کار مشکلی است زیرا به شناخت عمیقی از کلیت کسب و کار نیاز دارد.

راهکار سوم، جداسازی براساس تراکنش‌ها

در یک سیستم توزیع‌یافته، نرم‌افزار معمولاً نیاز دارد چندین میکروسرویس را برای انجام یک تراکنش کسب و کاری فراخوانی کند. زمانی که شما سرویس‌ها را براساس تراکنش‌ها جدا می‌کنید، چندین تراکنش در سیستم وجود دارد. یکی از عوامل دخیل در تراکنش‌های توزیع‌شده وجود هماهنگ‌کننده transaction است. در این حالت یک تراکنش توزیع‌شده در دو مرحله انجام می‌شود.

  • فاز آماده‌سازی: طی این مرحله همه شرکت‌کننده‌های تراکنش، آماده انجام تغییرات می‌شوند و به هماهنگ‌کننده اطلاع می‌دهند که آماده تکمیل تراکنش هستند.
  • فاز انجام یا لغو تراکنش: طی این مرحله هماهنگ‌کننده دستور انجام یا لغو تراکنش را برای همه شرکت‌کننده‌های تراکنش ارسال می‌کند.

یکی از بزرگ‌ترین مشکلات روال 2PC ، کُندی زیاد آن‌ها در مقایسه با زمان لازم برای انجام همان کار در قالب یک میکروسرویس است. این هماهنگی حتی زمانی که همه سرویس‌ها داخل یک شبکه داخلی باشند می‌تواند باعث کندی بسیار زیاد سیستم شود.

برای جلوگیری از تأخیر در پاسخ‌دهی و مشکلات two-phase commit می‌توان سرویس‌ها را براساس تراکنش‌های بین آن‌ها دسته‌بندی کرد. این راهکار زمانی که مدت زمان پاسخ‌دهی برای شما اهمیت دارد و یا ترکیب چند ماژول (سرویس) با هم منجر به ساخت یک مونولیت جدید نمی‌شود، می‌تواند مورد استفاده قرار‌گیرد.

مزایای این الگو:

  • کاهش مدت زمان پاسخگویی به کاربر
  • ازبین بردن نگرانی بابت ثبات داده‌ها یا Data consistency
  • افزایش دسترسی‌پذیری نرم‌افزار

مشکلات این الگو:

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

راهکار چهارم : جداسازی به روش Strangler

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

https://virgool.io/d/aphyynrugws3

(نرم‌افزار Greenfield به این‌صورت است که از پیش، نیازمند اجرای زیرساخت یا اعمال محدودیت نباشید).
باید بگم صرف‌نظر از اینکه واقعاً جداکردن یک پروژه از ابتدا به‌صورت میکروسرویس درست است یا نه؟ در ۸۰ درصد مواقع شما با نرم‌افزارهای BrownField که نرم افزارهای مونولیتیک بزرگ و قدیمی‌اند (Legacy codebase) سر و کار دارید.
در این زمان است که روش Strangler به کمک شما می‌آیند. در این روش:
دو نرم‌افزار را که به‌صورت همزمان، هر دو روی یک مجموعه آدرس هستند را ایجاد خواهد کرد. یکی کدبیس (Code Base) قدیمی و دیگری پروژه‌ای جدید.

  • با گذشت زمان نرم‌افزارهای (میکروسرویس‌های) جدید به تدریج جایگزین نرم‌افزار قبلی می‌شوند تا زمانی که بتوانید نرم افزار قدیمی را خاموش کنید.
  • مراحل این کار شامل  سه مرحله تبدیل، همزمانی و از رده خارج کردن کدبیس قبلی است.
  • مرحله تبدیل: یک مجموعه سرویس جدید با رویه‌های مدرن‌تر ایجاد می‌کنیم.
  • مرحله همزمانی: نرم‌افزار موجود را برای مدتی به حال خود می‌گذاریم و همزمان با تکمیل‌شدن بخش‌های مختلف نرم‌افزار جدید، ترافیک بیشتری را به سرویس‌های جدید منتقل می‌کنیم.
  • مرحله از رده خارج کردن: ویژگی جدیدی که پیاده‌سازی شده را از سرویس قدیمی حذف می‌کنیم.

مزایای این الگو:

  • ریسک را در زمان تبدیل پروژه کاهش میدهد.
  • سرویس قدیمی در زمان تبدیل و ریفکتور کردن به سرویس جدید به کار خود ادامه می‌دهد.
  • امکان اضافه کردن امکانات جدید را در زمان تبدیل کد فراهم می سازد.

مشکلات این الگو:

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