58.4 演进路线图
一句话
路线图不是甘特图,而是从当前状态到目标状态的分波迁移计划;每一波都要有可验证的退出标准与回滚点。
先破除四个误区
| 误区 | 现实 |
|---|---|
| “路线图 = 项目列表” | 项目是“做什么”,路线图是“先变成什么、再变成什么”;顺序错了,做完也不成体系 |
| “大爆炸式替换” | 一次性替换风险最高、回滚最难。能分波就不要一次做完 |
| “架构师画图,团队执行” | 路线图要按团队容量排,不按架构师的理想排;否则永远延期 |
| “路线图定了一年不变” | 业务、容量、依赖都在变;路线图要季度校准,方向稳定、波次可调 |
路线图的三层
| 层 | 回答 | 更新频率 | 负责人 |
|---|---|---|---|
| 目标架构 | 三年后系统长什么样(边界、数据、部署形态) | 年 | 架构负责人 |
| 迁移波次 | 先迁什么、后迁什么、每波的退出标准 | 季度 | 架构 + 团队 lead |
| 团队容量 | 每波能投入多少人、与业务需求如何分配 | 季度/月 | 工程经理 |
三层缺一:只有目标没有波次,是画饼;只有波次没有容量,是压榨;只有容量没有目标,是打杂。
五种迁移模式
| 模式 | 做法 | 适用 | 代价 |
|---|---|---|---|
| 绞杀者(Strangler) | 新实现逐步接管流量,旧实现最后下线 | 单体拆分、替换旧服务 | 双跑期长,路由/数据要兼容 |
| 分支抽象(Branch by Abstraction) | 先抽接口,再换实现 | 替换库/中间件 | 多一层抽象,短期复杂度上升 |
| 并行运行(Parallel Run) | 新旧同时处理,比对结果 | 核心计算、账务 | 双倍资源,比对逻辑要写 |
| 暗发布(Dark Launch) | 新链路接流量但不返回给用户 | 验证性能与正确性 | 写操作要隔离 |
| Expand-Contract | 先兼容扩展、再迁移、最后收缩 | 数据库/契约变更 | 多次发布,周期长(见 55.4 数据库变更管理 (Expand-Contract)) |
选择原则:能绞杀就不重写,能并行就不赌博,能分波就不大爆炸。 大型单体拆到微服务的完整案例与踩坑,见 81.6 大型单体到微服务的演进(待补)。
波次怎么排:依赖 + 容量 + 价值
package main
import (
"fmt"
"sort"
)
type item struct {
name string
deps []string
effort int // 团队·周
value int // 1–5
risk int // 1–5
}
// plan 按“依赖 + 容量 + 价值”切波次:同一波内互不依赖,前一波完成才开下一波。
func plan(items []item, capacity int) ([][]item, []string) {
done := map[string]bool{}
remaining := append([]item(nil), items...)
var waves [][]item
var notes []string
for len(remaining) > 0 {
var ready []item
var blocked []item
for _, it := range remaining {
ok := true
for _, d := range it.deps {
if !done[d] {
ok = false
break
}
}
if ok {
ready = append(ready, it)
} else {
blocked = append(blocked, it)
}
}
if len(ready) == 0 {
notes = append(notes, "依赖成环或引用了不存在的项,排期中止")
break
}
sort.Slice(ready, func(i, j int) bool {
if ready[i].value != ready[j].value {
return ready[i].value > ready[j].value
}
return ready[i].effort < ready[j].effort
})
var wave []item
used := 0
var next []item
for _, it := range ready {
if used+it.effort <= capacity {
wave = append(wave, it)
used += it.effort
} else {
next = append(next, it)
}
}
if len(wave) == 0 { // 单项就超过容量:强制单独一波并提示
wave = append(wave, ready[0])
used = ready[0].effort
next = ready[1:]
notes = append(notes, fmt.Sprintf("%s 超过单波容量,单独成波", ready[0].name))
}
for _, it := range wave {
done[it.name] = true
}
waves = append(waves, wave)
remaining = append(next, blocked...)
}
return waves, notes
}
func main() {
items := []item{
{"抽取订单域", nil, 6, 5, 2},
{"订单库拆分", []string{"抽取订单域"}, 5, 5, 4},
{"统一入口网关", nil, 4, 3, 2},
{"灰度发布平台", []string{"统一入口网关"}, 4, 3, 3},
{"消息队列迁移", []string{"抽取订单域", "统一入口网关"}, 4, 4, 2},
{"旧单体下线", []string{"订单库拆分", "消息队列迁移"}, 3, 5, 4},
{"可观测性统一", nil, 3, 2, 1},
}
const capacity = 10
waves, notes := plan(items, capacity)
fmt.Println("== 演进路线图:按波次排 ==")
totalWeeks := 0
for i, w := range waves {
used, value := 0, 0
var names string
for _, it := range w {
used += it.effort
value += it.value
names += fmt.Sprintf("%s(%d周) ", it.name, it.effort)
}
totalWeeks += used
fmt.Printf("Wave %d 容量 %d/%d 团队·周 价值 %d\n", i+1, used, capacity, value)
fmt.Printf(" %s\n", names)
}
fmt.Printf("\n共 %d 波、%d 团队·周(若容量允许可并行压缩)\n", len(waves), totalWeeks)
for _, n := range notes {
fmt.Println("注意:" + n)
}
// 验收:每个依赖都必须在更早的波次里
pos := map[string]int{}
for i, w := range waves {
for _, it := range w {
pos[it.name] = i
}
}
bad := 0
for _, it := range items {
for _, d := range it.deps {
if pos[d] >= pos[it.name] {
fmt.Printf("依赖违例:%s 依赖 %s,但没有排在它之前\n", it.name, d)
bad++
}
}
}
fmt.Printf("依赖检查:%d 处违例\n", bad)
}
本机运行结果:
== 演进路线图:按波次排 ==
Wave 1 容量 10/10 团队·周 价值 8
抽取订单域(6周) 统一入口网关(4周)
Wave 2 容量 9/10 团队·周 价值 9
订单库拆分(5周) 消息队列迁移(4周)
Wave 3 容量 10/10 团队·周 价值 10
旧单体下线(3周) 灰度发布平台(4周) 可观测性统一(3周)
共 3 波、29 团队·周(若容量允许可并行压缩)
依赖检查:0 处违例
三点解读:
- 波次不是按“项目大小”排的,是按依赖与容量排的。订单库拆分必须等订单域抽取完成;旧单体下线必须等订单库与消息队列都完成。
- 容量 10 团队·周/波,Wave 2 只用 9,是因为剩下的项放不下,不是浪费;强行塞满会让波次延期。
- 依赖检查 0 违例:如果出现“依赖项排在后面”,说明波次计划不可执行,必须重排。
每一波的退出标准
| 维度 | 退出标准(示例) | 不达标的处理 |
|---|---|---|
| 流量 | 新链路承接 ≥ 20% 生产流量 | 延长本波,不加下一波 |
| 质量 | 错误率不高于基线 +0.1pp;P99 不超过目标 | 回滚或修复后再推进 |
| 数据 | 对账/校验差异 = 0 | 停写、修数据,禁止继续 |
| 依赖 | 下游已切到新接口;旧接口调用量 = 0 | 清理遗留调用 |
| 债 | 本波新增债已登记并有还款计划 | 不许“先欠着再说” |
| 回滚 | 回滚脚本演练过,5 分钟内可回 | 没有回滚点不许开波 |
🔑 路线图的每一波都要回答两个问题:怎么算完成?失败怎么退? 答不上来,就不是一波,是一次冒险。
度量:路线图有没有真的推进
| 指标 | 算法 | 用途 |
|---|---|---|
| 迁移流量占比 | 新链路流量 / 总流量 | 判断“真的在用”而不是“代码合并了” |
| 服务/模块迁移数 | 已下线旧实现数 / 计划数 | 防止“只建新不拆旧” |
| 债利息变化 | 见 58.2 技术债管理 的月利息 | 判断演进是否降低了变更成本 |
| 发布 lead time | 提交到上线的中位数 | 判断解耦是否生效 |
| 回滚次数 | 每波回滚次数 | 判断退出标准是否太松 |
只看“完成了多少项目”会骗人:项目都“完成”了,旧系统还在跑、流量没迁、债没降,等于没演进。
沟通与治理
| 动作 | 频率 | 产出 |
|---|---|---|
| 一页纸路线图 | 季度 | 目标、波次、容量、风险 |
| 波次评审 | 每波开始前 | ADR(决策与代价,见 58.1 ADR 实践) |
| 季度校准 | 每季度 | 调整波次与容量,不改长期方向 |
| 架构评审 | 重大变更前 | 结论与行动项(见 58.3 架构评审) |
一页纸要能回答:三年后长什么样、今年走到哪、下季度做什么、需要多少容量、最大风险是什么。写不成一页,说明还没想清。
生产落地:指名 → 可抄 → 可验 → 可退
指名:路线图文档用 Markdown/Confluence;波次任务进 Jira/Linear 的 epic;决策落 ADR;容量用季度排期会确认。工具不是重点,依赖、容量、退出标准才是。
可抄(一页纸模板):
# 订单域演进路线图(2026)
## 目标(三年)
- 订单域独立部署;数据库按 user_id 分片;旧单体只剩报表
## 波次
| 波次 | 内容 | 容量 | 退出标准 | 回滚点 |
|---|---|---|---|---|
| W1 | 抽订单域 + 统一网关 | 10 人周 | 20% 流量、错误率 +0.1pp 内 | 路由切回单体 |
| W2 | 订单库拆分 + MQ 迁移 | 9 人周 | 对账差异 0 | 双写开关 |
| W3 | 旧单体下线 + 灰度平台 | 10 人周 | 旧接口调用 0 | 保留旧实例 7 天 |
## 风险
- 分片键选择错误 → 先做影子读验证
可验:本节排期器真跑过,输出如上;它验证“依赖顺序 + 容量 + 价值”三条约束。人工验证:随机抽一波,检查退出标准是否可量化、回滚点是否演练过、流量是否真的迁了。
可退:每波都必须有回滚点;如果一波失败,退回上一波状态,而不是硬着头皮往下走。长期方向可以调整,但每次调整要更新路线图与 ADR,避免“口头改方向”。
反模式
| 反模式 | 后果 |
|---|---|
| 大爆炸式替换 | 风险集中,回滚困难 |
| 只建新不拆旧 | 双份维护,债越滚越大 |
| 没有退出标准 | 波次永远“快完成了” |
| 按架构师理想排期,不看容量 | 团队被压垮,路线图失信 |
| 甘特图精确到人天 | 一变就全乱,没人再信 |
| 路线图只有目标没有波次 | 团队不知道下周做什么 |
| 不度量流量迁移 | 代码合并了,用户还在旧系统 |
| 一年不校准 | 业务变了,路线图还在原地 |
自检
Q1 路线图和项目列表的区别是什么?
▸ 答案:项目列表是“要做哪些事”,路线图是“系统先变成什么状态、再变成什么状态”,每一波有退出标准与回滚点。项目可以做完,但系统状态没变(旧系统还在跑、流量没迁),说明路线图没实现。
Q2 为什么波次规划要同时看依赖、容量和价值?
▸ 答案:只看依赖会排出“能执行但没价值”的顺序;只看价值会排出“依赖不满足、执行不了”的顺序;只看容量会平均分配、没有重点。三者一起,才能得到“可执行、有价值、不压垮团队”的波次。
Q3 绞杀者模式和“重写”有什么本质区别?
▸ 答案:绞杀者是逐步接管流量,旧实现继续服务直到新实现验证通过,随时可切回;重写是一次性替换,在新系统上线前旧系统无法验证新系统,失败时回滚代价极大。绞杀者的代价是双跑期长、路由与数据要兼容。
Q4 一波的退出标准至少包含哪些维度?
▸ 答案:流量(新链路承接比例)、质量(错误率/P99 不劣化)、数据(对账/校验差异为 0)、依赖(旧调用清零)、债(新增债已登记)、回滚(演练过且时限内可回)。缺一个,就容易“看起来完成了,实际没迁”。
Q5 路线图为什么要季度校准,而不是一年定死?
▸ 答案:业务优先级、容量、依赖、故障都会变;一年定死会让路线图与现实脱节,团队要么无视它,要么被它拖累。季度校准保持“长期方向稳定、短期波次可调”,并用 ADR 记录每次调整的原因。
关联
- 前置:58.2 技术债管理、58.1 ADR 实践
- 并列:58.3 架构评审
- 下游:55.4 数据库变更管理 (Expand-Contract)
- 待补:81.6 大型单体到微服务的演进