当我们在聊接口稳定性的时候,我们到底在聊什么?

很多人的第一反应其实都差不多,就是先看限流,再看熔断,再往后看降级、超时和重试。线上真出了问题,第一时间去确认这些开关和阈值,本来就没什么问题。

但这几年复盘做得多了以后,我更在意另一件事:有些接口到出事那一刻,看上去像是限流没挡住、熔断不够快,往前倒几步才发现,问题早就埋下去了。需求评审的时候顺手挂了一个依赖,设计时把本来不该同步查的东西塞进主链路,联调时一句“失败了就重试”没人继续追,压测时峰值打上去了,降级链路却没真的走过。

等到流量一抖、下游一慢,运行时这些手段能做的,很多时候也就是别让它立刻全线失控。

所以我现在更想问的是:一个接口到底是从什么时候开始变得不稳的?

先把稳定性理解对:不是永不出错,而是出错时仍然可控

我现在判断一个接口稳不稳,已经不太看“它会不会报错”,更关心的是出了问题以后会不会一路失控。高并发顶上来时还能不能守住核心能力,下游抖动时 RT 和错误码会不会一起乱掉,局部故障出现时影响会不会被圈在局部,而不是顺着调用链一路扩散。

这里有两个词比“绝对不挂”更重要。

一个是“可用”。
并不要求所有请求都完美成功,关键是核心能力不能因为局部问题整片失效。

另一个是“可控”。
慢一点、降一点、少一点都可以讨论,但不能失控。什么叫失控?线程池被拖死,连接池被打空,重试把流量放大,故障沿调用链扩散,错误码没有边界,排查时连到底是入口打爆、下游超时还是资源耗尽都说不清。

很多接口明明配了限流熔断,为什么还是不稳

线上不稳的来源,当然包括突发流量、下游超时、数据库抖动这些典型问题。不过真到线上看盘的时候你会发现,最麻烦的那类问题,多数时候并不在某个开关没配——系统本来就已经被带到了一个很脆的位置上。

第一类问题,是流量和资源真的扛不住。

比如热点活动、批量回放、上游误重试,把某个接口瞬间顶高;再比如线程池、连接池、数据库或者缓存命中率一起失衡。这类问题比较直观,所以大家也最容易想到限流、缓存、隔离、扩容。

第二类问题,是下游抖一下,整个调用过程跟着一起抖。

一个常见现象是:接口本身代码不重,结果一旦依赖的 RPC 服务 RT 飘高,调用线程就开始堆积;堆积一上来,入口 RT 变长,超时增多,重试变多,流量再被自己放大。到这一步,系统其实是被自己的调用链路拖进去了。

这种死法比直接被流量打爆更难防。

第三类问题,更隐蔽,也更难靠运行时手段单独救回来:接口依赖本来就接得不对。

比如核心数据流程里依赖一个非核心画像服务,少一个标签本来不该影响主结果,却被写成同步必经路径。
再比如为了方便控制,把配置查询、策略计算、页面态接口、后台开关校验一路塞进数据主流程,最后跑数据的接口反而被控制面抖动绑住。
还有一种也很常见:接口本身代码不重,真正重的是后面那串“顺手再查一下”的依赖。平时看着都能跑,一到高峰或者局部波动,这些顺手挂上去的东西就会一起把主流程拖慢。

麻烦就麻烦在这里:
你当然还能继续补限流熔断,但它们更多是在控制后果,不是在修正前面的结构性问题。

运行时这一层当然要做,但别把它当全部

运行时治理这一层当然还是要做,而且不能省。我自己平时主要看五件事。

如果把这些机制放到同一张图里,大概是下面这个关系。它们在不同位置各自兜不同的失控方式:

flowchart TD
    A["请求进入"] --> B{"是否热点 / 可缓存"}
    B -->|"是"| C["缓存命中<br/>直接返回"]
    B -->|"否"| D["限流<br/>先挡住超出承载的流量"]
    D --> E["隔离<br/>线程池 / 舱壁 / 资源池拆开"]
    E --> F["超时控制<br/>避免请求长时间挂死"]
    F --> G["调用下游"]
    G -->|"成功"| H["正常返回"]
    G -->|"超时 / 可重试错误"| R{"满足幂等<br/>且还有重试预算"}
    R -->|"是"| S["退避后重试"]
    S --> G
    R -->|"否"| I["熔断或降级"]
    G -->|"失败率 / 超时率持续升高"| I
    I --> J["降级路径<br/>默认值 / 缓存 / 精简结果 / 异步补偿"]
    D -->|"超过阈值"| K["拒绝 / 排队 / 进入降级"]
    E -->|"局部资源耗尽"| L["问题被圈在局部<br/>不拖垮整条链路"]

限流:先保护系统别被峰值直接打穿

我想先说限流。入口限流、网关限流、服务级限流,本质都在解决同一件事:别让系统一次性吞下它根本消化不了的流量。

削峰当然重要,但限流不是简单写个阈值就结束。真正难的地方是先想清楚:被限掉的到底是什么流量,核心流量和非核心流量能不能一刀切,限完以后是直接拒绝、排队,还是把它导进降级路径。

熔断:别让下游故障沿调用链扩散

熔断解决的是“明知道对面已经不行了,还继续把自己的人和资源往里送”这个问题。

这件事在线上特别重要,因为很多系统死在持续无效调用上。失败率、超时率、半开恢复这些机制本身都不复杂,难的是你得先定义好:哪些依赖值得熔,熔了以后主链路靠什么继续活。

降级:接受不完美,先保核心结果

很多系统稳不稳,经常就卡在这里:异常时能不能先给出一个次优但还能接受的结果。

返回缓存、精简字段、跳过非核心推荐、晚一点补异步结果,这些都属于降级。降级最怕平时没设计,出事以后临时想。到那时,接口通常只剩“全有”或“全无”两种状态。

超时和重试:别把恢复机制写成流量放大器

超时的意义是把等待时间收住,不让线程无限挂死。
重试也不是“失败了再试几次”这么简单,它一定要和幂等、退避、预算一起看。

我现在看重试,第一反应是“会不会把一次局部抖动放大成整个调用过程的连锁反应”。

隔离和缓存:把问题圈住,别让好链路陪葬

线程池隔离、舱壁、热点缓存、本地缓存、读写分流,看起来是在让系统更快,但更关键的作用是把风险圈在局部。

一个依赖挂了,最多拖它那一块;一个热点来了,先在局部被吃掉,不要把所有请求一起推给更贵的下游。

稳定性拉开差距的地方,还是在需求和设计阶段

只谈运行时这一层还不够。做下来的体感是,很多稳定性问题不是线上那一刻才冒出来的,而是在需求和设计阶段就已经把坑挖好了。

需求阶段:先问这个需求会不会把系统带向更不稳的方向

以前我也容易把需求评审理解成“功能能不能做”。后面做多了以后,我会先看几件事:

  • 有没有新增关键调用依赖
  • 有没有把非核心能力塞进核心接口
  • 有没有引入跨层调用或反向依赖
  • 有没有新增高频接口或高峰场景
  • 有没有把强一致、强实时、低延迟同时要满

这些问题如果一开始不拦,后面就很容易变成一种熟悉的局面:功能做出来了,接口也通了,但稳定性债是悄悄背上的。

尤其是那种“顺手再查一个服务”“顺手补一个控制规则”“顺手把页面态配置同步带进来”的需求,单看每一刀都很小,叠起来就会把主流程越带越重。

设计阶段:先把异常时怎么变差说清楚

如果只能选一个阶段多花点时间,我现在会把票投给设计阶段。

因为设计阶段最关键的,是先把异常时系统怎么变差讲清楚。至少要回答下面几件事:

  • 依赖关系怎么走,哪些依赖是主依赖,哪些只是增强依赖
  • 下游失败时主接口怎么活,哪些结果允许 fallback,哪些必须失败
  • 接口是否幂等,哪些调用允许重试,哪些重试一次都可能出事
  • 峰值流量大概在哪,容量预算怎么做,入口需不需要先限
  • 有没有可以提前缓存、异步化、削峰或者拆池的地方

我现在不太喜欢那种只画主流程、不画故障面和降级面的设计稿。因为这类设计稿往往只证明“系统正常时会跑”,但没有证明“系统异常时也不会失控”。

压测阶段:验证假设成不成立,不是看 QPS 漂不漂亮

很多设计文档写起来都成立,上了压测以后,结果未必是那样。

所以压测最关键的,其实是验证这些假设到底成不成立:

  • 峰值来了以后,入口 RT 是缓慢变差,还是突然雪崩
  • 限流有没有真的挡在入口,而不是故障已经扩散以后才触发
  • 熔断条件设得太松还是太紧
  • 重试是不是在帮系统恢复,还是在帮故障放大
  • 降级路径是不是真的能走通,还是只存在于设计文档里

这些东西如果在压测里没被打出来,很多方案就只是评审会上看着考虑过稳定性,真到线上出波动了,才知道哪些地方其实根本没兜住。

巡检阶段:看的是系统有没有在慢慢走向不稳

稳定性也绝不是上线那一刻就结束。

巡检这件事,对我来说要尽早看出哪个接口开始走形了。先看的通常还是这些:

  • 成功率
  • P95 / P99
  • 错误码分布
  • 超时率
  • 降级触发比例
  • 熔断状态
  • 关键依赖 RT 和异常率

光盯这些技术指标,很多时候还是会慢半拍。因为接口还没全面超时,业务侧往往已经先露信号了。比如创建类接口的成功提交率开始掉,回调补偿量突然变多,推荐结果空集比例往上走,导出任务越积越久,人工兜底量一点点变多。它们未必会立刻把 P99 顶上去,但很可能已经说明这个接口开始走形了。

所以业务指标最好别停在“关注业务影响”这种话上,而是直接跟接口承诺绑起来看。这个接口本来要完成什么动作,做到什么程度才算成功,真走降级了最差能接受到哪一步。先把这几个口径定出来,后面看波动时才知道这只是正常抖动,还是已经开始影响真实结果了。

如果再往前走一步,我甚至会单独盯依赖关系有没有继续变重。因为很多接口最开始是稳的,后面之所以越来越不稳,很少是因为吞吐突然暴涨,更多是“顺手再依赖一个东西”的动作一直没有停。

最后把话收回来

我现在看接口稳定性,已经不太会把问题停在“要不要配限流熔断”这一层了。

限流、熔断、降级、隔离、缓存,这些都要做,而且必须做。但它们更多是在解决:系统出了问题以后,怎么别立刻失控。

再往前看,更该盯的是:

  • 这个需求有没有把系统往更脆的方向带
  • 这个接口的依赖关系是不是已经开始失真
  • 控制面能力有没有侵入数据主链路
  • 异常时系统准备怎么有序变差,而不是整片崩掉

如果这些问题前面没有做,后面再多运行时保护,很多时候也只是把故障方式变得体面一点。

我现在更愿意把它当成一套工程约束,而不只是几项运行时配置。