拆得开不等于该拆开:微服务与模块化单体的边界判据、规模阈值、团队适配与一致性代价

目录

一、到底"要不要上微服务"呀?

(一)三种错误提问,导向三种可预期的失败

(二)五个决策变量:边界、规模、团队、部署、一致性

(三)可复用的形态判定骨架

二、先把"模块化单体"这个选项摆回桌面

(一)从单一进程到彻底拆散是一条谱系

(二)模块化单体的六条硬约束

1、按业务能力纵向分模块

2、模块之间只走显式接口

3、每个模块的表私有

4、依赖方向有向且无环

5、模块级的观测口径

6、模块级的开关与灰度

(三)三种形态的收益与代价对照

(四)"模块化单体只是过渡态"这句话的三个错误

三、边界维度:边界稳定度决定该不该固化成进程

(一)边界的三种成色

1、语义边界

2、事务边界

3、变更边界

(二)把"边界清楚吗"换成四个可查的数字

(三)限界上下文在两种形态下的落地差异

(四)拆错边界的两种形态

1、分布式单体

2、分布式泥球

四、规模维度:什么量级才真的需要独立进程

(一)三种规模量纲必须分开量

(二)纵向与横向扩展的天花板到底在哪?

(三)独立伸缩的台数账

(四)规模与形态的经验对应区间

五、团队维度:康威定律的两个方向

(一)服务数与团队数的对齐关系

(二)认知负载的三种类型

(三)拆分之前的十二项底座前置检查

六、部署维度:可独立部署才是拆分的核心收益

(一)部署单元、运行单元、故障单元

(二)模块化单体里的"部分发布"能做到什么程度

(三)部署耦合的三大来源

(四)故障隔离的四个层次

(五)按发布频率与故障半径给能力分类

七、一致性维度:拆分要付的最贵一笔账

(一)本地事务的免费午餐清单

(二)跨进程写的四种解法

(三)读一致性的四种失真

(四)不能跨进程的不变量族谱

(五)把一致性代价折算成可比较的数字

八、五维合成:形态选择的打分与推演

(一)五维打分表

(二)三个场景的完整推演

(三)混合形态才是常态

(四)什么时候应该合回去

九、演进纪律:可逆的外提路径

(一)外提一个模块的七步可逆路径

(二)数据归属搬迁的三个阶段

(三)迁移期间双形态共存的四项治理

十、落地清单与常见误判

(一)二十条决策清单与十六个高频误判

(二)三种场合下的表述范式

(三)回到那句"不要为了拆分而拆分"

参考资料

形态与边界

拆分模式与迁移

数据与一致性

团队与组织

部署、韧性与可观测

工程约束与工具


干货分享,感谢您的阅读!

"我们要不要上微服务?"在早年无数技术评审上被问出来,几乎总是以一场各说各话的争论收场。

争论之所以难以收敛,因为这个问句本身就藏着两个错误假设:形态只有两个选项,并且其中一个天然更先进。真实情形要复杂得多------从一个没有内部边界的单一进程,到数十个服务加一整套自助式平台,中间至少存在六档稳定形态,而绝大多数系统的健康终态就落在中间某一档上。

我们把"要不要拆"这道形态题拆开看,起作用的只有五个变量:边界画在哪里且稳不稳、纵向扩展的天花板到没到、有没有人能独立养活拆出来的东西、独立部署的收益会不会被真的用上、以及这一刀会不会切断某个必须原子成立的约束。前四个变量投票,第五个变量拥有一票否决。

这次我们把这五个变量逐一展开成可以查证的数字与可以核对的清单:边界稳定度的四项指标、扩展路上的五道闸门、独立伸缩的台数账、服务数与团队数的对齐关系、认知负载的三种类型、底座的十二项前置检查、部署单元与故障单元的区别、本地事务的六项免费午餐、六类不可降级的不变量,最后合成一张五维打分表、三个场景的完整推演、一套可以直接借用的混合参考架构、一条七步可逆的外提路径,以及一份二十条决策清单与十六个高频误判。

拆分要买的东西只有一样------让不同的人按不同的节奏,安全地改动同一个系统的不同部分;凡是拆完之后这份自由没有增加、而远程调用的代价、一致性的代价与治理的代价都实打实增加了的切法,都是错的,无论它在架构图上多么整齐。

一、到底"要不要上微服务"呀?

(一)三种错误提问,导向三种可预期的失败

一场技术讨论的结局,往往在提问的那一刻就已经写好。形态讨论尤其如此:同一群人、同一个系统、同一批数据,换一个问法就会得到完全不同的结论,而其中相当一部分结论在事后被证明是昂贵的。

第一种错误提问是"我们该不该上微服务"。这句话的隐含假设是形态只有两个选项,并且其中一个是终点。真实情形是:单一进程与彻底拆散之间存在一条连续的谱系,中间的每一档都有稳定的适用区间。把中间档当成"还没做完"的结果是,团队会持续朝更碎的方向推进,直到运维成本吃掉全部收益------而这个过程通常没有任何一步是明显错误的,每一步都只是"再往前一点"。

第二种错误提问是"别人都拆了,我们什么时候拆"。它的隐含假设是架构形态可以横向移植。但形态是团队规模、变更节奏、数据不变量与运维底座这四个参数的函数,而这四个参数没有一个能照抄。看到一张由几百人的组织演化十年得来的终态图,然后用二十个人在半年内实现它,结局几乎总是断在最贵的那一步------通常是数据归属的搬迁,或者是补偿事务的正确性收敛。

第三种错误提问是"拆成几个服务算合格"。它把服务数量当成了可以设定的目标。但服务数量是结果而不是目标:它由边界数量决定,而边界数量由业务语义与团队结构共同决定。为了凑数把一个完整的能力劈成三段,从此每个需求都要改三个仓库、协调三次发布、追三条链路,这是把复杂度买回家的最直接方式。

那么该问什么?把"形不形态"这个宏大命题换成五个具体问题,讨论立刻就能落地:这条边界在过去三个月里移动过几次;这块能力的变更节奏与相邻能力差几倍;负责它的人是不是一个能独立决策的小组;它的部署失败会不会连带拖垮别的能力;它与相邻能力之间有没有必须原子成立的不变量。

五问之后,结论只有三种,而且都是可执行的。边界清晰、节奏分化、团队独立、不变量不跨界,那就值得外提为独立服务,按可回退的路径推进;边界清晰但节奏一致、团队同源,那就留在模块化单体内,先把模块纪律做实;边界还在移动,那就什么都不拆,先把边界谈稳。三种结论都指向具体动作,而不是一个形态名词。

(二)五个决策变量:边界、规模、团队、部署、一致性

把无数关于形态的争论压缩之后,剩下的其实只有五个变量。它们不是同等地位的:前四个是投票权,第五个是否决权。

边界维度问的是"这条线画在哪里、稳不稳"。它要看语义边界、事务边界、变更边界三者是否重合------只有三条线重合的地方,才是可以固化成进程的地方。指向拆分的信号是边界连续三个月没有移动过;指向不拆的信号是领域模型每两周还要推翻一次。

规模维度问的是"纵向扩展的天花板到没到"。它要看请求量、数据量、变更量三种量纲的增长曲线,与单机、单库、单进程上限之间还剩多少距离。指向拆分的信号是某块能力的资源画像与其余部分差一个数量级,而且加机器、加缓存、优化查询这些常规手段都已经用尽。

团队维度问的是"有没有人能独立养活它"。它要看团队数量、单团队的认知负载与值班能力。服务数与团队数应当保持在同一个量级。指向拆分的信号是两个团队在同一个部署单元里持续争抢发布窗口、互相等待测试与回归。

部署维度问的是"独立部署的收益兑现了吗"。它要看部署单元、运行单元、故障单元三者是否需要分离,以及分离之后谁真的会用上这份自由。指向拆分的信号是一块能力的发布频率与故障半径都与其余部分明显不同,并且已经造成过实际事故。

一致性维度是唯一一个可以单独否决拆分的变量。只要两块能力之间存在必须原子成立的约束------同一笔分录的借贷必须相等、同一件库存不能被两个订单同时占用、同一个手机号只能绑定一个账号------把它们放进两个进程,就意味着要用补偿事务去模拟原子性。这不是多写几行代码的事:正确性风险、对账工作量、异常人工介入的频次会一次性跳一个台阶,而收益侧通常一分钱也没多。所以这一维的结论是"锁定"而不是"建议"。

五个变量的这种不对称性值得特别强调。实践中最常见的失误,是把一致性维度当成一个可以用工程手段克服的技术挑战,与其余四维平等地放进权衡里------"虽然一致性会麻烦一些,但独立发布的收益更大"。这个句式在账务、库存、额度这类场景里几乎总是错的,因为"麻烦一些"的真实含义是长期承担资损风险与固定的人工核销成本,而这两项都不会随时间下降。

(三)可复用的形态判定骨架

与其临场发挥,不如带一套骨架进场。下面七步的顺序不是随意排的:前三步定事实,中间两步定代价,后两步定路径,顺序颠倒会让整个讨论失去共同基础。

第一步是盘点能力清单。把系统当前承担的全部业务能力列出来,每条能力写清它的输入、输出与对外承诺。这一步只做描述,不做任何合并或拆分的判断。第二步是标注三条边界。对每两条相邻能力,分别标注语义边界、事务边界、变更边界的位置,三条线重合处标为实线,分歧处标为虚线并记录分歧原因。第三步是量出四组数字:每条能力的日均调用量、数据行数、月均变更次数、峰谷比,四组数字之间差一个数量级的地方就是候选切口。

第四步是对齐团队与值班。把能力映射到实际的人:谁负责、几个人、能否独立决策、值班表怎么排。映射不上的能力先合并,不要产生无主服务。第五步是清算一致性账,逐条列出跨能力的不变量,判断每条能否降级为最终一致;不能降级的那些,把相关能力锁在同一个进程内,不参与后续拆分。

第六步是给出形态与档位,结论要写成"主体模块化单体加若干外提服务"这样的具体形态,并标明每个模块在谱系里的位置与下一档的触发条件。第七步是写可逆的路径:每一次外提都配一份回退方案------开关怎么切、数据怎么回、契约怎么兼容;写不出回退方案的外提一律推迟。

前三步"只描述、不判断"这条纪律,看起来像是形式主义,实际上是整套方法里最关键的一环。把描述与判断混在一起,是形态讨论最容易失控的地方------一旦第一句话就是"订单应该独立成服务",后面所有数字都会被下意识地挑选成支持这个结论的那一部分:调用量高的数字被强调,联动提交的数字被忽略,团队独立性被高估。先把事实摆完,判断会自己浮出来,而且各方更容易接受,因为它不是某个人的主张,而是一组共同看到的数字的推论。

第五步的一致性清算在实践中最常被后置。它经常被放到最后才考虑,而那时服务边界已经画完、团队已经分好、代码已经动工,回头改的成本比一开始就锁定高出至少一个季度。把它提前到与第一步并行,成本是半天时间,收益是省下一次跨季度的返工。

第六步的输出应当具体到可以直接进评审文档。一个合格的表述形如:本系统共识别十一条业务能力,其中账务记账与额度冻结之间存在不可降级的原子约束,二者锁定在同一进程内;商品静态信息读取的调用量比其余能力高两个数量级、变更频率低一个数量级,已顶到单库连接上限,外提为独立读服务;风控策略计算依赖专用算力、由独立小组维护、发布节奏为每日多次,外提为独立服务;其余八条能力保留在模块化单体内,按模块纪律治理,下一档的触发条件为任一模块的月变更次数连续两个月超过其余模块合计的三分之一,或负责它的小组独立成建制并具备值班能力。这样一段话有边界、有数字、有条件、有触发点,半年后可以拿出来核对,也可以被后来的人质疑与修正。

二、先把"模块化单体"这个选项摆回桌面

(一)从单一进程到彻底拆散是一条谱系

形态讨论最常见的结构性错误,是把选项简化成两个点。把谱系摊开之后,很多争论会自动消失。

第一档是混沌单体:一个进程、一个库、没有模块边界,任意代码可以访问任意表。它在十人以下、业务尚在探索的阶段是合理的------此时任何边界都还是猜测,画错的代价高于不画。第二档是分层单体:按控制层、服务层、数据访问层横向分包,纵向的业务边界仍然模糊。这是绝大多数遗留系统的现状,也是唯一一档真正意义上的"过渡态"------它的退出条件很明确,就是业务边界已经能说清楚。

第三档是模块化单体:按业务能力纵向分模块,模块间只走接口、表私有、依赖有向无环。这是十到四十人团队的主力形态。第四档是单体加外挂:主体仍是模块化单体,少数非功能要求特殊的能力外提为独立服务,这在四十到一百人的组织里最常见。第五档是服务集群:按限界上下文拆成五到二十个服务,共用一套治理底座,适合百人以上、多团队并行的组织。第六档是细粒度微服务:数十至上百个服务,配套平台团队与自助式基础设施。

沿着这条谱系向右走,买到的东西是明确的:独立部署权,一个模块的发布不再需要等待其余模块的测试与排期;独立伸缩权,可以只给热点能力加机器而不是整个进程一起复制;故障隔离,一处内存泄漏不再拖垮全部能力;技术栈自由,可以为专用负载选专用运行时。付出的东西同样明确:远程调用的延迟、序列化开销与部分失败;跨进程的一致性建设,包括补偿、发件箱与对账;治理底座的建设与维护;认知负担,一次排查要跨越多个仓库与日志源;运营成本,实例数、连接数、值班表都在增长;以及回退成本------合并两个已经上线的服务比拆开它们更难。

由此可以得到档位选择的三条纪律。第一,只在被具体痛点推动时前进一档,不要为了"到位"连跳两档。第二,每一档都要能稳定运行至少一个季度,把这一档的纪律做实再谈下一档。第三,允许回退:某个外提服务如果一年内既没有独立发布也没有独立伸缩,就该考虑合回去。

值得单独讨论的是:为什么第三与第四档最常被跳过。原因很少是技术性的。模块化单体没有名分------它在简历上不好写、在汇报里不像成果、在技术分享里不够吸引人,于是很多团队宁愿承担第五第六档的全部成本,也不愿意停在一个"听起来还是单体"的形态上。但从交付速度、故障率与人均维护负担这三个可度量的指标看,四十人以下的团队停在第三、第四档的表现,普遍好于同规模团队强行推到第五、第六档的表现。这个现象值得每一次形态评审都拿出来对照一次。

(二)模块化单体的六条硬约束

模块化单体这个选项之所以经常被轻视,一部分原因是它常被理解成"把代码整理得整齐一点"。事实上它有一组明确的硬约束,少一条它就退化成一个整洁一点的泥球------这也是"我们做过模块化单体,不好用"这类反馈的多数来源。

1、按业务能力纵向分模块

模块的名字必须是业务名词而不是技术名词:订单、库存、结算,而不是控制器、服务、仓储。判定方法很简单:随便挑一个需求,看它需要改动几个模块。健康的模块化里,八成需求只改一个模块。

反面信号是包名里出现三层技术分层,任何需求都要横穿这三层。这种结构把纵向的业务切成了横向的三片,是分布式单体在单进程里的预演------它的问题不在于以后拆不动,而在于现在就已经拿不到模块化的收益:影响面无法圈定,回归范围只能全量。

2、模块之间只走显式接口

每个模块对外暴露一个明确的接口层,其余类型全部包内可见,不对外开放。跨模块调用只允许指向接口,不允许指向实现类、领域对象或内部工具。

反面信号是一个模块能直接构造出另一个模块的领域实体并修改它的字段。这类访问不会立刻出问题,但它会让被访问模块失去对自身不变量的控制权------它的领域对象可以被任何人置入任意状态,此后所有关于该模块正确性的推理都不再成立。

3、每个模块的表私有

表按模块前缀归属,跨模块不允许直接查,更不允许写。这是六条里最贵也最关键的一条。跨模块取数只能通过接口,联表查询由调用方在内存里组装,或者建一份显式的查询侧视图。

反面信号是一条查询语句里出现两个模块的表名。这是模块化单体退化最常见的起点,原因也很好理解:绕过接口直接联表在当时几乎总是更快、更省事,而它的代价要等到几个月后有人想改表结构时才显现。这一条也是六条里最值得用工具而不是评审来守的------把跨模块表访问做成构建期的检查,比每次评审靠人眼发现可靠得多。

4、依赖方向有向且无环

模块依赖必须画成有向无环图,并由构建工具或架构约束测试强制校验,而不是靠评审时的自觉。落地手段是多模块工程加依赖白名单,或者用架构约束测试断言禁止的包间引用。

反面信号有两个:两个模块互相引用,以及出现一个所有模块都依赖的"公共模块"并且不断膨胀。后者更隐蔽也更危险------公共模块一旦承载了业务语义,它就成了一个隐形的中心节点,任何改动都会波及全部模块,而依赖图上看不出这一点。

5、模块级的观测口径

日志、指标、追踪都带模块标签,能按模块回答"这一分钟谁在消耗资源、谁在报错"。落地手段是把统一的模块标识注入日志上下文,指标名带模块维度,慢查询按模块归集。

反面信号是进程整体的错误率能看到,但说不出这些错误分别归哪个模块。这一条的缺失会让前四条无法度量------半年之后没人知道模块纪律还在不在,也没人能证明某个模块确实需要外提。

6、模块级的开关与灰度

每个模块的新逻辑都能被单独开关控制,出问题时关掉一个模块而不是回滚整个进程。落地手段是模块粒度的特性开关与配置隔离,必要时把模块的入口做成可降级的空实现。

反面信号是任何一次上线出问题,唯一的处置手段是整体回滚到上一个版本。这一条的缺失意味着模块化带来的隔离只存在于代码结构里,运行期一点也兑现不了。

这六条为什么必须成套?因为它们互相支撑。没有第三条,第二条会被绕过------反正可以直接查表,何必调接口;没有第四条,第一条会退化------模块名还在,依赖已成一团;没有第五条,前四条无法度量,半年后没人知道纪律还在不在;没有第六条,模块化带来的隔离只存在于代码结构里。

做实这六条之后,团队实际上已经拿到了两件很值钱的东西。第一件是边界的真实成本被暴露出来:哪条边界一改就疼、哪条边界形同虚设,半年内会自己显形,而不需要靠争论去猜。第二件是外提的成本被大幅降低:一个满足六条的模块要变成独立服务,主要工作只剩下把接口调用换成远程调用、把表迁到独立库,领域逻辑几乎不需要动。

(三)三种形态的收益与代价对照

把传统分层单体、模块化单体与微服务集群放在同一张表上逐维对照,最直观的结论是:模块化单体拿到了微服务的大部分收益,代价只有一小部分。

在交付维度上,模块化单体能按模块圈定影响面、回归范围可控,这一点与微服务基本持平,而且它的本地启动成本仍然是秒级,微服务在这一项上明显吃亏。在发布粒度上微服务确实占优------按服务独立发布、互不等待,模块化单体只能整体发布,但可以按模块开关灰度与关闭。

在伸缩维度上微服务占优明显:按服务独立扩缩,资源利用率最高,技术栈也可以每服务独立选型。模块化单体只能整体复制,但仍然可以按模块限流隔离、按模块选库与算法。这一维是拆分收益最实在的地方,前提是不同能力之间的资源画像确实有差异。

在可靠性维度上情况变得微妙。微服务拿到了进程级隔离,单服务故障不直接扩散;但它同时多出了超时这一不确定态,需要幂等与补偿。模块化单体一处崩溃仍会拖垮进程,但它的失败语义是二值的------成功或抛异常,语义确定。哪一边更可靠,取决于团队更擅长处理哪一类问题。

数据维度是模块化单体的绝对优势区。跨能力写一致性靠本地事务、免费;跨能力查询用接口组合或查询侧视图,在进程内组装。微服务在这一维需要补偿事务或发件箱,还要配对账与人工兜底。组织维度上,模块化单体按模块并行,三四十人仍可控;认知负载上它只需要理解本模块加相邻接口,而微服务虽然也只需要理解本服务,排障却要跨多个系统。成本维度上模块化单体只需一套流水线与监控外加架构约束测试,微服务则是注册、配置、追踪、网关、发布,每服务一套值班与告警。

这张对照表的意义不在于评分高低------它并不是要证明某个形态更好------而在于看清每一维的收益归谁、代价归谁。当有人说"微服务更好"时,可以具体地问:更好在哪几维,而这几维对我们当前的痛点是否相关。

(四)"模块化单体只是过渡态"这句话的三个错误

"我们现在是模块化单体,明年就拆成微服务"------这类表述在评审里出现的频率很高,它掩盖了一个可以长期驻留的稳定形态,也让团队持续朝一个自己并不需要的方向消耗预算。

第一个错误是把它当成时间问题。形态前进的触发条件是具体痛点,不是日历。团队规模、变更节奏、数据量都没变,明年拆分的理由与今年一样不成立。正确的表述方式是写出触发条件而不是时间点。

第二个错误是把它当成能力问题。"还没拆是因为我们基础设施不够成熟"这句话把底座不足当作唯一障碍,隐含承认了拆分本身一定正确。实际上即便底座完备,只要边界未稳、团队未分化,拆分依然是净亏损。

第三个错误是把它当成面子问题。"对外总不好说我们还是个单体"是唯一一条与技术无关、却最常起决定作用的理由。它会让一个健康形态被主动放弃,换来一份没人真正需要的复杂度。

区分过渡态与稳定态的判据其实很清楚:过渡态的定义是存在一个明确的、可观测的退出条件,达到即离开。分层单体是过渡态------它的退出条件是"业务边界已经能说清楚"。模块化单体不是过渡态:它的退出条件是团队规模与变更节奏跨过某个阈值,而绝大多数系统一辈子都不会跨过那个阈值。

把这件事说清楚的一种表述是:"模块化单体是我们当前规模下的目标态,不是中间态。我们为它定义了六条纪律并在构建阶段强制校验;同时为每个模块定义了外提的触发条件与回退方案。任一模块满足触发条件时,我们会把它外提为独立服务;不满足时,把它留在进程内是更省钱、更可靠的选择。"这段话既不否认演进的可能,也不承诺一个没有理由的未来,它把形态从一个身份问题变回了一个工程问题。

三、边界维度:边界稳定度决定该不该固化成进程

(一)边界的三种成色

"边界要清晰"是形态讨论里最容易达成共识、也最难落地的一句话。难点在于"边界"这个词至少指三种不同的东西,而它们经常并不重合。只看其中一条就下刀,是拆分之后返工率最高的一类决定。

1、语义边界

语义边界指的是同一个业务名词在两侧指的不是同一个东西。识别方法很朴素:把两侧的人叫到一起描述同一个名词,看他们关心的字段是否明显不同。

以商品为例。在商品管理上下文里,它是类目、属性、图文详情的集合,关注信息的完整性;在交易上下文里,它退化为标题与价格的成交快照,关注成交时刻的不可变性;在库存上下文里,它只是一个可用数量的计数器键,连标题都不需要;在履约上下文里,它是有体积、重量、包装要求的物理实体;在推荐上下文里,它是一个特征向量。

误用的后果是强行做一个统一的商品模型,字段膨胀到上百个,四方都不满意------每一方都要处理另外三方的字段,每一次某一方的需求变更都会改动这个所有人共用的模型。

2、事务边界

事务边界问的是两侧的写操作是否必须原子成立。识别方法是问一句"这两件事只成一半会怎样"。如果答案是"对账时会发现,人工修一下就行",那么可以分开;如果答案是"客户会少钱或多货",那么不能分开。

下单与扣减库存必须原子,否则会超卖;下单与发送通知不必原子,补发即可。这条判据的价值在于它给出的是二元结论,不需要权衡------把必须原子的两侧拆开,从此就要用补偿去模拟原子性,长期承担资损风险。

3、变更边界

变更边界问的是两侧的改动是否总是同时发生。识别方法比前两条更客观:翻最近三个月的提交记录,统计同时改动两侧的提交占全部提交的比例。比例低于一成,说明两侧变更独立;高于三成,说明它们其实是一件事。

营销规则每天改、商品基础信息每周改,两者的提交几乎不重叠,变更边界清晰。反过来,如果两块能力总是同时被改,拆开它们的结果是每个需求都要两个仓库、两次发布、两轮联调。

三条边界的关系可以组合成四种情况,对应四种完全不同的处置。三线重合,可以外提为独立服务;语义分而事务合,留在进程内用模块隔离;语义合而变更分,同模块内分包、暂不外提;语义分而变更合,先查是不是领域模型没有对齐。

这里有一个值得反复强调的偏差:最常被单独使用的是语义边界。原因是领域划分的语言最丰富、最容易讲得漂亮,于是评审里常常只谈语义。但语义边界恰恰是三条里最不需要进程隔离的一条------同一个进程内用模块与接口就能把语义隔开,代价接近于零。真正需要付出进程代价才能获得的,是变更边界带来的独立发布;而事务边界的作用恰恰是拦住这一刀。

一条可以直接执行的检查:取最近九十天的提交记录,按模块两两统计"同一提交同时改动"的次数,画成一张热力矩阵。矩阵里颜色深的格子说明这两个模块的变更边界不成立,把它们拆到两个服务里必然产生跨仓库联动;颜色浅的格子才是候选切口。这份矩阵比任何领域划分的讨论都更难被立场影响,因为它的输入是已经发生过的事实。

(二)把"边界清楚吗"换成四个可查的数字

"边界清楚吗"是一个主观判断,不同的人给出的答案会系统性地偏向自己希望的结论。把它换成四个可以查出来的数字,讨论就有了共同基础。

第一个指标是跨模块联动提交率。口径是最近九十天内,同时改动两个模块的提交数占涉及这两个模块提交总数的比例;查法是按模块目录过滤提交,两两求交集;达标线是低于百分之十。不达标意味着这两个模块其实是一件事,拆开会把每个需求变成跨仓库联动。

第二个指标是接口契约变更频率。口径是模块对外接口的签名、字段语义、错误码在最近两个季度的变更次数;查法是对接口定义文件做变更历史统计;达标线是每季度不超过两次破坏性变更。不达标意味着契约还在探索期,固化成远程接口会让每次调整都要跨团队协商。

第三个指标是名词歧义度。口径是同一个核心业务名词在两个模块中对应的字段集合的重叠比例;查法是把两侧的模型字段列出来求交并集;达标线是重叠低于三成即语义确实不同。不达标意味着两侧说的是同一个东西,拆开只会产生两份需要同步的重复模型。

第四个指标是依赖环与公共膨胀。口径是模块依赖图中的环数量,以及被所有模块依赖的公共模块的代码量占比;查法由架构约束测试或依赖分析工具直出;达标线是环数为零、公共模块占比低于一成。不达标意味着边界名存实亡,此时拆分只是把一团环形依赖搬到网络上去。

四项指标里有任何一项不达标,正确的动作不是硬拆,而是在进程内演练边界。演练分五步:先把候选边界落成一个模块目录,代码搬进去,先不改逻辑;再把跨模块的直接引用逐个换成接口调用,并加上约束测试;然后把表按模块前缀归属,删掉所有跨模块联表查询;接着给模块加上独立的指标与错误率,观察它的真实负载;最后,上述四步稳定运行一个季度、四项指标全部达标,才动手外提。

这套演练顺序为什么能省下最贵的一笔钱?因为拆分中最贵的两个决定是服务边界与数据归属,而它们的错误只有在上线并被上下游依赖之后才会显形。在进程内演练的关键价值是把这两个决定的验证成本,从"跨团队协商加数据迁移"降到"一次重构加一次构建"。边界画错了,在模块阶段改一次目录结构就行;在服务阶段改,要协调多个团队改接口、要做双写与回填、要在两个季度里维持两份真相。同一个错误,代价差两个数量级。

这也是模块化单体最被低估的一项收益:它让边界成为一个可以反复试错的决定,而不是一个一次定终身的决定。

(三)限界上下文在两种形态下的落地差异

一个常见的误解是:限界上下文是微服务的方法论,单体不需要它。事实恰好相反------上下文划分的功课在两种形态下完全一样,差别只在于用什么机制守住它、守不住时代价有多高。

边界的定义处,在模块化单体里是模块目录与包可见性,在微服务里是独立仓库与服务边界。边界的守卫者,前者是构建工具的依赖白名单与架构约束测试,后者是网络隔离、鉴权与接口发布审批。跨界调用形式,前者是接口方法调用、编译期即可发现签名不符,后者是远程调用或消息、运行期才发现不兼容。

跨界失败语义的差异最值得注意:前者是成功或抛异常的二值语义,后者多出了超时这一不确定态,需要幂等与补偿。数据归属的守卫,前者靠表前缀约定与禁止跨模块联表的评审规则,后者靠独立数据库或独立账号,物理上无法跨库查------这是微服务在守卫强度上真正胜出的一处。

上下文映射的实现,前者是适配器类做模型转换、转换成本接近于零,后者是防腐层加序列化、每次调用都要付转换成本。契约演进方式,前者是一次编译器检查、改完即知所有调用点,后者要走两阶段兼容发布、需要等上游全部升级。边界画错的纠正,前者是重构目录、一次构建验证,后者要双写、校验、切读、下线,跨越多个迭代。

从这张对照里可以得出一条容易被忽略的推论:"做不好模块化就更做不好微服务"这句话是成立的,但理由不是通常说的"能力不足"。真正的理由是:微服务并不会替你把边界划对,它只会让划错的边界更难改回来。模块化单体用编译期与构建期守边界,成本低但可以被绕过;微服务用网络与部署守边界,绕不过去但每次跨界都要付运行期的代价,而且一旦画错,纠正的成本高出一到两个数量级。

(四)拆错边界的两种形态

边界画错之后,系统会呈现两种典型的病态。它们的表面症状有重叠,但成因相反,处置方向也完全相反------把它们混为一谈会让治理动作彻底跑偏。

1、分布式单体

分布式单体的特征是边界画对了,但切得太碎,或者切在了同一条变更链上。它的症状有七条:任何一个业务需求都要同时改动三个以上服务;服务必须按固定顺序发布,谁先谁后写在文档里;一个服务的集成测试需要拉起全部依赖才能跑;调用链深度超过五跳,尾部延迟被逐层放大;几个服务共用一个数据库,只是代码分开了;某个服务单独发布必然引发另一个服务的报错;全部服务的版本号必须锁在同一个大版本上。

它的处置方向是合并。既然边界正确,问题只是粒度过细或切在同一条变更链上,那就把总是一起改、总是一起发的服务合回去。具体做法是按"最近半年内联动发布次数"排序,把排名最前的两个服务合并成一个部署单元,模块边界保留在进程内。合并通常比外提更容易,因为契约本来就对。

2、分布式泥球

分布式泥球的特征是边界根本没画对,只是把原来的混乱搬到了网络上。它的症状同样有七条:说不清某个数据的权威副本到底在哪个服务;服务之间互相调用,依赖图上有明显的环;存在一个所有服务都依赖的公共服务并持续膨胀;同一个业务名词在多个服务里各有一份不同定义;查一个业务问题要同时看五个服务的日志;接口参数里带着调用方内部实现的细节;修一个缺陷不知道该改哪个服务,只能全都改。

它的处置方向是先退回进程内。边界没画对时,任何在网络层面的努力都是在错误的地基上施工。做法是把相关服务先合回一个进程,在进程内重新划模块、重新定归属、把依赖环打断,等四项边界指标达标之后再考虑外提。这一步在组织上很难承认,但继续在网络上重构的成本会随依赖数量的平方增长。

两种形态有一个共同点:它们都在为拆分付钱,却都没有拿到拆分的收益。拆分要买的东西只有一样------独立变更的自由。分布式单体没有独立变更,改一处要动三处;分布式泥球没有独立理解,看一处要读五处。

由此可以得到一个非常省事的判断指标:一个需求平均改动几个服务,一次排障平均要打开几个服务的日志。两个数字都接近一,拆分就是成功的;都大于三,形态就走错了。这两个数字不需要任何工具就能统计,也很难被立场影响,值得作为形态健康度的常规观测项。

四、规模维度:什么量级才真的需要独立进程

(一)三种规模量纲必须分开量

"我们的量很大"是形态讨论里最常见的一句失真表述。量有三种,它们的天花板不同、先撞到的位置不同、解法也不同,混着谈就会加错机器、拆错刀口。

请求量看的是每秒调用次数与峰谷比。它的天花板在单进程的连接数、线程数与算力上限,而通常先撞到的是下游连接池。先试的手段是加机器横向复制整个进程------这一招对模块化单体同样有效,且几乎不需要改代码,这是最常被忽略的事实。真正需要拆的情形是:某一块能力的调用量比其余部分高一到两个数量级,导致整体复制时绝大部分算力浪费在冷区上。

数据量看的是行数、单表体积与增长斜率。它的天花板在单库的连接上限、单表的索引维护成本、备份与变更窗口的时长。先试的手段是加索引、归档冷数据、读写分离、分区表------这些手段能把单库寿命延长很久,成本远低于拆分数据归属。真正需要拆的情形是:某一块数据的体积与访问模式与其余部分完全不同,例如日志型写入与交易型写入抢同一份磁盘带宽。

变更量看的是月均提交数与发布次数。它的天花板在主干冲突频率、回归测试时长、发布窗口的排队长度。先试的手段是把回归测试并行化、把发布从火车制改成随时可发、把模块开关做实------这三招能把并行度提高很多。真正需要拆的情形是:某一块能力的发布频率比其余部分高一个数量级,且它的每次发布都要拖着其余部分一起走完全流程。

三种量纲错配会产生三个真实后果。把数据量问题当请求量问题:加了一倍机器,数据库连接更快耗尽,故障提前到来。把变更量问题当请求量问题:拆了服务、加了机器,发布依然要排队,因为瓶颈在回归测试而不是算力。把请求量问题当变更量问题:为了一块高频读的能力拆出一个服务,结果它与主体的发布节奏完全一致,独立发布的自由一天也没用上。

所以在讨论形态之前,先在进程内用尽五条路径:加机器横向复制,对无状态服务几乎总是有效;加缓存,把热点读挡在数据库之前;优化查询与索引,单条慢查询往往贡献大半负载;异步化,把不需要同步返回的工作挪出主链路;归档与分区,把冷数据从热路径上移走。这五条都用尽了还不够,才轮到讨论拆分。

(二)纵向与横向扩展的天花板到底在哪?

把扩展路上会遇到的限制按出现顺序排开,一共五道闸门。它们的重要性质是:前四道都不需要改变形态,只有第五道真的需要。而多数系统在撞到第五道之前就宣布"单体扛不住了",真正拦住它的其实是前面某一道。

第一道是单机算力与内存。表现是中央处理器持续高位或频繁的内存回收停顿;解法是换更大规格的机器,或横向复制无状态进程。为什么不需要拆:进程复制与形态无关,模块化单体同样可以部署二十份,负载均衡照常工作。

第二道是下游连接与线程。表现是数据库连接池排队、线程池拒绝、下游被打满;解法是调池参数、加连接复用、把慢调用异步化。为什么不需要拆:连接总量取决于实例数乘以池大小,拆成多个服务往往让连接总量更难控制,而不是更容易。

第三道是单库读写与索引。表现是慢查询增多、写入延迟上升、变更窗口拉长;解法是加索引、读写分离、归档冷数据、分区表。为什么不需要拆:这些手段对单体与微服务同样有效,而且不需要先解决数据归属这个最贵的问题。

第四道是发布与回归节奏。表现是主干冲突频繁、回归时长超过一小时、发布排队;解法是测试并行化、按模块灰度、去掉发布火车。为什么不需要拆:这是流程与工具问题,不改流程就拆服务,只会得到十份同样缓慢的流水线。

第五道是组织并行度上限。表现是多个团队在同一部署单元里长期互相等待,任何改动都要跨组协商,责任无法归属。这一道闸门确实要靠形态解决------把部署单元拆开,让每个团队掌握自己的发布节奏。

跳过前四道闸门的代价是什么?拆分是一种非常昂贵的性能手段:它把一个可以靠参数、索引与机器解决的问题,换成了一个需要重新定义数据归属、重建一致性机制、重建观测链路的工程。常见的错误顺序是看到延迟上升就直接立项拆服务,半年后发现瓶颈其实是一条没有索引的查询。正确顺序是从左到右逐道排除,每排除一道就重新量一次;多数系统在第二或第三道就已经拿回足够的余量。

第五道闸门的判定方式与前四道不同:它不是一个性能指标,而是一个协作指标。有三个可查的信号。第一是跨组等待时长:一个改动从写完到上线,其中有多少时间花在等别人测试、等别人排期上。第二是发布窗口争抢次数:一个月里有多少次因为别的团队在发布而推迟了自己的上线。第三是责任归属模糊次数:一个月里有多少次事故的第一步是先争论"这该谁修"。三项都高,才是形态问题;只有一项高,通常是流程问题。

(三)独立伸缩的台数账

"拆分之后可以按需伸缩"是拆分收益里被引用最多的一条,但它需要算出来才算存在。算完之后经常发现,收益不是省了机器,而是省了某一类机器。

考虑五个场景。场景甲是均匀负载:四个模块的调用量与资源画像都接近。模块化单体按总量算需要十二台;拆分之后四个服务各三台,合计仍是十二台,另加网关与注册中心。结论是拆分净亏------台数不减,反而多出底座开销。

场景乙是单点热区:一个模块占八成调用量,其余三个各不足一成。模块化单体按热区算需要四十台,冷区的算力全部浪费;拆分之后热区三十六台、三个冷区各两台,合计四十二台。结论是接近持平------省下的冷区算力被实例底噪吃掉了。

场景丙是画像相反:一个模块吃内存不吃算力,一个模块吃算力不吃内存。模块化单体必须按两项的上限各自取大,取高配机型三十台;拆分之后内存型八台大内存机、算力型十六台高频机,合计二十四台。结论是拆分明显划算------机型可以按画像分别选择。

场景丁是峰值错峰:一个模块白天峰值,一个模块夜间批处理。模块化单体按两个峰值之和常备二十台;拆分之后日间型高峰十二台、低谷两台,夜间型反向弹性,均值九台。结论是拆分明显划算------弹性可以按各自曲线独立设置。

场景戊是可用性分级:一个模块挂了整站不可用,一个模块挂了只影响报表。模块化单体必须按最高等级为全部模块付费,多可用区、冗余两倍,二十四台;拆分之后核心十六台跨区冗余、报表四台单区,合计二十台。结论是拆分划算------可用性成本不必按最高档一律支付。

从五个场景里读出的规律是:独立伸缩产生收益的前提,是两块能力的资源画像、时间曲线或可用性等级之间存在显著差异。画像相同的能力拆开之后,台数不会减少,只会多出每个实例的固定底噪与一整套治理组件的常驻开销。所以"为了性能而拆"这句话必须补完------是为了让不同画像的负载能分别定价而拆。

还有一个容易被忽略的反向成本:拆分之后每个实例都要常驻一份运行时、一份连接池、一份缓存、一份监控代理。四个服务各三台,意味着这些固定开销要付十二份而不是三份。在小规模下这份底噪往往超过省下的算力------一个原本三台机器的模块化单体,拆成四个服务之后经常需要八到十二台才能维持同样的余量与可用性等级。

(四)规模与形态的经验对应区间

把研发人数、典型调用量与数据量放在一起看,形态的合理档位就基本确定了。下面的区间是经验值而非定律,它的用途是提供一个需要被反驳的起点,而不是一个需要被遵守的规则。

三到十人、每秒数十到数百次、单表百万级:合理形态是模块化单体、单进程单库;此时最该做的是把业务边界谈清楚、把模块目录建起来;最不该做的是拆服务与自建治理底座。

十到四十人、每秒数百到数千次、单表千万级:合理形态是模块化单体外加零到两个外挂服务;此时最该做的是把六条模块纪律做实、把回归与发布加速;最不该做的是按团队人数决定服务数。

四十到一百人、每秒数千到数万次、单表亿级或分库:合理形态是单体加外挂,或五到十个服务;此时最该做的是按变更边界外提热点、建统一的观测与发布;最不该做的是一次性拆成几十个服务。

一百到三百人、每秒数万次以上、多库多分片:合理形态是十到三十个服务的服务集群;此时最该做的是建平台能力与自助底座、定契约与兼容纪律;最不该做的是让每个小组自选技术栈。

三百人以上、跨地域多业务线:合理形态是细粒度微服务外加领域级聚合层;此时最该做的是把平台团队做成产品、按域治理而非按服务治理;最不该做的是用一套标准硬套全部业务线。

这张表最容易被误用的方式有两种。第一种是把人数当成唯一自变量。人数只是一个粗代理,真正起作用的是"独立决策的小组数量"。三十人分成一个组与三十人分成六个组,合理形态完全不同:前者应当停在模块化单体,后者已经具备外提两三个服务的条件。第二种是把区间当成进度条。看到自己在某一行,就认为下一步必然是走向下一行。人数增长并不自动产生拆分需求------如果新增的人都加入同一个团队做同一块能力,形态不需要变,需要变的是模块内部的分工。

正确用法是把对应的那一行读成一个默认值,然后逐项检查自己的实际情况与默认值不符的地方,把不符之处写进决策文档。凡是偏离默认值的选择都需要一条具体理由;找不出理由的偏离,通常就是"为了拆分而拆分"。

五、团队维度:康威定律的两个方向

(一)服务数与团队数的对齐关系

服务数与团队数的比值是形态健康度最灵敏的一个指标。它有一个明确的健康区间:一个团队负责一到三个服务,且每个服务都有明确的第一责任人。偏离这个区间的两个方向各有固定的症状与处置。

服务数远大于团队数(比值超过三)时的症状是:多数服务处于无人熟悉的状态,改动前先要考古;值班时一个人要背五个服务的告警;依赖升级永远排不上期,安全补丁长期滞后;文档与代码的偏离程度随时间单调增长。处置是按"最近半年联动发布次数"把服务合并,直到每个服务都有一个能说清它全貌的人。

服务数与团队数同量级(比值一到三)是健康区间。表现是每个团队掌握自己的发布节奏,跨团队协商只发生在接口变更时;一次排障通常只需要打开一到两个服务的日志;新人两周内能独立完成本服务的一个需求。此时的处置是维持现状,把精力放在契约纪律与观测能力上,而不是继续调整服务数量。

服务数远小于团队数(比值小于一)时的症状是:多个团队在同一个部署单元里争抢发布窗口;一个团队的缺陷会阻塞另一个团队上线;代码评审跨越业务边界,评审人看不懂改动;事故复盘的第一步总是先争论责任归属。这是唯一一种由团队维度单独支撑的拆分理由------按团队边界外提,一次只提一个。

康威定律在这里有两个方向,都需要考虑。正向是系统结构会趋向于组织的沟通结构:硬把服务边界画在组织沟通不畅的地方,边界会被绕过;画在组织沟通顺畅的地方,边界会自然被维护。反向是想要某种系统结构,就先调整组织结构。反向这一条比正向更有用,也更少被使用------先把团队按目标边界重组,再让代码跟着走,成功率高得多。

组织调整时还有一个有效的自检问题:如果明年组织架构调整,现有的服务边界需要跟着改吗?答案是"需要",说明当初的边界画的是人员分工而不是业务语义,它会随每次组织调整而失效;答案是"不需要",说明边界画在了业务上,组织可以在边界之上自由重组,这才是可维持的。

新增一个服务之前,有四件事必须先说清,否则不要新增。第一是第一责任人------一个具体的人名,而不是一个团队名。第二是值班覆盖------非工作时间它的告警响给谁、多久内响应。第三是变更授权------谁有权批准它的接口破坏性变更。第四是退役条件------什么情况下这个服务应该被合并或下线。这四件事在新增服务时写清,比任何架构图都更能防止服务数量失控,因为它把无主服务的产生成本前置了。

(二)认知负载的三种类型

认知负载是形态选择里最实在的约束,也是最难量化的一项。把它分成三类之后,拆分的净效果会变得清楚:拆分同时降低了一类负载、抬高了另两类,净效果取决于团队当前哪一类已经溢出。

本质负载是业务问题本身的复杂度:定价规则有二十七种组合、结算口径按地区各不相同。这一类无法削减,只能承担,它是团队存在的理由。形态对它的影响是:拆分不改变总量,但可以把它分配给不同的团队,让每个团队只承担其中一部分。这是拆分在认知维度上的全部收益。

附带负载是为了完成工作而额外要懂的东西:注册中心的保护阈值语义、链路追踪的采样配置、发布系统的分批策略、补偿事务的重试与幂等约定。拆分会显著增加这一类------一个原本只需要懂业务与数据库的团队,现在还要懂七八个治理组件,而这些知识不产生任何业务价值。

交互负载是为了协作而要维护的关系与上下文:要记住哪个接口由谁负责、改动要通知哪几个团队、上游的兼容窗口还有多久、对方的发布节奏是每天还是每周。拆分会增加这一类,且增速快于服务数量------服务两两之间都可能产生关系,关系数接近服务数的平方,这是复杂度失控的主因。

把交互负载降下来有三种团队交互模式可用。协作模式是两个团队高频共同工作,适合边界还没找到的探索期,代价是双方都要理解对方。服务模式是一方以明确契约向另一方提供能力,交互被压缩成接口,这是稳定期的目标模式。辅助模式是一方短期帮助另一方掌握某项能力,用完即退出,用于底座能力的扩散。三种模式的关键在于不要长期停在协作模式------协作模式的交互负载最高,它只应当是通往服务模式的过渡。

有一条可以直接问出来的判据:问团队成员"你花在业务逻辑上的时间占多少"。健康的比例在七成以上。如果答案是三四成,其余时间都在处理调用超时、追踪断链、版本兼容、跨团队对齐,说明附带负载与交互负载已经溢出;此时继续拆分会让这个比例继续下降,而业务产出不会增加。

反过来的情形也要识别。如果团队成员的时间几乎全在业务逻辑上,却持续抱怨"改一个字段要等两周才能上线""别人的缺陷阻塞了我",说明本质负载已经按边界分好,卡住的是部署耦合。这种情形下的拆分是净收益:附带负载的增量换来的是本质负载的兑现速度,这笔交易通常是划算的。

(三)拆分之前的十二项底座前置检查

底座不是拆分之后再补的东西------它是拆分的入场券,缺一项就把成本推给值班的人。下面十二项每一项都应当在外提第一个服务之前就已经具备。

统一的服务注册:调用方能在不改配置的情况下找到被调用方的健康实例。配置的集中下发:改一个开关不需要重新打包,且改动有审计与回滚。密钥与凭据托管:服务间身份可验证,密钥不落在代码与镜像里。结构化日志采集:所有服务的日志汇聚到一处,可按请求标识串联。

统一的指标口径:延迟分位、错误率、饱和度三类指标的定义在全局一致。分布式链路追踪:一次请求跨越的全部跳数可视,且上下文不会断链。自动化流水线:新增一个服务的流水线是模板化的,而不是手工搭建。渐进式发布能力:支持分批、按比例灰度与一键回滚,且不需要人工改配置。

健康检查与探针:存活与就绪两类探针语义分离,摘除延迟可量化。统一的容器编排:实例的调度、重启、扩缩由平台负责,而不是靠脚本。契约管理与校验:接口定义有单一来源,破坏性变更能在合并前被拦住。值班与告警路由:每个服务的告警都有明确的接收人与响应时限约定。

缺项的代价不是"不方便",而是"不可运维"。缺链路追踪,一次跨服务故障的定位时间从分钟级变成小时级,因为没有任何一处能看到完整的调用路径。缺契约校验,破坏性变更会在上线那一刻才被发现,而此时上游已经在生产环境上调用它了。缺渐进式发布,每次上线都是全量替换,一个缺陷立刻影响全部流量,回滚要等下一个构建。缺告警路由,新服务的告警发到一个没人看的群里,故障要等用户投诉才被发现。

有一条更省钱的顺序值得强调:把这十二项在模块化单体阶段就建起来,是被反复验证的省钱做法。原因是其中至少八项在单进程形态下同样有用------日志、指标、配置、密钥、流水线、灰度、探针、告警------而在单进程形态下建设它们的难度低得多,只有一个部署单元要接入,试错成本接近于零。

等到十个服务同时上线才开始建底座,接入工作要做十遍,而且每一遍都在生产环境上进行。这也是"先模块化再服务化"这条路径最实际的理由,与架构美学无关。

六、部署维度:可独立部署才是拆分的核心收益

(一)部署单元、运行单元、故障单元

三种单元不是同一件事,而拆分只直接改变第一种。把三者混为一谈,是"拆了却没拿到隔离"与"没拆却拿到了隔离"这两种意外的共同来源。

部署单元指一次发布同时替换的代码范围。在模块化单体里,整个进程是一个部署单元,但可以用开关让某个模块的新逻辑不生效;在微服务里,每个服务是独立部署单元,可以只替换其中一个而不影响其余。这是拆分直接改变的唯一一种单元,也是拆分要买的核心商品。

运行单元指共享同一份资源与运行时的范围。在模块化单体里,全部模块共享堆内存、线程池、连接池,一个模块的资源失控会影响全部;在微服务里,各服务独占资源,但共享节点、网络与下游存储,隔离并不彻底。值得注意的是:模块化单体也能做部分运行隔离------按模块拆线程池与连接池即可,成本很低。

故障单元指一次故障会同时影响的业务范围。在模块化单体里,进程崩溃会影响全部模块,但逻辑故障可以靠模块开关就地切除;在微服务里,进程级故障被隔离,但会出现级联失败------下游超时把上游线程全部占满。关键结论是:拆分把故障从"同时全崩"换成了"逐个级联",而不是消除了故障扩散。

三种单元可以组合成四种诉求,想清楚自己要的是哪一种,往往能省下一次拆分。只想要独立发布,拆部署单元即可;若发布频率差异不大,先改流程比拆服务便宜。只想要资源隔离,在进程内按模块拆线程池与连接池,或部署多份同一制品分流。只想要故障切除,把模块入口做成可降级的开关,出问题时关模块而不是回滚进程。三者都要且已经痛过,这才是外提为独立服务的适用场景,且要配齐底座与回退方案。

最常见的一次误判是这样的:"我们要拆服务,因为某个模块的内存泄漏拖垮过整站。"这条理由乍看充分,但它要的是运行隔离与故障切除,而这两件事都有更便宜的做法:把该模块的内存密集逻辑移出主路径、给它单独的线程池与限流、把它的入口做成可开关的降级实现。这三步通常一个迭代就能完成,而外提服务要跨越数据归属、契约、观测与值班四个工程。如果做完这三步问题依然存在,那时候再外提,理由才是充分的,而且外提的工作量也已经被前三步削掉一半。

(二)模块化单体里的"部分发布"能做到什么程度

独立发布是拆分的核心收益,但"独立发布"这个诉求其实包含好几件不同的事,其中大部分在模块化单体里也能做到。它们都不需要拆分进程,但都需要在模块纪律之上再加一层运行期控制。

第一种手段是模块级特性开关。做法是新逻辑写在开关之后,上线时默认关闭,按比例或按人群逐步放开,出问题立刻关掉。拿到的是发布与生效解耦------代码上线不等于行为变更,回退时间从一次构建降到一次配置推送。限制是开关会累积成技术债,必须有清理纪律:每个开关登记创建时间与预期下线时间。

第二种手段是同制品多份部署。做法是同一个制品部署成两组实例,用网关或负载均衡把不同流量导向不同组,例如读流量与写流量、线上流量与后台批处理分别落在不同组。拿到的是运行隔离与独立伸缩,且不需要拆代码。限制是所有组共享同一份代码与同一次发布,它解决的是运行期隔离,不解决发布节奏差异。

第三种手段是模块级灰度批次。做法是把发布过程按模块的重要度分批推进------先在一台实例上启用某模块的新版本行为,观察其指标。拿到的是把"整体替换"的风险降到单模块粒度,配合模块级指标可以在一分钟内判断是否继续。限制是需要模块级观测口径先建好,否则灰度过程中看不出是哪个模块出了问题,等于没有灰度。

第四种手段是模块入口可降级。做法是每个模块的对外入口都准备一份降级实现------返回缓存、返回默认值、或明确返回暂不可用。拿到的是故障切除能力:某模块异常时关掉它,其余模块继续服务,效果接近服务级熔断。限制是降级路径必须被定期演练,否则真正需要时才发现降级实现自己也依赖了那个已经挂掉的下游。

四种手段合起来覆盖了什么?独立发布诉求里最常被提到的四件事是:改动能不能只影响一块能力、出问题能不能只回退一块、热点能不能单独扩容、某块能力挂了能不能不影响别的。这四件事在模块化单体里分别对应上面的开关、灰度、多份部署与降级入口,都不需要跨进程。剩下真正做不到的只有一件:两个团队完全互不干扰地按各自节奏发布------这一件才需要拆开部署单元。

但这四种手段也有共同的代价:它们都把复杂度从部署阶段挪到了运行阶段。开关组合、灰度批次、降级路径都是运行期状态,会让"当前系统的实际行为"变得不那么显而易见。因此必须配三件事:开关的集中清单与过期清理、灰度状态在监控面板上的显式展示、降级路径的定期演练。缺了这三件,模块化单体会退化成一个行为不可预测的系统,此时它的可靠性反而低于一个朴素的整体发布。

(三)部署耦合的三大来源

"拆了进程就解耦了"这个直觉经常落空,原因是部署耦合有三个来源,而拆分只对其中一个半有效。先定位耦合来自哪一处,再判断这一处是不是只有拆分才能解决。

第一个来源是流程耦合,表现形式是发布火车、统一窗口、集中审批。症状是无论代码怎么组织,上线时间都由一个统一的窗口决定,改一行也要等到下周二。拆分对它几乎无效------十个服务共用一个发布窗口,等待时间一分钟也不会减少。更便宜的解法是把发布权下放到团队,把统一窗口改成随时可发加自动化门禁与自动回滚。识别方法是统计一个改动从合并到上线的时长,看其中有多少是纯等待、与代码规模无关。

第二个来源是共享库耦合,表现形式是公共模块、共享领域对象、统一版本。症状是改一个公共库要让所有使用方一起升级,否则版本不一致,于是升级变成一次集体行动。拆分对它部分有效,但常常反向恶化------服务多了之后,公共库的升级协调成本更高。更便宜的解法是把公共库的范围压到最小,只放真正无业务语义的工具,并允许多版本共存。识别方法是看公共模块的代码量占比与它在最近半年的变更次数,两个数都高就是耦合源。

第三个来源是数据库耦合,表现形式是共享表、跨模块联表、集中迁移。症状是一次表结构变更要协调多个使用方,变更脚本必须与多份代码的发布顺序严格对齐。拆分只有在同时拆开数据归属时才有效------只拆代码不拆库,耦合原封不动。更便宜的解法是先在同一个库里按模块归属表、禁止跨模块访问、把变更改成向后兼容的两阶段。识别方法是查有多少张表被两个以上模块写入,这个数字就是数据耦合的规模。

三者的排查顺序应当从最便宜的开始:先查流程耦合,因为它最常见、最便宜、也最容易被误诊成架构问题;再查共享库耦合,它可以靠收缩公共模块范围缓解;最后查数据库耦合,它是唯一一个真的需要跟着形态一起解决的。顺序倒过来做,会先付出最贵的代价,然后发现瓶颈其实在第一项------这是"拆完了发布还是很慢"这类抱怨最常见的成因。

有一个可以量化耦合程度的复合指标值得引入。取最近九十天的全部上线记录,对每次上线统计两个数字:它涉及几个部署单元,以及它从合并到全量生效经过了多少小时。把这两个数字分别求中位数,得到一对坐标。健康形态的坐标是"一个部署单元、数小时内生效"。"三个以上部署单元、数十小时生效"说明存在严重的部署耦合。"一个部署单元、数十小时生效"说明耦合在流程上,拆分帮不上忙。"三个以上部署单元、数小时生效"说明流程很好但边界画错了,该做的是合并而不是继续拆。

(四)故障隔离的四个层次

进程隔离带来了什么、带不来什么,是形态讨论里最容易被含糊处理的一处。把故障隔离拆成四个层次之后,拆分的真实贡献就清楚了:它只覆盖第二层。

第一层是代码层隔离,隔离对象是编译期的可见性与依赖方向,由模块边界与架构约束测试提供。拆分不需要即可获得,且拆分之后仍然需要在服务内部维持。缺失时的表现是任意代码访问任意实现,边界只存在于架构图上。

第二层是进程层隔离,隔离对象是堆内存、线程、类加载、崩溃,由独立进程或独立容器提供。这是拆分直接提供的一层,也是它在可靠性上的全部直接贡献。缺失时的表现是一个模块的内存泄漏或死循环会让整个进程不可用。

第三层是调用层隔离,隔离对象是等待、重试与线程占用的扩散,由超时预算、熔断、舱壁、限流提供。拆分反而制造了这一层的需求------不拆时没有远程等待,也就不需要这一层。缺失时的表现是下游变慢把上游线程占满,故障沿调用链逐级放大,最终全站不可用。

第四层是数据层隔离,隔离对象是存储、连接与写入争用,由独立数据库或独立账号与配额提供。拆分不自动带来这一层------共库拆服务是最常见的"假隔离"。缺失时的表现是一个服务的慢查询耗尽连接,所有共库服务同时不可用,与拆没拆无关。

共库拆服务这件事值得单独说。很多团队的第一步拆分是"代码拆开、数据库先共用",理由是数据搬迁太贵、以后再说。结果是进程隔离拿到了,数据隔离没拿到,而绝大多数真实故障的根因在数据层------连接耗尽、锁等待、慢查询。于是拆分之后可用性没有改善,团队反而多背了远程调用的失败模式与一整套治理组件。由此可以得到一条相当实用的判据:如果这一刀不打算连数据一起切,多半不值得切。

第三层是拆分的隐藏账单。不拆的时候,一次模块间调用是方法调用,不存在等待;拆开之后,每一次跨界都可能超时,于是必须为每一个调用点配超时预算、重试策略、熔断阈值与舱壁配额。这份工作量与服务对数量成正比,且它是纯附带负载------对业务能力没有任何增益,只是为了把拆分带来的新失败模式重新压回可控范围。把它算进拆分预算,很多边界上的拆分就不再划算了。

(五)按发布频率与故障半径给能力分类

前面四节讨论的是判据,这一节给出一个可以直接操作的分类工具。把每条能力按发布频率与故障半径标一个坐标,四个象限对应四种形态处置。

高频发布、小故障半径:例如营销文案、活动规则、推荐权重。处置是优先外提为独立服务,或至少做成配置驱动、可随时发布的模块。理由是收益最大而风险最小,这是外提的第一批候选,通常也最容易做。

高频发布、大故障半径:例如下单主链路、支付路由、风控拦截。处置是留在核心进程内,但把发布做成开关驱动的小步灰度,逐步降低半径。理由是这一类最需要独立发布,也最不能承受远程调用带来的新失败模式。

低频发布、小故障半径:例如内部报表、对账工具、后台配置。处置是保留在模块化单体内即可,必要时按资源画像单独部署一份实例。理由是外提的收益接近于零,而每个服务的固定运营成本是实打实的。

低频发布、大故障半径:例如账务记账、额度与授信、库存台账。处置是锁定在核心进程内,用本地事务保证不变量,任何拆分都要专项论证。理由是拆它的收益(低频发布用不上)与代价(一致性要自建)完全不对等。

这张四象限图的正确用法是给每条能力标一个坐标,而不是给整个系统标一个形态。把系统的全部业务能力逐条填进四个象限,得到的通常不是"全在某一格",而是一个分布。分布本身就是形态结论:左上格里的能力构成外提候选清单;右下格里的能力构成必须锁在一起的核心;左下格里的能力构成模块化单体的主体;右上格里的能力是最需要投入工程精力去降低故障半径的部分------降半径的手段是幂等、状态机与可回滚设计,而不是拆分。

所以"要不要拆"这个问题在填完这张图之后会自动变成"这七条能力里哪三条先外提",而后者是一个可以排期、可以验收、可以回退的具体工作。这个转换本身就是形态讨论最需要的产出。

七、一致性维度:拆分要付的最贵一笔账

(一)本地事务的免费午餐清单

单进程里有一批能力是数据库顺手提供的,代价接近于零,以至于很容易被当成理所当然。拆分之后,这些能力都要自己重建一次,而且每一件都要写代码、建监控、配人力。

原子性:单进程里一个事务里的多条写要么全成、要么全不成,回滚由数据库负责;跨进程之后需要补偿事务或事务性发件箱,并为每个动作设计可逆的反向操作。重建成本是每条跨服务写链路一到两周,外加长期的对账与人工兜底工作量。

一致的读视图:单进程里一次查询看到的是某一时刻的一致快照,不会读到写了一半的状态;跨进程之后不同服务的数据处于不同时刻,聚合出的视图可能自相矛盾。重建方式是为关键视图定义一致性口径,或建独立的查询侧模型并接受延迟。

跨表约束:单进程里唯一约束、外键、检查约束由数据库强制,写入时立刻失败;跨进程之后约束跨越了库,只能靠应用层校验,且校验与写入之间存在竞态窗口。重建需要引入分布式锁或唯一性服务,并处理它自身的可用性问题。

顺序与可见性:单进程里写完立刻可读,顺序与提交顺序一致,不需要考虑传播延迟;跨进程之后事件传播有延迟且可能乱序,写后立读会读到旧值。重建方式要么在写入方保留读路径,要么给客户端加版本号与等待逻辑。

调试与复现:单进程里一个堆栈就能看到完整调用路径,一次断点可以走完全部逻辑;跨进程之后需要链路追踪把片段拼起来,本地复现要么录制回放要么拉起全套依赖。重建成本是追踪接入与采样治理,外加每次排障的额外时间。

重构安全网:单进程里改接口签名有编译器兜底,所有调用点在构建时暴露;跨进程之后契约变更要走两阶段兼容发布,且要等全部上游升级完才能收尾。重建成本是契约管理、兼容校验、版本收敛的长期治理工作量。

这份清单的用途是把"拆分成本"从一个形容词变成一份可以估算的工时。在形态评审里,拆分的成本经常被表述为"会复杂一些"。把这六项逐条对照本次拆分涉及的写链路与查询视图,成本会立刻变成一个可以排期的数字。经验上,一条跨服务写链路的完整重建------补偿、幂等、发件箱、对账、告警、人工处置流程------在两到四周之间,且它的维护成本会长期存在。用这个数字乘以本次拆分涉及的链路条数,再与拆分要买的"独立发布自由"做对比,多数争论会在十分钟内收敛。

(二)跨进程写的四种解法

如果一条写链路确实必须跨进程,那么有四种主流解法,它们的工程量、正确性风险与适用边界各不相同。但选型的第一步不是挑哪一种,而是确认这条链路是否真的必须跨进程。

同库本地事务:把相关写操作放在同一个库、同一个事务里提交。工程量接近于零,数据库直接提供;没有分布式风险,但要求两块能力共库。这是不变量不可降级时的首选,也是默认答案。

事务性发件箱:业务写与消息记录同事务落库,由投递器异步读出并发送。工程量中等,一次基建投入可复用;残余风险是消息可能重复投递,消费方必须幂等。这是写方为权威、下游只需被通知时的标准解法。

编排式补偿:由一个流程引擎按步骤推进,失败时按逆序调用补偿动作。工程量较大,每步都要设计反向动作;残余风险是补偿本身可能失败,需要重试与人工兜底。适用于多方参与、步骤明确且每步都可逆的场景。

协同式补偿:各服务监听事件自行推进,没有中心协调者。工程量较大且难以观测,流程散落在各服务里;残余风险是全局状态无人掌握,排障与对账困难。只有在步骤少、参与方少、流程稳定时才考虑。

两阶段提交:由协调者统一预提交与提交,参与方在期间持有资源。工程量中等但运行代价高,需要支持该协议的资源;残余风险是协调者故障时资源长期挂起,吞吐显著下降。适用于跨库强一致且吞吐要求不高的少数场景。

选型之前应当先做两件事。第一件是确认不变量是否真的不可降级。很多被当作强一致的约束,在业务上其实允许短暂不一致加事后纠正------例如积分发放、等级升降、统计计数。把它们降级为最终一致,四种解法里最便宜的那一种就够用了。第二件是确认这条链路是否真的必须跨进程。如果两块能力本来就应该在同一个部署单元里,第一行的解法就是答案,整套补偿工程都不必存在。这一条最常被跳过。

补偿型方案还有三个真实难点值得预先知道。第一,不是所有动作都可逆:短信已经发出、优惠券已经被使用、账已经出到对方账户,逆向操作在业务上不存在;这类动作必须排到流程最后,或者改成先冻结后生效。第二,补偿也会失败:补偿链需要自己的重试、告警与人工处置入口,这部分工作量常常被漏算。第三,中间态可见:补偿完成之前,用户可能看到一个自相矛盾的视图,需要明确定义这段时间对外的表述。

(三)读一致性的四种失真

拆分之后查询变难,往往比写变难更早被用户感知。每一种失真都有对应的处置,但处置都要付出延迟、成本或复杂度中的一项。

写后立读读到旧值:成因是写落在权威服务,读走的是另一份冗余副本或查询侧模型,传播尚未完成。用户感知是刚改完的资料页面还是旧的,刚下的单在列表里查不到。处置是关键路径的写后读回权威服务,或客户端带版本号等待副本追上。

聚合视图自相矛盾:成因是一个页面从三个服务取数,三份数据对应三个不同时刻,拼在一起互相打脸。用户感知是订单显示已支付、金额却还是待付款状态,或总数与明细条数不相等。处置是为该视图定义一致性口径并显式标注数据时刻,或建统一的查询侧模型。

跨服务分页与排序失真:成因是分页与排序需要全局有序,而数据分散在多个服务里,无法一次排完。用户感知是翻页时出现重复条目或漏项,按某字段排序的结果与预期不符。处置是把该查询所需字段冗余到一处,或改成游标分页并接受排序维度受限。

统计口径分裂:成因是同一个指标在两个服务里各算一次,过滤条件与时间窗口的定义出现偏差。用户感知是两个页面上的同一个数字不一样,运营与财务对不上账。处置是把指标定义收敛到单一来源,其余位置只允许引用不允许重算。

为什么读的问题往往先暴露?因为写链路的不一致有对账与补偿兜底,且发生频率低;读链路的不一致每一次页面刷新都可能出现,而且直接被用户看到。所以拆分之后最先收到的投诉通常不是"钱算错了",而是"页面上的数字对不上"。

这一点在形态评审里几乎总被低估:评审会花很长时间讨论分布式事务,却很少有人问"拆完之后这个首页由几个服务拼起来、每份数据分别落后多久"。

由此得到一条实用的设计纪律:在决定拆分之前,把系统里"用户会同时看到多份数据"的页面列出来,逐个标注拆分后每份数据的来源服务。任何一个页面如果需要从三个以上服务取数,就要为它预先设计查询侧模型,并把这份工作量算进拆分预算。如果这样的页面很多,说明当前的边界划分与用户的使用场景不匹配------用户的一次操作总是横跨多个边界,这本身就是边界画错的一个强信号。

(四)不能跨进程的不变量族谱

有六类约束一旦跨界,正确性风险与工程量会同时跳一个台阶。识别它们的统一方法是问"只成一半会怎样",答案里出现钱、货、身份或法律责任的,就不要跨。

记账等式类:形式是一笔业务的多条分录借贷必须相等,任何时刻账套都要平。跨界后果是出现单边账,对账差异需要人工逐笔核销,且差异会随时间累积。处置是把账务相关的全部写操作锁在同一进程、同一事务内,不接受任何拆分理由。

库存与额度类:形式是可用量减去占用量不得为负,同一份资源不能被两个请求同时占用。跨界后果是超卖、超额授信,直接产生资损,且事后无法通过补偿完全恢复。处置是占用与释放在同一进程内完成;确需跨界时用预占加租约,不用事后补偿。

唯一性约束类:形式是一个手机号只能绑一个账号,一个身份证只能开一个户。跨界后果是并发窗口内产生重复主体,后续合并涉及历史数据,代价极高。处置是唯一性校验与写入放在同一事务,由数据库唯一索引兜底而非应用层判断。

状态机迁移类:形式是状态只能按允许的边迁移,且同一时刻只能有一次成功的迁移。跨界后果是状态回退或跳跃,出现已退款又发货、已取消又支付这类矛盾态。处置是迁移的判定与落库在同一事务里,用条件更新而不是先查后写;跨界时必须带幂等键与前置状态条件。

权限与身份类:形式是一次操作的鉴权结论与它产生的副作用必须基于同一时刻的授权状态。跨界后果是授权被撤销之后仍在执行的批量操作产生越权副作用。处置是授权时刻与生效时刻的间隔要能被观测,长任务需要中途重新校验。

法定留痕类:形式是业务动作与审计记录必须同时存在,不允许有动作而无记录。跨界后果是合规审计无法自证,且缺失的记录事后无法补造。处置是留痕与业务写同事务,或采用发件箱保证至少一次且可去重。

把这份族谱前置到形态讨论的第一步,能省下最多的返工。实践中它常常被放到最后:边界画完、团队分好、代码开始搬,才有人问"那记账怎么办"。此时的选择只剩两个------要么推翻已经做完的边界,要么接受用补偿去模拟原子性并长期承担资损风险。两个选择都很贵。

正确顺序是把六类不变量在盘点能力清单的同时列出来,把它们覆盖的能力先标成"锁定",剩下的能力才进入边界、规模、团队、部署四个维度的讨论。这一步花半天,能省下之后至少一个季度。

(五)把一致性代价折算成可比较的数字

抽象的"复杂度更高"很难支撑决策。把同一条业务链路在两种形态下的账单列出来,讨论就有了可比较的基础。以"下单同时扣减库存并记一笔预收账"这条链路为例。

首次实现工时:同进程本地事务约两到三人日,一个事务方法即可;跨服务补偿方案约十到二十人日,含补偿、幂等、发件箱。首次投入相差约五倍,且跨服务方案更难评审。

需要新增的组件:前者无;后者需要消息中间件、投递器、对账任务、告警规则。每个组件都带来自己的可用性与运维负担。

异常路径数量:前者一条,即事务回滚;后者至少六条------预占失败、投递失败、消费失败、补偿失败、超时未知、重复消费。异常路径数量决定了测试用例与值班手册的规模。

日常对账工作:前者不需要;后者需要每日定时对账外加差异人工核销,这项工作长期占用固定人力,且规模随业务量增长。

故障时的处置:前者回滚发布即可;后者需要判断中间态、决定继续推进还是补偿回退,常需人工介入。处置复杂度直接影响故障恢复时长。

新人上手成本:前者读一个方法;后者读流程定义、消息约定、幂等表与对账逻辑。认知负载差异会长期反映在交付速度上。

换来的收益:前者无额外收益;后者是库存能力可独立发布、独立伸缩、独立值班。

这张账单的结论不是"不要拆",而是"拆之前先确认收益会被用上"。最后一行是全表的关键:跨服务方案的全部代价,换来的是三项自由------独立发布、独立伸缩、独立值班。

如果库存能力的发布节奏与主体一致、资源画像与主体接近、值班仍由同一批人承担,那么这三项自由一项都没有兑现,而上面六行的代价一项也没有减少。这就是"为了拆分而拆分"在账面上的具体形状:成本全付,收益空置。

反过来,如果库存能力每天发布数次、峰值负载是主体的十倍、由一个独立小组值班,这份账单就是划算的。所以形态评审的最后一个问题永远是:拆完之后,谁会真的用上这份自由,以及从什么时候开始用。

八、五维合成:形态选择的打分与推演

(一)五维打分表

把五个维度各自打零到二分,合计八分以上考虑外提,四到七分留在进程内并补齐短板,三分以下不讨论。分数不是结论,而是决定下一步做什么。

边界维度:零分是领域模型仍在频繁调整、跨模块联动提交超过三成;一分是边界大致清楚但契约每季度仍有破坏性变更;两分是四项边界指标全部达标且连续三个月未移动。

规模维度:零分是前四道闸门都没撞到、加机器与索引仍有余量;一分是撞到某一道闸门但常规手段尚未用尽;两分是资源画像与其余部分差一个数量级且手段已用尽。

团队维度:零分是同一个小组负责全部能力;一分是名义上两个小组但决策与值班仍然共用;两分是独立成建制的小组、能独立决策并独立值班。

部署维度:零分是发布节奏与主体一致、耦合主要来自流程;一分是发布节奏有差异但进程内开关已能覆盖;两分是发布频率差一个数量级且已因互相等待出过事故。

一致性维度:零分是存在不可降级的不变量,此项零分即整体否决;一分是不变量可降级但涉及多条写链路与对账;两分是不跨越任何不变量,跨界只需被通知。

总分对应的处置是明确的。八到十分:外提为独立服务,按可回退路径推进,先提一个观察一个季度。五到七分:留在进程内,把得分最低那一维当作本季度的改进目标。三到四分:留在进程内,把精力放在模块纪律与观测口径上。零到二分,或一致性维度为零分:不讨论拆分,讨论怎么把这块能力在进程内做好。

这张表真正的作用不在算分,而在暴露分歧的位置。同一个团队里的两个人给同一块能力打分,分歧几乎总是集中在一到两维上。把分歧定位到"你认为团队维度是两分、我认为是一分",讨论就从"该不该上微服务"这种无法收敛的命题,变成"这个小组是否具备独立值班能力"这种可以查证的事实问题。

绝大多数形态争论之所以旷日持久,正是因为双方在不同的维度上各自成立却从未对齐维度。打分表的价值就是强制对齐维度,然后让证据而不是立场决定结果。

(二)三个场景的完整推演

同一套判据在不同上下文里会给出三种不同的答案。推演过程比结论更值得复用------每一步都写清依据,结论才经得起下一次质疑。

场景一是十二人的成长期业务。边界一分:核心流程半年内改过三次,两个主要模块的联动提交占两成五。规模零分:峰值每秒二百次,单表六百万行,加索引后余量充足,闸门一二三都未撞到。团队零分:全员一个小组,共用值班。部署一分:发布每周两次,冲突偶发,耦合主要来自集中的回归测试时长。一致性一分:下单与库存需原子,其余可降级。合计三分。处置是建模块目录、断跨模块引用、把回归并行化,把发布从每周两次改成随时可发。

场景二是六十人的多线业务。对营销与风控这两块能力,边界两分:联动提交低于半成,契约两个季度未破坏性变更。规模两分(对风控):风控吃专用算力,画像与主体完全不同,已顶到单机上限。团队两分:营销与风控各有独立小组与值班。部署两分(对营销):营销每天发布数次,已多次因主体回归未完成而推迟上线。一致性两分:两块都只需被通知,不跨不变量。合计十分,但仅对这两块成立。处置是先外提营销,观察一个季度后再外提风控;其余九块保持不动。

场景三是两百人的平台化组织。边界两分:十四个限界上下文已稳定两年以上,契约有单一来源并强制兼容校验。规模两分:多库分片,各域画像差异显著。团队两分:十一个独立小组,各自值班。部署两分:各域每天多次发布,互不等待。一致性一分:账务域内部锁定为一个部署单元,其余跨域写走发件箱与对账。合计九分。处置是维持服务集群,重点不再是继续拆分,而是把平台能力做成自助产品、按域治理,并把三个联动发布最频繁的服务合并回去。

三个场景有三个共同点。第一,没有一个场景的结论是"全部拆开"。规模最大的场景三,结论里甚至包含把三个服务合并回去。第二,打分都是按能力逐条进行的,而不是给整个系统打一个分------场景二的十分只对营销与风控两块成立。第三,处置都写成了具体动作与观察周期,而不是一个形态名词。"先外提营销,观察一个季度再提风控"这样的表述才可以排期、可以验收、可以中途停下。

推演里最容易出错的一步是把团队维度的分数给高了。这一维的两分要求是"独立成建制、能独立决策、能独立值班",三个条件同时满足。实践中经常出现的情况是:名义上分了小组,但技术决策仍由同一个人拍板、非工作时间的告警仍由同一批人接。这种状态下外提出去的服务,会在半年内退化成一个无人熟悉的远程模块------它有独立的仓库与流水线,却没有独立的主人,这是运维负债最常见的来源。

(三)混合形态才是常态

把前面所有判据落到一张具体的架构上,得到的通常是一个混合形态:核心模块化单体加三类外挂服务。这套形态在四十到一百人的组织里最常见,它把拆分的收益集中花在三个确实需要的地方。

核心是一个部署单元、一个主库的模块化单体,内部按业务能力分模块,模块间只走接口,表按模块归属,依赖有向无环。其中账务、支付、库存这几个模块之间存在不可降级的不变量------记账等式、库存不超卖、额度不超发------它们被锁定在同一事务内。

第一类外挂是高频变更型:营销活动与文案、推荐权重、实验分流。外提理由是发布频率比主体高一个数量级,且故障半径可控。交互方式是主体同步调用,调用失败时降级为默认策略。

第二类外挂是专用资源型:风控模型推理、图像处理、报表计算。外提理由是资源画像与主体完全不同,需要独立机型与独立弹性。交互方式是同步调用配超时与熔断,或异步提交。

第三类外挂是外部适配型:第三方支付通道、物流对接、短信通道。外提理由是依赖外部系统的不稳定性,需要把它挡在主体之外。交互方式是主体只依赖自定义的稳定契约。

共用底座包括配置中心、密钥托管、统一日志与指标、链路追踪、流水线、渐进式发布、告警路由。它在模块化单体阶段就建好,外提时直接复用,这是这套形态的关键。

为什么核心里有八个模块,外挂却只有三个?因为外挂的数量不由模块数量决定,而由"五维打分表里得两分的维度"决定。八个模块里,被一致性维度锁定的三个不参与拆分讨论;剩下五个模块的发布节奏与资源画像都与主体接近,四个投票维度里最多各得一分------它们留在进程内是净收益。被外提的三类能力有一个共同点:它们要么变更节奏与主体差一个数量级,要么资源画像完全不同,要么依赖一个自己无法控制的外部系统。这三类恰好覆盖了拆分能买到的三样东西:独立发布、独立伸缩、故障隔离。

这套形态有三条纪律。第一,外挂服务的数量要能用一只手数出来,每多一个都要能说清它对应五维打分表里的哪两维得了两分。第二,外挂服务不持有核心不变量------它们可以有自己的数据,但不参与账务、库存、额度这类原子约束。第三,主体对外挂的每一次调用都要有降级答案,外挂不可用时主链路仍然成立,这是外提的前置条件。

它与"先拆再合"路线的区别值得对比。常见的失败路线是先按领域一次性拆成十几个服务,上线后发现联动过多,再花一年合并回去。这套形态的思路相反:默认全部留在进程内,只有被具体痛点推动的能力才外提,且一次只提一个。代价是它看起来不够彻底、不容易在汇报里讲成故事;收益是每一次外提都有明确理由,且都可以回退。

(四)什么时候应该合回去

合并也是形态演进的一部分。允许回退的形态设计,才能让每一次外提成为低风险的实验而不是不可逆的承诺。有六个信号提示应该把服务合回去。

联动发布常态化:最近半年内,两个服务有超过一半的上线是必须同时进行的。这说明它们共用同一条变更链,边界画在了链条中间。

自由从未被使用:外提一年以来,它既没有独立发布过(节奏与主体一致),也没有独立扩缩过(画像与主体一致)。收益一项都没兑现。

无主状态出现:原来的负责人已经离开或转岗,现在没有任何人能说清它的全貌,改动前必须先花时间考古。

跨界调用过密:两个服务之间的调用量与内部调用量同一量级,且大量调用是细粒度取数,网络与序列化开销占比显著。

一致性长期挂账:它与主体之间的对账差异长期存在,每月都需要人工核销,且差异规模随业务量同步增长。

认知成本倒挂:排查一个业务问题平均要打开三个以上服务的日志,团队花在链路拼接上的时间超过花在业务逻辑上的时间。

合并的五步做法比外提简单,但同样需要按步骤来。第一步对齐契约:把被合并服务的接口在主体内实现一份同签名的本地实现。第二步切流量:调用方从远程调用切到本地实现,用开关控制,可随时切回。第三步并表:把数据表迁入主库并保留模块前缀,双写校验后停掉旧库写入。第四步留模块:被合并的服务在主体内保留为一个独立模块,纪律不放松。第五步下线:确认无流量与无写入之后,下线旧服务的实例、流水线与告警。

为什么合并在组织上比技术上更难?合并在技术上通常比外提简单:契约本来就是对的,数据搬迁方向明确,回退路径清晰。真正的阻力在组织层面------合并容易被读成"承认当初拆错了"。

降低这层阻力的一个有效做法,是在外提的那一刻就把退役条件写进文档:"若一年内该服务未发生独立发布或独立扩缩,则触发合并评审。"把合并预先定义成计划的一部分,它就不再是一次否定,而是一次按约定执行的调整。这一条写下来的成本几乎为零,省下的却是未来最难迈过的一步。

九、演进纪律:可逆的外提路径

(一)外提一个模块的七步可逆路径

路径的设计目标不是快,而是让每一步的失败都不需要回滚上一步。七步走完,每一步都能独立上线,也能在十分钟内切回。

第一步定契约:把模块的对外接口整理成一份显式契约,包含方法、字段语义、错误码与可重试性。此时仍是本地接口,调用方无感知。回退方式:不需要回退,纯增量整理。

第二步断直连:把所有绕过接口的直接引用与跨模块联表查询逐个替换掉,并加上架构约束测试。这一步通常是全程工作量最大的一步。回退方式:不需要回退,改动只增强约束。

第三步搬数据:把模块的表迁到独立库或独立账号,先双写、再比对、再切读、最后停旧写。过程中主体仍然可以直接读旧库。回退方式:切读开关回拨,旧库仍为权威。

第四步起进程:用同一份代码起一个独立进程,暴露远程接口,但线上流量仍走进程内的本地实现。此时是双实现并存,新进程只接影子流量。回退方式:停掉新进程即可,无任何影响。

第五步切流量:把调用方的本地调用替换为远程调用,按比例灰度,并配好超时、重试与熔断。全程对比两条路径的返回结果与延迟。回退方式:开关切回本地实现,秒级生效。

第六步删旧路:确认远程路径稳定运行一段时间之后,删除主体内的本地实现与相关依赖。这一步之后才算真正完成外提。回退方式:从此需要重新实现,成本上升。

第七步移交所有权:把仓库、流水线、告警、值班与变更授权正式移交给负责它的小组,并登记退役条件。没有完成这一步的外提只是搬了代码。回退方式:走合并流程,按预定条件触发。

第六步是这条路径上唯一的单向门。前五步都可以低成本回退:契约整理与断直连是纯增强,数据搬迁保留旧库为权威,新进程可以随时停掉,流量切换由开关控制。只有删除本地实现之后,回退才需要重新写代码。因此第六步应当延后到远程路径经历过一次真实的流量高峰与一次真实的故障演练之后再执行,不要为了"把代码删干净"而提前迈过这道门。

第七步最常被漏掉,代价也最隐蔽。很多外提在第五步或第六步之后就被宣布完成,仓库虽然独立了,但值班仍归原来的团队、变更授权仍在原来的负责人手里、告警仍发到原来的群里。这种状态下拆分的收益(团队自治与独立值班)没有兑现,代价(远程调用与治理成本)却全额支付。判断第七步是否真正完成的方法很简单:非工作时间这个服务报警,第一个被叫起来的人是谁。

把这条路径写进计划时要给出三个数字。第一个数字是每一步的预计工期:定契约三到五天、断直连一到三周(取决于跨模块引用的数量)、搬数据一到两周、起进程两到三天、切流量一周(含灰度观察)、删旧路一天、移交所有权半天加一次交接评审。第二个数字是每一步之后的观察周期:切流量之后至少观察一个完整的业务周期(通常是一周,含周末峰谷),删旧路之前至少经历一次真实高峰。第三个数字是并行外提的数量上限,建议为一------同时外提两个模块时,一旦出现问题很难判断是哪一次外提引入的,而串行推进的额外时间成本远低于一次归因失败的代价。

(二)数据归属搬迁的三个阶段

代码可以一次搬完,数据必须分阶段,因为它在搬迁过程中始终要对外提供正确答案。这是拆分里最贵的一段工程,也是最容易被低估的一段。

阶段一是双写并存,旧库为权威、新库为影子。动作是写路径同时写入旧库与新库,读路径仍然只读旧库,新库的写入失败不阻塞业务、只记录告警。要点是双写不能放在同一个事务里,否则新库的抖动会直接影响主链路------改成主写成功后异步补写,并记录差异。退出条件是新库写入成功率稳定在高位,且连续数日无新增结构性差异。

阶段二是比对与回填,目标是把两份数据的差异收敛到零。动作是跑全量比对任务,逐条核对两库的关键字段,并对历史数据做一次回填。差异要分三类处置:时序差异、逻辑差异、遗漏字段,三类的处理完全不同。要点是比对任务本身要能重复运行且可断点续跑,全量比对通常要跑很多轮。退出条件是全量比对差异为零,且增量比对连续一周差异为零。

阶段三是切读与停旧写,权威副本转移、旧库降为备份。动作是读路径按比例切到新库,观察业务指标与数据正确性;全量切读之后再停掉旧库的写入,并保留旧库一段时间。要点是切读要按开关分比例进行并保留一键切回,停旧写与切读之间至少间隔一个完整业务周期。退出条件是新库为唯一权威,旧库只读并在约定期限后归档。

经验上三个阶段的工期比例大约是二比五比三:比对与回填是最长的一段,因为差异总是比预想的多。有两种常见误判。第一种是以为双写上线就算搬完了一半------实际上双写只是开始,真正的工作量在把差异收敛到零。第二种是跳过全量比对直接切读,理由是增量看着没问题------但历史数据里的结构性差异只有全量比对能发现,而它们会在切读之后以"老订单查不到"的形式爆出来。

有一个前置动作能大幅降低这段成本:在模块化单体阶段就把表按模块归属、禁止跨模块联表查询、禁止跨模块写入。做到这三点之后,搬迁的对象是一组界限清楚的表,写入方只有一个,读取方都通过接口------此时三个阶段的工作量会缩小到原来的几分之一。反过来,如果搬迁开始时还有十几处跨模块直接访问,每一处都要在双写阶段被逐个改造,而且改造过程中旧路径与新路径并存,正确性验证的难度成倍上升。

(三)迁移期间双形态共存的四项治理

过渡期往往比终态持续得更久,必须当作常态来管。把过渡期当成临时状态而不加治理,是很多拆分在中途失控的直接原因。

契约的单一来源:问题是同一个能力在进程内有本地接口、在网络上有远程接口,两份定义容易漂移。做法是以一份接口定义为唯一来源,本地实现与远程契约都从它生成或校验。校验点是构建阶段断言两侧签名一致,不一致直接失败,不进入发布流程。

观测口径的统一:问题是进程内调用与远程调用的延迟、错误统计口径不同,指标无法横向比较。做法是两条路径都记录同名指标并带上路径标签,面板上并列展示。校验点是切流量期间两条路径的延迟分位与错误率必须能在同一张图上对比。

开关状态的可见:问题是某个能力当前走本地还是远程,只有改配置的人知道,排障时无从判断。做法是把全部路由开关的当前状态展示在一处,并在每次变更时留痕与广播。校验点是任何人在一分钟内能回答"现在这个能力走的是哪条路径"。

回退演练的常态化:问题是回退开关写好了但从未用过,真正需要时才发现它已经失效。做法是每个迭代在预发环境切一次回退,每季度在生产低峰期切一次并观察。校验点是最近一次成功执行回退的时间,写在该服务的运行手册第一行。

过渡期为什么会拖长?三个常见原因:一是外提被排在业务需求之后,只要有紧急需求,迁移就停摆;二是第六步与第七步没人推动,因为它们不产生可见的业务价值;三是负责迁移的人换了,新人不了解上下文,宁愿维持现状。结果是双实现长期并存:代码里有两条路径、面板上有两套指标、文档里有两份说明,认知负载翻倍,而拆分的收益一项也没兑现。这是过渡期最典型的失败形态。

有两条做法能显著缩短过渡期。第一条是给每次外提设一个明确的截止时间与责任人,并把"删除本地实现"与"移交所有权"列为验收项,而不是可选的收尾工作------没有验收项的迁移不会结束。第二条是一次只做一个:并行三个外提时,任何一次业务插单都会让三个都停在中途,而三个半成品的维护成本高于一个完成品加两个未开始。串行推进的总工期通常还更短,因为每一次都能复用上一次的经验。

十、落地清单与常见误判

(一)二十条决策清单与十六个高频误判

把整篇的判据压缩成一页可核对的表。清单用于评审前自查,误判清单用于评审中识别对方(也包括自己)的论证漏洞。

二十条决策清单:先列能力清单,再谈形态,不要反过来;语义、事务、变更三条边界都标注过;四项边界稳定度指标都查过实际数字;请求量、数据量、变更量分别量过;五道扩展闸门逐道排除过,不是猜的;独立伸缩的台数账算过,不是感觉;服务数与团队数的比值在一到三之间;每个候选服务都有具体的第一责任人;值班覆盖与响应时限已经约定;十二项底座前置检查逐项确认过;部署耦合来自流程、共享库还是数据库,已定位;要的是哪一层隔离(代码、进程、调用、数据),已明确;六类不可降级的不变量已列出并锁定相关能力;跨界写链路的条数与预计工时已估算;跨界之后受影响的查询页面已逐个列出;五维打分逐条打过,分歧位置已记录;外提顺序已排定,且一次只提一个;每一步的回退方式与生效时间已写明;退役条件(什么情况下合回去)已登记;结论写成动作与观察周期,不是形态名词。

十六个高频误判:把形态当进度条;用别人的终态图当自己的目标;把服务数量当成目标而非结果;按技术分层拆而不是按业务能力拆;只看语义边界就下刀;把性能问题直接当成形态问题;以为拆分能减少机器台数;忽略每个实例的固定底噪开销;把人数当成唯一自变量;给团队维度打分时高估独立性;拆代码但共用数据库;把一致性问题留到最后再考虑;以为超时等于失败;低估读一致性带来的用户可见问题;底座缺项也先拆、打算之后再补;把合并读成承认失败、于是从不合并。

(二)三种场合下的表述范式

同一份思考需要按场合裁剪。三种场合的目标不同,因此该说什么、不该说什么也不同。

技术评审现场(十到二十分钟):先用两句话交代约束------团队规模、独立小组数量、当前形态档位;然后展示能力清单与四象限分布,指出哪几条能力落在左上格;接着对这几条逐一给五维打分,把分歧位置显式记录下来;最后给出结论与观察周期。评审的目标不是一次定论,而是把分歧从"形态之争"降解到"某一维的事实核实",并约定谁在什么时候把事实查清。

面试或技术交流(三到十分钟):先说清判断框架,即五个变量与前四投票后一否决的结构;再举一个自己经手的具体例子,说明某一维如何单独改变了结论;最后主动交代代价------本地事务的六项免费午餐、调用层隔离的隐藏账单、每实例的固定底噪。这个顺序的作用是显示成本意识,而成本意识是形态类问题上最难伪装的一项。

架构文档(长期留存):文档要写的不是终态图,而是决策记录。每一条边界记录三件事:当时的五维打分与依据数字、外提或保留的结论、以及下一次重新评估的触发条件。触发条件要写成可观测的形式,例如"月变更次数连续两个月超过其余模块合计的三分之一",而不是"业务量显著增长时"。这样的文档半年后仍然可用,因为它记录的是推理过程而不是结论快照。

(三)回到那句"不要为了拆分而拆分"

这句话本身没什么争议,问题是它不可执行------没有人会承认自己在为了拆分而拆分。把它展开成一句更长的话,才有判据的作用。

拆分要买的东西只有一样:让不同的人按不同的节奏,安全地改动同一个系统的不同部分。凡是拆完之后这份自由没有增加,而远程调用的代价、一致性的代价与治理的代价都实打实增加了的切法,都是错的,无论它在架构图上多么整齐。

所以每一次外提都应当能回答同一组问题:这份自由归谁、从什么时候开始用、如果一年之后仍然没人用上,我们按什么条件把它合回去。能回答这三问的拆分,就不是为了拆分而拆分。

三个问题里最容易答错的是第一个。"归谁"要求一个具体的人或一个具体的小组,而不是"业务方"或"未来的团队"。第二个问题防止的是提前拆分------为一个还没有出现的痛点付出现在的代价。第三个问题防止的是不可逆承诺------它把每一次外提从一次赌注变成一次实验,而实验的价值恰恰在于允许失败。

最后值得重复的一点是:模块化单体不是微服务的候补,也不是通往微服务的必经站台。它是一个有自己纪律、自己收益、自己适用区间的独立形态,并且在相当大的组织规模范围内是更优的选择。承认这一点在技术上没有难度,在心理上却需要一点定力------因为它意味着放弃一个听起来更先进的标签,换取一组更具体的数字:更短的交付周期、更低的故障率、更少的人均维护负担。这三个数字才是形态选择真正要优化的目标。

参考资料

形态与边界

拆分模式与迁移

数据与一致性

团队与组织

部署、韧性与可观测

工程约束与工具

相关推荐
是江迪呀1 小时前
介绍一个开源项目 —— 烂笔头
前端·后端
captain3761 小时前
Spring Web MVC(2)
java·前端·spring boot·后端·spring·mvc
紧跟先前的步伐1 小时前
Go AI Agent 实战 17|模型适配层设计:让模型可替换
开发语言·人工智能·后端·ai·golang
IT_陈寒2 小时前
一文搞懂Java的核心概念
前端·人工智能·后端
铁皮饭盒2 小时前
workbuddy和codebuddy免费送1000积分, 10秒钟搞定
前端·后端
计算机就业还在扩大2 小时前
第2期-cmp.Ordered+类型推断+刻意未支持的特性
后端·面试·golang
王中阳Go11 小时前
两个入口答案不一致先别调模型:6 行入口口径对账表(附脚本)
人工智能·后端·python·大语言模型·1024程序员节
码事漫谈12 小时前
消除AI幻觉,普通人能做的五件事
后端
金銀銅鐵14 小时前
[Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(下)
java·后端·ai编程