承载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. 7.远程代码执行漏洞RCE

    远程代码执行漏洞RCE 1.RCE Remote Code Execute 远程代码执行 Remote Command Execute 远程命令执行 2.危害 窃取服务器的敏感数据.文件 对电脑的文件 ...

  2. MQ(消息队列)常见问题梳理

    MQ 中 broker 的作用,有无broker有什么差异? MQ(Message Queue)中的broker是消息队列的核心组件之一,它的作用是接收.存储.分发和传递消息.具体来说,broker主 ...

  3. window安装openslide库

    下载openslide二进制文件: 链接:https://openslide.org/download/  将下载好的ZIP文件解压到Anaconda的Library目录下(你也可以选择自己喜欢的目录 ...

  4. pandas这dataframe结构

    认识DataFrame结构 DataFrame 一个表格型的数据结构,既有行标签(index),又有列标签(columns),它也被称异构数据表,所谓异构,指的是表格中每列的数据类型可以不同,比如可以 ...

  5. C# System.ObjectDisposedException: Cannot access a disposed object, A common cause of thiserror is disposing a context that was resolved from dependency injection and then later trying touse...

    项目中使用了依赖注入,这个错误在我项目中的原因:在async修饰的异步方法中,调用执行数据库操作的方法时,没有使用await关键字调用,因为没有等待该调用,所以在调用完成之前将继续执行该方法.因此,已 ...

  6. spring boot过滤器实现项目内接口过滤

    spring boot过滤器实现项目内接口过滤 业务 由于业务需求,存在两套项目,一套是路由中心,一套是业务系统. 现在存在问题是,路由中心集成了微信公众号与小程序模块功能,业务系统部署了多套服务. ...

  7. ROS机器人摄像头寻线

    ROS机器人摄像头寻线 连接小车 注意:必须在同一区域网 ssh clbrobort@clbrobort 激活树莓派主板 roslaunch clbrobot bringup.launch 开启摄像头 ...

  8. liquibase初始化sql

    1.使用liquibase 集成依赖 <liquibase.version>4.1.1</liquibase.version> <dependency> <g ...

  9. Java Lambda Stream

    ::方法使用 条件:lambada表达式的主体仅包含一个表达式,且lambada表达式只调用一个已经存在的方法:被引用的方法的参数列表与lambada表达式的输入输出一致 以下是Java 8中方法引用 ...

  10. 关于java中的super

    首当其冲先说一下super的用途和含义.他是用于调用一些被重写的方法. 这里还可以复习一下子这个重写:重写是把新的方法放在被重写的方法前面.在被重写的子类中,优先调用重写后的方法.但是如果想要调用原本 ...