从"限流数字"到可维护策略:ASP.NET Core 固定窗口限流的落地边界
给接口加限流时,最容易写下的是一个数字:每分钟多少次。但真正影响线上行为的,往往是"按谁计数""超出后怎么办"以及"哪些端点被策略覆盖"。ASP.NET Core 的限流中间件把这些选择拆成了服务注册、中间件启用与端点绑定几个明确步骤。下面以官方文档中的固定窗口限流器为线索,整理一个可直接讨论和调整的最小方案。
先把策略范围说清楚
固定窗口(fixed window)把时间划分为连续窗口;窗口结束后会开始新窗口,请求额度随之重置。它适合规则可以明确表达为"一个窗口内最多允许 N 次"的场景。
在确定 PermitLimit 和 Window 前,先回答两个问题:
- 限流键是什么:整个服务共享、按用户、按租户,还是按其他请求特征?
- 额度耗尽后是否排队:若允许排队,队列上限和处理顺序会直接影响请求等待与失败方式。
这些是业务决策,而不是框架替你推导出的默认结论。下文的数值只用于演示配置形状,不应直接当作生产阈值。
注册一个命名的固定窗口策略
官方示例通过 AddRateLimiter 注册服务,并以 AddFixedWindowLimiter 添加名为 fixed 的策略:
ini
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("fixed", opt =>
{
opt.PermitLimit = 4;
opt.Window = TimeSpan.FromSeconds(12);
opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
opt.QueueLimit = 2;
});
});
var app = builder.Build();
这里有几个容易混淆的点:
PermitLimit是一个窗口内可获得的许可数量;它不是并发数,也不是全局 QPS 的同义词。Window决定何时进入下一窗口并重置该窗口的额度。QueueLimit为 0 时,拿不到许可的请求不会进入队列;大于 0 时,还需要同时关注队列处理顺序。
命名策略不会因为注册就自动作用到所有端点。官方文档明确指出,命名策略需要显式应用到页面或端点;这使得团队可以先保护成本高或容易被突发请求击中的入口,而不是无意中给健康检查、内部回调等端点套上同一规则。
中间件与端点绑定缺一不可
注册策略后,还需要在请求管道中启用限流中间件:
javascript
app.UseRateLimiter();
app.MapGet("/api/resource", () => "This endpoint is rate limited")
.RequireRateLimiting("fixed");
app.Run();
UseRateLimiter 负责启用中间件;RequireRateLimiting("fixed") 将命名策略绑定到指定端点。若使用控制器路由,官方示例也展示了可在映射控制器时调用 RequireRateLimiting("fixed")。
把绑定写在端点或路由组附近有一个实际好处:评审 API 时,读者能直接看到该入口是否有保护、使用哪一套策略,而不必在全局配置里猜测覆盖范围。
不要只返回 429:给调用方可操作的信息
官方文档展示了通过 OnRejected 处理拒绝结果的方式:当限流租约包含 RetryAfter 元数据时,可以把它写入响应的 Retry-After 头,再返回 HTTP 429。
ini
options.OnRejected = async (context, cancellationToken) =>
{
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
{
context.HttpContext.Response.Headers.RetryAfter =
((int)retryAfter.TotalSeconds).ToString(
System.Globalization.NumberFormatInfo.InvariantInfo);
}
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
await context.HttpContext.Response.WriteAsync(
"Too many requests. Please try again later.", cancellationToken);
};
这段处理并不承诺所有策略都会提供 RetryAfter 元数据,因此代码先调用 TryGetMetadata。对 API 调用方而言,明确的 429 和可用的重试提示通常比连接被动等待更容易处理;对服务端而言,也便于把"被拒绝"与应用异常区分开。
发布前检查清单
- 确认限流维度:策略是全局的,还是要按用户、租户或其他键分区。
- 确认每个受保护端点:命名策略需显式绑定,避免以为注册后已全面生效。
- 确认队列语义:是否排队、排多少、按什么顺序处理,都应符合调用方的超时预期。
- 确认拒绝响应:统一记录 429,并在可取得
RetryAfter时向客户端返回该信息。 - 在目标版本运行集成测试:本文代码依据官方文档整理,未在具体业务项目中实测;还需结合认证、代理与客户端重试策略验证。
参考资料
Microsoft Learn:Rate limiting middleware in ASP.NET Core(本文代码结构与行为说明的主要依据)