想象你经营一家周末爆满的网红餐厅。厨房是那个“核心服务”,前厅是“调用方”。如果没有规矩,会发生什么?一桌客人等菜等了二十分钟,愤怒离席;厨师忙不过来,菜品质量直线下降;门口挤满了人,整个餐厅乱成一锅粥。 聪明的老板会立三条规矩:超时——服务员在客人犹豫十分钟后主动来帮忙点单,因为等待必须有上限;重试——菜品不小心洒了,厨房快速重做一份,但绝不无限重做;限流——门口取号排队,保证店内客流不超过厨房的承载极限。这三条规矩,就是微服务世界里保护系统不被拖垮的“三板斧”。 这一篇,我们走进微服务世界,看看 Netflix 的 Hystrix 和年轻一代的 Resilience4j 这两位“大堂经理”,如何用超时、重试、限流(还有熔断、舱壁)守住系统的稳定。你会发现,餐厅管理的智慧,和分布式系统的设计哲学惊人地一致。

岗位关联

超时、重试与限流是微服务可靠性工程的基本功。掌握这套策略组合,你就能像资深架构师一样,让系统在洪峰流量和下游故障中稳如磐石。

目录
  1. 可视化演示
  2. 超时策略
  3. 重试策略
  4. 限流策略
  5. 熔断与舱壁
  6. 框架对比
  7. 组合实践
  8. 压测调优
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

本节配有交互动效(进入视口后自动加载,约 3KB)。

超时策略

假设你是餐厅老板,客人点完菜后,厨师却说“这个菜看心情,可能 5 分钟也可能 1 小时”。如果服务员就这么干等着,整个前厅的服务员都会被这一桌拖住,其他客人没人招呼。聪明的做法是给这桌一个承诺——“15 分钟没上菜,这道菜免单”。超时,就是给等待装一个闹钟。

微服务里,调用下游服务的等待和“等菜”一模一样。如果没有超时,一个慢吞吞的下游服务会无限占用调用方的线程资源;线程池一旦被占满,整个服务就像前厅没了服务员一样瘫痪——这就是雪崩的起点。设置超时,相当于给每个调用设定了“最晚回话时间”:到点不返回,就按失败处理,把线程腾出来干别的。

超时配置通常分两层:连接超时指建立连接的最长等待,读超时指连接建立后等待响应的时间。工程上常用“均值加三倍标准差”确定超时基线:先统计下游正常响应时间的分布,再留出足够的波动余量,避免把正常请求误判为失败。

有了这条超时闹钟,服务员再也不会被一桌慢菜耗死。超时让系统学会“快速失败”,把宝贵的线程资源留给真正健康的请求。不过闹钟也不能太急——如果客人刚坐下 5 秒你就催,客人反而会生气:超时设得太短,误伤的是正常请求。

Ttimeout=μ+3σ\displaystyle T_{timeout} = \mu + 3\sigma
这个公式表示:超时时间 = 平均响应时间 + 三倍波动幅度。既能覆盖绝大多数正常请求,又能及时甩掉慢调用。
  1. 平均基线(\(\mu\))\(\mu\) 是下游服务正常情况下的平均响应时间,比如 80ms,它是超时设置的“锚点”。
    μ=80 ms\displaystyle \mu = 80\,\text{ms}
  2. 波动幅度(\(\sigma\))\(\sigma\) 是响应时间的标准差,反映服务时快时慢的波动程度,\(\sigma\) 越大说明服务越不稳定。
    σ=10 ms\displaystyle \sigma = 10\,\text{ms}
  3. 安全余量(\(3\sigma\))三倍标准差大约覆盖 99.7% 的正常波动范围,把“误伤正常请求”的概率压到最低。
    3σ=30 ms\displaystyle 3\sigma = 30\,\text{ms}
  4. 最终超时值\(\mu + 3\sigma\) 相加得到最终的超时阈值,任何超过这条线的调用都按失败处理,快速释放资源。
    Ttimeout=80+30=110 ms\displaystyle T_{timeout} = 80 + 30 = 110\,\text{ms}

重试策略

菜没做好,顾客当然不高兴。有经验的服务员会赶紧赔礼,然后让厨房“重新做一份”,并安抚客人“这次很快”。但你会无限重做吗?不会——如果同一道菜连续三次都做砸了,说明是食材或菜谱出了问题,再催也没用。重试,就是有策略地催单。

微服务同理。网络抖动、连接池瞬时耗尽导致的失败,通常重试一次就能成功;但如果下游已经过载,重试只会雪上加霜。所以重试必须守两条规矩:有上限、有间隔。间隔采用指数退避:第一次失败后等 100ms,第二次等 200ms,第三次等 400ms……给下游留出喘息恢复的时间。

指数退避最关键的是加随机抖动。想象一下,餐厅停电后恢复供电,如果所有客人都同时涌向收银台,收银台会再次崩溃。抖动就是给每桌一个随机的“先后顺序”,让重试请求错峰到达,避免形成重试风暴。

重试是“有节奏地催单”,绝不能是“夺命连环催”。更关键的是,重试必须配合幂等设计才安全——比如支付场景里,第一次请求其实已经扣款成功,只是响应丢了,此时盲目重试就会变成重复扣款。给每次操作一个唯一幂等键,重试时服务端认出“这道菜已经做过了”,直接返回上次结果,才是安全的催单。

Twait=Tbase×2attempt+jitter\displaystyle T_{wait} = T_{base} \times 2^{attempt} + jitter
第 \(attempt\) 次重试前的等待时间 = 基础间隔 × 2 的尝试次数次方 + 一个随机抖动值。
  1. 基础间隔(\(T_{base}\))第一次重试前的等待时间,比如 100ms,目的是让下游从抖动中缓过来。
    Tbase=100 ms\displaystyle T_{base} = 100\,\text{ms}
  2. 指数系数(\(2^{attempt}\))\(attempt\) 从 0 开始计,每多一次重试,等待时间翻倍,形成“越等越久”的节奏。
    20=1,  21=2,  22=4\displaystyle 2^{0}=1,\; 2^{1}=2,\; 2^{2}=4
  3. 随机抖动(\(jitter\))一个随机偏移量,比如 0~50ms,让各请求的重试时刻错开,防止同时集中打到下游。
    jitter∈[0,50] ms\displaystyle jitter \in [0, 50]\,\text{ms}
  4. 总等待时间把基础间隔、指数系数和抖动三者相乘相加,得到本次重试前的实际等待时长。
    Twait=100×2attempt+jitter\displaystyle T_{wait} = 100 \times 2^{attempt} + jitter

限流策略

餐厅再火,餐位是有限的。门口取号排队,每走一桌再放一桌,店内客流永远不超过承载量。这就是限流。不让客人进店看似“不礼貌”,但总比客人进了店却没座位、服务质量彻底崩盘要好。

限流有几种经典算法。令牌桶像一个发牌员:每秒往桶里放固定数量的令牌,请求来了必须先拿到令牌才能进入处理。桶有容量上限,桶满则新令牌丢弃——这意味着桶越大,允许短时间积压的突发流量越多。漏桶则像入口闸机,无论门口多少人,每秒只放行恒定数量,把流量彻底磨平,适合保护对流量曲线极其敏感的下游。滑动窗口则解决了固定窗口的“边界突刺”问题:固定窗口按整秒计数,可能在 0.9 秒和 1.1 秒各放行 1000 个请求,形成两秒内 2000 个请求的瞬间冲击;滑动窗口把时间切成更小的格子,统计任意连续时间段内的请求数,曲线更平滑。

限流等于在“客人进门之前”设一道闸——进不来,总比进来后把厨房挤爆要好。值得注意的是,限流阈值不能拍脑袋定:定得太松起不到保护作用,定得太严又浪费系统资源。正确做法是通过压测摸清系统的真实承载力,再留出安全余量。

Ratepass=min⁡(Ratearrive,Ratebucket)\displaystyle Rate_{pass} = \min(Rate_{arrive}, Rate_{bucket})
实际通过的请求速率 = 到达速率与令牌桶速率的较小值。超过系统能力的请求拿不到令牌,直接被拒绝。
  1. 到达速率(\(Rate_{arrive}\))单位时间内到达限流闸门的请求数,比如每秒 200 个请求涌向服务。
    Ratearrive=200 req/s\displaystyle Rate_{arrive} = 200\,\text{req/s}
  2. 令牌速率(\(Rate_{bucket}\))令牌桶每秒补充的令牌数,代表系统承诺的每秒处理能力上限。
    Ratebucket=100 req/s\displaystyle Rate_{bucket} = 100\,\text{req/s}
  3. 取最小值(\(\min\))两者取较小值,超过令牌供给的请求无法获得令牌,直接被限流闸门挡下。
    min⁡(200,100)=100\displaystyle \min(200, 100) = 100
  4. 最终放行速率\(Rate_{pass}\) 是实际穿过闸门的请求速率,它永远不会超过系统的承载阈值,这是防雪崩的底线。
    Ratepass=100 req/s\displaystyle Rate_{pass} = 100\,\text{req/s}

熔断与舱壁

电路里有一个保险丝,电流过大时熔断,保护整条电路。餐厅同样有这套哲学——如果烤鸭炉总是出问题,干脆暂时停掉烤鸭档口集中检修,别让一个小档口拖垮整个餐厅的营业。这就是熔断器。

熔断器有三种状态:关闭表示一切正常,请求全部转发;打开表示下游已经不行了,所有请求直接快速失败,不再浪费任何资源;半开是恢复期,放一小撮试探请求去探路,如果下游恢复了就转回关闭,否则再次打开。Hystrix 的默认规则是:10 秒内请求数超过阈值且错误率超过 50%,熔断器打开;冷却 5 秒后进入半开。

舱壁是另一层保护,借鉴了轮船的设计:船舱用隔板分成多个独立舱室,一个舱进水不会淹没整条船。在代码里,就是给不同下游服务分配独立的线程池或信号量,A 服务被拖垮时,B 服务的线程池不受影响。熔断与舱壁都是“壮士断腕”的智慧——保全局比保局部更重要,一个档口可以暂停,但餐厅不能倒。

Erate=FcountNtotal→OpenwhenErate>θ\displaystyle E_{rate} = \frac{F_{count}}{N_{total}} \rightarrow \text{Open} \quad \text{when} \quad E_{rate} > \theta
错误率 = 失败请求数 ÷ 总请求数。当错误率超过阈值 \(\theta\) 时,熔断器从关闭跳转为打开,后续请求快速失败。
  1. 失败计数(\(F_{count}\))统计窗口内失败的请求数,比如最近 10 秒内失败了 120 个请求。
    Fcount=120\displaystyle F_{count} = 120
  2. 总请求数(\(N_{total}\))同一统计窗口内发起的全部请求数量,它是计算错误率的分母。
    Ntotal=200\displaystyle N_{total} = 200
  3. 错误率与阈值比较\(E_{rate}\) 算出失败占比,当它超过阈值(如 50%)时,保险丝熔断,熔断器进入打开状态。
    Erate=120200=0.6>0.5→Open\displaystyle E_{rate} = \frac{120}{200} = 0.6 > 0.5 \rightarrow \text{Open}

框架对比

Hystrix 像一位经验丰富但已经退休返聘的老经理,规矩多、功能全,但毕竟年事已高;Resilience4j 是年轻的新锐,轻装上任,效率优先。选哪位“大堂经理”,是微服务治理中的一道经典选择题。

Hystrix 由 Netflix 开源,曾是微服务容错的事实标准,但官方已于 2020 年停止维护,不再修复 bug 也不再适配新版本 JDK。它默认使用线程池隔离:为每个下游服务分配一个独立线程池,隔离性最强,但每个请求都要经历线程切换,内存开销大。Resilience4j 是社区公认的继任者,基于轻量级函数库实现,默认使用信号量隔离(默认 10 个并发许可),不额外创建线程,内存开销小得多。

在超时与重试上,Hystrix 内置超时控制但重试能力较弱;Resilience4j 把 TimeLimiter(超时)和 Retry(重试)拆成独立模块,可以灵活组合。在限流上,Hystrix 主要靠线程池饱和度间接限流,Resilience4j 则提供专门的 RateLimiter 模块,支持令牌桶风格限流。线程池隔离并非越大越好,工程上通常用下面的公式估算线程池大小:

选经理要看“性价比”:老经理 Hystrix 功能全面但已经退役,维护成本越来越高;新经理 Resilience4j 轻量灵活,是当前 Spring Cloud 生态的主流选择。对存量系统,迁移可以分批进行,但方向必须明确。

Psize=QPS×Lavg×(1+Mextra)\displaystyle P_{size} = QPS \times L_{avg} \times (1 + M_{extra})
线程池大小 = 每秒请求量 × 平均耗时(秒) × (1 + 冗余系数)。这是 Hystrix 线程池隔离的经典容量估算公式。
  1. 每秒请求量(\(QPS\))该下游服务需要支撑的每秒查询数,是容量估算的第一输入。
    QPS=50\displaystyle QPS = 50
  2. 平均耗时(\(L_{avg}\))下游处理一个请求的平均秒数,它决定了每个请求占用线程多久。
    Lavg=0.2 s\displaystyle L_{avg} = 0.2\,\text{s}
  3. 冗余系数(\(M_{extra}\))预留的缓冲比例,通常取 0.2 左右,应对流量波动和慢调用。
    Mextra=0.2\displaystyle M_{extra} = 0.2
  4. 线程池大小三者相乘得到建议的独立线程数,兼顾隔离效果与资源开销。
    Psize=50×0.2×1.2=12\displaystyle P_{size} = 50 \times 0.2 \times 1.2 = 12

组合实践

完整的餐厅接待流程是:门口取号(限流)→ 入座点单(超时)→ 菜没做好催一次(重试)→ 某个灶台反复出问题就停用(熔断)。三道防线缺一不可,顺序也大有讲究。

推荐的组合顺序是:先限流,让请求在系统入口就被过滤,保护整个服务不被打爆;再超时,每一个放入的请求都必须有等待上限,防止慢调用拖住线程池;最后重试,仅对快速失败的请求做指数退避重试,试探性地挽回瞬时故障。熔断则作为对“下游系统性故障”的最后防线——当下游持续亮红灯,就不再往里派任何请求。

组合时务必计算总时间预算。一次外部请求最多经历“1 次调用 + N 次重试”,每次尝试都有自己的超时阈值,总耗时不能超过业务的 SLO(服务等级目标)。另外,重试产生的流量也要计入限流配额,否则重试会成为绕过限流的后门——就像客人从后门悄悄溜进餐厅。

没有放之四海皆准的参数。超时多长、重试几次、限流多少,取决于下游服务有多稳、业务流量有多旺。只有通过压测,才能找到属于自家系统的“黄金参数组合”。

Ttotal=Ttimeout×(Nretry+1)\displaystyle T_{total} = T_{timeout} \times (N_{retry} + 1)
一次业务调用的总时间预算 = 单次超时 × (重试次数 + 1)。加一是因为最初的调用也算一次。
  1. 单次超时(\(T_{timeout}\))每次调用允许的最大等待时间,是总预算的基本单位。
    Ttimeout=200 ms\displaystyle T_{timeout} = 200\,\text{ms}
  2. 重试次数(\(N_{retry}\))允许的重试次数上限,每多一次重试,预算就多一份超时窗口。
    Nretry=2\displaystyle N_{retry} = 2
  3. 加一含义\(N_{retry} + 1\) 表示“第一次调用 + 2 次重试”的总尝试次数,共 3 次。
    Nretry+1=3\displaystyle N_{retry} + 1 = 3
  4. 总预算校验总时间预算必须在 SLO 之内,否则需要缩短超时或减少重试次数。
    Ttotal=200×3=600 ms\displaystyle T_{total} = 200 \times 3 = 600\,\text{ms}

压测调优

新菜单正式上线前,老板会请熟客免费试吃,验证菜品质量,也看看厨房在更大客流下的真实表现。试营业发现“上菜慢”,就调整厨师分工;发现“门口堵人”,就优化取号策略。这就是压测的智慧。

压测就是给系统办“试营业”。用 JMeter、k6、wrk 等工具模拟真实流量并逐步加压,观察系统的吞吐量(QPS)、响应时间(P99/P95)、错误率以及 CPU、内存等资源指标。压测的核心目标是找到性能拐点——继续加压,QPS 不再上升、错误率开始飙升的那个临界值。这个临界值就是限流阈值和线程池大小的上限参考。

根据压测结果调优的方法很直接:如果 P99 过高,优先排查下游慢调用,适当调低超时;如果错误率集中在“超时和重试耗尽”,考虑增加重试次数或优化退避参数;如果限流拒绝率很高,说明阈值过严,可以按业务重要性分级放行。调优不是一次性的——每次调整超时、重试、限流参数后都要回归压测,像餐厅每次改菜单后都要重新试营业。数字不会骗人,系统承载力几何,看压测曲线一目了然。

QPS=NreqTspan\displaystyle QPS = \frac{N_{req}}{T_{span}}
吞吐量 = 压测期间发出的请求总数 ÷ 压测持续的总时长。它衡量系统在特定压力下每秒能处理多少请求。
  1. 请求总数(\(N_{req}\))压测期间成功发出的请求总数,它是统计吞吐量的分子。
    Nreq=10000\displaystyle N_{req} = 10000
  2. 压测时长(\(T_{span}\))压测持续的总秒数,压测时长越久,数据越能反映真实场景。
    Tspan=100 s\displaystyle T_{span} = 100\,\text{s}
  3. 吞吐量结果两者相除得到每秒请求数,用这个数字与业务预期对比,判断系统是否达标。
    QPS=10000100=100\displaystyle QPS = \frac{10000}{100} = 100

AI 应用场景

电商大促的流量洪峰防护

阶段:AI 服务治理与流量保障

解决的问题:大促秒杀时流量是平时的 20 倍,若 AI 推荐服务和订单服务被高并发抖垮,全站就会瘫痪。传统做法是粗暴扩容,但成本高且无法无限扩展。通过令牌桶限流 + 超时兜底 + 重试补偿,把并发控制在压测测得的阈值内,超出承载的请求快速失败并提示“稍后再试”,保障核心链路可用。

具体案例:某电商平台的商品详情 AI 推荐服务,大促期间用 Resilience4j 的 RateLimiter 将 QPS 限制在压测阈值内,配合 200ms 超时和最多 1 次重试,服务可用率从 99.1% 提升到 99.99%,GPU 资源始终未被打满。

支付回调的幂等重试

阶段:分布式事务与一致性保障

解决的问题:支付回调失败会导致订单状态不一致,盲目重试又可能造成重复扣款。需要把“重试”与“幂等”绑在一起设计:给每次支付操作生成唯一幂等键,重试时服务端识别出“这笔操作已处理”,直接返回原结果。

具体案例:某支付网关对回调处理采用 0.5s、1s、2s、4s 的指数退避,最多重试 4 次,并用 Redis 记录处理幂等键。改造后,瞬时网络抖动引发的回调失败率从 8% 降到 0.2%,未出现一例重复扣款。

Service Mesh 中的策略注入

阶段:云原生基础设施与流量治理

解决的问题:微服务数量达到数百个时,为每个服务单独维护防御配置不现实。Service Mesh 可以把超时、重试、限流、熔断规则集中下沉到基础设施层,让业务代码零侵入地获得防护。

具体案例:某公司在 Istio 中通过 DestinationRule 为所有服务统一设置超时和重试规则,并把 AI 推理服务接入网格,用限流保护 GPU 集群不被突发推理请求拖垮。升级后,故障演练时的影响范围从“整个调用链”收敛到“单实例”。

Python 演示

python84 行
import randomimport time  class TokenBucket:    """令牌桶限流器: 以固定速率补充令牌, 桶满则丢弃多余令牌"""     def __init__(self, rate, capacity):        self.rate = rate        self.capacity = capacity        self.tokens = float(capacity)        self.last_refill = time.monotonic()     def try_acquire(self):        now = time.monotonic()        self.tokens = min(            self.capacity,            self.tokens + (now - self.last_refill) * self.rate        )        self.last_refill = now        if self.tokens < 1:            return False        self.tokens -= 1        return True  def one_call(fail_rate, slow_rate, slow_time):    """模拟一次下游调用: 慢调用返回耗时, 失败抛出异常, 正常返回短耗时"""    if random.random() < slow_rate:        return slow_time    if random.random() < fail_rate:        raise ConnectionError("下游连接异常")    return random.uniform(0.002, 0.008)  def safe_call(bucket, timeout, max_retry, fail_rate, slow_rate, slow_time):    """限流 + 超时 + 指数退避重试 的组合防护"""    if not bucket.try_acquire():        return "limited"    for attempt in range(max_retry + 1):        try:            cost = one_call(fail_rate, slow_rate, slow_time)            if cost >= timeout:                raise TimeoutError("调用超时")            return "success"        except (ConnectionError, TimeoutError):            if attempt == max_retry:                return "failed"            wait = 0.01 * (2 ** attempt) + random.uniform(0, 0.005)            time.sleep(wait)    return "failed"  def simulate():    bucket = TokenBucket(rate=20, capacity=10)    count = 200    fail_rate = 0.3    slow_rate = 0.2    slow_time = 0.1    timeout = 0.03    max_retry = 2     base_fail = 0    guard_fail = 0    for _ in range(count):        try:            cost = one_call(fail_rate, slow_rate, slow_time)            if cost >= timeout:                raise TimeoutError("超时")        except (ConnectionError, TimeoutError):            base_fail += 1         if safe_call(bucket, timeout, max_retry,                     fail_rate, slow_rate, slow_time) != "success":            guard_fail += 1     print("模拟请求数: %d" % count)    print("无防御失败率: %.2f%%" % (base_fail * 100.0 / count))    print("有防御失败率: %.2f%%" % (guard_fail * 100.0 / count))    print("重试降低失败率, 但以耗时和下游压力为代价")  if __name__ == "__main__":    simulate()
目的:模拟 200 次下游调用,对比“裸调用”与“限流 + 超时 + 指数退避重试”组合防护下的失败率,直观展示三板斧的防御效果。
  1. class TokenBucket:实现令牌桶限流器,rate 控制每秒补充令牌数,capacity 控制桶容量(最大突发量)。
  2. self.tokens = min(self.capacity, ...)令牌数不能超过桶容量,多出来的令牌直接丢弃,这是允许突发但不过载的关键。
  3. if self.tokens < 1: return False令牌不足一个时拒绝请求,这就是“门口取号”的代码化身。
  4. def one_call(fail_rate, slow_rate, slow_time):模拟下游的三种表现:正常、慢调用、直接失败。
  5. raise ConnectionError("下游连接异常")模拟网络抖动导致的瞬时失败,这类失败重试通常能救回来。
  6. if cost >= timeout:判断单次调用是否超时,超时也算一次失败,需要走重试逻辑。
  7. for attempt in range(max_retry + 1):重试上限控制:最多尝试 1 次初始调用 + 2 次重试,绝不无限催单。
  8. wait = 0.01 * (2 ** attempt) + random.uniform(0, 0.005)指数退避 + 随机抖动,让重试请求错峰到达。
  9. if not bucket.try_acquire(): return "limited"组合策略的第一步:先过限流闸门,进不来就直接拒绝。

常见误区

误区有同学觉得:“超时设短一点,系统不就能更快甩掉慢请求吗?干脆设 50ms,任何超过的都直接失败,反正错误率也不会增加多少。”

正解超时太短会误伤正常请求。如果下游 P99 是 300ms,你却设 100ms 超时,约 1% 的正常请求会被当成失败处理,业务方会收到大量“假错误”。正确的做法是先采集响应时间分布,用“均值 + 3 倍标准差”或“P99 + 缓冲”来设定,既覆盖正常波动,又能甩掉真正异常的慢调用。

为何常见:人天生厌恶等待,容易把“超时”想象成越短越好的事情,忽略了误伤正常请求的成本。

误区某次重试成功后,就觉得“多试几次总没错”,于是把重试上限设到 8 次、10 次,甚至对写操作也照着重试。

正解重试会放大下游压力。当下游已经过载时,重试等于反复捅一个正在流血的伤口,可能把单点故障放大成雪崩。必须设置重试上限,配合指数退避和抖动;对写操作(如付款)还必须保证幂等,否则重试会变成重复扣款。重试的正确姿势是“有节奏地催单”,不是“夺命连环催”。

为何常见:重试的收益是立竿见影的(重试一次就成功了),但压力放大的代价是慢慢累积的,不容易让人警觉。

误区不少初学者实现限流时用“固定窗口”:每秒一个计数器,清零后超过阈值就拒绝,实现简单又直观。

正解固定窗口有边界突刺问题:第 0.9 秒和第 1.1 秒之间,两个窗口各自放行一批请求,造成瞬间两倍流量冲击下游。生产环境更推荐滑动窗口(把时间切成更细的格子,统计任意连续时间段)或令牌桶(允许一定突发),让流量曲线更平滑。

为何常见:固定窗口最简单、直觉上够用,只有在大流量高并发场景下,窗口边界的缺陷才会暴露出来。

误区看到老项目还在用 Hystrix,觉得“能跑就行,迁移成本太高,先放着吧”。

正解Hystrix 官方已于 2020 年停止维护,不再修复 bug 也不兼容新版本 JDK,长期使用是隐形技术债。Resilience4j 提供相近的熔断、限流、隔离能力,且基于轻量级函数库、社区活跃,是 Spring Cloud 官方推荐的替代方案。迁移可以按服务分批进行,但方向要明确。

为何常见:存量系统惯性大,“还能用”掩盖了“不再维护”的风险,等到 JDK 升级或出现安全漏洞时,代价会更大。

面试题

Q1. 超时策略的核心收益是什么?

单选题 · easy

  • A.提高网络传输速度
  • B.释放占用线程资源← 正确答案
  • C.减少服务并发数量
  • D.提升数据一致性

答案:B。超时就像给等待装闹钟,到点不返回就按失败处理,把占用的线程快速释放出来。如果线程一直被慢调用占住,线程池就会耗尽,如同服务员都被一桌慢菜困住,其他客人无人招待。

Q2. 关于重试,下列说法正确的是?

单选题 · easy

  • A.重试次数越多越好
  • B.重试无需等待间隔
  • C.重试需配合幂等设计← 正确答案
  • D.重试只适合读请求

答案:C。重试是“有策略地催单”,必须配合幂等设计,否则写操作可能因重试而重复执行(如重复扣款)。重试次数越多风险越大,重试也要有等待间隔,且写请求同样可以安全重试——前提是幂等。

Q3. 限流策略的首要目标是?

单选题 · easy

  • A.保障系统在承受范围← 正确答案
  • B.提高单次请求速度
  • C.增加系统吞吐量上限
  • D.降低网络延迟水平

答案:A。限流如餐厅门口取号,优先保证店内客流不超过承载力。它的目标不是让单个请求更快,也不是提高吞吐上限,而是保护系统不被超量流量打垮。吞吐量上限由系统真实能力决定,不是限流能提升的。

Q4. 令牌桶的“桶容量”参数控制什么?

单选题 · mid

  • A.每秒补充令牌速率
  • B.允许最大突发流量← 正确答案
  • C.请求重试的次数上限
  • D.服务超时的时间阈值

答案:B。桶容量决定最多能积压多少令牌,即允许短时间爆发的最大流量。每秒补充速率才是稳态 QPS 上限;桶容量越大,瞬时突发能力越强。类比餐厅,桶容量相当于允许门口暂时排多长的队。

Q5. 熔断器“打开”状态下的请求处理方式是?

单选题 · mid

  • A.正常转发给下游
  • B.快速失败返回降级← 正确答案
  • C.排队等待下游恢复
  • D.自动切换到备用链路

答案:B。熔断器打开时,请求不再发往下游,而是直接快速失败并走降级逻辑,避免继续消耗资源。这如同烤鸭炉出问题后直接停掉档口,而不是继续往里塞鸭子。半开状态才会放少量试探请求探测下游是否恢复。

Q6. Hystrix 与 Resilience4j 的核心区别是?

单选题 · mid

  • A.前者仅支持同步调用
  • B.后者是轻量级函数库← 正确答案
  • C.前者内置限流算法
  • D.后者不支持舱壁隔离

答案:B。Hystrix 是重量级框架,基于线程池隔离;Resilience4j 是轻量级函数库,默认信号量隔离,且模块化更高。前者虽然功能全但已停止维护,后者是 Spring Cloud 官方推荐的继任者。两者都支持同步调用和舱壁隔离。

Q7. 组合策略中正确的防线顺序是?

单选题 · mid

  • A.重试在前限流在后
  • B.超时在前重试在后
  • C.限流在前超时重试后← 正确答案
  • D.熔断在前限流在后

答案:C。标准顺序是“先限流、再超时、最后重试”:限流在入口挡住超额流量,超时为每个放入的请求设等待上限,重试只对快速失败做有节制的补偿。熔断则作为下游系统性故障的最后防线,通常与前两者配合而非前置。

Q8. 压测中发现 P99 抬高但 QPS 未提升,应当优先排查?

单选题 · hard

  • A.增大令牌桶容量
  • B.降低超时时间阈值
  • C.降低并发线程数量
  • D.优化下游服务性能← 正确答案

答案:D。P99 抬高说明尾延迟恶化,QPS 上不去说明系统吞吐受限于慢请求,根因往往在下游服务本身。增大令牌桶会放大压力,降低超时和线程数只是压抑症状。先优化下游性能才能同时改善延迟和吞吐。

Q9. 流量治理“三板斧”包含下列哪些?

多选题 · easy

  • A.超时控制← 正确答案
  • B.重试机制← 正确答案
  • C.限流策略← 正确答案
  • D.服务发现

答案:ABC。超时、重试、限流是流量治理的三大基础手段:超时给等待设上限,重试有节奏地挽回瞬时失败,限流在入口控制流量。服务发现解决的是“找到服务”的问题,不属于这三大防御策略。

Q10. 指数退避中加入随机抖动(jitter)的原因有哪些?

多选题 · hard

  • A.防止重试风暴← 正确答案
  • B.避免请求集中打点← 正确答案
  • C.让失败请求错峰重试← 正确答案
  • D.提高单次重试成功率

答案:ABC。抖动是在退避间隔上叠加一个随机量,让大量同时失败的请求不会在同一时刻集中重试。它防止了重试风暴,让失败请求错峰,但它本身不会提高某一次重试的成功率——成功率取决于下游是否恢复。