Rate Limiting (محدودسازی نرخ) و مدیریت سراسری خطاها

Rate Limiting (محدودسازی نرخ) و مدیریت سراسری خطاها

Rate Limiting (محدودسازی نرخ) و مدیریت سراسری خطاها

منبع: Coding Clean, Reliable, and Safe REST APIs with ASP.NET Core 8 — Anthony Giretti

اعتبار ترجمه: ترجمه با کمک هوش مصنوعی

اتصال InputValidatorFilter به Endpoint

فیلتر جنریک فصل قبل را می‌توان روی Endpoint ایجاد کشور اعمال کرد. در اینجا Country نوع جنریک Filter است و خود Endpoint دیگر نیازی به دریافت مستقیم IValidator ندارد.

app.MapPost("/countries", ([FromBody] Country country) => {

        return Results.CreatedAtRoute("countryById", new {
         Id = 1 });
    }).AddEndpointFilter<InputValidatorFilter<Country>>();
Listing 5-20 — اتصال InputValidatorFilter<Country> به POST /countries

رفتار اعتبارسنجی همان است که Validator از طریق Dependency Injection مستقیماً در Endpoint دریافت می‌شد؛ تفاوت این است که منطق اعتبارسنجی از Lambda خارج شده و کد Endpoint تمیزتر است. Endpoint Filterها در سناریوهای دیگری نیز قابل استفاده‌اند.

Rate Limiting (محدودسازی نرخ)

Rate Limiting یکی از قابلیت‌های مهم ASP.NET Core 8 است و دسترسی به API را بر اساس قواعد مشخص محدود می‌کند. این قابلیت چند هدف اصلی دارد:

  • محافظت از سامانه: با محدود کردن تعداد درخواست‌هایی که یک کاربر یا برنامه می‌تواند ارسال کند، به کاهش اثر حملات Denial of Service (DoS) کمک می‌کند.
  • تضمین کیفیت خدمت: محدودکردن Throughput باعث می‌شود منابع به‌شکل عادلانه‌تری میان مصرف‌کنندگان تقسیم شوند و Performance (کارایی) سامانه در بار بالا حفظ شود.
  • ایجاد سطوح دسترسی تجاری: می‌توان برای Tier رایگان محدودیت بیشتر و برای Subscription پولی محدودیت کمتر یا دسترسی گسترده‌تر تعریف کرد.

مدل‌های Rate Limiter

ASP.NET Core 8 چهار دستهٔ اصلی Limiter ارائه می‌کند:

  1. Fixed Window: تعداد مشخصی درخواست در یک بازهٔ زمانی ثابت مجاز است. هر درخواست مجاز شمارنده را کم می‌کند و با پایان Window شمارنده دوباره تنظیم می‌شود. تعداد محدودی درخواست نیز می‌تواند تا آزاد شدن ظرفیت در Queue منتظر بماند و درخواست‌های بیشتر رد می‌شوند.
  2. Sliding Window: Window به چند Segment تقسیم می‌شود. ظرفیت درخواست میان Segmentها جابه‌جا می‌شود و در حرکت پنجره، ظرفیت آزادشده از Segmentهای قبلی به دورهٔ بعدی منتقل می‌شود. این مدل توزیع نرم‌تری از ظرفیت در طول زمان ایجاد می‌کند و می‌تواند Queue داشته باشد.
  3. Token Bucket: ظرفیت به‌صورت Token در یک Bucket نمایش داده می‌شود. هر درخواست مجاز یک Token مصرف می‌کند. تعداد مشخصی Token در دوره‌های تعیین‌شده دوباره اضافه می‌شود، اما از سقف Bucket فراتر نمی‌رود. اگر Token موجود نباشد درخواست به‌صورت خودکار رد می‌شود و این مدل برای نبود Token صف انتظار ایجاد نمی‌کند.
  4. Concurrency: ساده‌ترین مدل است و تعداد درخواست‌های هم‌زمان مجاز را محدود می‌کند. در صورت پر بودن ظرفیت، می‌توان تعدادی درخواست را در Queue نگه داشت و بقیه را رد کرد.

هر مدل می‌تواند Partition Key داشته باشد؛ برای مثال شناسهٔ کاربر، IP یا معیار دیگری که Limiting بر اساس آن مستقل شود. اگر Partition Key تعریف نشود، محدودیت Global است. در پیکربندی پیش‌فرض، رد درخواست می‌تواند با 503 Service Unavailable پاسخ داده شود؛ نویسنده برای انطباق بهتر با مفهوم محدودسازی نرخ، 429 Too Many Requests را پیشنهاد می‌کند.

فعال‌سازی Rate Limiting

قواعد با AddRateLimiter ثبت و با Middleware نوع UseRateLimiter در Pipeline فعال می‌شوند.

var builder = WebApplication.CreateBuilder(args);

    builder.Services.AddRateLimiter(options =>
    {
        // Code here
    });

    var app = builder.Build();

    // Your ASP.NET Core pipeline

    app.UseRateLimiter();

    // Your ASP.NET Core pipeline

    app.Run();
Listing 5-21 — AddRateLimiter و UseRateLimiter

مدل Fixed Window

نمونهٔ زیر برای هر IP در یک Window پانزده‌ثانیه‌ای حداکثر ۵۰ درخواست مجاز می‌کند. در صورت رسیدن به حد، تا ۱۰ درخواست در Queue قرار می‌گیرند. سایر درخواست‌ها رد می‌شوند و پاسخ سفارشی با وضعیت ۴۲۹ فرستاده می‌شود.

builder.Services.AddRateLimiter(options =>
    {
        options.RejectionStatusCode = (int)HttpStatusCode.TooManyRequests;
        options.OnRejected = async (context, token) =>
        {
            await context.HttpContext.Response.WriteAsync("Too many requests. Please try again later.");
        };
        options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
            RateLimitPartition.GetFixedWindowLimiter(
                partitionKey: httpContext.Connection.RemoteIpAddress.ToString(),
                factory: _ => new FixedWindowRateLimiterOptions
                {
                    QueueLimit = 10,
                    PermitLimit = 50,
                    Window = TimeSpan.FromSeconds(15)
                }));
    });
Listing 5-22 — پیاده‌سازی Fixed Window Limiter

PartitionedRateLimiter.Create دسترسی به HttpContext را فراهم می‌کند؛ بنابراین می‌توان Partition Key را از IP، شناسهٔ کاربر احراز هویت‌شده یا هر دادهٔ Contextual دیگری ساخت. در این نمونه چون Limiter به GlobalLimiter نسبت داده شده، روی همهٔ Endpointها اعمال می‌شود.

برای مستثنا کردن یک Endpoint از Limiter سراسری از DisableRateLimiting استفاده کنید.

app.MapGet("/notlimited", () =>
    {
        return Results.Ok();
    }).DisableRateLimiting();
Listing 5-23 — غیرفعال کردن Rate Limiting سراسری برای یک Endpoint

Policyهای اختصاصی در کنار Limiter سراسری

می‌توان چند Limiter نام‌گذاری‌شده ساخت و هرکدام را تنها روی Endpointهای مشخص اعمال کرد. در نمونهٔ زیر Policy با نام ShortLimit در کنار Limiter سراسری تعریف می‌شود.

builder.Services.AddRateLimiter(options =>
    {
        options.RejectionStatusCode = (int)HttpStatusCode.TooManyRequests;
        options.OnRejected = async (context, token) =>
        {
            await context.HttpContext.Response.WriteAsync("Too many requests. Please try again later.");
        };
        options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
        RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: httpContext.Connection.RemoteIpAddress.ToString(),
            factory: _ => new FixedWindowRateLimiterOptions
            {
                QueueLimit = 10,
                PermitLimit = 50,
                Window = TimeSpan.FromSeconds(15)
            }));

        options.AddPolicy(policyName: "ShortLimit", context =>
        {
            return RateLimitPartition.GetFixedWindowLimiter(
                context.Connection.RemoteIpAddress.ToString(),
                _ => new FixedWindowRateLimiterOptions
                {
                    PermitLimit = 10,
                    Window = TimeSpan.FromSeconds(15)
                });
        });
    });
Listing 5-24 — ترکیب Global Limiter با Policy نوع ShortLimit

Policy با RequireRateLimiting روی Endpoint اعمال می‌شود.

app.MapGet("/limited", () =>
    {
        return Results.Ok();
    }).RequireRateLimiting("ShortLimit");
Listing 5-25 — اعمال Policy با RequireRateLimiting

Rate Limiting بر اساس Pricing Tier

انعطاف Limiter اجازه می‌دهد قواعد بر اساس سطح تجاری مشتری تغییر کنند. فرض کنید Tier مشتری بر اساس IP تعیین می‌شود. قرارداد سرویس به‌شکل زیر است.

using Domain.Enum;

    namespace Domain.Services;

    public interface IPricingTierService
    {
        public PricingTier GetPricingTier(string ipAddress);
    }
Listing 5-26 — سرویس IPricingTierService

خروجی سرویس Enum زیر است.

namespace Domain.Enum;

    public enum PricingTier
    {
        Free = 0,
        Paid = 1
    }
Listing 5-27 — Enum نوع PricingTier

پیاده‌سازی سرویس با builder.Services.AddScoped<IPricingTierService, PricingTierService>(); ثبت می‌شود. چون HttpContext در Factory در دسترس است، سرویس ثبت‌شده را می‌توان با httpContext.RequestServices.GetRequiredService<T>() از DI دریافت کرد.

builder.Services.AddRateLimiter(options =>
    {
        options.RejectionStatusCode = (int)HttpStatusCode.TooManyRequests;
        options.OnRejected = async (context, token) =>
        {
            await context.HttpContext.Response.WriteAsync("Too many requests. Please try again later.");
        };
        options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
        {
            var priceTierService = httpContext.RequestServices.GetRequiredService<IPricingTierService>();
            var ip = httpContext.Connection.RemoteIpAddress.ToString();
            var priceTier = priceTierService.GetPricingTier(ip);

            return priceTier switch
            {
                PricingTier.Paid => RateLimitPartition.GetFixedWindowLimiter(
                    ip,
                    _ => new FixedWindowRateLimiterOptions
                    {
                        QueueLimit = 10,
                        PermitLimit = 50,
                        Window = TimeSpan.FromSeconds(15)
                    }),
                PricingTier.Free => RateLimitPartition.GetFixedWindowLimiter(
                    ip,
                    _ => new FixedWindowRateLimiterOptions
                    {
                        PermitLimit = 1,
                        Window = TimeSpan.FromSeconds(15)
                    })
            };
        });
    });
Listing 5-28 — Global Limiter وابسته به Pricing Tier

در این نمونه مشتری Paid تا ۵۰ Permit در پانزده ثانیه و Queue ده‌تایی دارد، درحالی‌که Tier رایگان تنها یک Permit در همان بازه دریافت می‌کند. نویسنده Fixed Window را به‌خاطر سادگی محدودکردن ورودی در یک بازهٔ مشخص، مدل مورد علاقهٔ خود معرفی می‌کند.

شکل ۵-۱۴ — پاسخ HTTP 429 Too Many Requests پس از رد درخواست
تصویر منبع — صفحهٔ 264Visual واقعی استخراج‌شده از PDF، مربوط به صفحهٔ 264.

مدل Sliding Window

در Sliding Window علاوه بر ظرفیت و طول Window باید تعداد Segmentها با SegmentsPerWindow تعیین شود. نمونهٔ زیر همان Tierها را با Sliding Window پیاده‌سازی می‌کند.

builder.Services.AddRateLimiter(options =>
    {
        options.RejectionStatusCode = (int)HttpStatusCode.TooManyRequests;
        options.OnRejected = async (context, token) =>
        {
            await context.HttpContext.Response.WriteAsync("Too many requests. Please try again later.");
        };

        options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
        {
            var priceTierService = httpContext.RequestServices.GetRequiredService<IPricingTierService>();
            var ip = httpContext.Connection.RemoteIpAddress.ToString();
            var priceTier = priceTierService.GetPricingTier(ip);

            return priceTier switch
            {
                PricingTier.Paid => RateLimitPartition.GetSlidingWindowLimiter(
                    ip,
                    _ => new SlidingWindowRateLimiterOptions
                    {
                        QueueLimit = 10,
                        PermitLimit = 50,
                        SegmentsPerWindow = 2,
                        Window = TimeSpan.FromSeconds(15)
                    }),
                PricingTier.Free => RateLimitPartition.GetSlidingWindowLimiter(
                    ip,
                    _ => new SlidingWindowRateLimiterOptions
                    {
                        PermitLimit = 2,
                        SegmentsPerWindow = 2,
                        Window = TimeSpan.FromSeconds(15)
                    })
            };
        });
    });
Listing 5-29 — Global Limiter با مدل Sliding Window

در این نسخه، GetSlidingWindowLimiter و SlidingWindowRateLimiterOptions جای همتایان Fixed Window را می‌گیرند.

مدل Token Bucket

Token Bucket به سه تنظیم اصلی نیاز دارد:

  1. TokenLimit: حداکثر تعداد Tokenهای موجود.
  2. TokensPerPeriod: تعداد Tokenهایی که در هر دوره دوباره افزوده می‌شوند.
  3. ReplenishmentPeriod: طول دورهٔ بازافزایی Tokenها.
builder.Services.AddRateLimiter(options =>
    {
        options.RejectionStatusCode = (int)HttpStatusCode.TooManyRequests;
        options.OnRejected = async (context, token) =>
        {
            await context.HttpContext.Response.WriteAsync("Too many requests. Please try again later.");
        };

        options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
        {
            var priceTierService = httpContext.RequestServices.GetRequiredService<IPricingTierService>();
            var ip = httpContext.Connection.RemoteIpAddress.ToString();
            var priceTier = priceTierService.GetPricingTier(ip);

            return priceTier switch
            {
                PricingTier.Paid => RateLimitPartition.GetTokenBucketLimiter(
                    ip,
                    _ => new TokenBucketRateLimiterOptions
                    {
                        TokenLimit = 50,
                        TokensPerPeriod = 25,
                        ReplenishmentPeriod = TimeSpan.FromSeconds(15)
                    }),
                PricingTier.Free => RateLimitPartition.GetTokenBucketLimiter(
                    ip,
                    _ => new TokenBucketRateLimiterOptions
                    {
                        TokenLimit = 10,
                        TokensPerPeriod = 5,
                        ReplenishmentPeriod = TimeSpan.FromSeconds(15)
                    })
            };
        });
    });
Listing 5-30 — Global Limiter با مدل Token Bucket

GetTokenBucketLimiter و TokenBucketRateLimiterOptions جای نسخه‌های Fixed Window را می‌گیرند. این مدل از دیگر مدل‌ها سخت‌گیرانه‌تر است، زیرا وقتی Tokenی وجود ندارد درخواست را در Queue نگه نمی‌دارد.

مدل Concurrency

Concurrency ساده‌ترین Limiter است و تنها QueueLimit و PermitLimit را نیاز دارد.

builder.Services.AddRateLimiter(options =>
    {
        options.RejectionStatusCode = (int)HttpStatusCode.TooManyRequests;
        options.OnRejected = async (context, token) =>
        {
            await context.HttpContext.Response.WriteAsync("Too many requests. Please try again later.");
        };

        options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
        {
            var priceTierService = httpContext.RequestServices.GetRequiredService<IPricingTierService>();
            var ip = httpContext.Connection.RemoteIpAddress.ToString();
            var priceTier = priceTierService.GetPricingTier(ip);

            return priceTier switch
            {
                PricingTier.Paid => RateLimitPartition.GetConcurrencyLimiter(
                    ip,
                    _ => new ConcurrencyLimiterOptions
                    {
                        QueueLimit = 10,
                        PermitLimit = 50
                    }),
                PricingTier.Free => RateLimitPartition.GetConcurrencyLimiter(
                    ip,
                    _ => new ConcurrencyLimiterOptions
                    {
                        QueueLimit = 0,
                        PermitLimit = 10
                    })
            };
        });
    });
Listing 5-31 — Global Limiter با مدل Concurrency

در اینجا GetConcurrencyLimiter و ConcurrencyLimiterOptions جای تنظیمات مدل Fixed Window را گرفته‌اند. Rate Limiting در ASP.NET Core 8 بسیار قابل سفارشی‌سازی است و نویسنده استفاده از آن را قویاً توصیه می‌کند.

مدیریت سراسری خطاها

مدیریت کارآمد خطا برای برنامه‌ای که به منابع خارجی مانند فایل، پایگاه داده یا سرویس‌های دیگر وابسته است ضروری است. هدف این است که وقوع خطا و نوع آن به‌شکل روشن و استاندارد به مصرف‌کنندهٔ API اعلام شود. در ASP.NET Core 8 می‌توان این کار را به‌صورت سراسری، تمیز و بدون تکرار منطق انجام داد.

در این بخش از ProblemDetails استفاده می‌شود؛ ساختاری استاندارد که در فصل ۱ معرفی شد و پاسخ خطا را با قالبی قابل انتظار برای Client تولید می‌کند.

برای مدیریت سراسری Exception، کلاسی باید رابط IExceptionHandler را پیاده‌سازی کند.

public interface IExceptionHandler
    {
        ValueTask<bool> TryHandleAsync(HttpContext httpContext,
            Exception exception, CancellationToken cancellationToken);
    }
Listing 5-32 — رابط IExceptionHandler

TryHandleAsync مقدار Boolean برمی‌گرداند. مقدار true یعنی Exception مدیریت شده و زنجیرهٔ Handlerها پایان یابد؛ مقدار false اجازه می‌دهد Handler بعدی بررسی شود.

DefaultExceptionHandler

Handler زیر هر Exception عمومی را به یک ProblemDetails با وضعیت ۵۰۰ تبدیل می‌کند و پاسخ را به JSON می‌نویسد.

using Microsoft.AspNetCore.Diagnostics;
    using Microsoft.AspNetCore.Mvc;
    using System.Net;

    namespace AspNetCore8MinimalApis.ExceptionHandlers;

    public class DefaultExceptionHandler : IExceptionHandler
    {
        public async ValueTask<bool> TryHandleAsync(HttpContext httpContext,
            Exception exception, CancellationToken cancellationToken)
        {
            await httpContext.Response.WriteAsJsonAsync(new ProblemDetails
            {
                Status = (int)HttpStatusCode.InternalServerError,
                Type = exception.GetType().Name,
                Title = "An unexpected error occurred",
                Detail = exception.Message,
                Instance = $"{httpContext.Request.Method} {httpContext.Request.Path}"
            });

            return true;
        }
    }
Listing 5-33 — کلاس DefaultExceptionHandler

نویسنده JSON را به‌صورت صریح انتخاب می‌کند، زیرا در APIهای مدرن تقریباً همیشه قالب مورد انتظار Client است. Handler با AddExceptionHandler<T> ثبت و با UseExceptionHandler فعال می‌شود.

var builder = WebApplication.CreateBuilder(args);

    builder.Services.AddExceptionHandler<DefaultExceptionHandler>();

    var app = builder.Build();

    // Your ASP.NET Core pipeline

    app.UseExceptionHandler(opt => { });

    // Your ASP.NET Core pipeline

    app.Run();
Listing 5-34 — فعال‌سازی DefaultExceptionHandler در Pipeline

Delegate تنظیمات UseExceptionHandler الزامی است، اما در این نمونه خالی می‌ماند. خطای عمومی با 500 Internal Server Error برگردانده می‌شود.

app.MapGet("/exception", () => {
        throw new Exception();
    });
Listing 5-35 — Endpoint تولیدکنندهٔ Exception
شکل ۵-۱۵ — پاسخ GET /exception در Postman
تصویر منبع — صفحهٔ 273Visual واقعی استخراج‌شده از PDF، مربوط به صفحهٔ 273.

زنجیره‌کردن Exception Handlerها

ASP.NET Core 8 اجازه می‌دهد چند Handler به‌ترتیب ثبت شوند. هر Handler نوع Exception را بررسی می‌کند و اگر آن را مدیریت نکرد مقدار false برمی‌گرداند تا Handler بعدی فرصت اجرا داشته باشد.

برای Timeout، نویسنده معتقد است پاسخ 408 برای این سناریوی Server-side مناسب نیست، زیرا خانوادهٔ 4xx معمولاً خطای Client را نشان می‌دهد. برای Timeout ناشی از Server یا منبع خارجی، 503 Service Unavailable انتخاب مناسب‌تری در این مثال است.

using Microsoft.AspNetCore.Diagnostics;
    using Microsoft.AspNetCore.Mvc;
    using System.Net;

    namespace AspNetCore8MinimalApis.ExceptionHandlers;

    public class TimeOutExceptionHandler : IExceptionHandler
    {
        public async ValueTask<bool> TryHandleAsync(HttpContext httpContext,
            Exception exception, CancellationToken cancellationToken)
        {
            if (exception is TimeoutException)
            {
                httpContext.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable;

                await httpContext.Response.WriteAsJsonAsync(new ProblemDetails
                {
                    Status = (int)HttpStatusCode.ServiceUnavailable,
                    Type = exception.GetType().Name,
                    Title = "A timeout occurred",
                    Detail = exception.Message,
                    Instance = $"{httpContext.Request.Method} {httpContext.Request.Path}"
                });
                return true;
            }
            return false;
        }
    }
Listing 5-36 — کلاس TimeOutExceptionHandler

ترتیب ثبت Handlerها اهمیت دارد. Handler اختصاصی Timeout باید پیش از Handler پیش‌فرض ثبت شود. اگر Timeout را مدیریت کند و true برگرداند، Default Handler دیگر اجرا نمی‌شود.

var builder = WebApplication.CreateBuilder(args);

    builder.Services.AddExceptionHandler<TimeOutExceptionHandler>();
    builder.Services.AddExceptionHandler<DefaultExceptionHandler>();

    var app = builder.Build();
Listing 5-37 — ثبت TimeOutExceptionHandler پیش از DefaultExceptionHandler

Endpoint زیر برای آزمایش یک TimeoutException ایجاد می‌کند.

app.MapGet("/timeout", () => {
        throw new TimeoutException();
    });
Listing 5-38 — GET /timeout و ایجاد TimeoutException
شکل ۵-۱۶ — پاسخ GET /timeout در Postman پس از مدیریت خطا

با همین الگو می‌توان برای انواع مختلف Exception، Handlerهای اختصاصی و استاندارد ساخت.

جمع‌بندی فصل

این فصل قابلیت‌هایی را معرفی کرد که بدون آن‌ها هم می‌توان API ساخت، اما استفاده از آن‌ها ساختار و نگهداری برنامه را بهتر می‌کند: کپسوله‌سازی Endpointها، Binding سفارشی، Middlewareها، Endpoint Filterها، Rate Limiting و مدیریت سراسری Exception. فصل بعد از سطح API عبور می‌کند و به دسترسی به داده از منابع مختلف و ساختاردهی لایه‌های مربوط به منابع خارجی می‌پردازد.

تصاویر منبع مرتبط با این بخش

تصویر منبع — صفحهٔ 276Visual واقعی استخراج‌شده از PDF، مربوط به صفحهٔ 276.

منبع: Coding Clean, Reliable, and Safe REST APIs with ASP.NET Core 8 — Anthony Giretti.

فروش یا انتشار این ترجمه منوط به داشتن مجوز لازم از صاحب حقوق اثر است.

امتیاز کاربران به این مقاله

☆☆☆☆☆

0 نفر امتیاز داده اند. میانگین: 0.0 از 5

 

0 نظر

نظر محترم شما در مورد مقاله های وب سایت برنامه نویسی و پایگاه داده

نظرات محترم شما در خدمات رسانی بهتر ما را یاری می نمایند. لطفا اگر مایل بودید یک نظر ما را مهمان فرمائید. آدرس ایمیل و وب سایت شما نمایش داده نخواهد شد.

0 / 500

اطلاعات تماس

  • آدرس:اصفهان-خیابان ام کلثوم غربی - بعد خیابان تخم چی - بیست متر بعد از پیتزا ننه شب - کوچه تعمیر گاه سمار زغالی - پلاک 354 - درب مشکی - طبقه هفتم
  • آدرس ایمیل:najafzade@gmail.com
  • وب سایت:http://www.a00b.com/
  • تلفن ثابت:(+98)9131253620
  • تلفن همراه:09131253620