Skip to content

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 时代,写新实现这件事会越来越像"按一下生成";而"在飞行中安全地把它换上去"这件事,会越来越成为人的核心价值。 实现廉价化得越彻底,"掌舵演进"的判断就越稀缺、越值钱。


🎯 随堂检验

🤔一个跑在生产上的核心计费模块,逻辑复杂、错了代价极高,团队(或 AI)重写了一版『理论上等价』的新实现。上线前最稳妥的架构判断是?
  • A跑通全部单元测试就直接切到新实现,旧的删掉
  • B用并行运行:旧实现的结果照常返回用户,新实现对同一批真实流量在背后跑、逐条比对,等不一致率足够低再切流
  • C开一个长命特性分支,把新实现完整改好、充分测试后,一次性合并并上线

本章小结

  • 大重写(big rewrite)几乎注定失败:旧系统不会停下等你(靶子在移动),且其丑陋代码里沉淀着踩坑换来的隐性知识——重写等于"对着移动靶射击、同时把血泪经验清零、还要求对手原地等你"。唯一主路是渐进演进
  • 绞杀者模式:在旧系统外围加一层门面/路由,新功能用新实现、旧功能逐块迁移,直到旧系统收不到流量、自然下线。命门是那层拦截。
  • 抽象分支:在一层抽象之后并存新旧实现、靠开关逐步切换,全程在主干、时刻可发布,以此替代灾难性的长命特性分支。
  • 并行运行 / 影子流量:新旧同时跑、旧的结果给用户、比对差异,用真实流量(而非自信)建立信心后再切流;GitHub Scientist 是其活样板,也是给"AI 改的代码"兜底的最佳搭档。
  • 零停机数据迁移:数据最难改,所以走 双写→回填→影子校验→切读→清理(expand-contract),让"旧存储一直是权威"成为随时能跳的安全网,只有最后清理不可逆。
  • 拆单体:用 DDD 限界上下文按业务能力找缝、用防腐层隔离新旧模型;纪律是先模块化单体把边界划对划稳,再按需、按瓶颈一块块抽成服务——服务数量从来不是目标。
  • 适应度函数:把架构约束写成会失败、能卡 CI 的自动化测试,给架构装上免疫系统,让它"长大但不腐化"。
  • AI / vibe coding 主线:AI 让重构/迁移的体力活廉价,并行运行天生适合给 AI 改的代码兜底;但**"该不该拆、先动哪、何时切、怎么退、是模块化单体还是真拆服务"——这些掌舵的判断 AI 给不了,正是人在 AI 时代越来越稀缺的核心价值。**

承上启下:这一章讲的全是"在技术层面怎么安全地演进系统"。但你大概已经从绞杀者、从拆单体、从 Amazon 的两个披萨团队里反复嗅到一股味道——架构怎么拆、能不能拆得动,根子常常不在技术,而在组织。 下一章(进阶篇第 6 章)《15 · 组织即架构》,我们把 [08] 提过的康威定律彻底摊开:为什么"系统的架构终将长得像设计它的组织",为什么很多"架构难题"其实是"组织难题"的伪装,以及——你如何反过来用组织设计,去塑造你想要的架构。

💬 评论