← 文章 / 云原生与基础设施
freeCodeCamp 2小时前 · 2026-09-18 14:20:25 · 2 阅读

渐进式迁移遗留单体:避免“大爆炸”重写的增量策略

大型遗留系统迁移往往在最终切换之前很久就已经失败了。

失败的根源通常在于把迁移当作一次性事件来做:搬应用、搬数据库、迁移所有用户、切换流量、关掉旧系统。

这就埋下了一个危险的假设:旧系统和新系统必须一次性完成交接。

实际上通常并不需要。

如果你已经了解遗留系统的行为,用特征测试(characterization tests)把它保护起来,建立便于迁移的边界,并能对比新旧实现,那么你还有另一种选择。

你可以每次只迁移一个能力。这会彻底改变问题的性质。

不是这样:

legacy monolith
      ↓
complete rewrite
      ↓
big-bang cutover

而是这样:

legacy monolith
      ↓
one capability extracted
      ↓
small percentage of traffic
      ↓
observe
      ↓
expand
      ↓
repeat

目标不是把迁移拖得更慢,而是让每一步变更更小、可观察、可回滚。

在本教程中,我将展示如何以渐进方式迁移遗留单体应用,内容包括:

  • 选择安全的首个迁移切片

  • 在新旧代码之间定义边界

  • 在不同实现之间路由请求

  • 使用 Strangler Fig 模式

  • 按业务能力而非技术分层进行迁移

  • 让新旧实现并行运行

  • 逐步放量引流

  • 在完全切换之前发现故障

  • 设计回滚路径

  • 谨慎处理数据所有权

  • 移除已迁移的遗留代码

  • 合理使用 AI,避免把渐进式迁移变成自动化重写

示例代码使用 TypeScript,但这套方法适用于大多数语言、运行时和架构。

目标很简单:把迁移变成一系列受控的变更,而不是一次不可逆的事件。

前置要求

要跟上本文内容,你需要熟悉:

  • TypeScript 或类似语言

  • API 与服务边界

  • 集成测试

  • 依赖注入

  • 路由与反向代理

  • 数据库事务

  • 可观测性

  • 渐进式重构

  • 遗留系统现代化改造

你应该已经理解想要迁移的功能的行为。

理想情况下,你需要清楚:

  • 它的输入

  • 它的输出

  • 它的重要业务规则

  • 它的副作用

  • 它的依赖项

  • 它的外部契约

  • 你将如何检测行为差异

如果尚未达到这一点,进行迁移可能为时过早。

目录

为何“大爆炸”式迁移风险极高

设想一个遗留的商务应用。

它包含:

customers
orders
payments
inventory
shipping
invoicing
notifications
reporting

现代化方案通常会写道:

replace the monolith

这听起来像是一个项目。

但在实际操作中,这可能意味着要同时更改:

runtime
framework
database
deployment model
API contracts
authentication
networking
observability
data model
business logic
external integrations

如果最终切换失败,可能的原因多不胜数。

例如:

Did pricing change?

Did the database migration lose data?

Is the payment provider failing?

Did authentication behave differently?

Did the new runtime change date handling?

Did a timeout become shorter?

Did an event stop being published?

Did the new deployment configuration fail?

这是“大爆炸”式迁移的核心问题之一:变量变更过多且集中发生。

增量迁移旨在减少每一步中变更的变量数量。

以迁移切片思考,而非以应用思考

不要问:

我们该如何迁移这个单体架构?

而要问:

我们能独立迁移的最小、但有意义的业务能力是什么?

举个例子:

计算订单总额
生成发票
发送订单确认
创建货运单
续订订阅
审批客户

一个迁移切片最好具备:

清晰的输入
清晰的输出
已知的副作用
明确的依赖关系
可观察的行为
可回滚的路径

这样你就有了一个具体可迁移的东西。

比如:

生成发票

可以拆解为:

输入:
orderId

行为:
加载订单
计算税费
生成发票号
创建发票

副作用:
存储发票
发布 invoice.created 事件

输出:
invoice

这比迁移下面这些要容易得多:

billing 模块

或者:

src/services/

业务能力比文件夹更适合作为迁移单元。

谨慎选择第一个迁移对象

第一个切片很关键。

我通常不建议从系统里最关键的能力入手。

你要找的是既能充分验证迁移方案、又不会因为出错就引发灾难性后果的对象。

合适的第一个切片通常具有:

中等流量
较少的外部依赖
行为清晰
测试覆盖良好
事务边界少
影响范围小

比如:

生成客户对账单

就比下面这个更适合作为首个迁移对象:

支付授权

第一次迁移在某种程度上是技术活,但更是一次学习演练。

你要验证的是:

路由
部署
可观测性
回滚
数据访问
测试
团队协作流程

验证通过后,再把这套模式应用到更关键的能力上。

在旧代码和新代码之间建立边界

假设遗留应用中有这样一个函数:

async function generateInvoice(
  orderId: string
) {
  // legacy implementation
}

迁移之前,先引入一个边界:

interface InvoiceGenerator {
  generate(
    orderId: string
  ): Promise<Invoice>;
}

遗留实现就变成了:

class LegacyInvoiceGenerator
  implements InvoiceGenerator {
  async generate(
    orderId: string
  ): Promise<Invoice> {
    // existing behavior
  }
}

新的实现如下:

class NewInvoiceGenerator
  implements InvoiceGenerator {
  async generate(
    orderId: string
  ): Promise<Invoice> {
    // migrated behavior
  }
}

调用方无需关心当前生效的是哪个实现。

这带来了一项关键能力:

replace implementation
without replacing caller

这也是渐进式迁移的基石之一。

采用绞杀榕模式

描述渐进式替换的一个常见方法就是绞杀榕(Strangler Fig)模式。

不是一次性替换整个应用,而是让新行为围绕旧系统逐渐生长。

概念上:

            incoming request
                   │
                   ↓
                router
              /        \
             /          \
      legacy path     new path

初期:

legacy: 100%
new:      0%

随后:

legacy: 95%
new:      5%

接着:

legacy: 50%
new:     50%

最终:

legacy:  0%
new:    100%

此时,该功能对应的旧实现就可以移除了。

关键在于替换是逐步进行的。旧应用继续承载系统的一部分,而新实现接管其余部分。

按功能迁移,而非按技术层迁移

一种诱人的迁移策略是:

move database
then move services
then move APIs
then move UI

这会导致在很长一段时间内,每个功能都横跨新旧两种架构。

例如:

new API
↓
legacy service
↓
new database
↓
legacy event publisher

有时这种情况无法避免。

但只要可能,我倾向于采用垂直切片。

一个垂直切片可能是:

Generate Invoice

request
↓
application logic
↓
persistence
↓
events
↓
response

该功能可以作为一致的整体进行迁移。

然后:

Create Shipment

可以独立迁移。

接着:

Renew Subscription

以此类推。

这样能让你更早拥有可用的已迁移功能。同时,也能减少临时的跨系统依赖数量。

让旧版与新版实现并存

在增量迁移过程中,新旧系统并存是常态。

在一段时间内,你可能会同时拥有:

LegacyInvoiceGenerator
NewInvoiceGenerator

两者同时部署。

这不是无意的重复代码,而是迁移策略的一部分。

关键问题在于请求如何在两者之间做选择。

你可以使用以下因素:

feature flag
tenant
user group
request header
region
percentage rollout
specific account IDs

例如:

class InvoiceRouter {
  constructor(
    private readonly legacy:
      InvoiceGenerator,
    private readonly migrated:
      InvoiceGenerator
  ) {}

  async generate(
    orderId: string,
    useMigrated: boolean
  ) {
    if (useMigrated) {
      return this.migrated.generate(
        orderId
      );
    }

    return this.legacy.generate(
      orderId
    );
  }
}

这个设计故意保持简单。关键在于路由逻辑是显式的,你知道每个请求是由哪个实现处理的。

显式路由流量

避免使用难以观察的迁移逻辑。

例如:

try {
  return await newService.call();
} catch {
  return legacyService.call();
}

这看似具有弹性,但可能掩盖故障。

假设新实现的失败率为 40%。如果所有失败都静默回退到旧版,用户可能察觉不到问题。但迁移过程并不健康。

问题在于,第一种写法将两个决策混为一谈:该由哪个实现接收请求 以及 该实现失败时该怎么办。由于回退逻辑隐藏在 catch 块中,已迁移的路径可能反复失败,却不会产生可量化的显式路由信号。

更好的方法是先做出路由决策,记录该决策,然后调用选定的实现。这样能将迁移策略与错误处理分离,并清晰记录实际上有多少流量进入了每条路径。

例如:

const route =
  migrationPolicy.route(request);

metrics.increment(
  `invoice.route.${route}`
);

if (route === "migrated") {
  return migrated.generate(
    request.orderId
  );
}

return legacy.generate(
  request.orderId
);

这样你就能度量:

路由到 legacy 的请求数
路由到 migrated 的请求数
迁移失败次数
回退次数
延迟
业务结果

迁移本身应该成为系统的一等行为,全程可观测。

从内部或低风险流量开始

在把大部分客户流量切到迁移后的路径之前,先从更安全的流量开始。

比如:

开发环境
测试环境
内部用户
员工账号
测试租户
特定的低风险客户

这样可以用更低的风险验证:

部署
路由
可观测性
数据访问
外部集成
故障处理

然后再逐步扩大范围,例如:

内部用户
↓
1% 生产流量
↓
5%
↓
10%
↓
25%
↓
50%
↓
100%

具体百分比不重要,重要的是原则。

每一步流量提升,都应该建立在前一阶段积累的足够证据之上。

逐步增加生产流量

假设你有:

10,000 次发票请求/天

不要一次性把所有请求从:

legacy → new

切换过去,而是先路由:

1%

这大约意味着:

100 个真实请求/天

走迁移后的路径。

此时持续监控:

错误率
延迟
输出差异
副作用
客户可见的故障
业务指标

系统表现正常就增加流量;表现异常就减少或停用迁移路由。

这样迁移就变成了一个可控的实验,和一次性切换完全是两回事。

在上线前后使用差分测试

本系列上一篇文章讲的就是差分测试,这一技术在迁移中尤其有用。

差分测试是指用相同的输入同时运行 legacy 和迁移后的实现,并对比它们的可观测行为。根据功能的不同,对比内容可能包括返回值、错误、状态变更和副作用。

目标并非证明两种实现在内部结构上完全一致,而是确保在行为差异波及所有生产流量之前将其发现。

在实际流量路由生效前,你可以进行如下比对:

same input
↓
legacy result

same input
↓
new result

在灰度发布阶段,只要安全条件允许,也可以抽样真实流量并对比其行为表现。

例如:

real request
      │
      ├────→ active implementation
      │
      └────→ shadow implementation

随后对比以下维度:

output
errors
side effects
business state

这能为你在扩大流量范围前提供数据支撑。

进而,发布决策可基于以下指标:

divergence
error rate
latency
business outcomes

而不是模糊的判断,例如:

看起来没问题。

一个小型端到端发票迁移示例

看到各组件协同工作,理解起来会更容易。

下面是一个刻意保持极简的内存级示例,基于全文贯穿的发票功能。它不包含真实数据库、反向代理、消息队列或部署平台,旨在集中展示迁移的控制流。

首先定义共享契约:

type InvoiceInput = {
  orderId: string;
  subtotal: number;
};

type Invoice = {
  orderId: string;
  total: number;
};

interface InvoiceGenerator {
  generate(
    input: InvoiceInput
  ): Promise<Invoice>;
}

旧版实现计算发票总额的方式如下:

class LegacyInvoiceGenerator
  implements InvoiceGenerator {
  async generate(
    input: InvoiceInput
  ): Promise<Invoice> {
    return {
      orderId: input.orderId,
      total: input.subtotal * 1.21,
    };
  }
}

假设我们已将该功能迁移到新实现中:

class MigratedInvoiceGenerator
  implements InvoiceGenerator {
  async generate(
    input: InvoiceInput
  ): Promise<Invoice> {
    const tax =
      input.subtotal * 0.21;

    return {
      orderId: input.orderId,
      total: input.subtotal + tax,
    };
  }
}

代码写法虽有不同,但预期行为保持一致。

接着,定义一个确定性的流量切换函数。示例中,我们将每个 orderId 分配给 0 到 99 之间的某个桶,以确保同一订单始终遵循同一路径:

function bucketFor(
  value: string
): number {
  const sum = [...value].reduce(
    (total, char) =>
      total + char.charCodeAt(0),
    0
  );

  return sum % 100;
}

function shouldUseMigrated(
  orderId: string,
  percentage: number
): boolean {
  return (
    bucketFor(orderId) < percentage
  );
}

percentage 设置为 10 时,大约 10% 的 ID 将被分配给迁移后的路径。

现在添加几个简单的内存指标:

const metrics = {
  legacyRequests: 0,
  migratedRequests: 0,
  mismatches: 0,
};

然后将旧版和新版的实现放在同一个感知迁移状态的入口点之后:

class IncrementalInvoiceService {
  migratedEnabled = true;
  rolloutPercentage = 10;

  constructor(
    private readonly legacy:
      InvoiceGenerator,
    private readonly migrated:
      InvoiceGenerator
  ) {}

  async generate(
    input: InvoiceInput
  ): Promise<Invoice> {
    const legacyResult =
      await this.legacy.generate(
        structuredClone(input)
      );

    const migratedResult =
      await this.migrated.generate(
        structuredClone(input)
      );

    if (
      migratedResult.orderId !==
        legacyResult.orderId ||
      migratedResult.total !==
        legacyResult.total
    ) {
      metrics.mismatches += 1;
    }

    const useMigrated =
      this.migratedEnabled &&
      shouldUseMigrated(
        input.orderId,
        this.rolloutPercentage
      );

    if (useMigrated) {
      metrics.migratedRequests += 1;
      return migratedResult;
    }

    metrics.legacyRequests += 1;
    return legacyResult;
  }
}

这个小服务融合了文章中的几个概念。

首先,它使用相同的输入运行两种实现并比较结果。由于该示例完全在内存中运行且没有外部副作用,这样做是安全的。

其次,它将一定比例的请求路由到迁移后的结果。

第三,它记录了多少请求使用了每条路径,以及发生了多少次行为不匹配。

你可以用几个请求来测试它:

const service =
  new IncrementalInvoiceService(
    new LegacyInvoiceGenerator(),
    new MigratedInvoiceGenerator()
  );

for (let i = 1; i <= 100; i++) {
  await service.generate({
    orderId: `order-${i}`,
    subtotal: 1000,
  });
}

console.log(metrics);

你可能会看到类似这样的输出:

legacyRequests:   89
migratedRequests: 11
mismatches:        0

由于分桶函数刻意设计得很简单,仅有 100 个输入时,实际比例未必正好是 90/10。重点在于路由是确定性的、可度量的,并且由 rolloutPercentage 控制。

如果迁移后的实现开始出现差异,mismatch 计数器会给你一个可观察的信号。

而如果你决定暂停灰度,回滚方式也很明确:

service.migratedEnabled = false;

从这一刻起,所有返回的响应又都来自旧实现。

这只是一个刻意简化的示例。生产系统还需要更强的路由能力、真实的指标采集、错误处理、持久化状态,以及对副作用的小心处理。

尤其是,如果生成发票会发送邮件、写入两个生产数据库、向客户扣款,或发布对外可见的事件,就不能盲目地同时执行两套实现。在这些场景下,影子路径需要借助录制适配器、隔离的基础设施,或其他机制,让你在不重复产生真实副作用的前提下完成行为对比。

但控制回路是一样的:

相同输入
↓
对比新旧实现的行为
↓
路由一小部分流量
↓
观察
↓
扩大范围或回滚

这就是增量迁移最小但可用的形态。

回滚要在需要之前就设计好

回滚机制不应该等到出事故时才临时设计。在切换流量之前,就要想清楚:如果迁移后的路径出问题会怎样。

对于路由层面的迁移,回滚可能非常简单:

migration flag = false

流量就会回到:

legacy implementation

例如:

if (
  featureFlags.useNewInvoices
) {
  return migrated.generate(
    orderId
  );
}

return legacy.generate(orderId);

一旦迁移后的路径行为异常,只需:

useNewInvoices = false

回滚几乎即时生效。

不过,回滚操作在以下情况下会变得更加复杂:

数据格式变更
写入了新数据
事件不一致
外部系统已更新
旧代码无法读取新记录

在这些场景中,回滚可能不仅仅是切换一个功能开关那么简单。你可能需要向后兼容的模式(schema),以便两个版本都能读取相同记录;针对外部副作用的补偿操作;可重放的事件;对账任务;或者让旧系统在短时间内继续消费新路径写入的数据。

对于风险较高的迁移,提前定义回滚边界也有助于解决问题。例如:在新 schema 版本写入之前,或发出特定外部事件之后,流量可以回切到旧系统,此时恢复需要补偿措施而非简单回滚。关键在于明确何时回滚仍具可逆性,何时已需切换至不同的恢复策略。

因此,回滚设计必须在部署之前完成。

将数据迁移视为独立问题

应用迁移与数据迁移虽有关联,但并非同一问题。

假设旧系统存储的是:

{
  "customer_type": "P",
  "status": 2
}

而新系统存储的是:

{
  "customerType": "PREMIUM",
  "status": "APPROVED"
}

此时你需要明确回答:

哪个数据库是权威数据源?

两个系统能否读取相同数据?

是在读取时转换数据吗?

是否批量迁移记录?

是否复制变更?

数据所有权何时转移?

这些决策必须明确。否则,应用迁移看似成功,数据边界却依然模糊不清。

谨慎对待双写

一种常见的过渡策略是:

写入旧数据库
+
写入新数据库

这被称为双写(dual writing),看起来简单直接。

例如:

await legacyOrders.save(order);
await newOrders.save(order);

但如果发生以下情况:

旧系统写入成功
新系统写入失败

两个系统的数据就不一致了。

或者:

旧系统写入失败
新系统写入成功

问题依旧。

双写引入了分布式一致性问题。

如果使用双写,你需要考虑:

重试
幂等性
对账
顺序
部分失败
监控

有时候更稳妥的做法是:

单一权威写入
↓
变更事件
↓
复制

或者使用事务性 outbox。

没有放之四海而皆准的方案。关键在于不要把双写视为简单的迁移技术。

明确数据归属

在双系统共存期间,数据归属权容易变得模糊。

举个例子:

旧系统写入客户数据
新系统写入发票数据
两个系统都读取订单数据

这种安排可能完全合理,但必须记录下来。

对于每个迁移的功能,需要定义:

系统记录源
写入所有者
读取方
复制方向
一致性预期

例如:

发票

写入所有者:
新系统

数据真相源:
新数据库

旧系统访问:
只读适配器

复制:
新 → 旧报表存储

这样架构就有了明确的流向。

如果没有明确的归属规则,迁移往往会制造出永久性的同步难题。

关注业务行为,而非仅仅盯着基础设施

在灰度发布期间,团队通常监控的是:

CPU
内存
延迟
HTTP 500 错误
数据库连接数

这些指标确实重要,但还不够。

假设:

HTTP 200 成功率 = 99.99%

同时:

发票总额计算错误

基础设施监控显示:

正常

但业务系统其实并不健康。

迁移可观测性必须包含领域信号。

例如:

处理订单量
授权支付量
生成发票量
折扣分布
续费失败数
平均发票总额
发布事件数

如果你了解正常的业务表现,异常波动就能暴露出技术指标抓不到的迁移缺陷。

判断迁移切片何时算作完成

流量达到 100% 并不意味着功能已完全迁移。

在宣布完成之前,我会核对以下事项:

100% 流量走新路径
错误率在可接受范围内
延迟在可接受范围内
行为差异已解决
副作用已验证
数据归属权已确立
回滚窗口已结束
移除旧系统调用方
停止旧系统写入
可观测性就绪

然后自问:

旧的实现还在承担任何职责吗?

如果没有,就删掉它。

让两套实现长期共存,只会带来:

额外的维护成本
理解混乱
重复出现的 bug
职责归属不清
未来的迁移债务

增量迁移的最终目标应该是简化系统,而不是让它永久性地重复。

移除旧路径

这一步经常被一拖再拖。

团队把流量切了过去,却把旧路径留在原地,理由是:

以防万一

几个月之后:

没人知道它是否还在被使用

删除之前,先确认:

路由指标显示流量为零
没有调用方依赖它
数据依赖已经解除
回滚观察期已结束
运维文档已更新

然后移除:

旧的实现
旧的功能开关
旧的数据库访问
不再使用的集成代码
临时的兼容层

删除本身就是迁移的一部分。

只增加新架构、不清理旧架构的迁移,往往会增加而不是降低系统复杂度。

如何在增量迁移中使用 AI

在这个过程中,AI 能帮上很多忙。

比如,它可以分析遗留代码库,帮你回答:

哪些模块实现了这个能力?

哪些调用方依赖它?

它涉及哪些数据库表?

它调用了哪些外部服务?

会产生哪些副作用?

已经存在哪些功能开关?

哪些路径需要适配器?

一个实用的 prompt 可以这样写:

分析 Generate Invoice 这个能力。

识别以下内容:

1. 入口点
2. 业务规则
3. 持久化依赖
4. 外部集成
5. 副作用
6. 调用方
7. 数据归属
8. 可能的迁移切分点

不要重新设计系统。

对每个结论都要给出证据,包括文件路径
和相关代码引用。

AI 还能帮助对比迁移前后的变更。

例如:

对比旧实现和迁移后的实现。

找出以下方面可能存在的行为差异:

- 返回值
- 错误处理
- 副作用
- 持久化
- 事件顺序
- 重试逻辑
- 幂等性
- 事务边界

不要默认新实现是正确的。

这很有用,因为迁移涉及大量重复性的分析工作,而 AI 可以显著加快这类分析。

别让 AI 把迁移变成重写

这是一种常见的失败模式。

当你这样提问时:

帮我迁移这个遗留功能。

模型往往回应如下:

新架构
新领域模型
新 API
新事件模型
新数据库模式
新校验层
新框架

此时,你不再是在迁移某个功能,而是在重新设计它。

虽然有时重新设计确有必要,但这应当是有意为之的结果。

在增量迁移过程中,我倾向于使用带有明确约束的提示词。

例如:

在无意改变可观察行为的前提下,迁移此功能。

保留:

- 输入,
- 输出,
- 错误处理,
- 副作用,
- 相关顺序,
- 事务性行为。

仅引入在目标环境中运行该功能所需的最小结构性变更。

列出任何你无法确信保留的行为。

这样能将变换范围收窄。

AI 应当用于减少机械性工作量,而不是悄悄扩大项目范围。

实用的增量迁移流程

以下是我会采用的流程。

1. 理解功能特性

识别以下要素:

输入
输出
规则
副作用
依赖项
未知项

2. 表征现有行为

使用以下测试保护关键行为:

表征测试
集成测试
契约测试

3. 为迁移重构代码

创建以下结构,且有意不改变行为:

接缝
适配器
显式依赖
清晰编排

4. 构建新实现

在目标环境中实现该功能,保持其可观察契约清晰。

5. 对旧新实现进行差异测试

使用代表性案例对比:

输出
错误
副作用
业务状态

6. 引入显式路由

允许请求通过可观察的迁移策略选择:

遗留路径
或
迁移路径

7. 从安全流量开始

使用:

内部用户
测试租户
选定客户

8. 逐步增加流量

例如按以下比例递增:

1%
5%
10%
25%
50%
100%

仅当证据支持进入下一阶段时,才继续扩大比例。

9. 监控技术与业务指标

同时观察以下两方面:

系统健康度
业务行为

10. 保留回滚能力

确保能快速、清晰地切回传统路径。

11. 转移数据所有权

明确界定各系统负责的数据操作:

写入
读取
复制

12. 移除传统路径

在迁移稳定后:

删除旧实现
移除临时路由
移除过时依赖

然后选择下一个待处理的功能模块。

增量迁移解决不了的问题

增量迁移降低了风险,但并未消除复杂性。

你可能仍需处理:

分布式事务
共享数据库
旧数据库结构
紧密耦合
不支持的运行时环境
低测试覆盖率
组织归属不明
监管合规约束

此外,有些系统进行部分迁移极其困难。

例如:

强状态系统
强耦合的桌面应用
大型事务批处理系统
依赖共享全局状态的系统

有时,迁移的边界需要划定得更大。

但核心原则不变:

做出最小且可逆的变更,以产生有效的迁移进展。

增量不一定指微小,而是指可控。

完整的遗留系统现代化工作流

本文总结了本系列文章一直在构建的完整工作流。

我们从这样一个基础问题出发:

如何在现代改造遗留应用时,避免不小心将其变成一次重写项目?

第一步是理解。

遗留系统
↓
调查
↓
映射行为与依赖

接着是特征化测试。

观察到的行为
↓
测试用例
↓
行为安全网

然后是重构。

纠缠的功能模块
↓
接缝与边界
↓
利于迁移的结构

之后是差异化测试。

旧实现
        +
新实现
        ↓
行为比对

最后执行增量迁移。

理解
↓
特征化
↓
重构
↓
迁移
↓
比对
↓
路由
↓
观测
↓
扩展
↓
移除遗留代码

顺序至关重要。

如果跳过理解环节,你可能会迁移错误的行为逻辑。

如果跳过特征测试,你可能察觉不到行为的变化。

如果跳过重构,迁移边界可能会一直太大。

如果跳过对比,差异就会被掩盖。

如果跳过灰度发布,问题只会在全面铺开后才暴露。

每一步都在化解一种不同的不确定性。

结语

现代化改造遗留应用,并不需要一次性全盘替换。

很多情况下,更稳妥的策略是设计一条路径,让新旧实现可以暂时共存。

迁移一个能力,然后对比验证。

导一小部分流量过去,观察运行情况。

有证据支撑就逐步加大流量,否则就回滚。

明确交接归属,然后移除旧路径。

如此循环往复。

完整的工作流如下:

理解
↓
特征测试
↓
重构
↓
增量迁移
↓
对比行为
↓
逐步导流
↓
观察
↓
移除旧代码

AI 可以加速每一个环节。

它能帮你梳理代码、识别依赖、生成适配器、对比实现、分析故障,以及审查迁移 diff。

但速度快不等于有把握。

关键决策仍然需要工程师的判断:

哪些行为是重要的?

哪些可以改变?

哪些必须保持兼容?

迁移边界在哪里?

多少证据才算够?

什么时候需要回滚?

什么时候可以移除旧路径?

这些问题不是靠生成代码就能回答的,它们属于迁移决策。

这也正是本系列文章想传达的核心。

AI 让写新代码越来越便宜,但遗留系统现代化并不会因此变得轻而易举——它只是让围绕代码做出的决策质量变得更加重要。

因为最安全的迁移,往往不是改动最多的那个方案,而是让你在改变系统的同时,始终清楚改了什么、为什么改、以及是否可以放心继续往前走。

原始来源: freeCodeCamp

评论 (0)