在构建现代Web API时,限流(Rate Limiting)是一个无法回避的关键话题。想象一下,你经营着一家网红咖啡店,突然有一天社交媒体上有人推荐了你的店铺,结果第二天门口排起了几百人的长队。如果没有合理的排队机制和限流措施,你的咖啡机可能会过热,服务员会手忙脚乱,最终所有顾客的体验都会变得极差——这就是API没有限流的真实写照。
在.NET 7.0中,微软终于将限流功能作为一等公民引入了框架。我记得在之前的项目中,我们不得不依赖第三方库或者自己造轮子来实现限流,而现在我们可以直接使用框架提供的解决方案。这不仅减少了外部依赖,更重要的是保证了更好的性能和稳定性。
.NET 7.0的限流系统建立在几个关键抽象之上:
这些组件共同构成了一个灵活而强大的限流系统。与之前的版本相比,.NET 7.0的限流实现更加精细,支持多种算法和策略的组合。
.NET 7.0内置了四种常用的限流算法:
每种算法都有其适用场景,我们可以根据API的具体需求选择合适的策略。
让我们从一个最简单的例子开始,为Web API添加全局限流:
csharp复制var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRateLimiter(options =>
{
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
{
return RateLimitPartition.GetFixedWindowLimiter(
partitionKey: context.Request.Headers.Host.ToString(),
factory: partition => new FixedWindowRateLimiterOptions
{
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1),
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 10
});
});
});
var app = builder.Build();
app.UseRateLimiter();
这段代码配置了一个基于主机名的固定窗口限流器,每分钟最多允许100个请求,同时可以排队10个请求。
更常见的情况是,我们需要对不同端点实施不同的限流策略:
csharp复制builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("ApiPolicy", opt =>
{
opt.PermitLimit = 30;
opt.Window = TimeSpan.FromSeconds(10);
});
options.AddSlidingWindowLimiter("AuthPolicy", opt =>
{
opt.PermitLimit = 5;
opt.Window = TimeSpan.FromSeconds(15);
opt.SegmentsPerWindow = 3;
});
});
// 然后在控制器中使用
[HttpGet]
[EnableRateLimiting("ApiPolicy")]
public IActionResult GetProducts()
{
// ...
}
[HttpPost]
[EnableRateLimiting("AuthPolicy")]
public IActionResult Login()
{
// ...
}
这种细粒度的控制让我们可以为关键端点(如登录)设置更严格的限制,而对普通API端点则采用相对宽松的策略。
在实际业务中,我们经常需要对不同级别的客户端实施不同的限流策略:
csharp复制options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
{
var clientId = context.Request.Headers["X-Client-ID"].FirstOrDefault() ?? "anonymous";
return clientId switch
{
"premium" => RateLimitPartition.GetTokenBucketLimiter(clientId, _ => new()
{
TokenLimit = 1000,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 50,
ReplenishmentPeriod = TimeSpan.FromSeconds(1),
TokensPerPeriod = 100,
AutoReplenishment = true
}),
"standard" => RateLimitPartition.GetSlidingWindowLimiter(clientId, _ => new()
{
PermitLimit = 500,
Window = TimeSpan.FromMinutes(1),
SegmentsPerWindow = 6
}),
_ => RateLimitPartition.GetFixedWindowLimiter(clientId, _ => new()
{
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1)
})
};
});
这种模式非常适合SaaS类应用,可以根据客户订阅级别提供不同的API调用配额。
当我们的API部署在多台服务器上时,内存中的限流器就无法满足需求了。虽然.NET 7.0没有直接提供分布式限流实现,但我们可以基于其扩展点构建自己的解决方案:
csharp复制public class DistributedRateLimiter : RateLimiter
{
private readonly IDistributedCache _cache;
private readonly string _prefix;
public DistributedRateLimiter(IDistributedCache cache, string prefix)
{
_cache = cache;
_prefix = prefix;
}
protected override RateLimitLease AttemptAcquireCore(int permitCount)
{
// 实现基于分布式缓存的限流逻辑
}
protected override ValueTask<RateLimitLease> AcquireAsyncCore(int permitCount, CancellationToken cancellationToken)
{
// 异步实现
}
// 其他必要方法实现
}
// 注册自定义限流器
builder.Services.AddRateLimiter(options =>
{
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
{
var cache = context.RequestServices.GetRequiredService<IDistributedCache>();
return new DistributedRateLimiter(cache, $"rate_limit:{context.Connection.RemoteIpAddress}");
});
});
提示:在实际生产环境中,可以考虑使用Redis等高性能分布式缓存来实现这一模式,并注意处理时钟同步和性能问题。
了解限流器的实际工作情况至关重要。我们可以通过以下方式收集限流指标:
csharp复制app.Use(async (context, next) =>
{
var limiter = context.RequestServices.GetRequiredService<RateLimiter>();
var metrics = limiter.GetMetrics();
// 记录或上报指标
LogMetrics(metrics);
await next(context);
});
结合Application Insights或Prometheus等监控工具,我们可以构建完整的限流监控仪表盘。
硬编码的限流参数往往无法适应变化的业务需求。我们可以实现动态配置:
csharp复制builder.Services.AddOptions<RateLimiterOptions>()
.BindConfiguration("RateLimiting")
.ValidateDataAnnotations();
builder.Services.AddRateLimiter(options =>
{
var config = builder.Configuration.GetSection("RateLimiting").Get<RateLimiterOptions>();
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
{
return RateLimitPartition.GetFixedWindowLimiter(
context.Connection.RemoteIpAddress?.ToString(),
_ => new FixedWindowRateLimiterOptions
{
PermitLimit = config.PermitLimit,
Window = config.Window,
QueueLimit = config.QueueLimit
});
});
});
这样,我们就可以在不重启应用的情况下通过配置中心调整限流参数。
限流器本身也会消耗资源,特别是在高并发场景下。以下是一些性能优化建议:
问题1:限流似乎没有生效
AddRateLimiter和UseRateLimiterDisableRateLimiting特性问题2:限流器导致性能下降
问题3:客户端收到429但日志中没有限流记录
良好的API设计应该帮助客户端正确处理限流情况。我们可以自定义限流响应:
csharp复制builder.Services.AddRateLimiter(options =>
{
options.OnRejected = (context, cancellationToken) =>
{
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
context.HttpContext.Response.WriteAsJsonAsync(new
{
Error = "Too many requests. Please try again later.",
RetryAfter = context.Lease.TryGetMetadata("RetryAfter", out var after)
? after.ToString()
: "unknown"
}, cancellationToken: cancellationToken);
return new ValueTask();
};
});
这样客户端不仅能知道被限流了,还能获取建议的重试时间。
限流通常需要与认证系统配合工作。例如,我们可以对已认证用户和匿名用户实施不同策略:
csharp复制options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
{
var user = context.User;
var isAuthenticated = user.Identity?.IsAuthenticated ?? false;
var partitionKey = isAuthenticated
? $"user:{user.FindFirstValue(ClaimTypes.NameIdentifier)}"
: $"ip:{context.Connection.RemoteIpAddress}";
return RateLimitPartition.GetFixedWindowLimiter(partitionKey, _ => new()
{
PermitLimit = isAuthenticated ? 200 : 50,
Window = TimeSpan.FromMinutes(1)
});
});
限流器的状态可以反映在健康检查中:
csharp复制builder.Services.AddHealthChecks()
.AddCheck<RateLimiterHealthCheck>("ratelimiter");
public class RateLimiterHealthCheck : IHealthCheck
{
private readonly RateLimiter _limiter;
public RateLimiterHealthCheck(RateLimiter limiter)
{
_limiter = limiter;
}
public Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
var metrics = _limiter.GetMetrics();
var status = metrics.AvailablePermits > 0
? HealthStatus.Healthy
: HealthStatus.Unhealthy;
return Task.FromResult(new HealthCheckResult(
status,
data: new Dictionary<string, object>
{
["AvailablePermits"] = metrics.AvailablePermits,
["TotalPermits"] = metrics.TotalPermits
}));
}
}
这样运维团队可以监控限流器的健康状态,及时发现潜在问题。
在实现API限流时,我最大的体会是:没有放之四海而皆准的完美策略。最适合的限流方案取决于你的具体业务场景、流量模式和用户体验需求。从简单的固定窗口开始,随着业务增长逐步引入更复杂的策略,这种渐进式的做法往往能取得最好的效果。