承載ASP.NET應(yīng)用的服務(wù)器資源總是有限的,短時間內(nèi)涌入過多的請求可能會瞬間耗盡可用資源并導(dǎo)致宕機。為了解決這個問題,我們需要在服務(wù)端設(shè)置一個閥門將并發(fā)處理的請求數(shù)量限制在一個可控的范圍,即使會導(dǎo)致請求的延遲響應(yīng),在極端的情況會還不得不放棄一些請求。ASP.NET應(yīng)用的流量限制是通過ConcurrencyLimiterMiddleware中間件實現(xiàn)的。(本文提供的示例演示已經(jīng)同步到《ASP.NET Core 6框架揭秘-實例演示版》)
[S2601]設(shè)置并發(fā)和等待請求閾值 (源代碼)
[S2602]基于隊列的限流策略(源代碼)
[S2603]基于棧的限流策略(源代碼)
[S2604]處理被拒絕的請求(源代碼)
[S2601]設(shè)置并發(fā)和等待請求閾值
由于各種Web服務(wù)器、反向代理和負載均衡器都提供了限流的能力,我們很少會在應(yīng)用層面進行流量控制。ConcurrencyLimiterMiddleware中間件由“Microsoft.AspNetCore.ConcurrencyLimiter”這個NuGet包提供,ASP.NET應(yīng)用采用的SDK(“Microsoft.NET.Sdk.Web”)并沒有將該包作為默認的引用,所以我們需要手工添加該NuGet包的引用。
當請求并發(fā)量超過設(shè)定的閾值,ConcurrencyLimiterMiddleware中間件會將請求放到等待隊列中,整個限流工作都是圍繞這個這個隊列進行的,采用怎樣的策略管理這個等待隊列是整個限流模型的核心。不論采用何種策略,我們都需要設(shè)置兩個閾值,一個是當前允許的最大并發(fā)請求量,另一個是等待隊列的最大容量。如代碼片段所示,我們通過調(diào)用IServiceCollection接口的AddQueuePolicy擴展方法注冊了一個基于隊列(“Queue”)的策略,并將上述的兩個閾值設(shè)置為2。
using App;
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
builder.Services
.AddHostedService<ConsumerHostedService>()
.AddQueuePolicy(options =>
{
options.MaxConcurrentRequests = 2;
options.RequestQueueLimit = 2;
});
var app = builder.Build();
app
.UseConcurrencyLimiter()
.Run(httpContext => Task.Delay(1000).ContinueWith(_ => httpContext.Response.StatusCode = 200));
app.Run();
public class ConsumerHostedService : BackgroundService { private readonly HttpClient[] _httpClients; public ConsumerHostedService(IConfiguration configuration) { var concurrency = configuration.GetValue<int>("Concurrency"); _httpClients = Enumerable .Range(1, concurrency) .Select(_ => new HttpClient()) .ToArray(); } protected override Task ExecuteAsync(CancellationToken stoppingToken) { var tasks = _httpClients.Select(async client => { while (true) { var start = DateTimeOffset.UtcNow; var response = await client.GetAsync("http://localhost:5000"); var duration = DateTimeOffset.UtcNow - start; var status = $"{(int)response.StatusCode},{response.StatusCode}"; Console.WriteLine($"{status} [{(int)duration.TotalSeconds}s]"); if (!response.IsSuccessStatusCode) { await Task.Delay(1000); } } }); return Task.WhenAll(tasks); } public override Task StopAsync(CancellationToken cancellationToken) { Array.ForEach(_httpClients, it => it.Dispose()); return Task.CompletedTask; } }
對于發(fā)送的每個請求,ConsumerHostedService都會在控制臺上記錄下響應(yīng)的狀態(tài)和耗時。為了避免控制臺“刷屏”,我們在接收到錯誤響應(yīng)后模擬一秒鐘的等待。由于并發(fā)量是由配置系統(tǒng)提供的,所以我們可以利用命令行參數(shù)(“Concurrency”)的方式來對并發(fā)量進行設(shè)置。如圖1所示,我們以命令行的方式啟動了程序,并通過命令行參數(shù)將并發(fā)量設(shè)置為2。由于并發(fā)量并沒有超出閾值,所以每個請求均得到正常的響應(yīng)。
圖1 并發(fā)量未超出閾值
由于并發(fā)量的閾值和等待隊列的容量均設(shè)置為2,從外部來看,我們的演示程序所能承受的最大并發(fā)量為4。所以當我們以此并發(fā)量啟動程序之后,并發(fā)的請求能夠接收到成功的響應(yīng),但是除了前兩個請求能夠得到及時處理之外,后續(xù)請求都會在等待隊列中呆上一段時間,所以整個耗時會延長。如果將并發(fā)量提升到5,這顯然超出了服務(wù)端的極限,所以部分請求會得到狀態(tài)碼為“503, Service Unavailable”的響應(yīng)。
圖2 并發(fā)量超出閾值
ASP.NET應(yīng)用的并發(fā)處理的請求量可以通過dotnet-counters工具提供的性能計數(shù)器進行查看。具體的性能計數(shù)器名稱為“Microsoft.AspNetCore.Hosting”,我們現(xiàn)在通過這種方式來看看應(yīng)用程序真正的并發(fā)處理指標是否和我們的預(yù)期一致。我們還是以并發(fā)量為5啟動演示程序,然后以圖26-3所示的方式執(zhí)行“dotnet-coutners ps”命令查看演示程序的進程,并針對進程ID執(zhí)行“dotnet-counters monitor”命令查看名為“Microsoft.AspNetCore.Hosting”的性能指標。
圖3 使用dotnet-counters monitor查看并發(fā)量
如圖3所示,dotnet-counters顯示的并發(fā)請求為4,這和我們的設(shè)置是吻合的,因為對于應(yīng)用的中間件管道來說,并發(fā)處理的請求包含ConcurrencyLimiterMiddleware中間件的等待隊列的兩個和后續(xù)中間件真正處理的兩個。我們還看到了每秒處理的請求數(shù)量為3,并有約1/3的請求失敗率,這些指標和我們的設(shè)置都是吻合的。
[S2602]基于隊列的限流策略
通過前面的示例演示我們知道,當ConcurrencyLimiterMiddleware中間件維護的等待隊列被填滿并且后續(xù)中間件管道正在“滿負荷運行(并發(fā)處理的請求達到設(shè)定的閾值)”的情況下,如果此時接收到一個新的請求,它只能放棄某個待處理的請求。具體來說,它具有兩種選擇,一種是放棄剛剛接收的請求,另一種就是將等待隊列中的某個請求扔掉,其位置由新接收的請求占據(jù)。
前面演示實例采用的等待隊列處理策略是通過調(diào)用IServiceCollection接口的AddQueuePolicy擴展方法注冊的,這樣一種基于“隊列”的策略。我們知道隊列的特點就是先進先出(FIFO),講究“先來后到”,如果采用這種策略就會放棄剛剛接收到的請求。我們可以通過簡單的實例證實這一點。如下面的演示程序所示,我們在ConcurrencyLimiterMiddleware中間件之前注冊了一個通過DiagnosticMiddleware方法表示的中間件,它會對每個請求按照它接收到的時間順序進行編號,我們利用它打印出每個請求對應(yīng)的響應(yīng)狀態(tài)就知道ConcurrencyLimiterMiddleware中間件最終放棄的是那個請求了。
using App; var requestId = 1; var @lock = new object(); var builder = WebApplication.CreateBuilder(); builder.Logging.ClearProviders(); builder.Services .AddHostedService<ConsumerHostedService>() .AddQueuePolicy(options => { options.MaxConcurrentRequests = 2; options.RequestQueueLimit = 2; }); var app = builder.Build(); app .Use(InstrumentAsync) .UseConcurrencyLimiter() .Run(httpContext => Task.Delay(1000).ContinueWith(_ => httpContext.Response.StatusCode = 200)); await app.StartAsync(); var tasks = Enumerable.Range(1, 5) .Select(_ => new HttpClient().GetAsync("http://localhost:5000")); await Task.WhenAll(tasks); Console.Read(); async Task InstrumentAsync(HttpContext httpContext, RequestDelegate next) { Task task; int id; lock (@lock!) { id = requestId++; task = next(httpContext); } await task; Console.WriteLine($"Request {id}: {httpContext.Response.StatusCode}"); }
我們在 IServiceCollection接口的AddQueuePolicy擴展方法中提供的設(shè)置不變(最大并發(fā)量和等待隊列大小都是2)。在應(yīng)用啟動之后,我們同時發(fā)送了5個請求,此時控制臺上會呈現(xiàn)出如圖4所示的輸出結(jié)果,可以看出ConcurrencyLimiterMiddleware中間件在接收到第5個請求并不得不作出取舍的時候,它放棄的就是當前接收到的請求。
圖4 基于隊列的處理策略
[S2603]基于棧的限流策略
當ConcurrencyLimiterMiddleware中間件在接收到某個請求并需要決定放棄某個待處理請求時,它還可以采用另一種基于“棧”的策略。如果采用這種策略,它會先保全當前接收到的請求,并用它替換掉存儲在等待隊列時間最長的那個。也就是說它不再講究先來后到,而主張后來居上。對于前面演示的程序來說,我們只需要按照如下的方式將針對AddQueuePolicy擴展方法的調(diào)用替換成AddStackPolicy方法就可以切換到這種策略。
... var builder = WebApplication.CreateBuilder(); builder.Logging.ClearProviders(); builder.Services .AddHostedService<ConsumerHostedService>() .AddStackPolicy(options => { options.MaxConcurrentRequests = 2; options.RequestQueueLimit = 2; }); var app = builder.Build(); ...
重新啟動改動后的演示程序,我們將在控制臺上得到如圖5所示的輸出結(jié)果??梢钥闯鲞@次ConcurrencyLimiterMiddleware中間件在接收到第5個請求并不得不做出取舍的時候,它放棄的就是最先存儲到等待隊列的第3個請求。
圖5 基于棧處理策略
[S2604]處理被拒絕的請求
從ConcurrencyLimiterMiddleware中間件的實現(xiàn)可以看出,在默認情況下因超出限流閾值而被拒絕處理的請求來說,應(yīng)用最終會給與一個狀態(tài)碼為“503 Service Available”的響應(yīng)。如果我們對這個默認的處理方式不滿意,可以通過對配置選項ConcurrencyLimiterOptions的設(shè)置來提供一個自定義的處理器。舉個典型的場景,集群部署的多臺機器可能負載不均,所以如果將被某臺機器拒絕的請求分發(fā)給另一臺機器是可能被正常處理的。為了確保請求能夠盡可能地被處理,我們可以針對相同的URL發(fā)起一個客戶端重定向,具體的實現(xiàn)體現(xiàn)在如下所示的演示程序中。文章來源:http://www.zghlxwxcb.cn/news/detail-481842.html
using Microsoft.AspNetCore.ConcurrencyLimiter; using Microsoft.AspNetCore.Http.Extensions; var builder = WebApplication.CreateBuilder(args); builder.Logging.ClearProviders(); builder.Services .Configure<ConcurrencyLimiterOptions>(options => options.OnRejected = RejectAsync) .AddStackPolicy(options => { options.MaxConcurrentRequests = 2; options.RequestQueueLimit = 2; }); var app = builder.Build(); app .UseConcurrencyLimiter() .Run(httpContext => Task.Delay(1000).ContinueWith(_ => httpContext.Response.StatusCode = 200)); app.Run(); static Task RejectAsync(HttpContext httpContext) { var request = httpContext.Request; if (!request.Query.ContainsKey("reject")) { var response = httpContext.Response; response.StatusCode = 307; var queryString = request.QueryString.Add("reject", "true"); var newUrl = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path, queryString); response.Headers.Location = newUrl; } return Task.CompletedTask; }
如上面的代碼片段所示,我們調(diào)用IServiceCollection接口的Configure<TOptions>擴展方法對ConcurrencyLimiterOptions進行了配置。具體來說,我們將RejectAsync方法表示的RequestDelegate委托作為拒絕請求處理器賦值給了ConcurrencyLimiterOptions配置選項的OnRejected屬性。在RejectAsync方法中,我們針對當前請求的URL返回了一個狀態(tài)碼為307的臨時重定向響應(yīng)。為了避免重復(fù)的重定向操作,我們?yōu)橹囟ㄏ虻刂诽砑恿艘粋€名為“reject”的查詢字符串來識別重定向請求。文章來源地址http://www.zghlxwxcb.cn/news/detail-481842.html
到了這里,關(guān)于ASP.NET Core 6框架揭秘實例演示[38]:兩種不同的限流策略的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!