承载ASP.NET应用的服务器资源总是有限的,短时间内涌入过多的请求可能会瞬间耗尽可用资源并导致宕机。为了解决这个问题,我们需要在服务端设置一个阀门将并发处理的请求数量限制在一个可控的范围,即使会导致请求的延迟响应,在极端的情况会还不得不放弃一些请求。ASP.NET应用的流量限制是通过ConcurrencyLimiterMiddleware中间件实现的。(本文提供的示例演示已经同步到《ASP.NET Core 6框架揭秘-实例演示版》)

[S2601]设置并发和等待请求阈值 (源代码

[S2602]基于队列的限流策略(源代码

[S2603]基于栈的限流策略(源代码

[S2604]处理被拒绝的请求(源代码

[S2601]设置并发和等待请求阈值

由于各种Web服务器、反向代理和负载均衡器都提供了限流的能力,我们很少会在应用层面进行流量控制。ConcurrencyLimiterMiddleware中间件由“Microsoft.AspNetCore.ConcurrencyLimiter”这个NuGet包提供,ASP.NET应用采用的SDK(“Microsoft.NET.Sdk.Web”)并没有将该包作为默认的引用,所以我们需要手工添加该NuGet包的引用。

当请求并发量超过设定的阈值,ConcurrencyLimiterMiddleware中间件会将请求放到等待队列中,整个限流工作都是围绕这个这个队列进行的,采用怎样的策略管理这个等待队列是整个限流模型的核心。不论采用何种策略,我们都需要设置两个阈值,一个是当前允许的最大并发请求量,另一个是等待队列的最大容量。如代码片段所示,我们通过调用IServiceCollection接口的AddQueuePolicy扩展方法注册了一个基于队列(“Queue”)的策略,并将上述的两个阈值设置为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();

ConcurrencyLimiterMiddleware中间件是通过调用IApplicationBuilder的UseConcurrencyLimiter扩展方法进行注册的。后续通过调用Run扩展方法提供的RequestDelegate委托模拟了一秒钟的处理耗时。我们演示的程序还注册了一个ConsumerHostedService类型的承载服务来模拟消费API的客户端。如下面的代码片段所示,ConsumerHostedService利用注入的IConfiguration对象来提供并发量配置。当此承载服务启动之后,它会根据配置创建相应数量的并发任务持续地对我们的应用发起请求。

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;
}
}

对于发送的每个请求,ConsumerHostedService都会在控制台上记录下响应的状态和耗时。为了避免控制台“刷屏”,我们在接收到错误响应后模拟一秒钟的等待。由于并发量是由配置系统提供的,所以我们可以利用命令行参数(“Concurrency”)的方式来对并发量进行设置。如图1所示,我们以命令行的方式启动了程序,并通过命令行参数将并发量设置为2。由于并发量并没有超出阈值,所以每个请求均得到正常的响应。

图1 并发量未超出阈值

由于并发量的阈值和等待队列的容量均设置为2,从外部来看,我们的演示程序所能承受的最大并发量为4。所以当我们以此并发量启动程序之后,并发的请求能够接收到成功的响应,但是除了前两个请求能够得到及时处理之外,后续请求都会在等待队列中呆上一段时间,所以整个耗时会延长。如果将并发量提升到5,这显然超出了服务端的极限,所以部分请求会得到状态码为“503, Service Unavailable”的响应。

图2 并发量超出阈值

ASP.NET应用的并发处理的请求量可以通过dotnet-counters工具提供的性能计数器进行查看。具体的性能计数器名称为“Microsoft.AspNetCore.Hosting”,我们现在通过这种方式来看看应用程序真正的并发处理指标是否和我们的预期一致。我们还是以并发量为5启动演示程序,然后以图26-3所示的方式执行“dotnet-coutners ps”命令查看演示程序的进程,并针对进程ID执行“dotnet-counters monitor”命令查看名为“Microsoft.AspNetCore.Hosting”的性能指标。

图3 使用dotnet-counters monitor查看并发量

如图3所示,dotnet-counters显示的并发请求为4,这和我们的设置是吻合的,因为对于应用的中间件管道来说,并发处理的请求包含ConcurrencyLimiterMiddleware中间件的等待队列的两个和后续中间件真正处理的两个。我们还看到了每秒处理的请求数量为3,并有约1/3的请求失败率,这些指标和我们的设置都是吻合的。

[S2602]基于队列的限流策略

通过前面的示例演示我们知道,当ConcurrencyLimiterMiddleware中间件维护的等待队列被填满并且后续中间件管道正在“满负荷运行(并发处理的请求达到设定的阈值)”的情况下,如果此时接收到一个新的请求,它只能放弃某个待处理的请求。具体来说,它具有两种选择,一种是放弃刚刚接收的请求,另一种就是将等待队列中的某个请求扔掉,其位置由新接收的请求占据。

前面演示实例采用的等待队列处理策略是通过调用IServiceCollection接口的AddQueuePolicy扩展方法注册的,这样一种基于“队列”的策略。我们知道队列的特点就是先进先出(FIFO),讲究“先来后到”,如果采用这种策略就会放弃刚刚接收到的请求。我们可以通过简单的实例证实这一点。如下面的演示程序所示,我们在ConcurrencyLimiterMiddleware中间件之前注册了一个通过DiagnosticMiddleware方法表示的中间件,它会对每个请求按照它接收到的时间顺序进行编号,我们利用它打印出每个请求对应的响应状态就知道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扩展方法中提供的设置不变(最大并发量和等待队列大小都是2)。在应用启动之后,我们同时发送了5个请求,此时控制台上会呈现出如图4所示的输出结果,可以看出ConcurrencyLimiterMiddleware中间件在接收到第5个请求并不得不作出取舍的时候,它放弃的就是当前接收到的请求。

图4 基于队列的处理策略

[S2603]基于栈的限流策略

当ConcurrencyLimiterMiddleware中间件在接收到某个请求并需要决定放弃某个待处理请求时,它还可以采用另一种基于“栈”的策略。如果采用这种策略,它会先保全当前接收到的请求,并用它替换掉存储在等待队列时间最长的那个。也就是说它不再讲究先来后到,而主张后来居上。对于前面演示的程序来说,我们只需要按照如下的方式将针对AddQueuePolicy扩展方法的调用替换成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所示的输出结果。可以看出这次ConcurrencyLimiterMiddleware中间件在接收到第5个请求并不得不做出取舍的时候,它放弃的就是最先存储到等待队列的第3个请求。

图5 基于栈处理策略

[S2604]处理被拒绝的请求

从ConcurrencyLimiterMiddleware中间件的实现可以看出,在默认情况下因超出限流阈值而被拒绝处理的请求来说,应用最终会给与一个状态码为“503 Service Available”的响应。如果我们对这个默认的处理方式不满意,可以通过对配置选项ConcurrencyLimiterOptions的设置来提供一个自定义的处理器。举个典型的场景,集群部署的多台机器可能负载不均,所以如果将被某台机器拒绝的请求分发给另一台机器是可能被正常处理的。为了确保请求能够尽可能地被处理,我们可以针对相同的URL发起一个客户端重定向,具体的实现体现在如下所示的演示程序中。

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;
}

如上面的代码片段所示,我们调用IServiceCollection接口的Configure<TOptions>扩展方法对ConcurrencyLimiterOptions进行了配置。具体来说,我们将RejectAsync方法表示的RequestDelegate委托作为拒绝请求处理器赋值给了ConcurrencyLimiterOptions配置选项的OnRejected属性。在RejectAsync方法中,我们针对当前请求的URL返回了一个状态码为307的临时重定向响应。为了避免重复的重定向操作,我们为重定向地址添加了一个名为“reject”的查询字符串来识别重定向请求。

ASP.NET Core 6框架揭秘实例演示[38]:两种不同的限流策略的更多相关文章

  1. ASP.NET Core 6框架揭秘实例演示[07]:文件系统

    ASP.NET Core应用具有很多读取文件的场景,如读取配置文件.静态Web资源文件(如CSS.JavaScript和图片文件等).MVC应用的视图文件,以及直接编译到程序集中的内嵌资源文件.这些文 ...

  2. ASP.NET Core 6框架揭秘实例演示[08]:配置的基本编程模式

    .NET的配置支持多样化的数据源,我们可以采用内存的变量.环境变量.命令行参数.以及各种格式的配置文件作为配置的数据来源.在对配置系统进行系统介绍之前,我们通过几个简单的实例演示一下如何将具有不同来源 ...

  3. ASP.NET Core 6框架揭秘实例演示[09]:配置绑定

    我们倾向于将IConfiguration对象转换成一个具体的对象,以面向对象的方式来使用配置,我们将这个转换过程称为配置绑定.除了将配置树叶子节点配置节的绑定为某种标量对象外,我们还可以直接将一个配置 ...

  4. ASP.NET Core 6框架揭秘实例演示[10]:Options基本编程模式

    依赖注入使我们可以将依赖的功能定义成服务,最终以一种松耦合的形式注入消费该功能的组件或者服务中.除了可以采用依赖注入的形式消费承载某种功能的服务,还可以采用相同的方式消费承载配置数据的Options对 ...

  5. ASP.NET Core 6框架揭秘实例演示[11]:诊断跟踪的几种基本编程方式

    在整个软件开发维护生命周期内,最难的不是如何将软件系统开发出来,而是在系统上线之后及时解决遇到的问题.一个好的程序员能够在系统出现问题之后马上定位错误的根源并找到正确的解决方案,一个更好的程序员能够根 ...

  6. ASP.NET Core 6框架揭秘实例演示[12]:诊断跟踪的进阶用法

    一个好的程序员能够在系统出现问题之后马上定位错误的根源并找到正确的解决方案,一个更好的程序员能够根据当前的运行状态预知未来可能发生的问题,并将问题扼杀在摇篮中.诊断跟踪能够帮助我们有效地纠错和排错&l ...

  7. ASP.NET Core 6框架揭秘实例演示[13]:日志的基本编程模式[上篇]

    <诊断跟踪的几种基本编程方式>介绍了四种常用的诊断日志框架.其实除了微软提供的这些日志框架,还有很多第三方日志框架可供我们选择,比如Log4Net.NLog和Serilog 等.虽然这些框 ...

  8. ASP.NET Core 6框架揭秘实例演示[14]:日志的进阶用法

    为了对各种日志框架进行整合,微软创建了一个用来提供统一的日志编程模式的日志框架.<日志的基本编程模式>以实例演示的方式介绍了日志的基本编程模式,现在我们来补充几种"进阶" ...

  9. ASP.NET Core 6框架揭秘实例演示[15]:针对控制台的日志输出

    针对控制台的ILogger实现类型为ConsoleLogger,对应的ILoggerProvider实现类型为ConsoleLoggerProvider,这两个类型都定义在 NuGet包"M ...

  10. ASP.NET Core 6框架揭秘实例演示[16]:内存缓存与分布式缓存的使用

    .NET提供了两个独立的缓存框架,一个是针对本地内存的缓存,另一个是针对分布式存储的缓存.前者可以在不经过序列化的情况下直接将对象存储在应用程序进程的内存中,后者则需要将对象序列化成字节数组并存储到一 ...

随机推荐

  1. 【ACM算法竞赛日常训练】DAY4题解与分析【树】【子序列】| 组合数学 | 动态规划

    DAY4共2题: 树(组合数学) 子序列(dp,数学) 作者:Eriktse 简介:19岁,211计算机在读,现役ACM银牌选手力争以通俗易懂的方式讲解算法!️欢迎关注我,一起交流C++/Python ...

  2. vue中使用西瓜视频中引入自定义样式,绝对可以

    首先配置sass-loader和raw-loader 方法,再vue-config.js中加上这一段代码 module.exports = { chainWebpack: config => { ...

  3. ARC149(A~E)

    Tasks - AtCoder Regular Contest 149 又是114514年前做的题,现在来写 屯了好多,清一下库存 A - Repdigit Number (atcoder.jp) 直 ...

  4. vue之箭头函数

    目录 说明 解决方法一 重新定义this 解决方法二 使用箭头函数 无参数的箭头函数 有一个参数的箭头函数 有两个参数的箭头函数 有一个参数一个返回值的箭头函数 说明 当在一个方法(函数)里面再定义一 ...

  5. [智能制造] 如何利用生产软件(MES)进行生产信息收集?

    1 如何保证生产管理软件所收集信息的准确性? 1.1 当前制造企业使用MES系统收集信息的现状 原以为使用了MES生产管理系统后,会得到稽核员的肯定. 但没想到,在实际的稽核过程中,稽核员还是发现目前 ...

  6. [IDE]IDEA build artifacts过程很慢的解决方案[转载]

    解决方案 可能1 可能是缓存的文件太多了导致: File->Invalidate Caches /Restart,清理缓存, 并重启IDEA.重启之后,会重建索引, 此过程较慢, 但build的 ...

  7. Django笔记十七之group by 分组用法总结

    本文首发于微信公众号:Hunter后端 原文链接:Django笔记十七之group by 分组用法总结 这篇笔记介绍 Django 里面 model 的 group by 对应的一些操作. 用到的 M ...

  8. 多线程结合自定义logback日志实现简单的工单日志输出

    前言 这周学习了logback自定义日志格式.多线程基础.以及常见的定时器,本篇博客主要是结合以上知识实现一个简单的定时全部工单输出任务,再通过自定义的日志打印输出到控制台. 1.logback自定义 ...

  9. 四月十号java知识点

    1.数组:若干个相同数据类型元素按照一定顺序排列的集合2.JAVA语言内存分为栈内存和堆内存3.方法中的一些基本类型变量和对象的引用变量都在方法中的栈内存中分配4.堆内存用来存放new运算符创建的数组 ...

  10. 版本依赖控制工具Maven

    Maven 简介 依赖管理工具 如果说A工程里面用到了B工程的类.接口.配置文件等这样的资源,那么就说A依赖B 构建管理工具 构建:使用原材料生产产品的过程 安装:把一个Maven工程经过打包操作生产 ...