از سالهای گذشته استفاده از معماری مایکروسرویس به علت مزایایی که برای سیستمهای Large Scale دارد در حال افزایش است، یکی از مسائلی که بیشتر در این معماری و معماریهای توزیع شده حائز اهمیت است ارتباط بین سرویسها میباشد.
در معماری Monolith باتوجهبه اینکه کل سرویس روی یک ماشین اجرا میشود، ارتباط بین بخشهای مختلف سرویس بهصورت In-Memory Call میباشد و یا همیشه کل سرویس در دسترس است یا بالعکس. اما زمانی که معماری متفاوت باشد سرویسها روی ماشینهای مختلف شبکه قرار دارند و ارتباط بهصورت Remote Call میباشد که تعامل بین سرویسها در سطح ارتباطات شبکهای انجام میشود. تفاوت مهم این دو نوع ارتباط این است که ارتباطات Remote میتوانند Fail شوند یا اینکه در زمان مشخص شده پاسخی دریافت نشود.
در معماری میکروسرویس یک سرویس ممکن است دچار مشکل شده باشد و در دسترس نباشد (Unavailable) یا مشکلی در سطح شبکهٔ آن سرویس باشد، در نتیجه سرویس به ریکوئست پاسخی نمیدهد.
الگوی Circuit Breaker یک الگوی طراحی است که در توسعه نرم افزار استفاده می شود. برای تشخیص خرابی ها استفاده می شود و منطق جلوگیری از تکرار مداوم خرابی در طول تعمیر و نگهداری، خرابی موقت سیستم خارجی یا مشکلات غیرمنتظره سیستم را در بر می گیرد.
در صورت بروز هر یک از این اتفاقات سرویسی(مثلا A) که منتظر دریافت پاسخ است و درخواست های زیادی را ارسال کرده است که منتظر پاسخ هستند و لحظه به لحظه به تعداد این درخواست ها افزوده میشود، در نتیجه میزان استفاده از منابع به علت تلنبار شدن درخواست های زیاد منتظر پاسخ، بالا می رود و اگر این اتفاقات بهصورت زنجیره ای رخ دهد و سرویس(های) دیگری مثل B و C منتظر پاسخ سرویس A باشند، همه سیستمهای این زنجیره ممکن است که دچار مشکل منابع شوند.
مثال : فرض کنید شما در یک فروشگاه پس از صدور فاکتور توسط فروشنده از طریق دستگاه کارتخوان اقدام به پرداخت صورتحساب میکنید. پس از اینکه کارت میکشید سرویس کارتخوان(A) سعی میکند درخواست را برای Provider خود (سرویس B) ارسال کند و Provider هم پس از دریافت درخواست آن را برای سرویس پرداخت نهایی (سرویس C) ارسال خواهد کرد و منتظر پاسخ میماند، حال فرض کنید سرویس پرداخت نهایی Down میباشد یا به هر علتی هیچ پاسخی به درخواستها نمیدهد.
کاملاً مشخص است که پرداخت شما Failed میشود. اما اتفاق مهمتری که رخ میدهد این است که درخواستهای زیادی که از کارتخوانهای مختلف سطح کشور به سرویس B آمدهاند، از سرویس B به سرویس C ارسال میشوند بدون اینکه پاسخی از سرویس C دریافت کنند و پس از طی مدتزمان مشخصی Timeout میشوند، البته با استفاده از الگوهایی مانند Retry Pattern میتوان برای دفعات دیگری درخواست را ارسال کرد تا نتیجه حاصل شود، اما اگر سرویس C به علت رخداد مشکل پیشبینینشدهای برای مدتی نامشخص در دسترس نباشد چه اتفاقی خواهد افتاد؟
درخواستهای زیادی در سرویس B در صف میمانند که مثلاً موجب باز ماندن کانکشنهای زیاد به دیتابیس (در صورت عدم مدیریت)، ایجاد Threadهای زیاد و در نتیجه مصرف منابع سرویس B شروع به بالارفتن میکند و در نتیجه امکان Down شدن سرویس B محتملتر خواهد بود و ما بخش دیگری از سیستم را از دست خواهیم داد.
استفاده از این الگو باعث میشود که زمانی که یک قسمت یک سیستم دچار مشکل شده است یا موقتاً Down شده است بقیه قسمتهای سیستم بکار خود ادامه دهند و مشکل Cascading Failure رخ ندهد.
پیادهسازی Circuit Breaker را میتوان بهصورت Central در سطح Gateway یا برای هر سرویس پیادهسازی کرد که بنا بر استفاده و نیاز هر سازمان مزایا و معایبی دارد که میتوان به موارد زیر اشاره کرد:
- اگر بهصورت متمرکز پیاده شود، نیاز نیست هر تیم بهصورت جداگانه روی پروژههای خود پیاده کند.
- اگر بهصورت متمرکز پیاده شود، نیاز نیست اعضای هر تیم دانشی برای پیادهسازی آن داشته باشند.
- اگر بهصورت متمرکز پیاده شود، و سرویس مرکزی از کار بیفتد کل سیستم به فنا میرود.
- اگر جداگانه پیاده شود، و اگر تیمها و سرویسها با تکنولوژیهای مختلفی پیادهسازی شده باشند، پیچیدگی کار، Learning Curve ایجاد شده برای Developer های هر سرویس قطعاً بیشتر خواهد بود.
انواع وضعیت در Circuit Breaker
الگوی Circuit Breaker دارای سه وضعیت است: بسته (Close)، باز (Open) و نیمهباز (Half-Open)
بسته (Close): وقتی همه چیز عادی است، circuit breaker در حالت بسته باقیمانده و تمام ارتباطات به سرویسها منتقل میشوند. هنگامی که تعداد خرابیها از یک تعداد از پیش تعیین شده بیشتر شود، circuit breaker به حالت Open میرود.
باز (Open): در این وضعیت circuit breaker برای ارتباطات بدون اجرای واقعی ارتباط، خطا برمیگرداند.
حالت نیمهباز (Half-Open): پس از مدت زمانی circuit breaker به حالت نیمهباز سوئیچ میشود تا وجود خطا را بررسی کند. اگر در این حالت تنها یک ارتباط خراب شود، یکبار دیگر circuit breaker قطع میشود. در صورت موفقیت، circuit breaker مجدداً به حالت بسته و طبیعی برمیگردد.
الگوی Circuit Breaker در لاراول
ما معمولاً درخواست http را با استفاده از فساد Http مانند زیر فراخوانی می کنیم:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
class APIController extends Controller
{
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
$response = Http::get('https://myapp.app/api/admins');
$content = json_decode($response->body());
dd($content);
}
}
اما فکر میکنم این راه خوبی نیست، زیرا ممکن است api از کار بیفتد یا زمان آن از کار بیفتد و سپس در حال انجام باشد. بنابراین این مشکل با الگوی قطع کننده مدار حل خواهد شد. http ارائه timeout که یک استثنا ایجاد می کند. مثال زیر را ببینید:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
class APIController extends Controller
{
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
$response = Http::timeout(10)->get('https://myapp.app/api/admins');
$content = json_decode($response->body());
dd($content)
}
}
حالا بیایید ببینیم که آیا درخواست با شکست مواجه میشود و زمان خرابی api، دوباره برای زمان خاصی که توسط ما ارائه شده است، درخواست میکند. بنابراین بهترین راه حل در زیر است:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Cache\RateLimiter;
use Exception;
class APIController extends Controller
{
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
$limiter = app(RateLimiter::class);
$actionKey = 'service_name';
$threshold = 5;
try {
if ($limiter->tooManyAttempts($actionKey, $threshold)) {
return $this->failOrFallback();
}
$response = Http::timeout(3)->get('https://myapp.app/api/admins');
$content = json_decode($response->body());
dd($content);
} catch (Exception $exception) {
$limiter->hit($actionKey, Carbon::now()->addMinutes(15));
return $this->failOrFallback();
}
}
}
امیدوارم که این بتونه به شما کمک کنه…