限流这件事,从来不是"每秒放几个"这么简单

Posted on 一 06 7月 2026 in Tech

Abstract 限流这件事,从来不是"每秒放几个"这么简单
Authors Walter Fan
Category Tech
Version v1.0
Updated 2026-07-06
License CC-BY-NC-ND 4.0

限流这件事,从来不是"每秒放几个"这么简单

先讲个我亲历的故障。有年做一个对外的开放接口,压测跑得漂漂亮亮,上线也太太平平。直到某天,一个合作方的脚本写出了 bug——重试逻辑没退避,一秒钟往我们这儿砸了几万次请求。结果不是这一个接口挂,是整个服务挂了:它把数据库连接池占满,把线程占满,连带那些跟它八竿子打不着的接口一起 502。

复盘会上有人问:"咱们不是加了限流吗?"加了。可我们加的是"整个服务每秒 5000 QPS"这么一个数字。这个合作方一个人就把 5000 的额度吃光了,剩下几百个正常客户全被挡在门外。限流是生效了,但它保护错了对象。

这件事让我明白一个道理:限流从来不是配一个"每秒放几个"的数字这么简单。 它是分层的、分维度的、分策略的——你得想清楚"在哪一层限、按谁来限、用什么算法、拒了之后怎么办"。这篇就把这几件事一件件拆开讲,最后给你一张能照着抄的决策清单。

先约定几个词,怕有读者不熟: - 限流(Rate Limiting):主动控制"单位时间内放多少请求进来",超了就拒绝或排队。像地铁早高峰的限流栏杆。 - QPS:每秒请求数(Queries Per Second),衡量流量大小的常用单位。 - 熔断(Circuit Breaker):下游坏了就快速失败、不再打无用的调用。跟限流是两回事,后面会区分。


一、先分清:限流、熔断、舱壁不是一回事

很多人把这几个概念搅在一起,配置的时候就容易张冠李戴。用一句大白话各自定位一下:

机制 管什么 一句话比喻
限流 进来多少 地铁站口的栏杆,人太多就先拦住
熔断 下游坏了怎么办 保险丝,电路短路了先跳闸,别把整栋楼烧了
舱壁 故障别扩散 船舱的隔水墙,一个舱进水不至于沉船

这篇只聊限流。但你得记住:限流只解决"流量太大",它救不了"下游已经坏了"(那是熔断的活儿),也防不住"一个慢接口拖垮全部资源"(那是舱壁的活儿)。开头那个故障,其实是限流维度错了 + 没做舱壁隔离两个问题叠加。指望一个限流数字包治百病,本身就是误区。


二、第一个维度:在哪一层限流

限流不是只在一个地方做的。一个请求从进门到落地,会穿过好几层,每一层限的东西不一样、目的也不一样。

我习惯把它想成机场安检:大门口先粗筛一遍(防止人群踩踏),到了具体登机口再细查(保护这趟航班)。限流也是这么分层的:

层级 在哪做 限什么 目的
接入层 / 网关 Nginx、API Gateway 粗粒度:总 QPS、单 IP、防 DDoS 挡住洪峰和恶意流量,别让脏流量进内网
应用 / 服务层 业务代码、框架 细粒度:单接口、单用户、单 API-Key 保护具体的慢接口和业务逻辑
资源层 DB、缓存、第三方 API 保护共享资源的全局配额 别把大家共用的数据库/第三方额度打爆

关键点:这几层要配合,不能只做一层。

  • 只在网关限总量 → 就是我开头踩的坑:一个客户吃光额度,内部一点没防。
  • 只在应用层限 → 洪峰流量已经进到内网、建了连接、耗了资源才被拒,晚了。
  • 忘了资源层 → 好几个服务共用一个数据库,各自限得好好的,加一块还是把 DB 打趴。

分层的思路,说到底是纵深防御:外层粗筛防洪水,内层细筛保重点,最里层兜底护命根子(共享资源)。

接入层怎么落地:Nginx 和 K8s Ingress

接入层的限流,绝大多数人是靠 Nginx 或 K8s Ingress 实现的,几行配置就能挡住洪峰。这部分给点能直接抄的例子(不写运维的读者可以跳过,不影响结论)。

原生 Nginxlimit_req(限速率)和 limit_conn(限并发连接)两个模块:

http {
    # 按客户端 IP 限,每秒 10 个请求;也可以按请求头 $http_x_api_key 限
    limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;

    server {
        location /api/ {
            # burst=20 是突发缓冲(相当于令牌桶的"桶容量")
            # nodelay 让突发请求立即处理,不强制匀速排队
            limit_req zone=perip burst=20 nodelay;
            limit_req_status 429;          # 限流返回 429,别用默认的 503
            proxy_pass http://backend;
        }
    }
}

有个细节值得记:Nginx 底层是漏桶,只有加上 burst + nodelay 才接近令牌桶那种"允许合理突发"的效果。只写 burst 不写 nodelay,超出的请求会被强制匀速放行,客户端会莫名其妙觉得"怎么突然变慢了"。

K8s ingress-nginx 则是把上面这套配置包装成了 annotation:

metadata:
  annotations:
    nginx.ingress.kubernetes.io/limit-rps: "10"              # 每秒请求数(按 IP)
    nginx.ingress.kubernetes.io/limit-connections: "5"       # 单 IP 并发连接数
    nginx.ingress.kubernetes.io/limit-burst-multiplier: "3"  # 突发倍数
    nginx.ingress.kubernetes.io/limit-whitelist: "10.0.0.0/8" # 白名单不限流

但这里藏着一个正好呼应后面"单机 vs 分布式"那节的大坑:ingress-nginx 默认是每个 Controller 副本各自计数的。你配了每秒 10 个,部署了 3 个副本,实际限额约等于 10 × 3 = 30。想精确全局限流,要么接共享存储,要么干脆别指望 Ingress——把精确配额下沉到服务层用 Redis 做。其他 Controller 里,APISIX、Kong 的插件支持接 Redis 做真正的分布式限流,Envoy/Istio 的 global rate limit 做得最正规。

一句话选型: 挡洪峰、防刷、粗粒度按 IP 限,用 Nginx / Ingress annotation 简单够用;要精确的全局配额、按业务逻辑限,别指望接入层,下沉到服务层 + Redis。


三、第二个维度:按谁来限流

这是开头那个故障最核心的教训。"每秒放几个"必须先回答"给谁放"。 同样一个 5000 QPS,你是所有人共享这 5000,还是每个客户各有配额,结果天差地别。

常见的限流维度(可以叠加使用):

  • 全局:整个服务/接口的总量。防止系统整体过载。
  • 单接口:某个特定接口单独限。专治那种又慢又重的接口,别让它拖累别人。
  • 单用户 / 单租户:每个用户或每个企业客户各有额度。保证公平——不让一个人吃垮所有人。
  • 单 IP:按来源 IP 限。防爬虫、防刷,但注意 NAT/代理后面可能一堆人共用一个 IP,容易误伤。
  • 单 API-Key / 单应用:对外开放平台的标配,按调用方的密钥分配配额,还能分等级(免费版 vs 付费版)。

回到我那个故障:正确的做法应该是全局限总量 + 按 API-Key 限单个客户双层叠加。总量保护系统不过载,单 Key 配额保证任何一个客户都吃不垮别人的份额。这两个维度缺一个,都会出事。

一句话:限流先分蛋糕,再定速度。 不先想清楚"额度按谁分",配多大的数字都是给某个异常客户开的绿灯。


四、第三个维度:用什么算法

到这一步才轮到"每秒放几个"的具体实现。常见四种算法,各有脾气:

算法 原理 优点 缺点
固定窗口 每个时间窗内计数,超了就拒 实现最简单 窗口交界处有"临界突刺",瞬间可达 2 倍
滑动窗口 窗口切小格,滑动统计 平滑,消除临界突刺 存储/计算开销略大
漏桶 请求入桶,恒定速率流出 输出绝对匀速 应对不了合理的突发
令牌桶 按速率放令牌,请求取令牌 允许突发又能限均值 参数要调

什么叫"临界突刺"? 举个例子,固定窗口限"每分钟 100 次"。如果一个客户在第 59 秒打了 100 次,又在下一分钟的第 1 秒打了 100 次——两次都没超窗口限制,但实际上 2 秒内来了 200 次。滑动窗口就是为了解决这个漏洞。

默认选令牌桶。 真实业务流量不是匀速的,是一阵一阵的——用户白天多晚上少,活动一开瞬间冲高。令牌桶用"桶容量"容忍这种正常突发,又用"放令牌的速率"限住长期均值,最贴合真实形态。漏桶那种绝对匀速,反而只适合"我下游必须匀速处理"的场景(比如按固定速率写库、发消息)。


五、第四个维度:单机还是分布式

微服务基本都是多实例部署,这里有个特别容易翻车的坑

你配了"每秒 100 个",如果是单机限流,每个实例各限 100——部署了 10 个实例,实际总量就是 1000。更麻烦的是自动扩缩容:实例从 10 个缩到 5 个,总限额悄悄从 1000 变成 500,你配置文件一个字没改,行为却变了。

  • 单机限流:每个实例各自计数。简单、无外部依赖、快。适合"只想防单个实例被打爆"。
  • 分布式限流:所有实例共享一个计数器(通常放 Redis)。能精确控制全局速率。适合"要保护共享的下游资源"。

选型原则很简单: - 目的是防止单实例过载 → 单机限流够了,别自找麻烦。 - 目的是保护共享资源(数据库、第三方 API 的全局配额)→ 必须分布式,否则每个实例都以为自己没超,加一块就爆了。

但分布式限流有代价:每次请求都要访问 Redis,高频轻量接口反而可能被 Redis 拖成瓶颈。折中办法是本地令牌预取——实例一次性从 Redis 批发一批额度,在本地慢慢用完再来批发,减少网络往返。

还有个必须提前想清楚的问题:Redis 挂了怎么办? 两个选择—— - fail-open(放行):Redis 不可用时不限流,保可用性,但失去保护。 - fail-closed(拒绝):Redis 不可用时全拒,保护住了但服务等于挂了。

没有标准答案,取决于你更怕"被打爆"还是"误伤正常用户"。但你必须在出事之前就决定好,别等 Redis 半夜宕机时才在告警群里现场吵。


六、拒绝之后,别忘了好好告诉客户端

限流最容易被忽略的一环:拒了之后怎么回复。 很多人限流直接返回 500 或者干脆丢弃连接,这是在给自己埋雷。

客户端不知道自己被限流了,只当是服务出错,于是——疯狂重试。重试的流量叠加到原本就过载的系统上,形成"重试风暴",越限越崩。我见过好几次雪崩,根子就在这儿。

正确的姿势:

  • 返回 HTTP 429 Too Many Requests,明确告诉对方"是你太快了,不是我坏了"。
  • 带上 Retry-After 头,告诉客户端"过 X 秒再来"。
  • 带上限流额度信息头,让客户端能自我节流:
  • X-RateLimit-Limit:你的总额度
  • X-RateLimit-Remaining:还剩多少
  • X-RateLimit-Reset:额度什么时候重置
HTTP/1.1 429 Too Many Requests
Retry-After: 5
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1720248900

补一句:X-RateLimit-* 是业界用了多年的事实约定,各家 API(GitHub、Twitter 等)大多长这样。IETF 其实还在推一份去掉 X- 前缀的标准草案(RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset,见 draft-ietf-httpapi-ratelimit-headers),截至我写这篇时还没正式成为 RFC。现阶段用带 X- 的老写法最稳妥,心里知道有个新标准在路上就行。

配合客户端的指数退避重试(每次重试间隔翻倍,加点随机抖动),才能真正把重试风暴摁下去。限流和客户端行为是一对搭档,只管拒绝不管善后,等于把问题从服务端甩给了全网。


七、Java 生态怎么落地

前面讲的是"设计怎么想",这一节讲"代码怎么写"。不写 Java 的读者可以跳过,不影响前面的结论。Java 里成熟方案不少,按场景对号入座:

1. Resilience4j —— Spring Boot 项目默认首选。 轻量、函数式,限流/熔断/舱壁/重试一整套都有。局限是默认单机限流。

RateLimiterConfig config = RateLimiterConfig.custom()
    .limitForPeriod(100)                          // 每个周期允许 100 次
    .limitRefreshPeriod(Duration.ofSeconds(1))    // 周期 1 秒
    .timeoutDuration(Duration.ofMillis(500))      // 拿不到许可最多等 500ms
    .build();

@RateLimiter(name = "backendService", fallbackMethod = "fallback")
public String callBackend() {
    return backendClient.doSomething();
}

public String fallback(RequestNotPermitted ex) {
    return "系统繁忙,请稍后再试";  // 这里应返回 429
}

2. Guava RateLimiter —— 最简单的单机方案。 令牌桶实现,一行搞定。适合单机、简单场景,进程重启即重置。

RateLimiter limiter = RateLimiter.create(100.0); // 每秒 100 个令牌
if (limiter.tryAcquire()) {
    handleRequest();
} else {
    reject429();
}

3. Bucket4j —— 分布式限流首选。 令牌桶,配合 bucket4j-redis 能让多实例共享同一个桶,这是 Java 里做精确分布式限流最成熟的选择。

4. Sentinel —— 阿里开源,功能最全。 限流/熔断/热点参数限流/系统自适应保护,带控制台能动态调规则。它的"热点参数限流"很实用——比如针对某个爆款商品 ID 单独限,而不是一刀切。适合大规模、需要运营态动态调整的场景,代价是概念比 Resilience4j 重。

5. Redis + Lua 自研 —— 最灵活。 用 Lua 脚本保证计数的原子性,规则完全自己掌控。适合有特殊需求、已有 Redis 基础设施的团队。上面那些库底层思路其实都类似。

选型速查:

需求 选它
Spring Boot,要限流+熔断+舱壁一套 Resilience4j
单机、简单、快速接入 Guava RateLimiter
精确的分布式限流 Bucket4j + Redis
大规模、要动态调规则、热点限流 Sentinel
有特殊规则、要完全掌控 Redis + Lua 自研

八、一张能照着抄的限流决策清单

把前面几节浓缩成一份 checklist,下次设计限流时按这个顺序问自己:

先想清楚(设计) - [ ] 在哪几层限?网关粗筛 + 应用细筛 + 资源兜底,至少想两层。 - [ ] 按限?全局 / 单接口 / 单用户 / 单 IP / 单 Key——通常要叠加,尤其别忘了"单客户配额"。 - [ ] 用什么算法?默认令牌桶;要绝对匀速才用漏桶;固定窗口小心临界突刺。 - [ ] 单机还是分布式?保护共享资源才上分布式,否则单机就够。 - [ ] 阈值来自压测数据,不是拍脑袋。

代码里落实(实现) - [ ] 拒绝返回 429,不是 500。 - [ ] 带 Retry-AfterX-RateLimit-* 头。 - [ ] 分布式限流的 Redis 调用有超时,并想好 fail-open 还是 fail-closed。 - [ ] 高频接口考虑本地令牌预取,别每次都查 Redis。

上线之后(运营) - [ ] 阈值可动态配置,别硬编码到代码里。 - [ ] 限流事件埋点告警——频繁触发往往是容量不足或被攻击的信号。 - [ ] 给重要客户/接口留白名单或独立配额。 - [ ] 配合客户端指数退避,防重试风暴。

一句话:

限流不是一个数字,是一套分层、分维度、有善后的策略。 配数字之前,先回答"在哪层、给谁、拒了怎么办"这三个问题。


最后一句

回头看我开头那个故障,根子从来不是"限流的数字配小了",而是我把一个多维度的策略问题,简化成了一个一维的数字问题。分层、分维度、算法、单机/分布式、拒绝后的善后——这五件事里少想一件,限流就会在你最想不到的地方漏风。

好的限流,像一个训练有素的门卫:既拦得住洪水和坏人,又不会把老熟客挡在门外,还会礼貌地告诉被拦下的人"您稍等五分钟再来"。配好这么一个门卫,比配一个冷冰冰的数字,难得多,也值得多。

你的系统限流,是"一个数字",还是"一套策略"?

全文思维导图

@startmindmap
<style>
mindmapDiagram {
  node {
    BackgroundColor #F8F9FA
    RoundCorner 10
    Padding 10
    FontSize 13
  }
  :depth(0) {
    BackgroundColor #1E3A5F
    FontColor white
    FontSize 18
    FontStyle bold
  }
  :depth(1) {
    FontSize 15
    FontStyle bold
  }
  :depth(2) {
    FontSize 13
  }
}
</style>

* 微服务限流设计
** 先分清概念
*** 限流 管进来多少
*** 熔断 管下游坏了
*** 舱壁 管故障扩散
** 在哪一层限
*** 网关 粗筛防洪峰
**** Nginx limit_req/conn
**** Ingress annotation
**** 副本各自计数的坑
*** 应用 细筛保接口
*** 资源 护共享配额
** 按谁来限
*** 全局
*** 单接口
*** 单用户/租户
*** 单 IP
*** 单 API-Key
** 用什么算法
*** 固定窗口 有临界突刺
*** 滑动窗口 平滑
*** 漏桶 绝对匀速
*** 令牌桶 允许突发 默认选
** 单机 vs 分布式
*** 单机 防实例过载
*** 分布式 护共享资源
*** Redis 挂了 fail-open/closed
** 拒绝后善后
*** 返回 429
*** Retry-After 头
*** X-RateLimit 头
*** 防重试风暴
** Java 方案
*** Resilience4j 首选
*** Guava 单机
*** Bucket4j 分布式
*** Sentinel 全功能
*** Redis+Lua 自研
@endmindmap

微服务限流设计 - 思维导图


本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。 欢迎在我的个人网站 https://www.fanyamin.com 访问原文并评论。