در اولین قسمت از این مجموعه در مورد معماری میکرسرویسها و مقایسه آنها با معماری Monolithic و مزایا و معایب آن صحبت کردیم. در ادامه و در قسمت دوم از این مجموعه به بررسی API Gatewayها پرداختیم و دیدیم چگونه به کمک یک واسط خوب میتوانیم مشکلات بسیاری را در راه توسعه نرم افزار حذف کنیم. در این قسمت قصد داریم در مورد ارتباط بین سرویسها و انواع روشهای صحبت کردن سرویسها صحبت کنیم.
مقدمه:
در برنامهها Monolithic با توجه به اینکه کل برنامه ما در یک نرم افزار با یک زبان برنامهنویسی پیاده سازی شده است ارتباط بین بخشهای مختلف به سادگی صدا زدن یک تابع انجام میشود. اما در میکروسرویسها با توجه به اینکه بخشهای مختلف برنامه ما روی چندین سیستم مختلف به صورت توزیع شده اجرا میشوند امکان استفاده از روش قبل وجود ندارد. هر بخش از نرم افزار ما به صورت یک پروسه کاملا جداگانه و ایزوله اجرا میشود. در میکروسرویسها ارتباط بین بخشهای مختلف اصطلاحا از روشی به نام Inter process Communication که از این به بعد به اختصار IPC میگوییم انجام می شود.
پیش از آنکه به سراغ انواع روشها و تکنولوژیهای پیاده سازی IPC برویم بیایید نگاهی به موارد مختلفی که هنگام طراحی ارتباط بین سرویسها باید مد نظر داشته باشیم بیاندازیم.
انواع روشهای تعامل:
پیش از طراحی و پیاده سازی IPC بهتر است در مورد چگونگی برقراری ارتباط بین سرویسها کمی تامل کنیم. انواع مختلفی از روشهای مختلف تعامل بین سرویسها و کلاینتها وجود دارد. چگونگی برقراری ارتباط بین سرویسها مختلف از دو جنبه مختلف و کلی قابل بررسی است. جنبه اول اینکه ارتباط بین کلاینت و سرویس دهنده یک به یک است یا یک به چند:
جنبه بعدی قابل بررسی در ارتباط بین سرویسها sync یا async بودن ارتباط است.
در جدول زیر انواع روشهای ارتباطی با توجه به جنبههای بالا را مشاهده میکنید:
در ادامه به معرفی هر یک از این روشهای ارتباطی خواهیم پرداخت. ابتدا به سراغ روشهای یک به یک میرویم که این روشها عبارتند از:
روشهای ارتباطی یک به چند نیز به گروههای زیر تقسیمبندی میشوند:
در پیاده سازی میکروسرویسها از این روشهای ارتباطی استفاده میشود. یک سرویس با توجه به شرایط طراحی و نیازمندی خاص خود ممکن است از یکی از این روشها صرفا استفاده کند و سرویس دیگر ممکن است از ترکیبی از این سرویسها استفاده نماید.
همانطور که در تصویر مشاهده میکنید سرویسهای مختلف از انواع روشهای ارتباطی برای انجام ماموریتکاری خود و رسیدن به هدف نهایی که ثبت درخواست تاکسی برای یک مسافر است استفاده میکنند. از نرم افزار موبایل یک notification برای سرویس Trip Management ارسال میشود. سرویس Trip Management با روش Request/Response از وضیعت حساب کاربر مطلع میشود. سپس سرویس Trip Management یک Trip ایجاد میکند و با روش Pub/Sub به بقیه سرویسها اطلاع میدهد. ادامه درخواستها تا زمان نهایی شدن درخواست را در تصویر میتوانید مشاهده کنید.
حال که با انواع روشهای ارتباطی بین سرویسها آشنا شدیم، بیایید با هم نحوه تعریف APIها و نکاتی که باید هنگام طراحی آنها مد نظر داشته باشیم را بررسی کنیم.
تعریف APIها:
هنگامی که نیاز به برقراری ارتباط بین سرویسها و کلاینتها داریم باید APIهایی تعریف کنیم و قراردادی برای این APIها بین کلاینت و سرویس برقرار باشد. بدون توجه به روش IPC که برای بخشهای مختلف سیستم انتخاب میکنید تعریف یک ساختار خوب و دقیق از هر API میتواند موفقیت یک سرویس را تضمین کند. بحثهای زیادی در مورد این مسئله صورت گرفته است که حتی بهتر است هنگام طراحی یک سرویس از روش API First استفاده کنیم و ابتدا APIهای سرویس را تعریف کنیم و با توجه به APIهای ارائه شده اقدام به طراحی و پیاده سازی خود سرویس کنیم. طرفداران این روش طراحی به این نکته قائل هستند که طراحی APIها در ابتدا شانس تولید سرویسی که خدمات درستی به کلاینتهای خود میدهد را افزایش میدهد.
در ادامه خواهیم دید که انتخاب روش IPC چکونه بر روال طراحی و پیاده سازی APIها تاثیر خواهد گذاشت.
تکامل APIها:
در گذر زمان APIهای ارائه شده توسط یک سرویس به دلایل مختلف تغییر و تکامل خواهد یافت. در یک برنامه Monolithic تغییر API یک قسمت بسیار ساده است و با توجه به اینکه استفاده کنندههای یک API با خطا مواجه خواهند شد تقریبا میتوان مطمئن بود با تغییر API تمامی کلاینتهای آن نیز به روز خواهند شد. اما در میکروسرویسها اطمینان از به روز بودن تمامی کلاینتهای کاری بسیار دشوار و در بعضی موارد غیرممکن است. در اغلب موارد امکان اجبار کلاینتها به بروزرسانی وجود ندارد. بعضا این احتمال وجود دارد که APIهای شما تغییر نکند و صرفا تکامل بیابد و در این شرایط میتوان توقع داشت که شما کلاینتهایی داشته باشید که با نسخهها و امکانات قدیمی کار میکنند یا کلاینتهایی که خود را به روز کرده و از ویژگیهای جدید APIهای شما استفاده میکنند. در هر صورت چه شما تغییر داشته باشید و چه تکامل داشتن استراتژیهایی برای برخورد با شرایط مختلف و کلاینتهای مختلف از نیازهای اولیه تیم توسعه است.
چگونه شما میتوانید تغییرات را در نسخههای مختلف API خود مدیریت کنید؟ برخی تغییرات جزئی است و قابل مدیریت و به سادگی میتوان تغییرات را به گونهای انجام داد که backward compatible باشد. مثالی از این تغییرات میتواند اضافه شدن یک خاصیت به پاخی باشد که سرویس برای یک درخواست خاص ارسال میکند، در این شرایط تغییر به شکلی است که کلاینت به عملکرد قبلی خود بدون تغییر میتواند ادامه دهد و فقط از ویژگی جدید بهرهمند نمیشود. هنگام طراحی سرویسها و کلاینت ها توجه به اصل Robustness میتواند مفید به فایده باشد. کلاینتهایی که از نسخههای قدیمی یک API استفاده میکنند باید بتوانند به کار خود ادامه دهند. سرویسها باید برای ویژگیهای اضافه ای که به ساختارد درخواست اضافه میکنند مقدار پیشفرض تخصیص دهند و کلاینتها هم باید توانایی صرفنظر کرد از ویژگیهایی که برای آنها تعریف نشده است را داشته باشند. انتخاب روش ارتباطی که به شما به سادگی قابلیت ارتقا و تغییر APIهای سرویسها را بدهد بسیار مهم و حیاتی است.
با همه این تفاسیر در برخی موارد ممکن است شما نیاز به تغییراتی داشته باشید که قابلیت backward compatibility را ندارند و به هرحال کلاینتها از کارخواهند افتاد. در این شرایط پشتیبانی از APIهای قدیمی تا زمان اطمینان ارتقا پیدا کردن همه کلاینتها تنها راه ممکن برای داشتن یک سرویس قدرتمند و قابل اطمینان است.( البته میتوانید خیلی ساده نسخههای قدیم را از دسترس خارج کنید. مطمئن باشید کلاینتهای بیدفاع چاره ای جز ارتقا سریع خود نخواند داشت. همینقدر بیفکر و از خود راضی). برای مثال اگر از REST APIها استفاده میکنید میتوانید نسخه API خود را در URL دخیل کنید تا کلاینتها بتوانند با نسخه مناسب خود ارتباط برقرار کنند.
مدیریت بحران هنگامی که بخشی از سیستم دچار مشکل میشود:
همانطور که در قسمت دوم هم گفته شد، با توجه به اینکه سرویسدهنده ها و کلاینتها کاملا مجزا هستند این احتمال وجود دارد که در زمانی که یک کلاینت درخواستی را برای سرویسی ارسال میکند سرویس امکان دریافت پیام و ارسال پاسخ مناسب را نداشته باشد. ممکن است در زمان ارسال درخواست سرویسدهنده به منظور ارتقا و به روزرسانی از دسترس خارج باشد یا به خاطر فشار بسیار بالا سرعت ارائه خدمات پایینتر از حد انتظار کلاینتها باشد. اگر از به سراغ مثال فروشگاه در مطالب قبلی بازگردیم فرض کنید سرویس پیشنهاد کالا در زمان نیاز به این سرویس از دسترس خارج باشد. یک پیاده سازی ناصحیح و فکر نشده ممکن است به شکلی باشد که کلاینت برای مدت زمان طولانی در انتظار پاسخ از طرف سرویس پشنهاد کالا بماند. این روش طراحی و پیاده سازی نه تنها UX مناسبی ندارد بلکه ممکن است موجب هدر رفتن منابع موجود در سیستم نیز بشود.
برای جلوگیری از بروز چنین مشکلاتی طراحی سیستم به شکلی که قابلیت مدیریت چنین خطاهایی را داشته باشد بسیار حیاتی است. در صورتی که سیستم با بروز خطا در قسمتی از آن توانایی ادامه حیات داشته باشد اصطلاحا میگوییم سیستم تحمل خطا را دارد یا Fault Tolerance است. توجه به این نکته که تحمل خطا در سیستمهای توزیع شده یک نیازمندی است نه یک ویژگی بسیار مهم است.
هنگام مواجه شدن با خطا در یکی از قسمتهای نرم افزار استراتژیهای متفاوتی را میتوانیم در پیش بگیریم که در ادامه به بررسی این استراتژیها خواهیم پرداخت.
در نظر گرفتن زمان Timeout: هنگامی که از روشهای Sync در توسعه نرم افزار استفاده میکنید از انتظار بیپایان برای دریافت پاسخ به شدت دوری کنید و با توجه به شرایط سرویسها و معماری سیستم مدتزمانی را به عنوان Timeout در سیستم تنظیم کنید که در صورتی که در زمان مناسب پاسخی ارسال نشد کلاینت عملیات مدیریت خطا را شروع کرده و از تله انتظار بیپایان رها شود.
محدود کردن تعداد درخواستهای بدون پاسخ: در صورتی که تعداد زیادی درخواست را برای یک سرویس ارسال کرده و هیچ پاسخی دریافت نکنیم با ارسال درخواست جدید احتمالا فقط صف درخواستهای بدون پاسخ طولانیتر شده و نتیجهای جز هزینه بالاتر هنگام وقوع خطا در بر نخواهد داشت. پس بهتر است تعداد درخواستهای در انتظار پاسخ را محدود کنیم و در شرایطی که به حداکثر تعداد درخواستهای قابل قبول رسیدیم درخواستهای جدید را بدون فوت وقت و درج در صف درخواستها لغو کنیم.
استفاده از الگوی Circuit Breaker: هنگامی که تعداد زیادی از درخواستها به یک سرویسخاص با خطا مواجه میشود میتوان حدس زد که سرویس مقصد دچار مشکل شده است. در این شرایط ارسال درخواست برای این سرویس به جز هدر دادن منابع و انتشار خطا در سیستم دستاورد دیگری ندارد. برای این مشکلها خوب است از روشی استفاده کنیم که مهندسین برق استفاده میکنند و ماژولی مانند فیوز در سیستم قراردهیم. در زمان بروز خطا همانطور که فیوز قطع میشود ماژول ما نیز سرویس را برای مدتزمانی خاص از دسترس خارج کرده و تمامی درخواستها به این سرویس فورا Reject شوند. بعد از گذشتن بازه زمانی خاص Circuit Breaker اجازه عبور تعداد کمی درخواست را خواهد داد. در صورتی که این درخواستها با موفقیت پاسخ داده شوند سیستم مطمئن میشود مشکل حل شده و به حیات خود ادامه میدهد و در صورت بروز مشکل در پاسخ به این تعداد اندک درخواست احتمال ادامه دار بودن خطا وجود دارد و مجددا سیستم برای مدت زمانی خاص از دسترس خارج بوده و تمامی درخواستها سریعا Reject میشود.
در نظر گرفتن Fallback: برای شرایط بحرانی و بروز مشکل راهکارهای جایگزین در نظر بگیرید. برای مثلا با توجه در صورتی که سرویس پیشنهاد کالا قادر به ارائه خدمات نبود میتوان 10 کالای هم خانواده کالای منتخب یا 10 کالای جدید اضافه شده به سیستم را باز گرداند. یا به عنوان یک راهکار دیگر میتوان نتیجه هر درخواست را کش کرد و در مواقع لزوم از اطلاعات کش شده برای پاسخ به کاربر استفاده نمود.
برای پیاده سازی این ویژگیها میتوانید از Netflix Hysterix استفاده کنید و با تنظیماتی که در این سیستم وجود دارد میتوانید به بسیاری از الگوهایی که در بالا توضیح داده شد دست پیدا کنید.
جمع بندی:
در این قسمت به بررسی روشهای ارتباط بین سرویسها پرداختیم. تمامی سیستمهای توزیع شده نیاز به برقراری ارتباط با سایر سرویسها دارند و با توجه به نیازمندی پروژه انواع روشهای متفاوت ارتباط وجود دارد که در این قسمت بررسی شد. در ادامه نیز به بررسی بایدها و نبایدهای طراحی APIها و ارائه راهکارهای ارتباطی پرداختیم. در قسمت بعد تکنولوژیها و پروتکلهای ارتباطی را بررسی خواهیم کرد.
پ.ن: امروز سومین همایش سالانه مایکروسافت رو با کمک دوستان خوبم توی مجموعه نیکآموز و چشمه برگزار کردیم. مطمئنا بعد از کار توی معدن برگزاری همایش دومین کار سخت دنیاست. D-:
ادامه مطلب: