عنوان:

‫گذار به تاب‌آوری بومی: بررسی راهبرد مایکروسافت با ارائه بسته Microsoft.Extensions.Resilience در دات‌نت نوین


نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۷/۱۱ ۱۱:۰۵
آدرس: www.dntips.ir
چکیده: در سیستم‌های توزیع‌شده و معماری‌های مبتنی بر ریزسرویس‌ها (Microservices)، بروز خطاهای گذرا (Transient Faults) امری اجتناب‌ناپذیر است. سال‌ها کتابخانه شناخته‌شده Polly مرجع اصلی پیاده‌سازی الگوهای تاب‌آوری نظیر تلاش مجدد (Retry)، قطع‌کننده مدار (Circuit Breaker) و محدودکننده نرخ (Rate Limiter) در جامعه دات‌نت بود. با این حال، نیاز مبرم به کارایی بالاتر، همگرایی با استانداردهای رصدپذیری (Observability)، کاهش سربار تخصیص حافظه و یکپارچگی عمیق‌تر با هسته فریم‌ورک، مایکروسافت را بر آن داشت تا با بازطراحی ساختاری Polly (نسخه ۸ به بعد) بسته رسمی Microsoft.Extensions.Resilience را ارائه کند. این مقاله به بررسی فنی معماری این بسته نوین، مزایای آن از منظر کارایی، رصدپذیری استاندارد، و جایگزینی انتزاعات قدیمی با خط‌لوله‌های تاب‌آوری (Resilience Pipelines) می‌‌پردازد.

مقدمه
تاب‌آوری (Resilience) توانایی یک سامانه در بازیابی خود پس از وقوع خطا و ادامه فعالیت بدون توقف کامل خدمات است. تا پیش از تحولات اخیر، پیکربندی الگوهای تاب‌آوری در دات‌نت معمولاً از طریق بسته Microsoft.Extensions.Http.Polly انجام می‌شد. این رویکرد، با وجود پاسخ‌گویی به نیازهای اولیه، با چندین محدودیت فنی اساسی همراه بود:
  • تخصیص بالای حافظه (Heap Allocations): ساختار مبتنی بر پالیسی‌های قدیمی Polly v7 سربار حافظه قابل‌توجهی برای Garbage Collector ایجاد می‌کرد.
  • جدایی از رصدپذیری نوین: معیارهای سنجش (Metrics) و تله‌متری به شکل یکپارچه درون خطوط لوله تعبیه نشده بودند.
  • پیچیدگی در ترکیب الگوها: ترکیب چند سیاست محافظتی (Policy Wrap) سینتکس ناخوانا و هزینه‌های اجرایی پنهانی به دنبال داشت.

در پاسخ به این نیازمندی‌ها، مایکروسافت با همکاری نزدیک تیم نگهدارنده Polly، هسته این پلتفرم را بر پایه خط‌لوله‌های کارآمد بازنویسی کرد و بسته Microsoft.Extensions.Resilience را به عنوان یک راهکار بومی/توکار (In-box) با عملکرد بالا معرفی نمود.

معماری خط‌لوله‌های تاب‌آوری (Resilience Pipelines)
تغییر بنیادین در رویکرد جدید، گذار از مفهوم «سیاست مجزا» (Policy) به «خط‌لوله تاب‌آوری» (Resilience Pipeline) است. یک خط‌لوله، توالی منظمی از استراتژی‌های محافظتی است که به صورت بهینه و پشت‌سرهم اجرا می‌شوند.
Request ──► [ Rate Limiter ] ──► [ Circuit Breaker ] ──► [ Retry ] ──► [ Timeout ] ──► Core Execution

ویژگی‌های فنی این معماری عبارتند از:
  • تخصیص نزدیک به صفر (Near-Zero Allocation): طراحی مجدد بر پایه ValueTask، خط‌لوله‌ها را برای سناریوهای فوق پرسرعت (High-Throughput) بهینه‌سازی کرده است.
  • جدایی استراتژی از نحوه ارسال: بر خلاف پکیج‌های گذشته که عمدتاً روی HttpClient متمرکز بودند، خط‌لوله‌ها از نوع اجرا مستقل‌اند؛ می‌توان آن‌ها را برای پایگاه‌داده، کش، فراخوانی‌های gRPC یا محاسبات درون‌برنامه‌ای به کار بست.

خط‌لوله استاندارد و جامع: Standard Hedging و Standard Resilience
مایکروسافت در این بسته، علاوه بر امکان پیکربندی دستی، دو الگو یا خط‌لوله آماده و اعتبارسنجی‌شده (Opinionated Defaults) ارائه کرده است:
Standard Resilience: ترکیب بهینه از پنج استراتژی رایج شامل:
  • محدودکننده نرخ (Rate Limiting)
  • مهلت زمانی سراسری (Total Request Timeout)
  • تلاش مجدد هوشمند با تأخیر تصادفی (Retry with Jitter)
  • قطع‌کننده مدار (Circuit Breaker)
  • مهلت زمانی تلاش مجزا (Attempt Timeout)

Standard Hedging: استراتژی اجرای همزمان چند درخواست موازی در صورت کندی درخواست اول، برای کاهش زمان تاخیر صدک بالا (Tail Latency P99).

مقایسه کد: روش سنتی در برابر رویکرد بومی دات‌نت

روش سنتی (Polly v7 و بسته‌های قدیمی HTTP)
کد قدیمی نیازمند تعریف دستی تک‌تک خط‌‌مشی‌ها و چسباندن آن‌ها از طریق اکستنشن‌های پراکنده بود:
// رویکرد سنتی و وابسته به پکیج‌های قدیمی
services.AddHttpClient("LegacyClient")
    .AddTransientHttpErrorPolicy(policy => 
        policy.WaitAndRetryAsync(3, _ => TimeSpan.FromSeconds(2)))
    .AddTransientHttpErrorPolicy(policy => 
        policy.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));

رویکرد بومی و نوین (Microsoft.Extensions.Resilience)
در رویکرد جدید، اضافه کردن یک بسته محافظتی کامل و صنعتی تنها با یک متد خطی انجام می‌شود:
// فعال‌سازی بسته جامع تاب‌آوری روی HttpClient
builder.Services.AddHttpClient("ModernClient", client =>
{
    client.BaseAddress = new Uri("https://api.example.com");
})
.AddStandardResilienceHandler(options =>
{
    // سفارشی‌سازی تنظیمات در صورت نیاز
    options.Retry.MaxRetryAttempts = 3;
    options.Retry.Delay = TimeSpan.FromSeconds(1);
    options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(5);
    options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(10);
});

کاربرد عمومی بر روی هر نوع عملیات (بدون وابستگی به HTTP)
این خط‌‌لوله‌ها به فراخوانی‌های وب محدود نیستند و به طور مستقیم در سرویس‌های دامنه نیز قابل استفاده‌اند:
// تعریف و ثبت خط‌لوله عمومی در DI
builder.Services.AddResiliencePipeline("database-pipeline", pipelineBuilder =>
{
    pipelineBuilder
        .AddRetry(new RetryStrategyOptions
        {
            MaxRetryAttempts = 2,
            Delay = TimeSpan.FromMilliseconds(500)
        })
        .AddTimeout(TimeSpan.FromSeconds(3));
});

// استفاده در یک سرویس کاری
public class ProductService(ResiliencePipelineProvider<string> pipelineProvider)
{
    public async ValueTask<Product?> GetProductAsync(int id, CancellationToken ct)
    {
        var pipeline = pipelineProvider.GetPipeline("database-pipeline");

        return await pipeline.ExecuteAsync(async state =>
        {
            return await QueryDatabaseAsync(id, state);
        }, ct);
    }

    private static ValueTask<Product?> QueryDatabaseAsync(int id, CancellationToken ct)
    {
        // شبیه‌سازی فراخوانی پایگاه داده
        return ValueTask.FromResult<Product?>(new Product(id, "لپ‌تاپ"));
    }
}

public record Product(int Id, string Name);

یکپارچگی سیستمی با تله‌متری و OpenTelemetry
یکی از متمایزترین وجوه تمایز بسته Microsoft.Extensions.Resilience، انتشار پیش‌فرض و استاندارد متریک‌های عملیاتی بر پایه کتابخانه System.Diagnostics.Metrics است.
بدون نیاز به نوشتن حتی یک خط کد اضافی:
  • رویدادهای باز شدن مدارهای ارتباطی (Circuit breaker state changes)
  • تعداد دفعات وقوع Retry و علت آن
  • تاخیرهای ناشی از مهلت زمانی (Timeout events)

همگی با تگ‌ها و ساختار استاندارد استخراج شده و به سمت داشبوردهای رصدپذیری نظیر Prometheus، Grafana یا سرویس‌های ابری صادر (Export) می‌شوند. این سطح از همگرایی پیش از این نیازمند پیاده‌سازی‌های دستی و پیچیده در سطح زیرساخت بود.

ارزیابی ابعاد تجاری و معماری
تحول ایجادشده در لایه تاب‌آوری دات‌نت پیامدهای روشنی دارد:
  • امنیت پایداری و کاهش ریسک وابستگی: تبدیل این مکانیزم‌ها به یک ماژول رسمی با پشتیبانی مستقیم مایکروسافت، ریسک ناشی از توقف پشتیبانی یا تغییر ناگهانی مدل‌های درآمدی کتابخانه‌های شخص ثالث را از بین برده است.
  • پیکربندی ساختاریافته (Options Pattern): تمامی پارامترهای تاب‌آوری قابلیت نگاشت خودکار به فایل‌های پیکربندی (appsettings.json) را دارند و بدون کامپایل مجدد، از طریق پیکربندی مجدد و Hot Reload قابل به‌روزرسانی هستند.
  • هم‌راستایی با فرهنگ Cloud-Native: عملکرد بالا، کمینه‌بودن مصرف حافظه و رصدپذیری خودکار، این ابزار را با نیازمندی‌های مقیاس‌پذیری افقی در زیرساخت‌هایی نظیر Kubernetes و .NET Aspire هم‌‌سو کرده است.

نتیجه‌گیری
گنجاندن قابلیت‌های تاب‌آوری پیشرفته در قالب بسته بومی Microsoft.Extensions.Resilience، نمونه‌ای برجسته از بلوغ اکوسیستم مدرن دات‌نت است. مایکروسافت با رفع کاستی‌های معماری سنتی و تکیه بر بازطراحی خط‌لوله‌های کم‌مصرف، الگوهای تاب‌آوری را از یک «پکیج اختیاری شخص ثالث» به یک «ستون بنیادین فریم‌ورک» ارتقا داده است.
در نتیجه، تیم‌های مهندسی نرم‌افزار می‌توانند بدون نگرانی از نوسانات لایسنس، بدون تحمیل بار اضافی بر حافظه و بدون درگیری با پیچیدگی‌های غیرضروری، سیستم‌هایی با تحمل خطای بالا و استاندارد در سطح سازمانی بسازند.

نظرات

  • وحید نصیری در ۱۴۰۵/۰۷/۱۱ ۱۲:۲۷
    یک نکته‌ی تکمیلی: این بسته‌ی جدید در پشت صحنه مستقیماً به Polly (نسخه ۸ به بعد) وابسته است و یک بازنویسی مستقل و جداگانه از پایه نیست.

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

    ۱. معماری پشت‌پرده؛ هسته مشترک (Polly.Core)
    در زمان انتشار .NET 8، مایکروسافت و تیم نگهدارنده Polly تصمیم گرفتند کل ساختار Polly را از نو بنویسند:
    • Polly.Core: تمام الگوهای قدیمی (v7) کنار گذاشته شدند و یک هسته جدید بر مبنای ResiliencePipeline با تخصیص حافظه نزدیک به صفر (Zero-Allocation) درون مخزن گیت‌هاب رسمی Polly بازنویسی شد. مهندسان تیم .NET مایکروسافت مستقیماً در نوشتن کد و بهینه‌سازی این هسته مشارکت داشتند.
    • Microsoft.Extensions.Resilience: بسته‌ای رسمی از سوی مایکروسافت است که لایه‌ای از انتزاعات بالاتر را روی Polly.Core می‌کشد. کار این پکیج تزریق وابستگی، ساخت زنجیره‌های پیش‌فرض استاندارد (AddStandardResilienceHandler)، اتصال به سیستم لاگینگ دات‌نت، پیوند با IOptions و انتشار متریک‌های تله‌متری است.

    اگر گراف وابستگی پروژه را بررسی کنید، ساختار به شکل زیر است:
    پروژه شما
      └── Microsoft.Extensions.Resilience (تحت نظارت و لایسنس مایکروسافت)
            └── Microsoft.Extensions.Http.Resilience
                  └── Polly.Core (نسخه 8+)

    ۲. چرا گفته می‌شود «بومی» (In-Box) در حالی که به Polly وابسته است؟
    عبارت راهکار بومی در اینجا به دو جنبه راهبردی اشاره دارد:
    حمایت و پشتیبانی مستقیم سازمانی (Enterprise Support):
    • با آمدن زیر چتر بسته Microsoft.Extensions.*، این کتابخانه وارد چرخه حیات رسمی فریم‌ورک دات‌نت شده است. اگر در شرکتی کار می‌کنید که لایسنس‌های پشتیبانی مایکروسافت را دارد، باگ‌ها و مشکلات عملکردی این پکیج توسط تیم دات‌نت بررسی و پشتیبانی می‌شود.

    پایداری لایسنس:
    • Polly پروژه بنیاد دات‌نت (.NET Foundation) است و تحت لایسنس‌های کاملاً آزاد (مانند Apache-2.0 / BSD) قرار دارد، نه یک پروژه شخصی که توسعه‌دهنده منفرد بتواند مدل مالی آن را یک‌شبه به پولی یا تجاری تغییر دهد. به همین دلیل استفاده مایکروسافت از آن ریسک تغییر لایسنس را در پی ندارد.

    ۳. تفاوت این رویکرد با پکیج‌های شخص ثالث دیگر
    • هم‌افزایی به جای انشعاب (Fork): مایکروسافت به جای اینکه Polly را فورک کند یا کدهای آن را کپی کند، استانداردها و تجربیات بهینه‌سازی دات‌نت نوین را به خود مخزن Polly تزریق کرد.
    • یکپارچگی عمیق: شما در کد دیگر نیازی به نوشتن مستقیم کدهای Polly ندارید؛ متدهایی مثل AddStandardResilienceHandler() کامپوننت‌های تاب‌آوری را به عنوان بخشی استاندارد و یکدست از اکوسیستم برنامه به شما تحویل می‌دهند، هرچند که در موتورخانه، این Polly v8 است که بار اجرا را به دوش می‌کشد.