# 14 · 演进与拆分大型系统:给飞行中的飞机换引擎 > 一句话点题:**一个跑着真实业务、停不下来、还在天天变的系统,你永远不可能"先停下来、推倒、重写一遍、再上线"——所有真本事,都在于「让飞机一边飞、一边换引擎」。** 上一类问题是"系统怎么不出事"(分布式、失败、规模);这一章是另一类——**系统已经又大又关键,你怎么在不摔下来的前提下,把它改成更好的样子。** --- > **🧭 接着 [08 · 架构决策记录与演进](08-架构决策记录与演进.md) 往下走。** 第 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 · 数据与状态](05-数据与状态.md) 早就立过一条铁律:**无状态的东西好改,数据最难改。** 代码可以蓝绿切换、可以回滚,但**数据只有一份、改坏了往往救不回来**。所以当演进涉及"换数据库 / 改表结构 / 拆库"时,你需要一套近乎仪式般严谨的流程。 它的总思想和上面的「抽象分支」「Parallel Change(expand-contract,Joshua Kerievsky 提出)」是同一个——**先扩张(让新旧并存),再收缩(确认无误后删旧)**,中间任何一步都能停、能退: ``` 零停机数据迁移五步(expand → … → contract),任何一步可回滚: ① 双写 (dual write) 应用同时往【旧存储】和【新存储】写。读还走旧的。 旧 ◀── 写 ──┤ 应用 ├── 写 ──▶ 新 └─ 读 ─▶ 旧 → 退路:停掉对新存储的写即可,旧存储一直是权威。 ② 回填 (backfill) 把"开始双写之前"的存量历史数据,批量搬进新存储。 → 退路:回填是只写新存储的批处理,随时可中止重来。 ③ 影子读校验 (shadow read / compare) 读请求两边都读,把结果返回用户的仍是旧存储那份; 同时比对"新存储读出来的"和"旧的"是否一致(就是上一节的并行运行!)。 → 退路:只比对不切流,差异率不达标就继续修,绝不切。 ④ 切读 (cut over read) 确认一致率足够高,把"读"切到新存储。此时仍在双写。 → 退路:读再切回旧存储,因为旧的一直被双写、依然新鲜。 ⑤ 清理 (contract) 观察一段时间稳了,停掉对旧存储的双写,最后下线旧存储。 → 这是唯一"不可逆"的一步,所以放在最后、且要留足观察期。 ``` 整条链路最值钱的设计是:**前四步,旧存储始终是"权威且新鲜的",所以前四步你都能瞬间退回去。** 真正不可逆的只有最后的"清理",而那时你已经用前面四步把信心攒满了。 > **架构智慧**:**数据迁移的灾难,几乎都源于"一刀切":某天半夜停服、跑个迁移脚本、改完上线、然后祈祷。** 正确姿势是把它拉成"双写→回填→影子校验→切读→清理"的长链条,**让"旧存储一直是权威"成为你随时能跳的安全网**——直到最后一刻才剪断它。这套手法在 [11 · 数据一致性工程](11-数据一致性工程.md) 的双写/回填里有更细的工程展开,本质同源。 --- ## 六、拆单体:先找接缝、用防腐层隔离,且"先模块化单体,再按需拆服务" 前面五招是"怎么安全地换"。这一节回答一个更大的战略问题:**一个大单体,到底该不该拆成微服务?怎么拆?** 先泼一盆冷水。新手最容易犯的错,是**把"拆微服务"当成进步本身**——大厂都拆了,我们也拆。但 [04 · 十大核心架构模式](04-十大核心架构模式.md) 早说过:**微服务是"成熟期的解药",不是"成长期的标配"**;过早拆分,只会把"函数调用"升级成"网络调用",凭空背上分布式的全部苦头([10] 那一整章的硬道理)。 **怎么找该拆的"缝"?** 答案来自 DDD(领域驱动设计)的**限界上下文(Bounded Context)**——不要按技术分层拆(那只会拆出贫血的 CRUD 服务),要**按业务能力的自然边界拆**:订单是一个上下文、库存是一个、计费是一个。**好的服务边界,是业务概念的边界,不是数据库表的边界。**(这也直接呼应 [08] 的康威定律:服务边界对不齐团队边界,拆了也白拆。) **拆的时候,新旧两套领域模型必然打架**——旧单体里"订单"这个概念可能又脏又杂糅,你不想让它污染新服务里干净的模型。隔离它的标准武器叫**防腐层(Anti-Corruption Layer, ACL)**: ``` 防腐层(ACL):在新服务和旧系统之间,架一道"翻译 + 隔离"墙 ┌────────────────┐ ┌─────────┐ ┌──────────────────────┐ │ 新服务 │────▶│ 防腐层 │────▶│ 旧单体 (混乱的旧模型) │ │ (干净的新模型) │◀────│ ACL │◀────│ │ └────────────────┘ └─────────┘ └──────────────────────┘ ▲ 把旧系统的脏概念翻译成新模型能接受的样子, 新服务永远只跟"干净的接口"打交道,旧模型的腐烂不会渗进来。 ``` **而最关键的战略判断是拆分的"姿势"**——别一上来就拆成一堆独立部署、各有数据库、靠网络互调的微服务: ``` 单体演进的正确顺序(别跳级): 一坨泥单体 ──▶ 模块化单体 ──▶ (只在确有瓶颈时) 按需抽离微服务 (没有边界) (Modular Monolith: (把真正需要独立伸缩/ 一个部署单元,但内部 独立发布的那个上下文, 有强制的模块边界) 才升级成服务) ① 先在"一个进程内"把边界划清楚 —— 改边界几乎零成本,改错了重划就行 ② 边界经过真实业务捶打、稳定了,再把"确实需要独立伸缩/发布"的那块拆出去 ③ 不需要独立的,就一直留在模块化单体里 —— 这本身就是一个好归宿 ``` **模块化单体**的妙处在于:它把"边界设计"和"分布式部署"这两件事**解耦**了。你先用近乎零成本的方式(进程内的模块边界)反复打磨"缝划在哪",等缝被真实业务验证稳定了,才为"确实需要独立伸缩或独立发布"的那一两块,支付分布式的代价去抽成服务。**先拿到微服务最值钱的那部分(清晰边界),把最贵的那部分(分布式运维)推迟到非付不可的时候。** > **架构智慧**:**"拆不拆微服务"是个被严重带偏的问题。真正的问题是"边界划对了没有"——边界对了,模块化单体就足够好;边界错了,拆成微服务只会把『改一处动一片』升级成『改一处、跨网络地动一片』。** 所以纪律是:**先模块化单体,把缝划对、划稳;再按需、按瓶颈,一块块抽成服务。** 服务数量从来不是目标,它是为某个具体质量属性(独立伸缩、独立发布、故障隔离)付出的代价——回到 [06](06-质量属性与取舍.md) 那把尺子量,而不是数。 --- ## 七、适应度函数(Fitness Functions):把架构约束写成自动化测试,让系统"长大但不腐化" 最后一个问题:你费尽心力把单体拆成了漂亮的模块化结构、划好了边界——**怎么保证它不会在后续几百次提交里,被一点点地又揉回一坨泥?** [08] 讲过技术债会无意识地累积,而架构边界恰恰是最容易被悄悄违反的东西:某天某人图省事,让"订单模块"直接 import 了"计费模块"的内部类,边界就破了一个口子,然后破口越来越多。 **演进式架构**(Evolutionary Architecture,Neal Ford / Rebecca Parsons / Patrick Kua)给的解药是**适应度函数**:**把你在乎的架构约束,写成一段能自动运行、会失败、能卡住 CI 的测试。** 架构规则一旦能被机器持续验证,它就从"墙上贴的、靠自觉的规范",变成了"违反就红、合不进去的硬约束"。 ``` 适应度函数 = 给架构装上"持续体检",一违反就报警 你在乎的架构约束 → 写成自动化适应度函数(进 CI) ─────────────────────── ────────────────────────────────── "订单模块不准依赖计费模块内部" → 依赖检查:扫到这条 import 就 fail "任何服务的 p99 不准超 200ms" → 性能测试:超了就 fail "领域层不准 import 框架/Web 层" → 分层检查:违反就 fail "没有循环依赖" → 依赖图分析:出现环就 fail "API 不准引入破坏性变更" → 契约测试:不兼容就 fail 每次提交都跑一遍 → 架构腐化在"破口刚出现"时就被挡住,而不是攒成大窟窿才发现。 ``` 这正是把 [08] 的两件事**自动化**了:它让"演进式架构要留好接缝"从一句口号,变成"接缝被破坏就立刻报警"的护栏;也让"技术债要记账"从事后追悔,变成"想欠这笔架构债?CI 先拦住你,逼你显式地确认"。 > **架构智慧**:**架构不是画完图就定型的雕像,是一个要持续维护的活系统——而"活的"东西若没有免疫系统,必然腐化。适应度函数就是架构的免疫系统:它不阻止系统长大,它只阻止系统在长大的过程中烂掉。** 没有它,你今天辛苦划清的每一道边界,都只是在等一个赶时间的下午被人捅穿。 --- ## 📌 真实案例:三个"换引擎"的经典,和一次著名的"拆了又拼回去" **① GitHub Scientist——给关键路径重构兜底信心(并行运行的活样板)。** GitHub 当年要重构其**权限判断**这条关键路径——这是错一点点就会造成"该看见的看不见、不该看见的泄露"的高危代码。他们没有"改完祈祷",而是做了并行运行,并把这套机制抽成了开源库 **Scientist**:旧权限逻辑(control)的结果照常返回给用户,新逻辑(candidate)在背后对同一请求跑一遍,**两边结果不一致就记录、报警,但绝不影响线上**。靠真实流量喂出来的"不一致清单",他们把那些自己都没想到的边界情况一个个补平,等不一致率降到足够低,才放心切流。这正是本章第四节的精确落地。 📎 [github/scientist](https://github.com/github/scientist) · 设计文章 [Move Fast and Fix Things](https://github.blog/engineering/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)](https://highscalability.com/amazon-architecture/) **③ Netflix——七年迁云,坚持"重写架构而非搬运"。** 2008 年 8 月,Netflix 核心的 Oracle 单体数据库损坏,导致**三天发不出 DVD**。痛定思痛,他们决定上云。这场迁移**整整走了七年(2008 → 2016 年 1 月最后一个计费系统切完)**,而且 Netflix 反复强调:**他们不是"lift-and-shift(原样搬到云上)",而是逐个服务地重构成微服务**——典型的绞杀者式渐进迁移,新服务一块块长出来、老单体一块块萎缩,而不是停服去赌一个大版本。 📎 [Netflix: Completing a Decade of Cloud Migration](https://about.netflix.com/en/news/completing-the-netflix-cloud-migration) **④ Segment《Goodbye Microservices》——拆得太碎,又拼回了单体。** 这是给所有"微服务崇拜"最响的一记警钟。Segment 早年(约 2013)为了**故障隔离**把系统拆成微服务,一个数据目的地(destination)一个 worker。2016-2017 业务爆发,目的地数量飙升(一个月新增约 3 个),**每个服务一个仓库、还共享一堆公共库**——结果改一次公共库要花掉**约一周**的开发量(全卡在测试上);而"理论上彻底的故障隔离"需要**上万个微服务**(每客户每队列一个),根本不现实。2017 年他们**回退到一个叫 Centrifuge 的单体**,主动放弃了部分隔离性,换回了"一个仓库、统一版本、分钟级部署、能继续做新功能"。 > 复盘里最扎心的一句:**"如果微服务用错了地方、或被当成创可贴去贴根本问题,你会因为淹没在复杂度里而再也做不了新产品。"** 📎 [Segment: Goodbye Microservices](https://segment.com/blog/goodbye-microservices/) · [InfoQ 报道](https://www.infoq.com/news/2020/04/microservices-back-again/) **⑤ 反面教材:Netscape 的"推倒重写"之殇。** 2000 年,Joel Spolsky 在 [《Things You Should Never Do, Part I》](https://www.joelonsoftware.com/2000/04/06/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 / 工作流平台](../templates/ai-agent-platform/README.md) 一以贯之的"渐进可控"理念:能力可以让 AI 飞速堆,但**演进的方向盘、刹车和退路,必须攥在人手里**。 > **架构智慧**:在 AI 时代,**写新实现这件事会越来越像"按一下生成";而"在飞行中安全地把它换上去"这件事,会越来越成为人的核心价值。** 实现廉价化得越彻底,"掌舵演进"的判断就越稀缺、越值钱。 --- ## 🎯 随堂检验 --- ## 本章小结 - **大重写(big rewrite)几乎注定失败**:旧系统不会停下等你(靶子在移动),且其丑陋代码里沉淀着踩坑换来的隐性知识——重写等于"对着移动靶射击、同时把血泪经验清零、还要求对手原地等你"。唯一主路是**渐进演进**。 - **绞杀者模式**:在旧系统外围加一层**门面/路由**,新功能用新实现、旧功能逐块迁移,直到旧系统收不到流量、自然下线。命门是那层拦截。 - **抽象分支**:在一层抽象之后并存新旧实现、靠开关逐步切换,**全程在主干、时刻可发布**,以此替代灾难性的长命特性分支。 - **并行运行 / 影子流量**:新旧同时跑、旧的结果给用户、**比对差异**,用真实流量(而非自信)建立信心后再切流;**GitHub Scientist** 是其活样板,也是给"AI 改的代码"兜底的最佳搭档。 - **零停机数据迁移**:数据最难改,所以走 **双写→回填→影子校验→切读→清理(expand-contract)**,让"旧存储一直是权威"成为随时能跳的安全网,只有最后清理不可逆。 - **拆单体**:用 DDD **限界上下文**按业务能力找缝、用**防腐层**隔离新旧模型;纪律是**先模块化单体把边界划对划稳,再按需、按瓶颈一块块抽成服务**——服务数量从来不是目标。 - **适应度函数**:把架构约束写成会失败、能卡 CI 的自动化测试,给架构装上免疫系统,让它"长大但不腐化"。 - **AI / vibe coding 主线**:AI 让重构/迁移的体力活廉价,并行运行天生适合给 AI 改的代码兜底;但**"该不该拆、先动哪、何时切、怎么退、是模块化单体还是真拆服务"——这些掌舵的判断 AI 给不了,正是人在 AI 时代越来越稀缺的核心价值。** > **承上启下**:这一章讲的全是"在技术层面怎么安全地演进系统"。但你大概已经从绞杀者、从拆单体、从 Amazon 的两个披萨团队里反复嗅到一股味道——**架构怎么拆、能不能拆得动,根子常常不在技术,而在组织。** 下一章(进阶篇第 6 章)[《15 · 组织即架构》](15-组织即架构.md),我们把 [08] 提过的康威定律彻底摊开:为什么"系统的架构终将长得像设计它的组织",为什么很多"架构难题"其实是"组织难题"的伪装,以及——你如何反过来用组织设计,去塑造你想要的架构。