限流、熔断、降级常被放在一起讲,但它们是三个位置、依据、动作完全不同的职责。剥掉具体框架的实现细节,这三个模式在任何语言里都能落地,判断逻辑一致,坑也一致。
为了把这些概念落到具体场景,先给一个贯穿全文的例子:一个电商系统的商品详情页。用户登录后,从浏览器或 App 发起请求,先经过 Nginx,再经过 API 网关,最后到达商品服务。商品服务在渲染页面时需要调用下游的库存服务和价格服务。下面所有的讨论,都挂在这个链路上。
一、三者本质不同
| 限流 | 熔断 | 降级 | |
|---|---|---|---|
| 位置 | 入口(保护自己) | 调用下游处(保护自己不被拖死) | 拒绝/异常后的返回路径 |
| 依据 | 自己的容量(压测报告) | 对方的表现(RT、异常率) | 业务方确认的兜底方案 |
| 动作 | 拒绝多余请求 | 一段时间内短路,不再调用 | 返回纯本地兜底值 |
| 数据来源 | 压测 | 监控 | 业务决策 |
术语说明
- RT(Response Time,响应时间):一次调用从发出请求到收到响应所花的时间,通常用 P99、P95 等分位数衡量。熔断里的“慢调用比例”看的就是 RT 超过阈值的请求占比。
- 异常率:统计窗口内调用失败(抛异常、超时、返回错误码)的次数占总调用次数的比例。它和“异常数”(失败的绝对次数)一起,是熔断器判断下游是否崩溃的依据。
回到例子:限流发生在 Nginx 或网关入口,依据是商品服务自己能扛多少 QPS;熔断发生在商品服务调用库存服务的地方,依据是库存服务最近响应有多慢、错误有多少;降级是当请求被限流拒绝、或熔断打开后,商品详情页返回什么——比如库存显示“暂时无法获取”,价格显示缓存价。三个数据源凑不齐,高可用方案就是空中楼阁。
二、先厘清“网关”和 Nginx 的关系
Nginx 是反向代理和 Web 服务器,通常部署在最前面,做 TLS 终止、静态资源服务、以及入口级的粗粒度限流。它的 limit_req 按 IP 或 key 限制 QPS,limit_conn 限制并发连接数,性能极高。标准 Nginx 的限流不感知业务语义——它不知道“商品详情页”和“下单接口”的区别,只按配置的 key 数请求。OpenResty 通过 Lua 可以在 Nginx 进程内实现一定程度的业务感知,但那是另一层扩展,不是标准 Nginx 的能力。
API 网关(如 Spring Cloud Gateway、APISIX、Kong)在 Nginx 之后或与之集成,承担业务感知的流量治理:按路由、按接口、按用户身份限流,以及鉴权、路由转发、日志。网关能看到“这是 /product/detail 的请求,来源用户是 VIP”,标准 Nginx 看不到这些。生产环境中网关通常也是集群部署,前面由 Nginx 做负载均衡。
登录后的场景最能说明问题:用户拿到 token 后,每次请求页面数据都携带 token 到达网关。网关校验 token、解析出用户身份,再根据路由规则把请求转发到商品服务。如果没有网关,商品服务、库存服务、价格服务每个都要自己写一遍 token 解析和权限判断,重复且容易出错。
在实际链路里,两者是分层关系:Nginx 拦第一波洪峰和恶意流量,网关控业务级流量和路由。一个典型的链路是:用户请求 → Nginx 限流 → API 网关限流/鉴权/路由 → 商品服务 → 库存服务/价格服务。两层的阈值不同、职责不同,不能互相替代。
三、API 网关到底做什么:鉴权与路由
这是落地时最实际的问题。API 网关在登录后的场景里,核心就干两件事,而且顺序是先鉴权、后路由。
第一件:校验登录态,确认“你是谁”。 用户登录成功后拿到 JWT 或 session token,后续每次请求都带着它到网关。网关验签、检查过期时间、解析出用户 ID 和角色,判断这个请求有没有资格访问目标接口。没资格就直接在网关拒掉,请求根本到不了商品服务。把 token 校验这类横切逻辑收敛到网关,商品服务的代码里就不需要写任何鉴权代码。
第二件:根据路径和路由规则,决定“去哪”。 网关看到 /api/product/detail,匹配到路由规则“这条路径属于 product-service”,然后从注册中心(如 Nacos)拿到商品服务的健康实例列表,选一个转发过去。网关作为服务订阅者,从 Nacos 获取实例的 IP、端口和权重,缓存后用于动态路由和负载均衡;实例上下线时 Nacos 推送变更,网关更新缓存,路由自动生效。
这里需要区分“登录”和“登录态校验”。 网关不负责校验用户名密码这个登录动作本身。用户提交账号密码的登录请求到达网关后,网关把它路由到认证服务,认证服务完成密码校验、签发 token,再经网关返回给客户端。后续请求的 token 校验才是网关的职责。把“登录”和“登录态校验”分开,是因为登录涉及密码加密、token 签发、刷新、吊销等完整生命周期,属于认证服务的业务逻辑;而每次请求都要做的 token 校验是横切逻辑,放在网关最合适。
四、应用怎么接入网关
商品服务写好之后,怎么让网关知道它的存在,并把请求转给它?核心机制是服务发现。
商品服务启动时,把自己的 IP、端口、服务名注册到注册中心(如 Nacos)。网关作为注册中心的客户端,订阅商品服务的实例列表。当请求到达网关时,网关根据路由规则匹配到“这个请求要去商品服务”,从本地缓存的实例列表里选一个健康的实例,把请求转发过去。
以 Spring Cloud Gateway + Nacos 为例,应用侧的接入步骤只有两步:
第一步:商品服务加依赖、配注册中心地址。 在 pom.xml 中加入 spring-cloud-starter-alibaba-nacos-discovery,在 application.yaml 中配置 Nacos 地址和本服务名(如 product-service)。启动类加 @EnableDiscoveryClient。服务启动后自动注册到 Nacos,不需要在网关里手动写商品服务的 IP。
第二步:网关配路由规则,指向服务名而不是 IP。 网关的 application.yaml 中配置路由:
1 | spring: |
这里 lb://product-service 的意思是“从注册中心找名叫 product-service 的实例,做负载均衡转发”。StripPrefix=2 去掉路径的前 2 段——以 /api/product/detail 为例,去掉 /api 和 product,商品服务收到的是 /detail。实际配置时按路径段数校准,这是常见的配置错误点:多剥一段或少剥一段,服务收到的路径就不对了。
关键点:应用代码不需要知道网关的存在。商品服务的 Controller 里写的仍然是 /detail 这样的路径,网关负责把外部请求的 /api/product/detail 翻译成 /detail 再转发。接入的代价是加一个服务发现依赖、配一个注册中心地址,路由治理的事情全部在网关侧完成。
五、限流:入口按容量拒,用 QPS 或并发数保资源
先说明本节限流的范围:这里讲的限流指的是业务层限流,也就是 API 网关对 /api/product/detail 这类业务接口的限流,依据是网关和后端服务能承受的容量上限,而不是 Nginx 按 IP 做的粗粒度防刷。Nginx 那层只负责拦住明显的恶意流量,真正按接口、按用户的配额控制发生在网关。通用本质:用计数器、令牌桶或滑动窗口判断“当前是否还有余量”,没有就快速失败。
同一个限流器,可以选两种计数方式:
- QPS 模式:数单位时间内的请求个数。请求进来就 +1,时间窗口过了清零。它不关心每个请求执行了多久,适合挡突发流量、控制单位时间内的请求总量。
- 并发数模式:数“当前正在执行、还没返回的请求数”。请求进来 +1,返回时 -1。它不关心单位时间内来了多少,只关心同时有多少个在跑,适合保护资源,防止下游变慢时线程或协程被逐渐占满。
这两种方式可以只选一种,也可以同时配置:QPS 挡量,并发数保命,任一维度超限就拒绝。在链路分层上,网关层通常用 QPS 模式按接口/用户限流,服务内部则常用并发数模式(信号量)限制同时执行的数量,防止慢调用把线程池耗尽。这里的“网关层”和“服务内部”是两个不同的限流点,各自都可以选择 QPS 或并发数模式,不是把 QPS 和并发数分别绑定到入口和出口。
三种控制行为:
- 直接拒绝:最快,绝大多数接口用这个。判断超限立即返回错误,不占用后续资源。
- 预热:让冷启动从低阈值爬坡。刚发布、缓存没热时必须用,否则一上线就被打死。限流器从一个较低的阈值开始,随时间逐步放开到配置上限。
- 匀速排队:让请求在队列里等待,按固定速率放行。它会让请求线程等待,别在网关用——网关的线程被排队请求占住,整个入口的吞吐都会受影响。
这三种在任何限流库里都有对应物。
阈值算法跨语言通用:压测找 RT 突然翻倍的点(不是 QPS 上不去的点),乘 0.7 留余量给 GC、慢查询、邻居抢 CPU,再乘实例数得集群总量。
集群限流需要集中判定。10 台商品服务各限 500,热点实例先撞墙、旁边 9 台闲着。要精确就得有个 Token Server 统一发票。任何语言的分布式限流(Redis + Lua 令牌桶、etcd 租约)都在解决同一个问题:单机限流挡不住流量倾斜。
回到例子:Nginx 配置 limit_req 按 IP 限制每秒 20 个请求,挡住刷接口的脚本;网关层对 /api/product/detail 按用户 ID 做 QPS 限流,普通用户每秒 5 次、VIP 每秒 15 次,这个阈值由压测得出——压测商品服务和库存服务,找到 RT 开始翻倍的点,乘 0.7 留余量;商品服务内部用信号量限制同时处理的详情请求不超过 200 个,这是并发数模式,库存服务变慢时商品服务的线程不会被无限占用。三层各管一个维度,不重复。
六、熔断:主力策略是慢调用比例,不是异常比例
先说明本节熔断的范围:这里讲的熔断发生在商品服务调用下游库存服务、价格服务的地方,依据是下游最近的表现(RT、异常率),而不是网关入口的流量控制。熔断保护的是调用方自己——下游变慢或出错时,调用方不再把线程耗在等待上。
通用本质:一个状态机,根据下游表现决定是否短路。任何熔断器都是这套三态语义:
- CLOSED(关闭):正常放行,所有调用都发给下游,同时统计 RT 和失败次数。下游表现正常时一直停在这个状态。
- OPEN(打开):短路状态,不再调用下游,直接走降级逻辑。进入 OPEN 时开始计时,到熔断时长后转入 HALF_OPEN。
- HALF_OPEN(半开):探路状态,放少量探测请求去调下游,根据结果决定回 CLOSED 还是 OPEN。
HALF_OPEN 的探测数量和恢复条件,不同框架行为不同,不能一概而论:
- Sentinel 默认放 1 个请求探路。慢调用比例模式下,这个请求的 RT 小于阈值就结束熔断,大于则继续熔断;异常比例模式下,请求没有错误就恢复。
- Resilience4j 的
permittedNumberOfCallsInHalfOpenState默认是 10,且可配置。需要这些探测请求的统计结果满足条件(如失败率低于阈值)才切换回 CLOSED。
生产上建议探测数设为 3~10 个,只放 1 个容易因单个偶发失败误判,把已经恢复的下游继续熔断。理解这个差异后,再看“三态语义跨语言通用”才准确:状态流转的骨架相同,半开阶段的判定细节因框架而异。
线上最常见的故障形态是“慢而不死”:下游没挂,只是每个请求从 80ms 变成 4s。异常数 0,异常比例 0,熔断器全程 CLOSED,线程一个个被占死。所以熔断器通常提供三种统计策略,各自防的问题不同:
- 慢调用比例:RT 超过阈值算慢调用,慢调用占比超阈值就打开熔断。专治“慢而不死”,是线上主力策略。
- 异常比例:失败调用占比超阈值就打开熔断。防的是下游持续报错、崩溃。
- 异常数:失败调用绝对次数超阈值就打开熔断。适合低流量接口,用绝对次数代替比例判断。
异常比例和异常数只防下游崩溃,没防下游变慢。只配异常比例,等于没装熔断——下游只是变慢不报错时,异常率是 0,熔断器全程 CLOSED,线程照样被占死。
四个参数怎么定:
- RT 阈值:取 P99 的 2~3 倍,或“用户开始重复点击”的时长。Resilience4j 的
slowCallDurationThreshold默认 60 秒,对多数在线接口来说形同虚设,必须按业务调低。 - 最小请求数:没有跨框架通用的正确默认值,必须按接口流量校准。Sentinel 的
minRequestAmount默认是 5,低频接口 QPS 2,不到 3 秒凑够 5 个样本,随便抖一下就误断。Resilience4j 的minimumNumberOfCalls默认是 100,同样 QPS 2 的接口需要 50 秒才能凑够样本,统计窗口内根本触发不了熔断。一种可操作的参考是:最小触发请求数不低于“统计窗口时长 × 接口平均 QPS”的 50%。 - 熔断时长:从进 OPEN 起算,到点转 HALF_OPEN 放探测请求。Sentinel 的
timeWindow默认 1 秒,生产上通常需要调大。 - 统计窗口:这里有一个容易被忽略的框架差异。Resilience4j 的
slidingWindowSize默认是 100,且slidingWindowType默认是 COUNT_BASED(按调用次数统计),不是时间窗口。要按时间统计必须显式配TIME_BASED。Sentinel 的statIntervalMs默认是 1000ms,是按时间窗口统计的。配 Resilience4j 时如果只改minimumNumberOfCalls不碰slidingWindowType,它仍然按调用次数算,和直觉不同。
回到例子:商品服务调用库存服务时配置熔断,RT 阈值设为 500ms(库存服务正常 RT 的 2~3 倍),Sentinel 统计窗口 60 秒,最小请求数 20,慢调用比例超过 50% 就打开断路器 30 秒。库存服务如果只是变慢(从 80ms 变成 2s)但没报错,异常比例是 0,只有慢调用比例能拦住它。熔断打开后,商品服务不再调用库存服务,直接走降级逻辑。
七、降级:独立的返回路径,不是熔断的附属品
先说明本节降级的范围:这里讲的降级指的是请求被拒绝或业务异常之后的返回路径——被限流拒了、被熔断拦了、或者业务代码抛异常了,系统给用户返回什么。它不是熔断的一部分,也不是限流的附属品,而是一条独立的、必须能无条件返回的本地路径。
通用本质:当请求被限流或熔断拒绝,或业务代码抛异常时,接住它并返回一个纯本地的兜底值。
“被拒绝”和“业务异常”是两条路径。被限流/熔断拒绝走 blockHandler,业务代码异常走 fallback。只写一个,等于漏掉一半场景。以商品详情页为例:Nginx 返回 429 时,前端展示“访问太频繁,请稍后再试”;网关限流拒绝时,返回结构化的 JSON 提示;商品服务调用库存服务被熔断时,走 fallback 返回“库存信息暂时不可用,可正常下单,以结算页为准”。三条路径的触发原因不同,返回给用户的话术也应该不同。
兜底三铁律,边界要说清楚:
- 不能有远程调用:包括远程数据库、远程缓存、HTTP 服务。远程意味着兜底本身可能失败,那就不是兜底了。
- 可以使用进程内本地缓存或本地只读副本:本地缓存命中就直接返回,这是合理的降级手段,不违反第一条。
- 内部必须用 try-catch 包死:即使前两条都遵守了,本地代码也可能出意外(空指针、反序列化失败)。兜底方法最外层必须包住一切异常,确保任何情况下都能返回一个值。
反例是 fallback 里调 remoteCache.get()——库存服务挂的时候远程缓存也可能跟着挂,用户还是 500,这个 fallback 白写了。降级代码必须是“无论如何都能返回”的代码路径。
降级标记是容易被忽略的细节。兜底返回值带上降级标记,调用方和日志系统能区分“正常返回”与“降级返回”。否则监控曲线一切正常,实际上所有商品详情页的库存都显示“暂时无法获取”,问题被掩盖了。
八、四个打架点,跨语言都会踩
① 阈值大于容量。配置里写着商品服务限流 5000 QPS,但压测发现 3000 就扛不住了。数字没人验证过,等于没有限流。
② 熔断没配降级。商品服务对库存服务的熔断打开了,但 fallback 没写,用户看到白屏或 500。熔断必须和 fallback 成对出现。
③ 重试喂饱熔断。商品服务调用库存服务超时,HTTP 客户端默认重试 2 次,一次失败变三次。库存服务已经吃力,重试把请求量翻了三倍,熔断打开得更快。先关重试,再开熔断。更隐蔽的一条:熔断打开后返回的拒绝事件本身不应该被重试,否则熔断器刚拦下来的请求又被重试塞回去,熔断白做了。如果确实需要重试,带上指数退避和 jitter,避免把刚恢复的下游再次打崩。
④ 多层限流叠加。Nginx 配了 5000 QPS,网关配了 3000,商品服务配了 1000。实际生效的是 1000,前面两层基本是摆设。要么把业务级限流统一到网关,Nginx 只做 IP 级防刷;要么分层定职责:Nginx 防刷(按 IP),网关保命(按接口/用户),服务兜底(按并发数),每一层管不同的维度,不重复配置同一维度。
九、规则必须离开进程内存
规则默认只活在进程内存里,后果是:重启即忘、新扩容机器拿不到、多实例不一致。商品服务扩容了 5 台新实例,熔断规则只配在老实例的内存里,新实例裸奔。
正确做法是规则外置到配置中心(Nacos、etcd、Consul、Apollo),客户端主动订阅,控制台只当只读看板,改规则走配置中心发布,留下变更记录和审批。以 Sentinel + Nacos 为例,Dashboard 修改规则后推送到 Nacos,应用监听 Nacos 变更实现热更新,多实例秒级同步,不需要逐台配置。
换成任何语言,对应物都是“配置中心 + 客户端 watch + 规则热更新”。重启一次三道闸全回出厂设置,是所有把规则放内存里的系统的通病。
十、一句话收束
- 三者不同:限流在入口按自己的容量拒;熔断在调用下游的地方,按下游最近的 RT 和异常率决定是否短路;降级是拒绝/异常后的纯本地返回。
- 限流两种计数:QPS 挡量,并发数保命,可单选可双配,网关和服务内部各自选。
- 入口两层:Nginx 按 IP 防刷,网关管鉴权和路由;登录请求转发给认证服务,token 校验才归网关。
- 接入靠服务发现:应用注册到 Nacos,网关按服务名负载均衡,
StripPrefix按实际路径段数校准。 - 熔断三态:CLOSED 放行、OPEN 短路、HALF_OPEN 探路;Sentinel 探 1 个请求,Resilience4j 默认 10 个,生产建议 3~10 个。主力是慢调用比例,只配异常比例等于没装。
- 两个默认值陷阱:Sentinel
minRequestAmount默认 5 偏小,Resilience4jminimumNumberOfCalls默认 100 偏高,且slidingWindowType默认按调用次数而非时间。 - 降级边界:兜底不能有远程调用,但可用本地缓存;内部 try-catch 包死,带降级标记。
- 两条铁律:熔断拒绝的事件不要重试;规则外置到配置中心,否则重启即回出厂设置。
理解“各查各的规则”这个责任链思想,比记住框架类名有用。换成 Go 的 middleware、Python 的装饰器、Node 的中间件,职责划分一模一样。
AI
https://juejin.cn/post/7706354997100888104
评论已关闭