在构建现代Web API服务时,限流(Rate Limiting)是一个无法回避的基础设施级需求。想象一下你经营着一家网红咖啡店,突然有1000个顾客同时涌入——如果没有排队机制,你的咖啡机可能会直接崩溃。API服务也是如此,当突发流量超过系统承载能力时,轻则响应变慢,重则服务完全不可用。
我在去年参与的一个电商项目中就遇到过惨痛教训:促销活动期间由于缺少限流措施,支付接口被脚本刷单导致正常用户无法完成交易。事后分析日志发现,某些IP地址的请求频率高达每秒300次,而我们的API设计容量仅为每秒50次请求。这就是为什么.NET 7.0将限流作为框架级功能引入,让开发者能像拧水龙头一样精确控制流量。
在.NET生态中,实现限流主要有三种路径:
我们选择原生方案的核心原因在于:
app.UseRateLimiter()即可启用实际测试数据显示,原生方案在10000 RPS压力下,比第三方方案减少约15%的内存占用
.NET 7.0主要提供这些算法选项:
| 算法类型 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 固定窗口 | 简单粗暴的流量控制 | 实现简单,内存消耗低 | 窗口切换时可能产生毛刺 |
| 滑动窗口 | 需要平滑限制的场景 | 流量控制更精确 | 实现复杂度较高 |
| 令牌桶 | 允许突发流量的系统 | 处理突发更灵活 | 需要预热期 |
| 并发控制 | 保护资源密集型操作 | 防止资源耗尽 | 不限制总体请求量 |
我们的电商项目最终选择滑动窗口+令牌桶的混合模式:对支付接口用滑动窗口(精确控制),对商品查询用令牌桶(允许促销时的突发流量)。
首先安装必要的NuGet包:
bash复制dotnet add package Microsoft.AspNetCore.RateLimiting
然后在Program.cs中添加服务注册:
csharp复制// 定义名为"sliding"的限流策略
builder.Services.AddRateLimiter(options => {
options.AddSlidingWindowLimiter("sliding", opt => {
opt.Window = TimeSpan.FromSeconds(10);
opt.PermitLimit = 100;
opt.QueueLimit = 50;
opt.SegmentsPerWindow = 5;
opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
});
});
// 启用限流中间件
app.UseRateLimiter();
关键参数说明:
Window=10s:时间窗口长度为10秒PermitLimit=100:每个窗口周期允许100个请求QueueLimit=50:超额请求排队数量上限SegmentsPerWindow=5:将窗口分为5个子段(使滑动更平滑)可以对不同端点设置不同限制:
csharp复制[EnableRateLimiting("sliding")]
[ApiController]
[Route("api/[controller]")]
public class PaymentController : ControllerBase
{
[DisableRateLimiting] // 特别开放健康检查接口
[HttpGet("health")]
public IActionResult HealthCheck() => Ok();
[EnableRateLimiting("strict")] // 使用更严格的策略
[HttpPost("create")]
public async Task<IActionResult> CreateOrder([FromBody] OrderDto dto)
{
// 业务逻辑
}
}
当请求被限流时,默认返回503状态码。我们可以定制响应:
csharp复制options.OnRejected = (context, _) => {
context.HttpContext.Response.StatusCode = 429;
return new ValueTask<object?>(Results.Json(
new { Error = "请求过于频繁,请稍后再试" },
statusCode: 429
));
};
单机限流在集群环境下会失效,需要结合Redis实现分布式限流:
csharp复制builder.Services.AddStackExchangeRedisCache(opt => {
opt.Configuration = builder.Configuration.GetConnectionString("Redis");
});
builder.Services.AddRateLimiter(opt => {
opt.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
{
// 按客户端IP分组
return RedisRateLimiter.Create(
key: context.Request.Headers["X-Real-IP"],
factory: partition => RateLimitPartition.GetSlidingWindowLimiter(
partitionKey: partition,
factory: _ => new SlidingWindowRateLimiterOptions
{
Window = TimeSpan.FromMinutes(1),
PermitLimit = 1000
})
);
});
});
通过IOptionsMonitor实现热更新:
csharp复制// 定义可刷新的策略
builder.Services.Configure<SlidingWindowRateLimiterOptions>("dynamic", options => {
options.Window = TimeSpan.FromSeconds(30);
options.PermitLimit = 200; // 默认值
});
// 在后台服务中动态调整
app.Services.GetRequiredService<IOptionsMonitor<SlidingWindowRateLimiterOptions>>()
.OnChange((newOpts, _) => {
Console.WriteLine($"新限制值: {newOpts.PermitLimit}");
});
限流数据对系统健康度至关重要:
csharp复制// 配置Prometheus指标
options.MetricsCollector = (metricsContext) => {
var tags = new List<KeyValuePair<string, object?>> {
new("path", metricsContext.HttpContext.Request.Path),
new("method", metricsContext.HttpContext.Request.Method)
};
Meter meter = new("Microsoft.AspNetCore.RateLimiting");
Counter<long> counter = meter.CreateCounter<long>("rate_limit_events");
counter.Add(1, tags);
};
限流不生效
UseRateLimiter调用Redis连接超时
突发流量处理不佳
真实案例:某次大促前,我们通过压力测试发现当PermitLimit超过5000时,滑动窗口算法会产生明显性能开销。最终方案是调整为固定窗口+本地缓存计数,使吞吐量提升了30%。
使用BenchmarkDotNet对三种方案进行测试(100并发):
| 方案 | 平均延迟 | 吞吐量 (RPS) | 内存分配 |
|---|---|---|---|
| 无限流 | 12ms | 8500 | 2.1GB |
| AspNetCoreRateLimit | 28ms | 6200 | 3.5GB |
| .NET 7原生方案 | 19ms | 7900 | 2.4GB |
测试环境:AWS c5.2xlarge实例,.NET 7.0 Linux运行时
从数据可见,原生方案在性能损耗和功能完整性上取得了很好的平衡。特别是在高并发场景下,其内存管理优势更为明显。