工程治理
收录于 工程治理体系
领域稳定性,比接口不挂更难守
一次围绕配置归属的架构争论,让我重新理解稳定性:它不只是限流、熔断和可用性,还包括依赖关系是否健康、语义 owner 是否收敛,以及系统后面还能不能改。
前阵子我写《接口稳定性不只是限流熔断》时,主要想说的是:很多稳定性问题,多半在需求和设计阶段就已经埋下了。
不过那篇更多还是停在接口和调用这一层。有些系统接口没挂、运行时保护也做了,后面还是越来越脆。问题不只在运行时,还在领域归属、依赖关系和后续演进上。
把我往这一步推过去的,是一场很具体的架构争论。
这篇想记下的,是我后来为什么开始把“领域稳定性”也当成稳定性的一部分。
我以前对稳定性的理解,其实只看见了一半
早些年我理解稳定性,重点一直都在运行时。
接口别被流量打垮,下游抖的时候别把自己拖死,故障来了别雪崩,关键链路要有隔离,压测、巡检和监控要完整。现在回头看,这些都没有错。它们解决的是稳定性里最直观的那一层,也就是系统在线上能不能扛住。
但问题也正出在这里。
如果脑子里只有这一层,你就很容易把稳定性理解成一个纯运行时问题。好像只要把限流熔断配好,把容量做够,把告警拉全,系统就会稳。
后来我才慢慢意识到,很多系统后面越来越脆,并不是不会做运行时治理,是更早的地方已经埋了问题:
- 能力归属没想清楚
- 调用链挂错了层
- 同一语义被拆到多个系统里
- 当前方案看起来能跑,但后面越来越没人敢动
这些问题不一定会立刻把接口打挂,却会慢慢把系统推向另一种不稳定。
它在接口层面表现出来,可能只是调用过程越来越重、结果越来越难对齐、排查时越来越说不清到底卡在哪。
让我改观的,是一场很普通的方案争论
那次事情的背景并不复杂。
我们有一份配置能力,最开始放在一个对外的 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 有没有被拆散。
至少对我自己来说,这就是这次争论留下来的东西。它没有给我一个标准答案,却逼着我承认:有些系统不是坏在没做保护,而是坏在一开始就把稳定性想窄了。