前阵子我写《接口稳定性不只是限流熔断》时,主要想说的是:很多稳定性问题,多半在需求和设计阶段就已经埋下了。

不过那篇更多还是停在接口和调用这一层。有些系统接口没挂、运行时保护也做了,后面还是越来越脆。问题不只在运行时,还在领域归属、依赖关系和后续演进上。

把我往这一步推过去的,是一场很具体的架构争论。

这篇想记下的,是我后来为什么开始把“领域稳定性”也当成稳定性的一部分。

我以前对稳定性的理解,其实只看见了一半

早些年我理解稳定性,重点一直都在运行时。

接口别被流量打垮,下游抖的时候别把自己拖死,故障来了别雪崩,关键链路要有隔离,压测、巡检和监控要完整。现在回头看,这些都没有错。它们解决的是稳定性里最直观的那一层,也就是系统在线上能不能扛住。

但问题也正出在这里。

如果脑子里只有这一层,你就很容易把稳定性理解成一个纯运行时问题。好像只要把限流熔断配好,把容量做够,把告警拉全,系统就会稳。

后来我才慢慢意识到,很多系统后面越来越脆,并不是不会做运行时治理,是更早的地方已经埋了问题:

  • 能力归属没想清楚
  • 调用链挂错了层
  • 同一语义被拆到多个系统里
  • 当前方案看起来能跑,但后面越来越没人敢动

这些问题不一定会立刻把接口打挂,却会慢慢把系统推向另一种不稳定。

它在接口层面表现出来,可能只是调用过程越来越重、结果越来越难对齐、排查时越来越说不清到底卡在哪。

让我改观的,是一场很普通的方案争论

那次事情的背景并不复杂。

我们有一份配置能力,最开始放在一个对外的 web 服务里。这个服务本来就是给前端用的,前端通过它读配置、做交互,这件事本身没有问题。

后来数据链路这边也需要用这份配置。更具体一点,是数据湖和富化这一侧也想拿到同样的配置结果。

问题马上就出来了。

如果数据湖直接去调这个 web 服务,那么它在拿配置之前,就要先经过鉴权逻辑,而鉴权背后又会依赖授权服务。调用链一下就变成了这样:

flowchart LR
    DL["数据湖 / 富化链路"] -->|"同步取配置"| WEB["对外 Web 配置服务"]
    WEB -->|"鉴权依赖"| AUTH["授权服务"]
    OWNER["配置所属业务领域"] -. "语义 owner" .-> WEB

当时一起讨论的另一位架构师,态度非常明确:核心数据链路不应该依赖 web 服务,更不应该被授权服务牵住。理由也很直接,授权一旦抖,数据那一侧就会被拖进去。

如果只看调用链,这个判断非常顺。

但我当时的直觉,其实是站在另一边的。

我脑子里想的压根不是“这条路径稳不稳”,我在意的是另一件事:这份配置到底属于哪个领域。

如果它本来就属于某个明确的业务领域,那它是不是应该在这个领域里闭环?如果只是因为数据湖调用不方便,就把它拆出去,或者顺手挂到另一个服务里,那这个能力的归属还算清楚吗?

当时我甚至很排斥把它塞进 IDT 这一类服务里。原因也不复杂。IDT 在我的理解里是资产唯一性服务,它解决的是身份、归一、唯一性这一类问题。眼前这份配置只是和资产有关,但“和资产有关”不等于“属于资产唯一性领域”。

所以我那时一直很坚持一句话:

路径上的经过点,不等于领域上的归属点。

数据湖会经过 IDT,只能说明它是调用路径上的稳定节点,不能说明这份能力就该归到它那里。

我后来开始动摇,是因为我忽略了另一个同样硬的约束

后面让我开始不那么确定,并非突然觉得领域边界不重要了。更多是因为我慢慢发现,对方守住的那条原则同样很硬:

核心数据链路,不应该为外层控制面能力买单。

这个约束一旦放回那条调用链里,问题就会变得非常具体。

如果数据湖为了拿配置,必须同步依赖一个带鉴权的 web 服务,那么意味着:

  • 数据链路会直接受外层服务波动影响
  • 授权服务会变成它的间接依赖
  • 原本不属于同一层的能力,被绑进了同一条调用路径
  • 故障传播面会被放大

那一刻我才意识到,我以前的判断不是错,而是不完整。

我守的是领域边界,是语义 owner,是能力别被拆散;对方守的是调用链稳定性,是核心路径别被外层系统牵住。两边说的都有道理,也都不是在比“谁更懂稳定性”——各自保护的,是两种不同的稳定性。

后来我才把这件事想清楚:稳定性至少有三层

那次讨论之后,我才慢慢把自己脑子里的“稳定性”拆开。

以前我讲稳定性,几乎默认只在讲第一层。现在我更愿意把它至少分成三层:

1. 运行时稳定性

这一层最好理解,就是接口在线上别挂。

高峰扛得住,下游抖的时候别雪崩,超时、重试、熔断、隔离和降级都能兜住。这一层解决的是可用性问题,也是工程里最容易被显性看见的一层。

2. 依赖稳定性

这一层关心的已经不是“单个接口会不会报错”,它看的是调用关系本身健不健康。

比如谁依赖谁,底层有没有反过来依赖上层,数据面有没有依赖控制面,核心路径里是不是被塞进了不该同步依赖的东西。

很多系统挂在依赖设计上。你表面上看接口都还在,实际上调用关系已经被绑得很脆了。

3. 演进稳定性

这一层更隐蔽,也更容易被低估。

它关心的是系统后面还能不能继续改。语义是不是只有一个 owner,同一份能力有没有被拆成双接口、双实现、双来源,一次变更是不是要改多个地方,结构是不是越走越碎。

很多系统的“死”不是某天直接挂掉,而是慢慢变成没人能完整解释,也没人敢随便修改。

后来我再回头看那次争论,才想清楚我们当时在争什么。

表面上是在争配置该放哪,实际上是在争:

  • 是优先保护运行时路径
  • 还是优先保护领域归属
  • 以及这两者撞上时,到底谁先让步

再回头看几个方案,我才发现它们问题不在谁对谁错,在于复杂度被转移到了哪里

那次讨论里,大家其实看过不止一个方案。现在再看,我已经不太会问“哪个方案对”,而是先问“复杂度和不稳定会被留在哪一层”。

flowchart TB
    Q["同一个问题:数据链路怎么拿配置"] --> A["方案一:消息推送副本"]
    Q --> B["方案二:富化链路返回结果视图"]
    Q --> C["方案三:同步接口直接调用"]
    A --> A1["优点:切断同步依赖"]
    A --> A2["代价:副本维护、一致性、回放补偿"]
    B --> B1["优点:语义更集中"]
    B --> B2["代价:边界必须更克制"]
    C --> C1["优点:实现最直接"]
    C --> C2["代价:风险继续留在运行时"]

方案一:通过消息把配置推给数据湖

比如通过 Pulsar 把全量或增量配置推过去,数据湖自己维护一份副本,富化服务再从 Redis 或本地存储里读取。

这个方案最大的好处,是把同步依赖切掉了。数据侧不用每次都回头找 web 服务,也不用再被鉴权和授权流程牵着走。

但它的代价也非常明确。数据湖要开始维护一份投影,要处理全量和增量一致性,要考虑幂等、重建、回放和补偿。这些事情本来不是它的核心职责,现在却被它接住了。

也就是说,它把运行时复杂度换成了长期维护复杂度。

方案二:在富化链路里直接返回配置结果

这个方向在我看来更收一点。

配置能力还是留在原来的领域里,消费方拿到的只是一个结果视图,而不是完整的配置管理能力。这样做的好处是语义 owner 还算集中,消费方也不用再维护一整套副本。

但它对边界要求很高。因为这条路很容易一路长成“反正这里顺手把结果一起带出去”,最后富化接口什么都背一点,边界再次膨胀。

所以这个方案其实是在用更强的边界约束,换更好的语义收敛。

方案三:继续提供同步接口给数据湖调用

这个方案实现最直接,也最容易在短期里看起来省事。

不过它危险的地方也最明显:同步依赖还在,运行时风险还在,核心路径还是会被外层服务波动影响。它并没有消灭问题,只是把“先跑起来”放到了更前面。

所以这种方案我后来变得很警惕。

这件事改变的,是我对“稳定性”这个词的警惕

如果现在回头总结那次讨论,我不会说“我错了”,也不会说“对方对了”。

说再直白一点,是我以前把一个多维问题,当成了单维问题在处理。

我当时并不是在守错东西。领域边界不能乱,语义 owner 不能散,不能因为局部调用方便就改写能力归属,这些判断我今天依然认。

但我当时的问题是,我默认这套判断足够强,强到可以压过另一类稳定性问题。后来我才承认,真实系统往往不是这样。

真实系统最难的地方,通常是:

  • 一个正确,撞上另一个正确
  • 一种稳定性,撞上另一种稳定性
  • 一个复杂度被拿掉,另一种复杂度又被引进来

也正因为这样,我现在会对“这个方案更稳定”这句话多问一步。

它是真的让系统更稳了,还是只是把不稳定从运行时转移到了维护期,从调用链转移到了边界上,从短期实现转移到了长期演进里。

今天如果再有人问我什么叫稳定性,我会先问三个问题

现在我再看一个接口或者一个能力设计,脑子里会先过三个问题。

第一,这个能力的 owner 到底是谁

如果 owner 不清,后面很多收敛动作都只是在补救。接口放在哪、配置谁维护、数据谁消费,这些都能讨论,但前提是语义归属先清楚。

第二,这个依赖关系是不是合理

核心路径是不是在依赖不该依赖的东西?有没有跨层调用?有没有把控制面能力塞进数据面?有没有为了方便,把一个本该异步、离线或投影化的动作硬塞成同步依赖?

第三,这个方案是在减少复杂度,还是在转移复杂度

很多架构优化最大的陷阱就在这里。它说自己更稳定,但实际上只是把问题换了个位置。短期实现变简单了,后面维护变重了;运行时风险降了,语义却散掉了;依赖切断了,投影维护成本又上来了。

这三个问题没有想清楚之前,我现在已经不太相信“加几层限流熔断就稳了”。

这次讨论之后,我对“稳定性”这个词少了一点确定感。

以前我会先问接口会不会挂,现在我更习惯同时看三件事:运行时能不能兜住,依赖关系有没有绑错层,语义 owner 有没有被拆散。

至少对我自己来说,这就是这次争论留下来的东西。它没有给我一个标准答案,却逼着我承认:有些系统不是坏在没做保护,而是坏在一开始就把稳定性想窄了。