
1. 开篇:卫星链路恢复后的"重试风暴"某次卫星地面站设备维护,链路中断 40 分钟。42 条船的船端同步客户端全部进入指数退避重试。链路恢复的那一分钟,灾难发生了:42 条船的客户端同时退避结束,向岸端/api/sync/push发起请求每个客户端的 Polly 策略是"重试 5 次 + 指数退避",失败后再退避——但所有客户端的退避计时器几乎同步岸端同步服务 Pod 连接池瞬间打满,健康检查超时,K8s 开始重启 Pod重启期间请求失败,触发客户端新一轮重试,风暴自我加强最终:同步接口 100% 503 持续 12 分钟,期间正常的备件查询业务也被拖垮事后复盘发现两件事:服务端没有任何限流——所有请求照单全收,直到资源耗尽429 响应缺失——客户端只认 503 会"疯重试",如果服务端返回 429 +Retry-After,客户端本来可以被"指挥"着错峰限流的本质不是"拒绝请求",而是在资源耗尽之前主动、可控地拒绝一部分请求,并告诉被拒绝的人什么时候再来。💬互动一下:你的服务端接口有速率限制吗?如果答案是"只在 Nginx 配了个 limit_req"或者"靠 K8s HPA 扛",这篇值得读完——HPA 扩容是分钟级的,重试风暴是秒级的。2. 概念辨析:限流、熔断、配额、背压四个词经常混用,职责完全不同:机制作用对象触发条件行为所在位置限流 Rate Limiting入站请求请求速率超过阈值拒绝超额请求(429)服务端入口熔断 Circuit Breaking出站调用下游错误率/超时超阈值主动停止调用下游客户端(Polly)配额 Quota租户/用户周期内调用总量超额拒绝,周期重置服务端(业务层)背压 Backpressure异步流水线下游处理速度跟不上降低上游生产速度进程内(Channels)关键关系:限流保护的是自己,熔断保护的是下游(也是保护自己不被慢调用拖死)限流是"每秒多少个"(速率),配额是"每天多少个"(总量)限流拒绝要快速、要带Retry-After;背压是让上游"慢下来"而不是"失败"3. .NET 8 内置限流四算法System.Threading.RateLimiting提供四个原语,全部内置在框架里(Microsoft.AspNetCore.RateLimiting):3.1 固定窗口 FixedWindowRateLimiter// 每 60 秒允许 100 个请求varoptions=newFixedWindowRateLimiterOptions{PermitLimit=100,Window=TimeSpan.FromSeconds(60),QueueProcessingOrder=QueueProcessingOrder.OldestFirst,QueueLimit=0,// 0 = 不排队,直接拒绝};原理:窗口内计数,窗口结束清零。致命缺陷——临界突发:第 59.9 秒来 100 个,第 60.1 秒又来 100 个,0.2 秒内实际通过 200 个。船端同步整点触发时容易踩。3.2 滑动窗口 SlidingWindowRateLimitervaroptions=newSlidingWindowRateLimiterOptions{PermitLimit=100,Window=TimeSpan.FromSeconds(60),SegmentsPerWindow=6,// 60秒切成6段,每段10秒QueueLimit=0,};原理:窗口切成 N 段,每 10 秒滑动一次,过期段的配额归还。临界突发问题大幅缓解(两段重叠时的配额是平滑过渡的)。代价:统计精度换内存,段数越多越平滑、开销越大。3.3 令牌桶 TokenBucketRateLimitervaroptions=newTokenBucketRateLimiterOptions{TokenLimit=200,// 桶容量:允许突发到200TokensPerPeriod=10,// 每 1 秒补 10 个令牌ReplenishmentPeriod=TimeSpan.FromSeconds(1),QueueLimit=0,AutoReplenishment=true,};原理:桶里以恒定速率补充令牌,请求拿令牌,拿不到就拒绝。桶满了允许短时突发(攒下的令牌),长期速率被补充速率锁死。最适合船端同步:链路恢复瞬间允许突发一批(攒下的令牌),但持续速率被锁死,不会形成风暴。3.4 并发限制 ConcurrencyLimitervaroptions=newConcurrencyLimiterOptions{PermitLimit=8,// 同时最多8个请求在处理QueueLimit=0,};原理:不限速率,限同时在飞的请求数。请求进来占一个坑,完成(成功/失败/异常都算)还坑。这是舱壁模式(Bulkhead)的官方实现——特别适合报表导出、批量同步这类"单个请求吃 1 个 CPU 核 30 秒"的重活。Polly 篇讲过舱壁思想,服务端这就是落地。3.5 四算法选型表场景算法理由普通查询 API滑动窗口平滑、无临界突发船端数据同步(突发+持续)令牌桶允许恢复瞬间突发,锁死长期速率报表导出/批量任务并发限制保护的是 CPU/内存,不是 QPS简单粗暴的内部接口固定窗口开销最小,临界问题可接受按天计调用次数配额(业务层)限流原语不负责长周期总量4. ASP.NET Core 集成4.1 注册与命名策略varbuilder=WebApplication.CreateBuilder(args);builder.Services.AddRateLimiter(options={// ===== 拒绝时的默认响应 =====options.RejectionStatusCode=StatusCodes.Status429TooManyRequests;// ===== 全局默认:滑动窗口,每IP每分钟120次 =====options.GlobalLimiter=PartitionedRateLimiter.CreateHttpContext,string(httpContext={varclientKey=httpContext.Connection.RemoteIpAddress?.ToString()??"unknown";returnRateLimitPartition.GetSlidingWindowLimiter(partitionKey:clientKey,factory:_=newSlidingWindowRateLimiterOptions{PermitLimit=120,Window=TimeSpan.FromMinutes(1),SegmentsPerWindow=6,QueueLimit=0});});// ===== 命名策略:同步接口令牌桶 =====options.AddPolicy("sync-push",httpContext={varshipId=httpContext.Request.Headers["X-Ship-Id"].FirstOrDefault()??"unknown";returnRateLimitPartition.GetTokenBucketLimiter(partitionKey:$"sync:{shipId}",factory:_=newTokenBucketRateLimiterOptions{TokenLimit=50,// 突发最多50个TokensPerPeriod=5,// 每秒补5个ReplenishmentPeriod=TimeSpan.FromSeconds(1),QueueLimit=0});});// ===== 命名策略:报表导出并发限制(全局共享,不分船) =====options.AddPolicy("heavy-report",httpContext=RateLimitPartition.GetConcurrencyLimiter(partitionKey:"report-export",factory:_=newConcurrencyLimiterOptions{PermitLimit=4,// 全系统同时只允许4个大报表QueueLimit=