عنوان:

‫بررسی قابلیت بازآزمایی توکار (Configurable Retry Logic) در Microsoft.Data.SqlClient


نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۶/۱۴ ۱۰:۳۰
آدرس: www.dntips.ir
چکیده: خطاهای گذرا (Transient Faults)، نظیر قطع ناگهانی ارتباط، تاخیرهای موقت شبکه و تغییر وضعیت سرور در محیط‌های ابری، از اجتناب‌ناپذیرترین چالش‌های معماری مدرن نرم‌افزار هستند. سال‌ها توسعه‌دهندگان پلتفرم دات‌نت (.NET) برای مقابله با این خطاها به پکیج‌های ثالث نظیر Polly متکی بودند. با معرفی فضای نام Microsoft.Data.SqlClient و اضافه شدن قابلیت بازآزمایی پیکربندی‌پذیر (Configurable Retry Logic)، این مسئولیت به طور توکار و در لایه درایور پایگاه‌داده تعبیه شده‌است. این مقاله به بررسی چرایی مهاجرت از درایور سنتی System.Data.SqlClient به Microsoft.Data.SqlClient، نحوه پیاده‌سازی مکانیزم‌های بازآزمایی برای SqlConnection و SqlCommand، تفاوت‌های کلیدی آن با Polly، و ملاحظات حساس معماری پیرامون تراکنش‌ها و پایستگی (Idempotency) می‌پردازد.

۱. مقدمه و سیر تکاملی درایورهای SQL در دات‌نت
در سال ۲۰۰۲، همراه با نسخه ابتدایی دات‌نت فریم‌ورک (.NET Framework)، مایکروسافت مجموعه‌کتابخانه ADO.NET و درایور System.Data.SqlClient را معرفی کرد. برای بیش از یک دهه و نیم، این فضای نام ستون فقرات ارتباطات داده‌ای میلیون‌ها سیستم سازمانی (Line-of-Business) بود. با گذشت زمان و گسترش رایانش ابری (مانند Azure SQL)، این پکیج به دلیل وابستگی مستقیم به چارچوب Base Class Library (BCL) دچار کندی در به‌روزرسانی و «پوسیدگی کد» (Code Calcification) شد.
در سال ۲۰۱۹، مایکروسافت مسیر آینده را با معرفی Microsoft.Data.SqlClient تغییر داد. این درایور مستقل از سیستم‌عامل، از طریق بسته‌های NuGet عرضه شده و چرخه‌های انتشار سریع‌تری دارد. اگرچه این درایور جایگزین مستقیم (Drop-in Replacement) برای System.Data.SqlClient است و سازگاری عقروعطف (Backward Compatibility) بالایی دارد، اما آمارها نشان می‌دهد بخش قابل‌توجهی از پروژه‌های فعال هنوز این مهاجرت ضروری را انجام نداده‌اند.
یکی از برجسته‌ترین قابلیت‌هایی که در نسخه ۴ به وضعیت عرضه عمومی (GA) رسید و در نسخه‌های فعلی به تکامل رسیده، مدیریت بومی خطاهای گذرا بدون وابستگی خارجی است.

۲. خطاهای گذرا و فلسفه منطق بازآزمایی (Retry Logic)
خطاهای گذرا به شکست‌هایی اطلاق می‌شوند که ریشه ساختاری (نظیر اشتباه دستوری SQL یا نبود جدول) ندارند، بلکه ناشی از شرایط موقت زیرساختی هستند:
  • جابه‌جایی گره‌ها در خوشه‌های ابری (Failover)
  • پر شدن لحظه‌ای پهنای باند شبکه
  • بن‌بست‌های لحظه‌ای (Deadlocks) یا تخلیه نشست‌ها (Connection Throttling)

در مواجهه با چنین خطاهایی، ساده‌ترین و کارآمدترین راهبرد، «تلاش مجدد هوشمند» پس از وقفه‌ای کوتاه است. بازآزمایی موثر باید شامل پس‌روی نمایی (Exponential Backoff) و نوسان تصادفی (Jitter) باشد تا از بروز مشکل «ازدحام هم‌زمان» (Thundering Herd Problem) جلوگیری شود.

۳. پیاده‌سازی فنی بازآزمایی در Microsoft.Data.SqlClient
پیاده‌سازی بازآزمایی در سطح درایور پایگاه‌داده بسیار منعطف است. به طور پیش‌فرض، قابلیت Retry غیرفعال است و با انتساب یک Provider فعال می‌شود.

۳.۱. تنظیم بازآزمایی باز کردن ارتباط (SqlConnection)
برای مدیریت خطاهای باز شدن ارتباط، از کلاس SqlConfigurableRetryFactory و گزینه‌های SqlRetryLogicOption استفاده می‌شود:
using System;
using System.Threading.Tasks;
using Microsoft.Data.SqlClient;

public class DatabaseConnectionFactory
{
    private readonly string _connectionString;
    private readonly SqlRetryLogicBaseProvider _retryProvider;

    public DatabaseConnectionFactory(string connectionString)
    {
        _connectionString = connectionString;

        // تعریف پارامترهای منطق بازآزمایی
        var retryOptions = new SqlRetryLogicOption
        {
            NumberOfTries = 5,                         // یک بار تلاش اولیه + ۴ بار بازآزمایی
            DeltaTime = TimeSpan.FromSeconds(1),        // وقفه پایه برای پس‌روی نمایی
            MaxTimeInterval = TimeSpan.FromSeconds(20),  // حداکثر سقف مجاز برای هر وقفه
            TransientErrors = null                      // null به معنای استفاده از لیست خطاهای گذرای پیش‌فرض درایور است
        };

        // ایجاد ارائه‌دهنده بازآزمایی نمایی با جیتر (Jitter) داخلی
        _retryProvider = SqlConfigurableRetryFactory.CreateExponentialRetryProvider(retryOptions);
    }

    public async Task<SqlConnection> CreateOpenConnectionAsync()
    {
        var connection = new SqlConnection(_connectionString)
        {
            RetryLogicProvider = _retryProvider
        };

        // در صورت بروز خطای گذرا، به صورت خودکار بازآزمایی انجام می‌شود
        await connection.OpenAsync();
        return connection;
    }
}
نکته:MaxTimeInterval سقف هر وقفه منفرد را مشخص می‌کند، نه کل زمان سپری‌شده در تمامی دفعات تلاش.

۳.۲. مدیریت اجرای دستورات (SqlCommand) و شرط توانمندی مجدد (Idempotency)
بازآزمایی ارتباط امن است، اما اجرای مجدد دستورات پایگاه‌داده چالش‌های ایمنی متفاوتی دارد. برای نمونه، اگر اجرای یک دستور پس از اعمال تغییرات در پایگاه‌داده دچار قطعی سوکت شود، تکرار دوباره آن ممکن است منجر به درج اطلاعات تکراری (Duplicate Insert) گردد. برای حل این چالش، AuthorizedSqlCondition امکان فیلتر کردن دستورات ایمن را فراهم می‌کند:
var commandRetryOptions = new SqlRetryLogicOption
{
    NumberOfTries = 3,
    DeltaTime = TimeSpan.FromMilliseconds(500),
    MaxTimeInterval = TimeSpan.FromSeconds(5),
    // اعمال بازآزمایی صرفاً روی کوئری‌های فاقد اثر جانبی (Idempotent)
    AuthorizedSqlCondition = cmd => 
        cmd is SqlCommand sqlCmd && 
        sqlCmd.CommandText.TrimStart().StartsWith("SELECT", StringComparison.OrdinalIgnoreCase)
};

var commandRetryProvider = SqlConfigurableRetryFactory.CreateExponentialRetryProvider(commandRetryOptions);

using var command = new SqlCommand("SELECT * FROM Products WHERE CategoryId = @id", connection)
{
    RetryLogicProvider = commandRetryProvider
};
command.Parameters.AddWithValue("@id", 10);

۴. مقایسه تحلیلی: درایور نیتیو یا Polly؟

معیارکتابخانه Pollyدرایور Microsoft.Data.SqlClient
دامنه پوشش (Scope)سراسر لایه‌های اپلیکیشن (HTTP، صف‌ها، کش و ...)صرفاً لایه پایگاه‌داده و ارتباطات SQL Server
آگاهی از بافتار SQL (SQL-Aware)نیازمند تنظیم دستی کد خطاها یا فیلترهای استثنادرک بومی خطاهای گذرا و ساختار پکت‌های TDS
مدیریت تراکنش‌هابی‌اطلاع از وضعیت تراکنش‌های باز در سطح سوکتشناسایی فعال تراکنش‌ها و جلوگیری از بازآزمایی مخرب
سادگی و سربارنیازمند تعریف Pipeline و الگوهای Decorator/Wrapperانتساب مستقیم یک Provider قابل استفاده مجدد (Singleton)
الگوهای پیشرفتهCircuit Breaker, Fallback, Hedging, Rate Limiterصرفاً راهبردهای Retry (Fixed, Incremental, Exponential)

قاعده تصمیم‌گیری معماری:

چنانچه استراتژی تاب‌آوری سازمان نیازمند الگوهایی چون قطع‌کننده مدار (Circuit Breaker) برای پیشگیری از اشباع منابع در زمان Down بودن دیتابیس است، Polly پلتفرم جامع‌تری است. اما برای مدیریت خطاهای زودگذر در سطح درایور، استفاده از قابلیت نیتیو SqlClient ساده‌تر، امن‌تر و دارای سربار محاسباتی کمتر است.

۵. ملاحظات حساس معماری (Best Practices)
۵.۱. خطای تراکنش‌ها و شکست اتمیک بودن
اگر دستوری در داخل یک تراکنش باز (SqlTransaction) شکست بخورد، وضعیت تراکنش در سرور نامشخص (Orphaned / Uncommitted) می‌شود. به همین دلیل، درایور نیتیو به صورت پیش‌فرض دستوراتی را که درون یک تراکنش فعال اجرا می‌شوند بازآزمایی نمی‌کند.

راه حل: در فرآیندهای تراکنشی، باید کل بلوک تراکنش (BeginTransaction تا Commit) از نقطه شروع مجدداً تکرار شود، نه اینکه دستورِ شکست‌خورده به تنهایی درون تراکنش معیوب Retry گردد.

۵.۲. تداخل با مکانیسم‌های ORM (نظیر Entity Framework Core)
اگر از Entity Framework Core استفاده می‌کنید، توجه داشته باشید که EF Core دارای ویژگی ExecutionStrategy (مانند EnableRetryOnFailure) است:
options.UseSqlServer(connectionString, sqlOptions =>
{
    sqlOptions.EnableRetryOnFailure(
        maxRetryCount: 5,
        maxRetryDelay: TimeSpan.FromSeconds(10),
        errorNumbersToAdd: null);
});
فعال‌سازی هم‌زمان بازآزمایی در لایه EF Core و SqlClient می‌تواند چرخه تو در تو تولید کند (مثلاً ۵ بار بازآزمایی در لایه درایور به ازای هر ۱ بار بازآزمایی لایه ORM که منجر به ۲۵ بار تلاش می‌شود). تعیین مسئولیت باید در یک لایه متمرکز باشد.

۵.۳. شخصی‌سازی و گسترش کدهای خطای گذرا
از طریق مشخصه BaselineTransientErrors در نسخه‌های جدید، می‌توان کدهای اختصاصی کسب‌وکار یا خطاهای خاص سرورهای ابری را بدون حذف موارد پیش‌فرض اضافه کرد.

۶. نتیجه‌گیری
مهاجرت از System.Data.SqlClient به Microsoft.Data.SqlClient دیگر یک پیشنهاد اختیاری نیست، بلکه پیش‌نیازی ضروری برای سیستم‌های مدرن دات‌نت به شمار می‌رود. قابلیت Configurable Retry Logic یک مکانیزم سبک، درونی و به شدت آگاه به جزئیات SQL Server را فراهم می‌کند که نیاز به چارچوب‌های جانبی را برای کنترل خطاهای موقت برطرف می‌سازد. به کارگیری صحیح این قابلیت - با در نظر گرفتن پایستگی عملیات و ملاحظات تراکنشی - پایداری سیستم‌های نرم‌افزاری را در مقیاس‌های بزرگ و محیط‌های ابری به صورت چشم‌گیری ارتقا می‌دهد.

نظرات

  • وحید نصیری در ۱۴۰۵/۰۶/۱۴ ۱۰:۴۲
    مقایسه‌ای بین ExecutionStrategy در Entity Framework Core و Configurable Retry در SqlClient به همراه سناریوهای انتخاب میان آن‌ها

    در اکوسیستم دات‌نت، دو رویکرد بومی و ساختاریافته برای مدیریت خطاهای گذرا در تعامل با SQL Server وجود دارد: یکی در بالاترین لایه دسترسی به داده یعنی ORM (ExecutionStrategy در Entity Framework Core) و دیگری در پایین‌ترین سطح انتزاع درایور شبکه و پروتکل TDS (Configurable Retry Logic در Microsoft.Data.SqlClient). اگرچه هدف نهایی هر دو، تاب‌آوری سیستم در برابر خطاهای زودگذر است، اما تفاوت‌های عمیق معماری در سطح بافتار (Context Awareness)، شیوه مدیریت وضعیت (State Management)، تراکنش‌ها و محل وقوع خطا میان این دو وجود دارد.

    ۱. مقایسه عمیق فنی و معماری

    معیار مقایسهExecutionStrategy در EF CoreConfigurable Retry در SqlClient
    لایه انتزاعی (Layer of Abstraction)لایه ORM / Application Domain (سطح بالا)لایه ADO.NET / TDS Protocol Driver (سطح پایین)
    مدیریت وضعیت شیء (ChangeTracker)کاملاً آگاه از وضعیت Entityها؛ در صورت شکست SaveChanges، وضعیت Tracker برای تلاش بعدی حفظ یا بازنشانی می‌شود.کاملاً بی‌اطلاع از موجودیت‌های لایه دامین و صرفاً ناظر بر بایت‌های سوکت و دستورات SQL.
    مدیریت تراکنش‌های کاربر (User Transactions)از طریق متد CreateExecutionStrategy().Execute() کل بلوک تراکنش از ابتدا تا انتها مجدداً اجرا می‌شود.دستورات درون تراکنش را بازآزمایی نمی‌کند (تراکنش در سطح سرور Rollback یا نامعتبر می‌شود).
    بازآزمایی باز کردن کانکشن (OpenAsync)توسط استراتژی پوشش داده می‌شود (بخشی از فرآیند اجرای دستور یا تراکنش).مستقیماً در متد OpenAsync() و بدون نیاز به ایجاد کانتکست یا تراکنش بازآزمایی می‌شود.
    دامنه سازگاری (Compatibility)صرفاً محدود به کدهایی که از DbContext استفاده می‌کنند.عمومی و قابل استفاده در Dapper، ADO.NET خالص، ابزارهای BulkCopy و Micro-ORMها.
    سربار پردازشی (Overhead)بیشتر؛ نیازمند ساخت Delegates، بازبینی تغییرات و ردیابی وضعیت اشیاء.بسیار ناچیز و کمینه؛ بررسی مستقیم کد خطای TDS و تاخیر در همان سوکت فعال.
    ۲. ریشه‌یابی تفاوت در مدیریت تراکنش‌ها (The Transaction Dilemma)
    تراکنش‌ها بزرگ‌ترین وجه تمایز فنی میان این دو مکانیزم هستند:

    الف) رفتارSqlClientدر تراکنش‌ها
    درایور پایگاه‌داده دستورات را تک‌به‌تک می‌شناسد. اگر یک تراکنش شامل سه دستور باشد:
    • INSERT INTO Orders ...
    • UPDATE Inventory ... (شکست به دلیل قطعی موقت یا Deadlock)
    • COMMIT

    درایور SqlClient نمی‌تواند دستور شماره ۲ را به تنهایی Retry کند؛ زیرا به محض قطعی ارتباط یا وقوع خطای بن‌بست (Deadlock Error 1205)، موتور SQL Server کل نشست یا تراکنش را لغو (Abort/Rollback) می‌کند. تلاش مجدد فقط برای دستور ۲ باعث بروز خطای Transaction has completed; it is usable no longer می‌شود. به همین دلیل درایور هوشمندانه دستورات دارای Transaction فعال را نادیده می‌گیرد.

    ب) رفتارEF Core Execution Strategyدر تراکنش‌ها
    در EF Core، واحد کار (Unit of Work) کل فرآیند تراکنش است. وقتی از الگوی زیر استفاده می‌کنید، در صورت بروز خطای گذرا در هر نقطه‌ای از تراکنش، کل فرآیند از ابتدا اجرا می‌شود:
    var strategy = dbContext.Database.CreateExecutionStrategy();
    
    await strategy.ExecuteAsync(async () =>
    {
        using var transaction = await dbContext.Database.BeginTransactionAsync();
        
        // ۱. ثبت سفارش
        dbContext.Orders.Add(new Order { Total = 100 });
        await dbContext.SaveChangesAsync();
    
        // ۲. کاهش موجودی انبار
        var product = await dbContext.Products.FindAsync(productId);
        product.Stock -= 1;
        await dbContext.SaveChangesAsync();
    
        await transaction.CommitAsync();
    });
    اگر خطایی رخ دهد، EF Core تغییرات ذخیره‌نشده را پاک کرده، تراکنش معیوب را دور می‌اندازد و کل اکشن را با بازخوانی مجدد داده‌ها از نو آغاز می‌کند.

    ۳. خطر فاجعه‌بار ترکیب هم‌زمان (Retry Amplification / Storm)
    یکی از رایج‌ترین خطاهای معماری، فعال‌سازی هم‌زمان هر دو مکانیزم است:
    • تنظیم EnableRetryOnFailure(maxRetryCount: 5) در EF Core
    • اختصاص RetryLogicProvider با NumberOfTries = 5 به کانکشن SqlClient زیرین

    در این سناریو، پدیده ضرب هندسی دفعات بازآزمایی (Retry Multiplication) رخ می‌دهد: Max Attempts = 5 * 5 = 25 attempts
    اگر یک کوئری با کندی موقت دیتابیس مواجه شود، درایور ۵ بار تلاش می‌کند؛ پس از شکست نهایی درایور، استراتژی EF Core خطای نهایی را می‌گیرد و دور دوم را آغاز می‌کند که خود شامل ۵ بار تلاش درایور است! این مسئله در محیط‌های پرترافیک منجر به Retry Storm و اشباع کامل Connection Pool و CPU سرور پایگاه‌داده خواهد شد.

    ۴. ماتریس سناریوهای تصمیم‌گیری (چه زمانی از کدام استفاده کنیم؟)


    سناریو ۱: سیستم‌های سازمانی با معماری مبتنی بر EF Core
    • انتخاب:EF Core ExecutionStrategy
    • دلیل: کنترل کامل چرخه حیات ChangeTracker، مدیریت بازآزمایی SaveChanges، و پشتیبانی از ExecuteAsync برای بلوک‌های تراکنشی.
    • اقدام: قابلیت بازآزمایی را در سطح کانکشن SqlClient غیرفعال بگذارید.

    سناریو ۲: سیستم‌های با کارایی بالا (High-Performance)، Micro-ORMها و Dapper
    • انتخاب:SqlClient Configurable Retry
    • دلیل: در پروژه‌هایی که از Dapper استفاده می‌کنند یا کوئری‌های خام ADO.NET می‌نویسند، EF Core وجود ندارد. SqlClient سبک‌ترین و نزدیک‌ترین نقطه به شبکه برای کنترل قطعی‌های لحظه‌ای است.
    • اقدام: یک SqlRetryLogicBaseProvider به‌صورت Singleton بسازید و به تمام کانکشن‌ها و کامندهای فاقد تراکنش اختصاص دهید.

    سناریو ۳: پروژه‌های هیبریدی (ترکیب EF Core و Dapper در یک مخزن داده)
    • انتخاب:ترکیب مشروط و تفکیک مسئولیت
    • راهکار:
    • در لایه EF Core، ویژگی EnableRetryOnFailure را برای عملیات‌های EF فعال کنید.
    • برای فراخوانی‌های Dapper/ADO.NET، کانکشن‌هایی بسازید که RetryLogicProvider دارند؛ اما هرگز کانکشن مشترک مدیریت‌شده توسط EF Core را به Provider مجهز نکنید تا از اثر تداخلی جلوگیری شود.

    جمع‌بندی نهایی
    • SqlClient Configurable Retry روی لایه انتقال (Transport Layer) و پروتکل تمرکز دارد؛ بسیار سریع و بهینه است اما از منطق دامین و تراکنش‌های چندمرحله‌ای بی‌خبر است.
    • EF Core ExecutionStrategy روی لایه عملیات دامین و تراکنش (Business Unit of Work) تمرکز دارد؛ توانایی مدیریت وضعیت‌های پیچیده و Rollback کل فرآیند را دارد اما سربار بیشتری به برنامه تحمیل می‌کند.
    • قاعده طلایی: هرگز این دو مکانیزم را روی یک خط لوله داده‌ای به‌صورت هم‌زمان فعال نکنید.