14 · 演进与拆分大型系统:给飞行中的飞机换引擎
一句话点题:一个跑着真实业务、停不下来、还在天天变的系统,你永远不可能"先停下来、推倒、重写一遍、再上线"——所有真本事,都在于「让飞机一边飞、一边换引擎」。 上一类问题是"系统怎么不出事"(分布式、失败、规模);这一章是另一类——系统已经又大又关键,你怎么在不摔下来的前提下,把它改成更好的样子。
🧭 接着 08 · 架构决策记录与演进 往下走。 第 8 章告诉你"架构会长大、要留好接缝、何时该升级"——那是演进的世界观;这一章给你演进的手艺:当系统已经是一坨跑在生产上的庞然大物,具体用哪几招,能在零停机、可回滚的前提下把它一点点改对、拆开、换掉。
这也是 AI 时代最反直觉的一条主线。AI 几秒就能生成一版"看起来更干净"的新实现,vibe coding 更是能让你半天堆出一个能跑的原型——但**"把一坨能跑的东西,在它继续服务、继续赚钱、需求继续变的同时,不停机地演进成可维护的系统",这件事 AI 给不了你,因为它要的不是代码,是对"先动哪、怎么兜底、什么时候切、出事怎么退"的连续判断**。实现越来越廉价,掌舵越来越值钱。
一、为什么"推倒重写"几乎注定失败:你在追一个移动的靶子
每个接手过烂摊子的工程师,心里都冒过同一个念头:"这代码没救了,不如推倒重来。" 这个念头几乎总是错的——不是因为旧代码有多好,而是因为重写这个动作本身,从物理上就赢不了。
"大重写(big rewrite)"为什么是个陷阱:
时间轴 ──────────────────────────────────────────────▶
旧系统: v1 ── 改 ── 改 ── 修 bug ── 加需求 ── 改 ── … (一直在动!)
▲
新系统: 从 0 写 ─────────────────────… 要追上这里
(你以为靶子在这,其实它一直在跑)
等你新系统终于追平旧系统"当初那个版本"时,
旧系统早已跑到了别处 —— 你永远差着一截,且这一截在变大。问题的核心是:旧系统不会停下来等你。 它一边被你重写,一边还在被别人改 bug、加需求——目标在移动。你以为只要把现有功能照搬一遍就行,可"现有功能"是个一直在变的活物;更要命的是,那些看起来又脏又怪的代码里,沉淀着多年踩坑换来的、谁也没写进文档的隐性知识(承接 [08] 那个"删掉双写、三周后炸了"的开场):某个莫名其妙的 if 是为了绕过某客户的脏数据,某段重试是为了兜住某个第三方的抽风。重写一遍,这些血泪全部清零,你会把同样的坑再踩一遍。
架构智慧:重写最大的成本不是"重新写一遍代码",而是"重新踩一遍坑、且在这期间你完全停止了进步"。 旧系统每一处难看的疤,几乎都是一次线上事故结的痂。big rewrite 等于一边把疤全撕掉、一边对着移动靶射击,还要求竞争对手原地等你——三个不可能叠在一起。
所以本章只有一条主路:不推倒、只渐进——让新旧并存,旧的一块块萎缩,新的一块块长出来,任何一步都能停、能回滚、系统始终在线。 下面六节,全是这条主路上的具体手艺。
二、绞杀者模式(Strangler Fig):在旧系统外围拦截,让它慢慢"被绞杀"下线
Martin Fowler 2001 年在澳洲雨林里见到一种植物:绞杀榕(strangler fig)——种子落在宿主树的枝桠上发芽,藤蔓顺着树干往下扎到土里生根、往上铺开抢阳光,几年后,新长成的榕树自给自足,而当初那棵宿主树,枯死了。他说:这就是改造大型遗留系统该有的样子。
绞杀者模式:在旧系统"外面"加一层门面/路由,逐步把流量引向新实现
┌──────────────────────────────────────────┐
请求 ─▶│ 门面 / 路由层 (Facade / Interceptor) │ ← 关键就是这一层
└───────┬──────────────────────┬───────────┘
│ 这部分还没迁 │ 这部分已迁
▼ ▼
┌─────────────┐ ┌──────────────┐
│ 旧单体 │ │ 新服务/新实现 │
│ (逐步萎缩) │ │ (逐步长大) │
└─────────────┘ └──────────────┘
时间推移:路由表里"走新实现"的条目越来越多,旧单体越来越少被命中,
直到某天旧单体一条流量都收不到 —— 它"被绞杀"了,安全下线。它和 big rewrite 的根本区别在于:新旧始终并存、流量逐块切换,每一块切过去都是一次小而可回滚的发布,而不是攒一个大版本去赌命。
整个模式的命门,是那层「门面/路由」——所有外部请求必须先经过它,它才有资格决定"这个请求走旧的还是走新的"。这层门面可以是 API 网关、反向代理(按 URL 前缀路由)、也可以是单体内部的一个分发函数。没有它,你连"把某个功能悄悄切到新实现"的开关都没有。
架构智慧:绞杀者模式不是"快",它常常是慢的、要并存维护两套一段时间的——但它把"一次性赌上整个系统"的巨型风险,拆成了"一次切一小块"的可控风险序列。你不是在和旧系统决斗,而是在它身上慢慢长出新系统,直到旧的无人问津、自然枯死。 适用前提:旧系统外围能插进一层拦截。这一层,往往就是 [08] 说的"留好接缝"在改造期的兑现。
三、抽象分支(Branch by Abstraction):在抽象层之后换实现,主干永远可发布
绞杀者管的是"系统外围、整块功能"的迁移。但很多时候你要换的是系统内部、被无数地方调用的一个核心组件——比如把自研的缓存层换成 Redis、把一个数据访问层从 ORM-A 换成 ORM-B。这种东西被调用方缠得密密麻麻,你不可能"在外面拦一层"。
新手的本能是:开一个长命特性分支(long-lived feature branch),躲进去吭哧吭哧改两个月,改完再合回主干。 这是另一个经典陷阱——分支活得越久,和主干的差异越大,最后那次合并就是一场灾难(别人这两个月也在改主干),而且改的过程中主干根本没法发布你这部分。
抽象分支(Fowler / Paul Hammant)给出的是反过来的做法:不开长命分支,所有改动都在主干上进行,靠一层"抽象"让新旧实现并存。
抽象分支五步,全程在主干上,系统时刻可编译、可发布:
① 在"要替换的组件"前面,插入一层抽象(接口)
调用方 ──▶ 【抽象层】──▶ 旧实现
② 让所有调用方都改成依赖这层抽象(不再直接调旧实现)
调用方 ──▶ 【抽象层】──▶ 旧实现 ← 此时行为完全不变
③ 在抽象层背后,把新实现也写出来(用开关/feature flag 控制走哪个)
调用方 ──▶ 【抽象层】─┬─▶ 旧实现 (默认)
└─▶ 新实现 (开关关着,先不放量)
④ 逐步把开关切到新实现,出问题随时切回(这就是可回滚)
调用方 ──▶ 【抽象层】─┬─▶ 旧实现 (逐步弃用)
└─▶ 新实现 (逐步放量)
⑤ 新实现稳了,删掉旧实现;抽象层可留可拆
调用方 ──▶ 【抽象层】──▶ 新实现它和绞杀者是一对孪生兄弟:绞杀者在系统"外面"拦截、换整块功能;抽象分支在系统"里面"、在某个抽象之后换实现。 两者的共同信念完全一致——主干永远处于可发布状态,绝不靠一个长命大分支去赌,这正是持续集成 / 主干开发(trunk-based development)的精神。
架构智慧:"开个分支慢慢改、改完再合"是直觉,却是规模化重构里最贵的反模式——因为你在制造一颗『合并核弹』,且引信时间由别人决定。 抽象分支把它倒过来:先花力气立一层抽象当"插座",让新旧实现都能插上去,然后在主干上不慌不忙地切换。多写一层抽象的成本,买的是『随时能停、随时能发、随时能回退』的安全感。
四、并行运行 / 影子流量(Parallel Run / Dark Launch):让新旧同时跑,先比对、再切流
绞杀者和抽象分支都给了你"切换开关"。但切之前,你凭什么相信新实现是对的? 跑通了单元测试?线上的真实流量,永远比你想得出的测试用例更刁钻。
最硬核的建立信心的办法,叫并行运行:新旧两套实现,对同一批真实请求同时跑;把旧实现的结果返回给用户(用户毫无感知),同时把新实现的结果和旧的悄悄比对、记录差异。 这也叫"暗发布(dark launch)"——新代码已经在生产里跑了,只是它的输出还不算数。
并行运行 / 影子流量:新实现先"陪跑",只比对不生效
┌──▶ 旧实现 ──▶ 结果A ──────────────▶ 返回给用户 ✅
真实请求 ──┬──┤ │
│ └──▶ 新实现 ──▶ 结果B ─────┘
│ ▼
│ 比对 A vs B
│ │
│ ┌─────────────┴─────────────┐
│ ▼ ▼
│ 一致 → 记一笔 不一致 → 报警 + 落日志
│ (信心 +1) (这就是你不知道的坑!)
│
注意:用户永远拿到旧实现的结果A,新实现错了也不影响线上。
等"不一致率"降到足够低,你才有底气把流量真正切给新实现。这套思路最有名的开源实现,是 GitHub 的 Scientist 库(下面真实案例细讲)。它的精髓:控制组(control,旧代码)的结果永远返回给用户,候选组(candidate,新代码)在背后跑、随机打乱执行顺序以暴露顺序依赖、吞掉候选组抛出的异常(绝不让实验代码搞挂线上),最后把"两边结果是否一致、各自耗时多少"发布出去供分析。
架构智慧:重构关键路径最大的恐惧是"我以为等价,其实不等价"。并行运行把这种恐惧变成了一组可量化的数据——用真实流量当裁判,而不是用你的自信当裁判。 它特别适合那些"逻辑复杂、错了代价极高、又说不清到底有多少边界情况"的核心计算(计费、权限、风控、定价)。代价是要同时跑两套、有额外开销,所以它是关键路径的重器,不是哪都用的日常工具。
五、零停机数据迁移:数据最难改,所以要"扩张—收缩",每步可回滚
05 · 数据与状态 早就立过一条铁律:无状态的东西好改,数据最难改。 代码可以蓝绿切换、可以回滚,但数据只有一份、改坏了往往救不回来。所以当演进涉及"换数据库 / 改表结构 / 拆库"时,你需要一套近乎仪式般严谨的流程。
它的总思想和上面的「抽象分支」「Parallel Change(expand-contract,Joshua Kerievsky 提出)」是同一个——先扩张(让新旧并存),再收缩(确认无误后删旧),中间任何一步都能停、能退:
零停机数据迁移五步(expand → … → contract),任何一步可回滚:
① 双写 (dual write)
应用同时往【旧存储】和【新存储】写。读还走旧的。
旧 ◀── 写 ──┤ 应用 ├── 写 ──▶ 新
└─ 读 ─▶ 旧
→ 退路:停掉对新存储的写即可,旧存储一直是权威。
② 回填 (backfill)
把"开始双写之前"的存量历史数据,批量搬进新存储。
→ 退路:回填是只写新存储的批处理,随时可中止重来。
③ 影子读校验 (shadow read / compare)
读请求两边都读,把结果返回用户的仍是旧存储那份;
同时比对"新存储读出来的"和"旧的"是否一致(就是上一节的并行运行!)。
→ 退路:只比对不切流,差异率不达标就继续修,绝不切。
④ 切读 (cut over read)
确认一致率足够高,把"读"切到新存储。此时仍在双写。
→ 退路:读再切回旧存储,因为旧的一直被双写、依然新鲜。
⑤ 清理 (contract)
观察一段时间稳了,停掉对旧存储的双写,最后下线旧存储。
→ 这是唯一"不可逆"的一步,所以放在最后、且要留足观察期。整条链路最值钱的设计是:前四步,旧存储始终是"权威且新鲜的",所以前四步你都能瞬间退回去。 真正不可逆的只有最后的"清理",而那时你已经用前面四步把信心攒满了。
架构智慧:数据迁移的灾难,几乎都源于"一刀切":某天半夜停服、跑个迁移脚本、改完上线、然后祈祷。 正确姿势是把它拉成"双写→回填→影子校验→切读→清理"的长链条,让"旧存储一直是权威"成为你随时能跳的安全网——直到最后一刻才剪断它。这套手法在 11 · 数据一致性工程 的双写/回填里有更细的工程展开,本质同源。
六、拆单体:先找接缝、用防腐层隔离,且"先模块化单体,再按需拆服务"
前面五招是"怎么安全地换"。这一节回答一个更大的战略问题:一个大单体,到底该不该拆成微服务?怎么拆?
先泼一盆冷水。新手最容易犯的错,是把"拆微服务"当成进步本身——大厂都拆了,我们也拆。但 04 · 十大核心架构模式 早说过:微服务是"成熟期的解药",不是"成长期的标配";过早拆分,只会把"函数调用"升级成"网络调用",凭空背上分布式的全部苦头([10] 那一整章的硬道理)。
怎么找该拆的"缝"? 答案来自 DDD(领域驱动设计)的限界上下文(Bounded Context)——不要按技术分层拆(那只会拆出贫血的 CRUD 服务),要按业务能力的自然边界拆:订单是一个上下文、库存是一个、计费是一个。好的服务边界,是业务概念的边界,不是数据库表的边界。(这也直接呼应 [08] 的康威定律:服务边界对不齐团队边界,拆了也白拆。)
拆的时候,新旧两套领域模型必然打架——旧单体里"订单"这个概念可能又脏又杂糅,你不想让它污染新服务里干净的模型。隔离它的标准武器叫防腐层(Anti-Corruption Layer, ACL):
防腐层(ACL):在新服务和旧系统之间,架一道"翻译 + 隔离"墙
┌────────────────┐ ┌─────────┐ ┌──────────────────────┐
│ 新服务 │────▶│ 防腐层 │────▶│ 旧单体 (混乱的旧模型) │
│ (干净的新模型) │◀────│ ACL │◀────│ │
└────────────────┘ └─────────┘ └──────────────────────┘
▲
把旧系统的脏概念翻译成新模型能接受的样子,
新服务永远只跟"干净的接口"打交道,旧模型的腐烂不会渗进来。而最关键的战略判断是拆分的"姿势"——别一上来就拆成一堆独立部署、各有数据库、靠网络互调的微服务:
单体演进的正确顺序(别跳级):
一坨泥单体 ──▶ 模块化单体 ──▶ (只在确有瓶颈时) 按需抽离微服务
(没有边界) (Modular Monolith: (把真正需要独立伸缩/
一个部署单元,但内部 独立发布的那个上下文,
有强制的模块边界) 才升级成服务)
① 先在"一个进程内"把边界划清楚 —— 改边界几乎零成本,改错了重划就行
② 边界经过真实业务捶打、稳定了,再把"确实需要独立伸缩/发布"的那块拆出去
③ 不需要独立的,就一直留在模块化单体里 —— 这本身就是一个好归宿模块化单体的妙处在于:它把"边界设计"和"分布式部署"这两件事解耦了。你先用近乎零成本的方式(进程内的模块边界)反复打磨"缝划在哪",等缝被真实业务验证稳定了,才为"确实需要独立伸缩或独立发布"的那一两块,支付分布式的代价去抽成服务。先拿到微服务最值钱的那部分(清晰边界),把最贵的那部分(分布式运维)推迟到非付不可的时候。
架构智慧:"拆不拆微服务"是个被严重带偏的问题。真正的问题是"边界划对了没有"——边界对了,模块化单体就足够好;边界错了,拆成微服务只会把『改一处动一片』升级成『改一处、跨网络地动一片』。 所以纪律是:先模块化单体,把缝划对、划稳;再按需、按瓶颈,一块块抽成服务。 服务数量从来不是目标,它是为某个具体质量属性(独立伸缩、独立发布、故障隔离)付出的代价——回到 06 那把尺子量,而不是数。
七、适应度函数(Fitness Functions):把架构约束写成自动化测试,让系统"长大但不腐化"
最后一个问题:你费尽心力把单体拆成了漂亮的模块化结构、划好了边界——怎么保证它不会在后续几百次提交里,被一点点地又揉回一坨泥? [08] 讲过技术债会无意识地累积,而架构边界恰恰是最容易被悄悄违反的东西:某天某人图省事,让"订单模块"直接 import 了"计费模块"的内部类,边界就破了一个口子,然后破口越来越多。
演进式架构(Evolutionary Architecture,Neal Ford / Rebecca Parsons / Patrick Kua)给的解药是适应度函数:把你在乎的架构约束,写成一段能自动运行、会失败、能卡住 CI 的测试。 架构规则一旦能被机器持续验证,它就从"墙上贴的、靠自觉的规范",变成了"违反就红、合不进去的硬约束"。
💧 深水区(名字唬人,意思很简单):「适应度函数」这个学名听着玄,其实就是「给架构装一套自动体检 + 门禁」。你平时写单元测试是检查「功能对不对」;适应度函数只是把检查对象换成「架构规矩有没有被破坏」——比如「订单模块是不是偷偷 import 了计费模块的内部代码」。一旦有人违反,CI 直接亮红灯、合不进去。它是架构的免疫系统:不阻止系统长大,只阻止它在长大的过程中烂掉。
适应度函数 = 给架构装上"持续体检",一违反就报警
你在乎的架构约束 → 写成自动化适应度函数(进 CI)
─────────────────────── ──────────────────────────────────
"订单模块不准依赖计费模块内部" → 依赖检查:扫到这条 import 就 fail
"任何服务的 p99 不准超 200ms" → 性能测试:超了就 fail
"领域层不准 import 框架/Web 层" → 分层检查:违反就 fail
"没有循环依赖" → 依赖图分析:出现环就 fail
"API 不准引入破坏性变更" → 契约测试:不兼容就 fail
每次提交都跑一遍 → 架构腐化在"破口刚出现"时就被挡住,而不是攒成大窟窿才发现。这正是把 [08] 的两件事自动化了:它让"演进式架构要留好接缝"从一句口号,变成"接缝被破坏就立刻报警"的护栏;也让"技术债要记账"从事后追悔,变成"想欠这笔架构债?CI 先拦住你,逼你显式地确认"。
架构智慧:架构不是画完图就定型的雕像,是一个要持续维护的活系统——而"活的"东西若没有免疫系统,必然腐化。适应度函数就是架构的免疫系统:它不阻止系统长大,它只阻止系统在长大的过程中烂掉。 没有它,你今天辛苦划清的每一道边界,都只是在等一个赶时间的下午被人捅穿。
📌 真实案例:三个"换引擎"的经典,和一次著名的"拆了又拼回去"
① GitHub Scientist——给关键路径重构兜底信心(并行运行的活样板)。 GitHub 当年要重构其权限判断这条关键路径——这是错一点点就会造成"该看见的看不见、不该看见的泄露"的高危代码。他们没有"改完祈祷",而是做了并行运行,并把这套机制抽成了开源库 Scientist:旧权限逻辑(control)的结果照常返回给用户,新逻辑(candidate)在背后对同一请求跑一遍,两边结果不一致就记录、报警,但绝不影响线上。靠真实流量喂出来的"不一致清单",他们把那些自己都没想到的边界情况一个个补平,等不一致率降到足够低,才放心切流。这正是本章第四节的精确落地。 📎 github/scientist · 设计文章 Move Fast and Fix Things
② Amazon——从单体 Obidos 到面向服务,顺便重塑了组织。 Amazon.com 1996 年起是一个叫 Obidos 的大单体,所有展示、推荐、Listmania、评论都揉在里面。到 2001 年前后,这个单体在规模面前撑不住了——"一百来个工程师全挤在同一坨代码上",谁都动不利索。Amazon 启动了向**面向服务架构(SOA)**的渐进迁移,把功能切成可独立开发、独立部署、独立测试的服务;并配套发明了著名的 "两个披萨团队(two-pizza teams)"——一个团队小到两个披萨能喂饱(约 8-10 人),各自完整拥有一块服务。这是康威定律([08])的正向应用:想要独立演进的服务,就先把人组织成能独立负责的小队。 📎 Amazon Architecture (High Scalability)
③ Netflix——七年迁云,坚持"重写架构而非搬运"。 2008 年 8 月,Netflix 核心的 Oracle 单体数据库损坏,导致三天发不出 DVD。痛定思痛,他们决定上云。这场迁移整整走了七年(2008 → 2016 年 1 月最后一个计费系统切完),而且 Netflix 反复强调:他们不是"lift-and-shift(原样搬到云上)",而是逐个服务地重构成微服务——典型的绞杀者式渐进迁移,新服务一块块长出来、老单体一块块萎缩,而不是停服去赌一个大版本。 📎 Netflix: Completing a Decade of Cloud Migration
④ Segment《Goodbye Microservices》——拆得太碎,又拼回了单体。 这是给所有"微服务崇拜"最响的一记警钟。Segment 早年(约 2013)为了故障隔离把系统拆成微服务,一个数据目的地(destination)一个 worker。2016-2017 业务爆发,目的地数量飙升(一个月新增约 3 个),每个服务一个仓库、还共享一堆公共库——结果改一次公共库要花掉约一周的开发量(全卡在测试上);而"理论上彻底的故障隔离"需要上万个微服务(每客户每队列一个),根本不现实。2017 年他们回退到一个叫 Centrifuge 的单体,主动放弃了部分隔离性,换回了"一个仓库、统一版本、分钟级部署、能继续做新功能"。
复盘里最扎心的一句:"如果微服务用错了地方、或被当成创可贴去贴根本问题,你会因为淹没在复杂度里而再也做不了新产品。" 📎 Segment: Goodbye Microservices · InfoQ 报道
⑤ 反面教材:Netscape 的"推倒重写"之殇。 2000 年,Joel Spolsky 在 《Things You Should Never Do, Part I》 里痛陈:Netscape 做了"一家软件公司能犯的最严重的战略错误"——把浏览器代码从头重写。从 4.0 到 6.0 之间空了近三年没有可用的新版本,而这三年里 IE 把市场份额几乎全吃光了。Joel 的两个论点至今成立:那些看着丑陋的旧代码里,藏着多年踩坑换来的、修复无数诡异 bug 的隐性知识;而重写是个漫长工程,期间你完全停止改进现有产品,竞争对手却在飞奔。 这正是本章第一节的反证。
把五个案例并排看,本章的主路一目了然:①③ 是"渐进演进 + 并行验证"的胜利;② 是"演进同时重塑组织"的胜利;④ 警告你"拆过头"和盲目拆同样致命;⑤ 警告你"推倒重写"的下场。 没有一个赢家是靠"停下来、推倒、重写"赢的。
🤖 AI / vibe coding 视角:AI 能改代码,但"如何不停机地演进"必须人来掌舵
AI 正在实实在在地改变"演进"这件事的成本结构,但它改不了判断的归属。
AI 让大规模重构/迁移的"体力活"变廉价了。 跨上千个文件的 codemod、把一套老 API 调用批量改写成新 API、读懂一段没人敢碰的 legacy 并解释它在干什么、甚至自动生成抽象层和适配代码——这些过去要一个团队啃几个月的脏活累活,AI 能极大提速。"绞杀者的迁移工作量""抽象分支里改所有调用方""数据迁移的回填脚本",正是 AI 最能帮上忙的环节。
而"并行运行 + 结果比对",天生就是给"AI 改的代码"兜底信心的最佳搭档。 你不敢全信 AI 重构出来的关键路径?那就别全信——把它当成第四节里的 candidate,用真实流量和旧实现逐条比对。AI 负责高速产出新实现,并行运行负责用数据替你审判它对不对。这是 AI 时代重构最该养成的肌肉记忆:让 AI 快,让比对慢;速度交给模型,信心交给数据。
但最核心的那层判断,AI 给不了,只能你来下:
AI 很擅长的(交给它): 只能人掌舵的(本章的真本事):
───────────────────────── ──────────────────────────────────
• 生成新实现 / codemod / 适配层 • 这块到底该不该拆?边界划在哪?
• 读懂并解释 legacy 代码 • 先动哪一块?什么顺序最安全?
• 写回填脚本 / 迁移样板 • 切流的"信心阈值"定多少才敢切?
• 把老 API 调用批量改写成新的 • 出事了,退到哪一步、怎么退?
• 这是该模块化单体,还是真要拆服务?这正是 vibe coding 最大的"甜蜜陷阱"。 vibe coding 让你半天就能堆出一个能跑的原型——这太诱人了。但一个能跑的原型,和一个"能在持续服务、需求持续变的同时被安全演进"的系统,中间隔着的正是本章这七节手艺。AI 能帮你飞快地造出那架飞机,却没法替你在飞机满载乘客、引擎还转着的时候,判断"先拆哪个引擎、怎么保证不失速、出问题怎么退回去"——因为这要的不是代码,是对你的业务、你的风险、你愿意拿什么换什么的连续判断。"如何把一坨能跑的原型,不停机地演进成可维护的系统",恰恰是 AI 给不了、必须人来掌舵的那部分。 这也正是 AI Agent / 工作流平台 一以贯之的"渐进可控"理念:能力可以让 AI 飞速堆,但演进的方向盘、刹车和退路,必须攥在人手里。
架构智慧:在 AI 时代,写新实现这件事会越来越像"按一下生成";而"在飞行中安全地把它换上去"这件事,会越来越成为人的核心价值。 实现廉价化得越彻底,"掌舵演进"的判断就越稀缺、越值钱。
🎯 随堂检验
- A跑通全部单元测试就直接切到新实现,旧的删掉
- B用并行运行:旧实现的结果照常返回用户,新实现对同一批真实流量在背后跑、逐条比对,等不一致率足够低再切流
- C开一个长命特性分支,把新实现完整改好、充分测试后,一次性合并并上线
本章小结
- 大重写(big rewrite)几乎注定失败:旧系统不会停下等你(靶子在移动),且其丑陋代码里沉淀着踩坑换来的隐性知识——重写等于"对着移动靶射击、同时把血泪经验清零、还要求对手原地等你"。唯一主路是渐进演进。
- 绞杀者模式:在旧系统外围加一层门面/路由,新功能用新实现、旧功能逐块迁移,直到旧系统收不到流量、自然下线。命门是那层拦截。
- 抽象分支:在一层抽象之后并存新旧实现、靠开关逐步切换,全程在主干、时刻可发布,以此替代灾难性的长命特性分支。
- 并行运行 / 影子流量:新旧同时跑、旧的结果给用户、比对差异,用真实流量(而非自信)建立信心后再切流;GitHub Scientist 是其活样板,也是给"AI 改的代码"兜底的最佳搭档。
- 零停机数据迁移:数据最难改,所以走 双写→回填→影子校验→切读→清理(expand-contract),让"旧存储一直是权威"成为随时能跳的安全网,只有最后清理不可逆。
- 拆单体:用 DDD 限界上下文按业务能力找缝、用防腐层隔离新旧模型;纪律是先模块化单体把边界划对划稳,再按需、按瓶颈一块块抽成服务——服务数量从来不是目标。
- 适应度函数:把架构约束写成会失败、能卡 CI 的自动化测试,给架构装上免疫系统,让它"长大但不腐化"。
- AI / vibe coding 主线:AI 让重构/迁移的体力活廉价,并行运行天生适合给 AI 改的代码兜底;但**"该不该拆、先动哪、何时切、怎么退、是模块化单体还是真拆服务"——这些掌舵的判断 AI 给不了,正是人在 AI 时代越来越稀缺的核心价值。**
承上启下:这一章讲的全是"在技术层面怎么安全地演进系统"。但你大概已经从绞杀者、从拆单体、从 Amazon 的两个披萨团队里反复嗅到一股味道——架构怎么拆、能不能拆得动,根子常常不在技术,而在组织。 下一章(进阶篇第 6 章)《15 · 组织即架构》,我们把 [08] 提过的康威定律彻底摊开:为什么"系统的架构终将长得像设计它的组织",为什么很多"架构难题"其实是"组织难题"的伪装,以及——你如何反过来用组织设计,去塑造你想要的架构。
💬 评论