API 网关 - 限流与去重
概述
API 网关是实施幂等性的第一道防线。通过在网关层进行限流和去重,可以:
- 保护后端服务:避免重复请求冲击
- 快速失败:在网关层直接拒绝重复请求
- 统一策略:集中管理幂等性规则
架构设计
客户端 → API Gateway → 后端服务
↓
┌─────────────┐
│ 限流模块 │ ← 控制请求频率
├─────────────┤
│ 去重模块 │ ← 检测重复请求
├─────────────┤
│ 认证授权 │ ← 验证身份
└─────────────┘YARP 网关实现
1. 安装 YARP
bash
dotnet add package Yarp.ReverseProxy2. 配置代理
json
// appsettings.json
{
"ReverseProxy": {
"Routes": {
"orders-route": {
"ClusterId": "orders-cluster",
"Match": {
"Path": "/api/orders/{**catch-all}"
},
"Transforms": [
{ "PathRemovePrefix": "/api" }
]
}
},
"Clusters": {
"orders-cluster": {
"Destinations": {
"destination1": {
"Address": "http://localhost:5001/"
}
}
}
}
}
}3. 限流中间件
csharp
using System.Collections.Concurrent;
public class RateLimitingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RateLimitingMiddleware> _logger;
// 每个用户的请求计数
private readonly ConcurrentDictionary<string, RequestCounter> _counters = new();
public RateLimitingMiddleware(RequestDelegate next, ILogger<RateLimitingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var userId = context.User.FindFirst("sub")?.Value
?? context.Connection.RemoteIpAddress?.ToString()
?? "anonymous";
var key = $"{userId}:{context.Request.Path}";
// 获取或创建计数器
var counter = _counters.GetOrAdd(key, _ => new RequestCounter());
// 检查限流
if (!counter.AllowRequest())
{
_logger.LogWarning("Rate limit exceeded for user {UserId} on {Path}",
userId, context.Request.Path);
context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
context.Response.Headers.Add("Retry-After", "60");
await context.Response.WriteAsJsonAsync(new
{
error = "Too many requests",
message = "Please slow down and try again later"
});
return;
}
await _next(context);
}
}
public class RequestCounter
{
private int _count = 0;
private DateTime _windowStart = DateTime.UtcNow;
private readonly object _lock = new();
private const int MaxRequests = 100; // 每个窗口最大请求数
private static readonly TimeSpan WindowSize = TimeSpan.FromMinutes(1);
public bool AllowRequest()
{
lock (_lock)
{
var now = DateTime.UtcNow;
// 重置窗口
if (now - _windowStart >= WindowSize)
{
_count = 0;
_windowStart = now;
}
// 检查是否超过限制
if (_count >= MaxRequests)
{
return false;
}
_count++;
return true;
}
}
}4. 请求去重中间件
csharp
public class RequestDeduplicationMiddleware
{
private readonly RequestDelegate _next;
private readonly IDistributedCache _cache;
private readonly ILogger<RequestDeduplicationMiddleware> _logger;
public RequestDeduplicationMiddleware(
RequestDelegate next,
IDistributedCache cache,
ILogger<RequestDeduplicationMiddleware> logger)
{
_next = next;
_cache = cache;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
// 仅处理 POST/PUT/PATCH 请求
if (!IsWriteMethod(context.Request.Method))
{
await _next(context);
return;
}
// 获取请求指纹
var requestFingerprint = await GenerateRequestFingerprint(context);
var cacheKey = $"dedup:{requestFingerprint}";
// 检查是否已处理过
var existingResponse = await _cache.GetStringAsync(cacheKey);
if (!string.IsNullOrEmpty(existingResponse))
{
_logger.LogInformation("Duplicate request detected: {Fingerprint}", requestFingerprint);
// 返回缓存的响应
context.Response.ContentType = "application/json";
context.Response.Headers.Add("X-Cache", "HIT");
await context.Response.WriteAsync(existingResponse);
return;
}
// 捕获响应
var originalBodyStream = context.Response.Body;
using var responseBody = new MemoryStream();
context.Response.Body = responseBody;
try
{
// 继续处理请求
await _next(context);
// 如果响应成功,缓存结果
if (context.Response.StatusCode >= 200 && context.Response.StatusCode < 300)
{
responseBody.Seek(0, SeekOrigin.Begin);
var responseContent = await new StreamReader(responseBody).ReadToEndAsync();
// 缓存 5 分钟
await _cache.SetStringAsync(
cacheKey,
responseContent,
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
});
responseBody.Seek(0, SeekOrigin.Begin);
}
// 写回响应
responseBody.Seek(0, SeekOrigin.Begin);
await responseBody.CopyToAsync(originalBodyStream);
}
finally
{
context.Response.Body = originalBodyStream;
}
}
private bool IsWriteMethod(string method)
{
return method.Equals(HttpMethods.Post, StringComparison.OrdinalIgnoreCase) ||
method.Equals(HttpMethods.Put, StringComparison.OrdinalIgnoreCase) ||
method.Equals(HttpMethods.Patch, StringComparison.OrdinalIgnoreCase);
}
private async Task<string> GenerateRequestFingerprint(HttpContext context)
{
// 读取请求体
context.Request.EnableBuffering();
var body = await new StreamReader(context.Request.Body).ReadToEndAsync();
context.Request.Body.Position = 0;
// 生成指纹:HTTP方法 + 路径 + 用户ID + 请求体哈希
var userId = context.User.FindFirst("sub")?.Value ?? "anonymous";
var data = $"{context.Request.Method}:{context.Request.Path}:{userId}:{body}";
using var sha256 = SHA256.Create();
var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(data));
return Convert.ToHexString(hash)[..32].ToLowerInvariant();
}
}5. 注册中间件
csharp
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// 添加 YARP
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
// 添加分布式缓存(用于去重)
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration["Redis:ConnectionString"];
});
var app = builder.Build();
// 注册中间件(顺序很重要)
app.UseAuthentication();
app.UseAuthorization();
app.UseMiddleware<RateLimitingMiddleware>(); // 限流
app.UseMiddleware<RequestDeduplicationMiddleware>(); // 去重
// YARP 代理
app.MapReverseProxy();
app.Run();Kong 网关实现
1. 安装限流插件
bash
# 启用限流插件
curl -X POST http://localhost:8001/plugins \
--data "name=rate-limiting" \
--data "config.second=10" \
--data "config.minute=100" \
--data "config.policy=redis" \
--data "config.redis_host=localhost" \
--data "config.redis_port=6379"2. 配置请求去重
lua
-- custom-deduplication.lua
local redis = require "resty.redis"
local red = redis:new()
red:set_timeout(1000)
red:connect("127.0.0.1", 6379)
local function generate_fingerprint()
local ngx = ngx
local method = ngx.req.get_method()
local uri = ngx.var.uri
local body = ngx.req.get_body_data() or ""
local str = require "resty.string"
local sha256 = require "resty.sha256"
local digest = sha256:new()
digest:update(method .. uri .. body)
local sha256_hex = str.to_hex(digest:final())
return sha256_hex
end
local fingerprint = generate_fingerprint()
local key = "dedup:" .. fingerprint
-- 检查是否已存在
local exists = red:get(key)
if exists then
ngx.status = 200
ngx.say(exists)
return ngx.exit(200)
end
-- 继续处理请求
ngx.req.read_body()
local body = ngx.req.get_body_data()
-- 调用后端服务
local http = require "resty.http"
local httpc = http:new()
local res = httpc:request_uri("http://backend:5000" .. ngx.var.uri, {
method = ngx.req.get_method(),
body = body,
headers = ngx.req.get_headers()
})
if res.status >= 200 and res.status < 300 then
-- 缓存响应 5 分钟
red:setex(key, 300, res.body)
end
ngx.status = res.status
for k, v in pairs(res.headers) do
ngx.header[k] = v
end
ngx.say(res.body)监控与告警
1. 指标收集
csharp
public class GatewayMetrics
{
private readonly Counter<long> _requestsTotal;
private readonly Counter<long> _rateLimitedRequests;
private readonly Counter<long> _duplicateRequests;
private readonly Histogram<double> _requestDuration;
public void RecordRequest(bool rateLimited, bool duplicate, double durationMs)
{
_requestsTotal.Add(1);
if (rateLimited)
{
_rateLimitedRequests.Add(1);
}
if (duplicate)
{
_duplicateRequests.Add(1);
}
_requestDuration.Record(durationMs);
}
}2. Prometheus 配置
yaml
scrape_configs:
- job_name: 'api-gateway'
static_configs:
- targets: ['gateway:5000']
metrics_path: '/metrics'最佳实践总结
✅ DO
- 分层防护:网关 + 应用层双重保障
- 合理限流:根据业务场景设置阈值
- 快速失败:在网关层拒绝无效请求
- 监控告警:及时发现异常流量
- 灰度发布:逐步调整限流策略
❌ DON'T
- 不要过度限流:影响正常用户体验
- 不要忽略白名单:为重要用户放行
- 不要忘记清理缓存:避免内存泄漏
- 不要硬编码配置:使用动态配置
总结
API 网关层的限流与去重是幂等性的第一道防线:
✅ YARP 实现:灵活、可定制
✅ Kong 实现:开箱即用、插件丰富
✅ 性能优化:减少后端压力
✅ 统一策略:集中管理规则
通过网关层的防护,可以有效保护后端服务,提升系统整体稳定性。