高可用(一):限流、熔断、隔离与降级
高可用(一):限流、熔断、隔离与降级
导语:高可用的第一原则是「承认故障必然发生」。本篇围绕流量治理这套组合拳展开:限流(算法选择、单机 vs 分布式、放在哪层、阈值怎么定)、熔断(状态机、Sentinel 与 Hystrix 的分野、隔离方式)、超时(最容易被忽略却性价比最高)、降级(分级预案、开关、演练),共 10 题。
一、限流
1. 从架构视角看,限流应该放在哪几层?
答: 限流不是"某个组件的事",而是贯穿全链路的分层配额体系:
| 层级 | 手段 | 典型粒度 |
|---|---|---|
| 客户端 | 防重复提交、退避重试、本地开关 | 用户 / 设备 |
| 边缘(CDN / WAF) | 黑白名单、IP 频控、CC 防护 | IP / 地域 |
| 接入层(LVS / Nginx) | limit_req(漏桶)、limit_conn | IP / 连接数 |
| API 网关 | Sentinel / Spring Cloud Gateway / Envoy | 接口 / 租户 / AppKey |
| 应用 / 服务层 | Sentinel 单机或集群限流、信号量、线程池 | 方法 / 服务 / 调用方 |
| 下游保护 | DB 连接池上限、Redis 连接数、第三方并发信号量 | 资源 / 依赖 |
两条架构原则:
- 越靠上越省资源(早丢弃早释放),但粒度越粗;越靠下越精准,但资源已经消耗了。
- 配额要透传、不能各层乱设:入口放进来多少,下游必须接得住;否则上游敞开、下游被打爆(典型的"限流形同虚设")。
加分点:能指出"限流的最终目的不是限制用户,而是保证核心业务与下游资源的可用性"——所以配额分配应遵循"核心接口优先、非核心先降"。
2. 四种限流算法怎么对比?分别适合什么场景?
答:
| 算法 | 原理 | 允许突发 | 优点 | 缺点 | 适用 |
|---|---|---|---|---|---|
| 固定窗口计数 | 按秒/分计数,超阈值拒绝 | 是(临界突刺) | 最简单、内存 O(1) | 临界问题(见下题) | 粗粒度频控 |
| 滑动窗口 | 把窗口切成 N 格,滚动统计最近窗口 | 部分 | 平滑、比固定窗口准 | 内存 O(N) | 统计类限流(Sentinel) |
| 漏桶(Leaky Bucket) | 请求入桶,恒定速率流出 | 否(超出排队/丢弃) | 输出绝对匀速、天然整形 | 无法应对合法突发、排队延迟 | 保护脆弱下游(如 DB 写入) |
| 令牌桶(Token Bucket) | 定速放令牌,取到才能过;桶容量=突发上限 | 是 | 平均速率可控 + 容忍突发 | 实现稍复杂 | 最常用(Sentinel / Guava / Nginx) |
记忆口诀:
要"平均限速 + 容忍突发" → 令牌桶
要"输出绝对匀速、整形" → 漏桶
要"统计式、平滑窗口" → 滑动窗口
要"最简单、能接受误差" → 固定窗口补充:令牌桶的桶容量决定了"允许的突发量"——容量为 0 就退化成"匀速",容量很大则接近"只限平均"。
3. 固定窗口的「临界问题」是什么?滑动窗口怎么解决?
答: 临界问题:以"限 100 QPS 的固定窗口"为例——
窗口1 [0.0s ~ 1.0s],在 0.9s ~ 1.0s 通过 100 个
窗口2 [1.0s ~ 2.0s],在 1.0s ~ 1.1s 通过 100 个
→ 0.2 秒内实际通过 200 个,是阈值的 2 倍即在两个窗口的交界处,瞬时流量可以达到阈值的两倍,对下游是突发冲击。
滑动窗口的解法:不按"整点窗口"计数,而是把时间切成 N 个小格,统计"最近一个完整窗口"内的总量(窗口随时间平滑滑动),交界处就不会出现双倍突刺。
Sentinel 的实现(高频追问):用 LeapArray(等分时间窗 + 环形数组)——把 1 秒切成若干 bucket,用环形数组复用槽位,current 指针随时间滑动;统计时累加当前窗口内的所有 bucket。它牺牲少量内存换取平滑度,且能天然支持"QPS + 并发数"的双维度统计。
另注:令牌桶天然没有临界问题(令牌是连续的),所以它比固定窗口更平滑。
4. 单机限流 vs 分布式限流?分布式限流怎么做?
答:
| 单机限流 | 分布式(集群)限流 | |
|---|---|---|
| 实现 | 本地内存计数器 / 令牌桶(Guava、Sentinel 单机) | Redis + Lua、Sentinel 集群限流、网关集中限流 |
| 精度 | 集群总量 = 单机阈值 × 副本数,负载不均时不准 | 全局精确 |
| 性能 | 零网络开销,最快 | 有网络 RT,Redis 可能成为瓶颈 |
| 可用性 | 不依赖外部组件 | 依赖限流中心(挂了怎么办?) |
分布式限流的三种做法:
| 方案 | 说明 | 注意 |
|---|---|---|
| Redis + Lua | 用 Lua 原子实现令牌桶/滑动窗口 | Redis 单点/瓶颈;必须考虑"限流器不可用时是否放行" |
| Sentinel 集群限流 | Token Server(独立)或嵌入式模式,统一发放配额 | 引入了新的组件与部署复杂度 |
| 网关集中限流 | 入口唯一,天然是"分布式"的 | 只能限入口,服务间调用仍需单机兜底 |
架构选型结论:
大多数场景 = 网关集中限流(粗粒度) + 服务单机限流(细粒度兜底)
只有"配额必须严格全局精确"(如对外 API 售卖配额)才上强一致分布式限流关键取舍:限流器本身也是一个依赖,它不可用时的策略必须先定义好——通常选择"降级为单机限流"或"放行(fail-open)",绝不能让"限流组件故障"变成"业务全挂"。
5. 限流的粒度和阈值怎么定?被限流后应该怎么处理?
答:
粒度(按"谁在滥用、要保护谁"来选):
| 粒度 | 场景 |
|---|---|
| 接口 / 方法 | 最常用:单接口能力上限 |
| 用户 / 租户 / AppKey | 防单用户刷接口、多租户配额隔离 |
| IP | 防爬虫、防 CC 攻击 |
| 调用来源 | 服务间调用,防某个上游打爆自己 |
阈值来源(不是拍脑袋):
压测得出的单机/集群能力(容量规划)
→ 按上游预算分配配额
→ 留 20%~30% 余量
→ 配置中心动态调整(高峰期临时收紧/放宽)被限流后的处理方式:
| 方式 | 说明 |
|---|---|
| 快速失败 | 返回 429 Too Many Requests + Retry-After,让客户端退避 |
| 排队等待 | Sentinel "匀速排队"模式,削峰但会增加 RT(要防止排队过长) |
| 降级返回 | 返回兜底数据(默认值、缓存、静态页),用户可感知但不报错 |
| 客户端退避重试 | 指数退避,必须有上限,否则重试风暴会二次冲击 |
一句话:限流只是"挡住",真正决定用户体验的是"挡住之后给什么"——所以限流设计必须和降级预案一起做。
二、熔断、隔离与降级
6. 熔断器的工作原理?三个状态与关键参数是什么?
答: 熔断器是依赖调用的"保险丝",核心是一个三态机:
Closed(闭合/正常) ──失败率/慢调用超阈值──▶ Open(断开)
▲ │
│ 冷却时间到
└──试探成功──────────────────── Half-Open(半开)
│
试探失败
▼
Open| 状态 | 行为 |
|---|---|
| Closed | 正常放行,统计失败率 / 慢调用比例 / 异常数 |
| Open | 快速失败,不再真正调用下游(保护调用方线程,也给下游恢复时间) |
| Half-Open | 冷却结束后放少量请求试探:成功 → 回到 Closed;失败 → 回到 Open |
关键参数:
| 参数 | 含义 |
|---|---|
| 统计窗口 / 时间窗口 | 统计的时间粒度(如 1s / 10s 滑动窗口) |
| 最小请求数 | 样本太少不熔断(避免"1 个请求失败就熔断") |
| 失败率阈值 / 异常数阈值 | 触发熔断的条件 |
| 慢调用比例 / RT 上限 | 慢调用也可触发熔断(保护被拖死的线程) |
| 熔断时长 | Open → Half-Open 的冷却时间 |
| 半开放行数 | Half-Open 时允许试探的请求数 |
核心意义:熔断同时保护两边——对调用方避免线程被慢依赖拖死,对下游避免被持续冲击(让它喘口气恢复)。这也是"雪崩防护"的第一道闸门。
7. Sentinel 与 Hystrix 的架构对比?为什么 Hystrix 被替代?
答:
| 维度 | Hystrix(Netflix) | Sentinel(阿里) |
|---|---|---|
| 隔离方式 | 以线程池隔离为主(也可信号量) | 信号量 / 并发数控制为主,无额外线程 |
| 线程模型 | 每个依赖一个线程池,线程上下文切换开销大 | 复用调用线程,低开销、高吞吐 |
| 统计 | 滑动窗口(RxJava 实现) | LeapArray 滑动窗口 |
| 规则配置 | 硬编码/配置文件为主 | Dashboard 动态推送 + 配置中心 |
| 功能范围 | 熔断、隔离、降级 | 熔断、限流、系统自适应保护、热点参数限流 |
| 生态与维护 | 已停止维护,社区转 Resilience4j | 活跃,Spring Cloud Alibaba 默认 |
核心区别一句话:Hystrix 用"线程池隔离"换"可超时中断 + 异步"的能力,代价是线程开销;Sentinel 用"信号量 + 滑动窗口统计"换低开销,但无法主动中断阻塞调用。
为什么被替代:
- Hystrix 停止维护,而线程池隔离在容器化 + 高并发下开销过重(双层线程模型);
- 限流能力缺失,需要额外引入组件;
- Sentinel 的动态规则 + Dashboard + 阿里生态更贴合国内实践;Resilience4j 则提供轻量的函数式方案。
8. 隔离(舱壁模式)有哪些实现?线程池隔离 vs 信号量隔离怎么选?
答: 舱壁模式(Bulkhead)源于船舱分格——一个舱进水不沉船。落到系统上就是把资源切成互不影响的独立池:
| 隔离维度 | 实现 |
|---|---|
| 线程池隔离 | 每个依赖一个独立线程池(Hystrix) |
| 信号量隔离 | 用 Semaphore 限制某依赖的并发数(Sentinel 默认思路) |
| 连接池隔离 | 每个下游独立连接池 / 独立 HTTP client(限制最大连接数) |
| 进程/容器隔离 | 核心链路独立部署、K8s 副本与资源限额 |
| 数据隔离 | 核心库与报表库分离、读写分离 |
线程池 vs 信号量:
| 线程池隔离 | 信号量隔离 | |
|---|---|---|
| 开销 | 大(每个依赖多一层线程 + 队列) | 小(无额外线程) |
| 是否可超时中断 | 可以(Future.cancel) | 不可以(阻塞调用无法被中断) |
| 是否支持异步 | 支持 | 天然同步 |
| 适用 | 慢且不可控的下游(第三方 HTTP、老系统) | 内部快速 RPC(RT 可控)、高吞吐场景 |
选型结论:
延迟可控的内部 RPC → 信号量隔离(省资源)
延迟不可控的第三方 → 线程池隔离(要能超时、能被隔离)
核心 vs 非核心 → 一定隔离(否则非核心拖垮核心)架构上,隔离必须与限流、熔断、超时组合使用——单靠隔离只能"限制爆炸半径",不能"止损"。
9. 超时该怎么设?为什么说"超时是性价比最高的高可用改造"?
答: 因为没有超时的远程调用,等于把调用方的线程/连接永久交给下游——下游一慢,调用方线程池瞬间被耗尽("线程池打满 → 全站不可用")。
设超时的核心方法:超时预算(Timeout Budget)倒推
入口 SLA 1000ms
├─ 网关 1000ms
│ ├─ 服务A 800ms
│ │ ├─ RPC 调用B 500ms
│ │ └─ 缓存/DB 300ms
│ └─ 服务C 200ms三条铁律:
- 下级超时 < 上级剩余预算,逐层递减,避免"上级已超时、下级还在跑"(资源白白浪费)。
- 预留重试空间:若允许 2 次重试,单次超时必须 < 总预算 / 重试次数。
- 重试会放大流量:
重试次数 × 并发 = 流量倍数,重试必须有上限 + 指数退避 + 熔断前置校验(下游已熔断则不再重试,否则形成重试风暴)。
配合使用:
- 慢调用比例熔断(Sentinel
DegradeRule):把"逐渐变慢"的依赖提前熔断,而不是等它彻底不可用; - 超时后的兜底:超时不等于失败——可以返回兜底数据(降级),也可以异步补偿。
一句话:给每一个远程调用(HTTP、RPC、DB、缓存、MQ)显式设置超时,是投入产出比最高的稳定性改造,没有之一。
10. 降级预案体系怎么建?如何验证它真的有效?
答: 降级是"有损但可控"地牺牲部分功能,保住核心业务。完整的预案体系包含四件事:
(1)分级降级
| 级别 | 业务 | 降级动作 |
|---|---|---|
| P0 | 交易、支付 | 永不降级(保底) |
| P1 | 订单查询、详情 | 读走缓存、去掉非关键字段 |
| P2 | 推荐、评价、营销 | 直接关闭,返回静态兜底 |
故障时的降级顺序:先降 P2 → 再降 P1 → 死保 P0(2)开关中心:所有降级点做成动态开关(Nacos/Apollo/配置中心),支持灰度、一键生效、一键恢复;开关必须可观测(记录谁在什么时间降级了)——避免"静默降级"长期存在而无人知晓。
(3)触发通道:自动 + 人工双通道——自动(熔断/限流阈值、错误率告警触发)+ 人工(大促值班、应急预案手动执行)。
(4)验证:混沌工程
| 手段 | 说明 |
|---|---|
| 故障注入 | ChaosBlade / Chaos Monkey:注入延迟、异常、网络丢包、杀进程 |
| 降级演练 | 主动触发每个降级开关,验证"降级后业务是否可用、能否恢复" |
| 演练常态化 | 定期(大促前)无预告演练,检验预案而不是检验文档 |
必须点出的现实:大多数降级预案失效在"从来没人验证过"——开关写好了没人点、熔断阈值设了却与真实流量不匹配。所以"演练"比"设计"更能体现架构能力。
本章小结:稳定性的四件套是「限流(挡住)+ 熔断(止损)+ 隔离(限爆)+ 超时(兜底)」,而降级决定"挡住了之后用户看到什么"。它们的共同前提是:承认故障必然发生,并提前定义好"坏情况下怎么活"。
