参考资料:
一、为什么需要服务韧性
服务韧性是指系统在流量突增、资源紧张、依赖变慢或局部故障时,仍能维持核心功能,并在故障消失后自动恢复的能力。
一个服务的处理能力不是恒定不变的。即使入口 QPS 没有增加,也可能因为以下因素进入过载状态:
-
下游服务、数据库或中间件响应变慢;
-
CPU、内存、线程池、连接池等资源被其他任务抢占;
-
请求结构变化,导致单次请求的计算成本上升;
-
突发流量、批处理任务或大量连接同时建立;
-
重试缺乏约束,将一次故障放大为重试风暴。
过载通常会形成一条恶性循环:
依赖变慢或流量升高
↓
请求排队、并发增加
↓
线程池/连接池耗尽,RT 继续上升
↓
超时和重试增多
↓
系统负载进一步升高,最终发生级联故障
因此,韧性治理的目标并不是“让所有请求都成功”,而是:
-
只接收当前能够处理的请求,避免服务被拖垮;
-
优先保证核心流量,主动舍弃非核心流量;
-
将故障隔离在局部,防止沿调用链扩散;
-
在依赖或系统恢复后,安全、渐进地恢复流量。
二、核心概念辨析
| 机制 | 主要解决的问题 | 常见触发依据 | 典型动作 |
|---|---|---|---|
| 限流 | 进入系统或资源的流量过大 | QPS、并发数、系统负载 | 拒绝、排队、匀速通过 |
| 自适应限流 | 服务能力随环境动态变化,固定阈值失效 | 成功率、RT、CPU、Load、并发数 | 动态调整放行比例 |
| 熔断 | 下游持续变慢或失败,继续调用会拖垮自身 | 慢调用比例、异常比例、异常数 | 暂时停止调用下游 |
| 降级 | 完整功能无法稳定提供或成本过高 | 限流、熔断、超时、资源不足 | 返回兜底、缓存或简化结果 |
| 隔离 | 一个资源耗尽全部共享资源 | 线程池、信号量、连接池、资源配额 | 将故障限制在独立资源池内 |
这些机制并非互相替代。入口限流保护本服务,下游熔断保护调用方,资源隔离限制故障影响范围,降级则决定无法正常处理时返回什么结果。
三、固定限流及其局限
3.1 什么是限流
限流是为服务设置可接受的流量边界。例如,压测得到服务单机可稳定处理 2,000 TPS,便可以在入口设置阈值;超过阈值的请求将被拒绝、排队或降级。
固定阈值实现简单、行为可预测,适合以下场景:
-
服务能力相对稳定;
-
请求成本差异较小;
-
需要明确的租户配额或接口配额;
-
需要阻止异常调用方占用全部容量。
3.2 为什么仅靠固定 QPS 不够
QPS 只表示请求数量,不等于资源消耗。两个 QPS 相同的时间窗口,可能因为请求参数、缓存命中率、数据规模或下游 RT 不同,消耗完全不同。
假设服务的固定上限是 2,000 TPS:正常情况下单次调用耗时 20 ms,系统可以稳定运行;当下游耗时升高到 500 ms 时,即使入口只有 1,000 TPS,在途请求数也会显著增加,线程池和连接池仍可能被耗尽。此时固定阈值虽然没有被触发,却已经失去保护作用。
因此,容量判断应优先贴近真实瓶颈,综合关注:
-
CPU 使用率和系统 Load;
-
内存、GC 和对象分配速率;
-
平均 RT 以及 P95、P99 延迟;
-
活跃线程数、队列长度;
-
数据库或 RPC 连接池使用率;
-
请求成功率、慢调用比例;
-
单类请求的实际资源成本。
四、客户端自适应限流
4.1 为什么服务端限流还不够
服务端应该在过载时快速拒绝请求,但仅依赖服务端拒绝仍有两个问题。
第一,失败请求可能触发客户端重试。 如果每个调用方都立即、无限制地重试,原始流量会被成倍放大,形成重试风暴。
第二,拒绝本身也有成本。 对 Redis、Memcached 一类内存访问服务而言,业务处理可能很轻,网络 I/O、连接维护和协议解析反而占据主要开销。大量请求即使最终被拒绝,也可能先耗尽服务端资源。
因此,当客户端发现近期大量请求被后端以“超出配额”或“过载”拒绝时,应主动降低出站流量,使超出预算的请求直接在本地失败,不再进入网络。
4.2 Google SRE 自适应节流算法
每个客户端实例维护一个滑动时间窗口,例如最近两分钟,并统计:
-
requests:应用层尝试发起的请求数,包括客户端本地拒绝的请求; -
accepts:实际被后端接受的请求数; -
K:容忍系数,K > 1。
客户端拒绝新请求的概率为:
P(reject) = max(0, (requests - K × accepts) / (requests + 1))
这不是“拒绝请求的数量”,而是每个新请求被客户端本地拒绝的概率。
-
正常情况下,
requests ≈ accepts,计算结果为 0,不限流; -
当
requests > K × accepts时,拒绝概率开始大于 0; -
失败或本地拒绝越多,
requests与accepts的差距越大,限流越强; -
后端恢复接收请求后,滑动窗口中的
accepts上升,拒绝概率自动下降。
例如,最近窗口内 requests = 100、accepts = 40、K = 1.5:
P(reject) = (100 - 1.5 × 40) / 101 ≈ 39.6%
而当请求数为 3、成功接受数为 2、K = 1.5 时:
P(reject) = max(0, (3 - 1.5 × 2) / 4) = 0
这类小样本中的偶发失败不会立即触发限流。工程实现中还应设置最小样本量,进一步过滤启动阶段和低流量场景的随机抖动。
4.3 K 值如何影响限流
-
K越小,客户端越早开始拒绝请求,保护更激进; -
K越大,允许更多流量到达后端,恢复感知更快,但后端承担的无效请求更多; -
Google SRE 文中更倾向于
K = 2,而具体系统应通过压测和故障演练确定参数,不应机械照搬。
4.4 与重试策略配合
自适应限流不能代替重试治理。合理的重试策略应同时包含:
-
仅重试可恢复错误:参数错误、业务拒绝等确定性错误不应重试;
-
限制单请求重试次数:避免一个请求无限放大;
-
设置全局重试预算:例如将重试流量限制在总请求量的一定比例内;
-
指数退避并加入随机抖动:避免大量客户端在相同时间点同时重试;
-
只在紧邻失败依赖的上一层重试:多层同时重试会形成乘法放大;
-
传播“不应重试”的错误语义:让上游明确停止重试并进入降级逻辑;
-
保证幂等性:写操作重试必须使用幂等键或其他去重机制。
4.5 客户端限流的适用边界
客户端限流依赖本地观察数据。对于请求非常稀疏的客户端,样本量太小,难以及时、准确判断后端状态。此时可结合服务端返回的 Retry-After、集中配额、服务发现健康状态或更长统计窗口,但需要权衡额外的依赖和延迟。
五、入口系统自适应保护
客户端自适应限流主要保护下游,入口系统自适应保护则用于防止当前服务自身被压垮。
5.1 为什么要从系统整体进行保护
一个服务可能同时承接 Web、Dubbo、消息消费和内部任务等多种流量。这些流量最终共享 CPU、内存、线程池和连接池,只对单个接口限流,无法保证应用整体稳定。
因此,系统保护规则应作用于应用级入口流量,而不是只作用于某一个接口或资源。接口级限流仍然有价值,但它解决的是局部配额和流量公平问题,两者需要配合使用。
5.2 Sentinel 支持的系统指标
Sentinel 系统自适应保护综合应用的整体指标,使入口流量与系统负载保持平衡。常见阈值类型包括:
-
Load:仅对 Linux/Unix-like 系统生效。当
load1超过阈值,并且当前并发线程数超过估算容量时触发保护。阈值可从CPU 核数 × 2.5起步,再通过压测校准; -
CPU usage:CPU 使用率达到阈值时触发,取值范围为
0.0~1.0; -
平均 RT:单机所有入口流量的平均响应时间达到阈值时触发;
-
并发线程数:单机入口请求的并发线程数达到阈值时触发;
-
入口 QPS:单机所有入口流量的 QPS 达到阈值时触发。
不建议孤立地依赖单一指标。例如 CPU 很低并不代表系统健康,I/O 阻塞或下游变慢时,线程和连接可能已经大量堆积。
5.3 BBR 思想与系统容量估算
Sentinel 借鉴 TCP BBR 的思想,将请求处理过程类比为水管:
-
QPS 表示单位时间进入或流出水管的水量;
-
RT 表示请求在水管中停留的时间;
-
并发数表示水管中正在处理和排队的请求数。
根据 Little’s Law(利特尔法则),稳定状态下:
并发数 ≈ QPS × 平均 RT(秒)
Sentinel 使用历史观测到的 maxQps × minRt 估算系统容量:
系统容量 ≈ maxQps × minRt
其中 minRt 应换算为秒后再参与计算。minRt 近似表示请求没有明显排队时的处理时延,maxQps 表示系统曾达到的最大处理速率。当 load1 已超过启发阈值,且当前并发数又超过估算容量时,系统开始限制入口流量。
这样做比“Load 一高就全部拒绝”更合理:Load 只作为启动保护的信号,实际容量由系统已观测到的吞吐和时延共同决定,从而尽量在稳定的前提下维持吞吐量。
5.4 系统规则触发后如何处理
触发系统保护后,不应让请求长时间阻塞等待,因为排队会继续占用线程、连接和内存。更合理的处理方式是:
-
快速拒绝超出容量的请求;
-
返回明确、可识别的过载状态,避免客户端盲目重试;
-
对可降级请求返回缓存、默认值或简化结果;
-
优先保留登录、支付、存档等核心链路;
-
记录触发规则、系统指标、调用来源和降级结果,便于告警与复盘。
5.5 系统自适应保护的限制
-
Load 是滞后指标,不能单独反映瞬时容量;
-
其他进程造成的高 Load,不一定能通过限制本应用入口流量解决;
-
平均 RT 容易掩盖长尾延迟,应同时监控 P95/P99;
-
容器环境中必须确认 CPU、内存指标来自正确的 cgroup 配额;
-
启动、预热和版本发布阶段的历史指标不稳定,需要设置冷启动保护;
-
自适应策略仍需静态上限兜底,防止算法或观测异常导致流量完全失控。
六、下游自适应熔断与降级
6.1 为什么要熔断下游
当下游服务响应变慢时,上游请求会持续占用线程、连接和内存。若上游仍不断发起调用和重试,下游的局部故障会逐层向上传播,最终形成服务雪崩。
熔断器的作用类似电路保险丝:当某个依赖在统计窗口内表现持续异常时,暂时切断调用,使请求快速失败或直接进入降级逻辑,为下游恢复和上游释放资源争取时间。
熔断通常配置在调用端,并针对具体依赖或资源生效,而不是针对应用所有入口统一生效。
6.2 熔断器的三个状态
达到熔断阈值
Closed(关闭) ───────────→ Open(打开)
↑ │
│ 探测成功 │ 熔断时间结束
│ ↓
└──────────────── Half-Open(半开)
│
└─ 探测失败 → Open
-
Closed:正常放行并统计请求结果;
-
Open:在熔断时间内不再调用下游,直接快速失败或降级;
-
Half-Open:熔断时间结束后放过少量探测请求;成功则恢复,失败则再次熔断。
半开状态必须限制探测流量,否则大量实例可能同时探测,形成恢复瞬间的流量冲击。可以加入随机抖动、限制探测并发,并采用渐进式恢复。
6.3 常见熔断策略
Sentinel 支持三类主要策略:
-
慢调用比例:请求 RT 超过“最大允许 RT”即视为慢调用;统计窗口内请求达到最小样本量,且慢调用比例超过阈值时熔断;
-
异常比例:统计窗口内请求达到最小样本量,且异常比例超过阈值时熔断;
-
异常数:统计窗口内异常数量超过阈值时熔断。
建议优先使用“慢调用比例”或“异常比例”,并配置最小请求数。只使用异常数,在流量规模变化较大时容易过于敏感或过于迟钝。
6.4 熔断参数设计
关键参数包括:
-
统计窗口:太短容易受抖动影响,太长则反应迟缓;
-
最小请求数:避免 3 次请求失败 1 次就因 33% 错误率而误熔断;
-
慢调用 RT:应依据下游 SLO 和上游超时预算设置;
-
比例或数量阈值:决定熔断敏感度;
-
熔断时长:给下游恢复时间,又不能长到影响正常恢复;
-
半开探测量:用于验证恢复,且不能造成二次冲击。
阈值应通过历史监控、容量压测和故障演练校准,而不是只凭经验设定。
6.5 降级策略
熔断只决定“是否继续调用”,降级决定“不调用后返回什么”。常见降级方式包括:
-
返回本地缓存或允许一定陈旧度的历史数据;
-
返回默认值、空结果或简化字段;
-
跳过推荐、排行榜、装饰信息等非核心功能;
-
将同步操作转为异步排队,稍后补偿;
-
使用备用数据源或备用服务;
-
明确提示“系统繁忙,请稍后重试”,并携带合理的重试时间。
降级结果必须符合业务语义。支付结果、库存扣减、账号资产等强一致场景不能用模糊默认值伪装成功,应明确失败、查询最终状态或进入补偿流程。
七、流量优先级与资源隔离
当系统进入过载状态时,平均地拒绝所有请求通常不是最佳选择。应根据业务重要程度对请求分类,例如:
-
最高优先级:登录鉴权、支付、资产变更、游戏存档;
-
核心在线流量:直接影响当前用户操作的请求;
-
可降级流量:推荐、排行榜、画像、非关键查询;
-
可延迟流量:报表、批处理、离线同步和后台任务。
低优先级流量应更早被限流或降级,高优先级流量使用独立配额。优先级还应沿调用链传播,否则下游无法判断应该优先保留哪些请求。
同时,可通过独立线程池、信号量、连接池、队列和租户配额进行资源隔离,避免一个慢依赖、异常接口或大客户耗尽整个应用的共享资源。
八、完整的韧性保护链路
一次请求可以依次经过以下保护:
调用方重试预算/客户端节流
↓
网关配额、来源限流、流量整形
↓
应用入口系统自适应保护
↓
接口级限流与资源隔离
↓
下游调用超时、熔断与半开探测
↓
缓存、默认值、异步化等业务降级
各层职责应明确:网关处理全局和租户流量,应用入口保护单实例,接口规则保护局部资源,客户端熔断保护调用方,业务降级保证用户获得可解释的结果。
九、实施与观测建议
9.1 实施顺序
-
明确核心链路、非核心链路和可接受的降级结果;
-
为所有远程调用设置连接超时、请求超时和总体截止时间;
-
建立接口级限流和资源隔离,设置静态安全上限;
-
为不稳定依赖配置慢调用或异常比例熔断;
-
增加客户端自适应节流和全局重试预算;
-
基于 CPU、RT、并发等指标启用系统自适应保护;
-
通过压测、混沌演练和故障复盘持续校准参数。
9.2 必须监控的指标
-
请求量、通过量、限流量、降级量;
-
成功率、异常率、超时率、慢调用比例;
-
平均 RT、P95、P99、最大 RT;
-
熔断器状态、熔断次数、半开探测结果;
-
CPU、内存、GC、Load、活跃线程数;
-
线程池队列、连接池使用率和等待时间;
-
重试次数、重试流量占比、本地拒绝概率;
-
按接口、调用方、租户和优先级拆分的指标。
规则触发必须可观测,否则“保护生效”很容易被误判为“服务故障”。日志和告警至少应说明:哪条规则被触发、触发时的指标值、被影响的资源、执行了什么降级逻辑。
十、总结
-
固定 QPS 只是基础防线。 服务容量会随请求成本、系统资源和下游状态变化,不能只依赖固定阈值。
-
入口保护与下游保护解决不同问题。 入口自适应限流防止本服务过载;客户端节流和熔断防止不稳定依赖拖垮调用方。
-
快速失败优于无限排队。 过载时继续堆积请求只会消耗更多线程、连接和内存,并放大长尾延迟。
-
重试必须有预算。 限制次数、指数退避、随机抖动,并且只在紧邻失败依赖的上一层重试。
-
熔断必须能够自动恢复。 通过 Open、Half-Open、Closed 状态转换,以少量探测请求验证下游是否恢复。
-
降级必须符合业务语义。 非核心信息可以缓存或简化,资金、库存、账号资产等关键操作必须保持明确且可追踪的结果。
-
核心流量应获得优先保障。 结合请求优先级、独立配额和资源隔离,避免所有流量一起失败。
-
所有阈值都需要验证。 以真实监控、容量压测和故障演练为依据,并保留静态安全上限作为兜底。
最终目标不是消灭所有错误,而是在容量不足或依赖故障时,让系统以可控方式拒绝、降级和恢复,避免局部问题演变为整体不可用。