افزایش پایداری (Resilience) در API ها با استفاده از Polly
چرا به Polly نیاز داریم؟
تصور کنید سرویس شما برای انجام یک عملیات، نیاز به فراخوانی یک سرویس دیگر دارد. اگر آن سرویس به دلیل یک مشکل موقتی در دسترس نباشد، فراخوانی شما با خطا مواجه میشود. یک رویکرد سادهانگارانه این است که عملیات را ناموفق اعلام کنیم. اما این کار تجربه کاربری نامناسبی ایجاد کرده و قابلیت اطمینان سیستم را کاهش میدهد.
Polly به ما اجازه میدهد تا استراتژیهای مختلفی را برای مدیریت اینگونه خطاها به شکلی روان و خوانا تعریف کنیم. با استفاده از "سیاستها" (Policies)، میتوانیم کدهای خود را در برابر خطاهای قابل پیشبینی محافظت کنیم.
نصب و راهاندازی اولیه Polly
برای شروع، کافی است پکیج Polly را از طریق NuGet به پروژه ASP.NET Core خود اضافه کنید:
dotnet add package Polly
سپس میتوانید با استفاده از IHttpClientFactory و افزودن سیاستهای Polly، کلاینتهای HTTP پایدار بسازید. این روش، بهترین راه برای یکپارچهسازی Polly با فراخوانیهای API است.
// In Startup.cs or Program.cs
services.AddHttpClient("MyApiClient")
.AddPolicyHandler(GetRetryPolicy());
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError() // Handles HttpRequestException, 5xx and 408 status codes
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}
در کد بالا، ما یک سیاست "تلاش مجدد" (Retry) تعریف کردهایم که در صورت بروز خطاهای گذرا، فراخوانی را تا ۳ بار با فاصله زمانی افزایشی (Exponential Backoff) تکرار میکند.
الگوهای کلیدی پایداری در Polly
Polly الگوهای متنوعی را برای مدیریت خطا ارائه میدهد که هر کدام برای سناریوی خاصی مناسب هستند. در ادامه به مهمترین آنها میپردازیم.
۱. الگوی تلاش مجدد (Retry Pattern)
این الگو، سادهترین و پرکاربردترین الگوی پایداری است. همانطور که از نامش پیداست، در صورت بروز خطا، عملیات را به تعداد مشخصی تکرار میکند. این الگو برای خطاهای موقتی که انتظار میرود به سرعت برطرف شوند، بسیار مؤثر است.
انواع استراتژیهای تلاش مجدد:
-
Retry: تلاش مجدد فوری.
-
WaitAndRetry: تلاش مجدد پس از یک تأخیر زمانی مشخص. این تأخیر میتواند ثابت یا متغیر (مانند Exponential Backoff) باشد تا به سرویس مقصد فرصت بازیابی داده شود.
مثال پیادهسازی:
Policy
.Handle<HttpRequestException>()
.OrResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.ServiceUnavailable)
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(retryAttempt * 2),
onRetry: (outcome, timespan, retryCount, context) =>
{
Console.WriteLine($"Retrying... attempt {retryCount}");
});
در این مثال، سیاست تعریفشده نه تنها HttpRequestException را مدیریت میکند، بلکه پاسخهای با کد وضعیت 503 Service Unavailable را نیز به عنوان خطا در نظر گرفته و عملیات را با تأخیر زمانی فزاینده تکرار میکند.
۲. الگوی قطعکننده مدار (Circuit Breaker Pattern)
تلاشهای مجدد پیدرپی برای فراخوانی سرویسی که به مشکل جدی برخورده است، نه تنها بیفایده است، بلکه میتواند با مصرف منابع، وضعیت را بدتر کند. الگوی قطعکننده مدار از این کار جلوگیری میکند.
این الگو مانند یک فیوز الکتریکی عمل میکند. پس از تعداد مشخصی خطای متوالی، مدار "باز" میشود و برای مدتی از ارسال درخواستهای جدید به سرویس معیوب جلوگیری میکند. پس از گذشت این مدت، مدار به حالت "نیمهباز" درمیآید و اجازه میدهد یک درخواست آزمایشی عبور کند. اگر این درخواست موفقیتآمیز بود، مدار "بسته" شده و به حالت عادی بازمیگردد. در غیر این صورت، مدار دوباره باز میشود.
مثال پیادهسازی:
Policy
.Handle<HttpRequestException>()
.CircuitBreakerAsync(
exceptionsAllowedBeforeBreaking: 5,
durationOfBreak: TimeSpan.FromSeconds(30)
);
این سیاست، پس از ۵ خطای متوالی، مدار را برای ۳۰ ثانیه باز میکند و از فشار بیشتر بر روی سرویس جلوگیری مینماید.
۳. الگوی مهلت زمانی (Timeout Pattern)
گاهی اوقات یک فراخوانی API به دلیل مشکلات عملکردی در سرویس مقصد، برای مدت طولانی بدون پاسخ میماند. این وضعیت میتواند منابع سیستم فراخواننده (مانند thread ها) را مسدود کند. الگوی مهلت زمانی با تعیین یک محدودیت زمانی برای هر عملیات، از این مشکل جلوگیری میکند.
انواع استراتژیهای Timeout:
-
Pessimistic: این استراتژی فرض میکند که عملیات به CancellationToken احترام نمیگذارد و آن را در یک thread جداگانه اجرا میکند تا بتواند آن را خاتمه دهد.
-
Optimistic: این استراتژی بهینه، CancellationToken را به عملیات واگذار میکند و انتظار دارد که خود عملیات به درخواست توقف پاسخ دهد. این روش برای فراخوانیهای مدرن async/await توصیه میشود.
مثال پیادهسازی:
Policy.TimeoutAsync(TimeSpan.FromSeconds(10), Polly.Timeout.TimeoutStrategy.Optimistic);
اگر عملیات بیش از ۱۰ ثانیه طول بکشد، این سیاست یک TimeoutRejectedException پرتاب میکند.
۴. الگوی جایگزین (Fallback Pattern)
حتی با وجود تمام الگوهای بالا، ممکن است یک عملیات در نهایت با شکست مواجه شود. در چنین شرایطی، بهجای نمایش یک خطای ناخوشایند به کاربر، میتوان یک پاسخ جایگزین ارائه داد. این پاسخ میتواند یک مقدار پیشفرض، دادههای کششده یا یک پیام دوستانه باشد.
مثال پیادهسازی:
Policy<HttpResponseMessage>
.Handle<Exception>()
.FallbackAsync(
fallbackAction: (cancellationToken) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("Returning cached data.")
}),
onFallback: (delegateResult, context) =>
{
Console.WriteLine($"Fallback initiated due to: {delegateResult.Exception?.Message}");
return Task.CompletedTask;
}
);
این سیاست در صورت بروز هرگونه استثنا، یک پاسخ HttpResponseMessage موفقیتآمیز با محتوای جایگزین برمیگرداند.
۵. الگوی دیواره (Bulkhead Isolation Pattern)
در یک سیستم با ترافیک بالا، اگر یک سرویس کند شود، میتواند باعث اشغال تمام worker thread های سرویس فراخواننده شود و آن را نیز از کار بیندازد. این پدیده به عنوان "شکست آبشاری" (Cascading Failure) شناخته میشود. الگوی دیواره با محدود کردن تعداد درخواستهای همزمان به یک سرویس خاص، از این اتفاق جلوگیری میکند و خطا را در همان بخش ایزوله نگه میدارد.
مثال پیادهسازی:
Policy.BulkheadAsync(
maxParallelization: 10, // تعداد فراخوانیهای همزمان مجاز
maxQueuingActions: 5 // تعداد درخواستهایی که در صف قرار میگیرند
);
اگر تعداد فراخوانیهای همزمان از ۱۰ بیشتر شود، درخواستهای جدید تا سقف ۵ عدد در صف قرار میگیرند و پس از آن با BulkheadRejectedException رد میشوند.
ترکیب سیاستها (Policy Wrap)
قدرت واقعی Polly زمانی آشکار میشود که این الگوها را با هم ترکیب میکنیم. میتوانیم چندین سیاست را به صورت تودرتو (wrap) تعریف کنیم تا یک استراتژی پایداری جامع و قدرتمند ایجاد نماییم. ترتیب قرارگیری سیاستها اهمیت دارد؛ سیاست بیرونیتر، زودتر اجرا میشود.
مثال ترکیب سیاستها:
var retryPolicy = GetRetryPolicy();
var circuitBreakerPolicy = GetCircuitBreakerPolicy();
var timeoutPolicy = GetTimeoutPolicy();
// ترتیب: Timeout -> Circuit Breaker -> Retry
var policyWrap = Policy.WrapAsync(retryPolicy, circuitBreakerPolicy, timeoutPolicy);
// استفاده در HttpClient
services.AddHttpClient("MyResilientClient")
.AddPolicyHandler(policyWrap);
در این مثال، یک درخواست ابتدا توسط سیاست Timeout کنترل میشود. اگر در زمان مقرر پاسخ ندهد، خطای آن توسط قطعکننده مدار بررسی میشود و در نهایت، سیاست تلاش مجدد وارد عمل خواهد شد.
نتیجهگیری
خطاها در سیستمهای توزیعشده اجتنابناپذیر هستند. رویکرد ما در مواجهه با این خطاهاست که تفاوت بین یک سیستم شکننده و یک سیستم پایدار را رقم میزند. کتابخانه Polly با ارائه مجموعهای غنی از الگوهای پایداری، به توسعهدهندگان داتنت این امکان را میدهد که APIهای قوی، قابل اعتماد و مقاوم در برابر خطا بسازند. با به کارگیری هوشمندانه الگوهایی مانند Retry، Circuit Breaker، Timeout، Fallback و Bulkhead، میتوانید از در دسترس بودن سرویسهای خود اطمینان حاصل کرده و تجربه بهتری را برای کاربران نهایی فراهم آورید.
0 نظر
هنوز نظری برای این مقاله ثبت نشده است.