接手一个七年没人维护的服务,第一周最典型的心态是"先看代码"。看三天之后你会发现:能读懂的都读懂了,读不懂的还是读不懂,而你依然不敢重启它。真正的卡点不是理解,是你还没有一次安全失败的能力。
接管的顺序由风险决定,不由好奇心决定。理解可以慢慢长,回滚能力必须在第一次改动之前就位。
1. 四件事的顺序:能回滚比能理解更早
| 能力 | 具体形态 | 没有它的后果 | 到位判据 |
|---|---|---|---|
| 能部署 | 有可重复的发布路径,不靠某人手敲命令 | 环境是黑盒,改不动 | 在干净环境里跑通一次发布 |
| 能回滚 | 有上一版产物 + 一条回滚命令,且演练过 | 每次改动都是单向门 | 回滚一次并确认状态干净 |
| 能观测 | 有日志、指标、错误率可查 | 出问题只能猜 | 能回答"现在是否正常" |
| 能求助 | 知道谁懂哪一块、响应时限多少 | 卡在一个问题上干等 | 联系人清单带时限 |
顺序不能换:没有回滚的部署是赌博,没有观测的回滚只能靠肉眼确认。四件事里最容易被跳过的是第三件——"我先读代码,观测以后再补",而观测恰恰是唯一能让你安全做完后面所有事情的东西。
"能求助"经常被当成软技能,其实是硬能力:你要在没出事的时候就知道谁懂哪一块、响应时限多少。等到出事才去翻通讯录,正是这个系统最脆弱的时刻。把联系人清单写进运行手册,并注明"谁已经没有权限了、谁是名义上的 owner"。
─── 接管的第一个动作清单(一周内完成,顺序固定)
1. 找到产物落点与进程启动路径,记录下来
2. 拉一份当前版本的完整备份(代码 + 配置 + 数据 schema)
3. 在非生产环境重跑一次发布,确认可重复
4. 演练一次回滚,记录耗时不需人工重建的部分
5. 补一条从真实入口发起的探活,而不是只看内部指标
判断"能不能回滚"只问一句:回滚动作是否需要人工重建任何东西?需要重建数据、重新导配置、按顺序手工改回去的,那不是回滚,是第二次上线。
2. 读代码的三个入口与排除法
| 入口 | 怎么找 | 回答的问题 | 暂时别看什么 |
|---|---|---|---|
| 流量入口 | 网关/nginx 配置、监听端口、路由表 | 谁在调、调多频繁 | 内部工具类 |
| 数据边界 | 库表、缓存 key、消息 topic | 状态存在哪、谁能改 | 业务分支细节 |
| 定时任务 | crontab、调度平台、常驻线程 | 没人触发时它在做什么 | 启动参数细则 |
─── 摸清流量入口的最小清单
1. 它监听什么? ss -lntp | grep <pid>
2. 谁在调它? 从网关访问日志按 upstream 反查
3. 有几种调用方? 客户端标识 / UA / 内网段
4. 有直连绕过网关的吗? 查防火墙规则与内网 DNS 记录
5. 停掉一分钟谁会报错? 这是"能不能动"的最终判据
研究路径要用排除法:先确定输入从哪来、输出到哪去,再看状态存在哪,最后才进业务分支。反过来读——从某个类往下追调用链——会在几百个调用点上迷路,而且学不到系统边界。
数据边界这一层还额外回答一个关键问题:能不能只读地把它接过来。如果状态只存在一张表里且没有写入竞争者,你就能先把查询挪到副本上观察一段时间;如果多个进程在争抢同一份状态,任何改动都要先解决竞争,而不是先解决代码。
"这行代码为什么这么写"不要靠猜。查变更历史:提交信息、关联的工单号、那段时间的配置变更。查到的是"当时的约束",猜出来的是"我觉得合理的原因"——后者在评审里站不住,也会在半年后被下一个人推翻。
3. 风险标注与第一行改动
| 标注 | 判据 | 处置 |
|---|---|---|
| 不可碰 | 表结构、对外字段、时序假设、部署顺序 | 在文档里写清后果,本人不动 |
| 一碰就炸 | 有人在绕过它的正常路径使用它 | 先加观测,等看清调用方 |
| 可动 | 有测试或可回滚,影响面局部 | 作为第一处改动 |
// 逆向记录的最小格式,随改动增量写,不要攒到最后
─── 模块名(原维护者、最后改动时间)
它做什么:一句话
被谁调用:入口清单
状态在哪:库表 / 缓存 key / 队列 topic
已知坑:现象 → 触发条件 → 临时处置
不可动的部分:具体项 + 动了会怎样
未解问题:我不知道的部分 ← 这一栏比上面所有栏都重要
第一行改动要等到观测就位之后,并且同时满足三个条件:影响面局部、能回滚、出问题会被人发现。不要拿最核心的路径做第一次实验——实验的目的不是修 bug,是验证"你的部署、回滚、观测这条链路真的能用"。
风险标注的价值不在分类本身,而在它能否挡住临时破例。一份标注如果谁都可以说"这次就改一下",它等于不存在。标注要写成"动了会发生什么"的句子,而不是"高风险"这种形容词:形容词不提供信息,后果才提供。
4. 重写的代价与原维护者的记忆边界
| 维度 | 重写 | 增量改造 |
|---|---|---|
| 隐性知识 | 随旧代码一起丢掉("那个延时为什么是 3 秒") | 保留在仍在运行的旧代码里 |
| 风险窗口 | 一次性暴露,切换日决定成败 | 分散,每次改动都小 |
| 参照系 | 有"它原本怎么跑"的对照物 | 无对照,只剩测试 |
| 适用时机 | 边界清晰、用例可枚举、旧行为可穷举 | 绝大多数接管场景 |
重写最贵的不是人力,是同时失去"当前行为"这个参照系。旧系统再烂,它现在的行为就是基线;换掉它之后,所有分歧都变成"新系统对还是我记得对"的争论,而争论没有判决依据。
原维护者在场的价值很高,但要注意他的记忆边界:他记得的是当时最花时间的那部分,不是全部。那些"顺手就对了"的代码,他既想不起来,也说不清楚——它们得靠逆向记录补。把"问老同事"当成完整方案,等于把系统的正确性押在别人的记性上。
// 要不要重写:三个前置条件,缺一个就继续增量改造
1. 旧行为可枚举 有可回放的用例,或可重放的线上流量
2. 有对照基线 同一输入能比对新旧输出,而不是靠人回忆
3. 有知识载体 隐性知识已落到人或文档,不会随旧代码消失
增量改造也有它的失效点:当"每次改动都要碰十处",改造成本会追上重写。判断依据不是代码新旧,是改一处要连带改几处——连带面长期不收敛,增量改造就只是在给旧结构续命。
接管的验收判据只有一条,但很硬:**能独立完成一次小改动——改、发、验证、再回滚,全程不需要任何人帮忙。**做不到这一条,前面所有阅读都是复习,不是接管。
5. 排错:六种"改了像没改、回滚像没回滚"
| 现象 | 根因 | 处置 |
|---|---|---|
| 改了没生效 | 发布路径不是你以为的那条(有第二套产物或缓存目录) | 先确认产物落点与进程加载路径 |
| 回滚后问题还在 | 配置或数据没跟着回 | 回滚清单必须含配置与迁移脚本 |
| 观测全绿,用户说不能用 | 只有内部指标,没有从入口发起的探活 | 补外部探针,按真实入口发请求 |
| 不知道会不会影响别人 | 调用方没枚举完 | 按入口清单逐条核对,缺的直接问 |
| 每次发布都靠手工熬夜 | 发布未脚本化 | 先把流程脚本化,再谈频率 |
| 文档越写越没人看 | 与代码脱节且无增量机制 | 只跟着改动增量更新对应模块 |
第四条要单独强调:接管期最大的未知不是"代码多难懂",而是"谁在用"。摸清调用方这件事没有捷径,也没有一次性完成的时刻——每次改动的第一件事都是回到入口清单核对一遍。
─── 调用方枚举的核对顺序(后一步总要推翻前一步)
网关 upstream 清单 → 访问日志里去重的调用方标识
→ 防火墙与内网 DNS 里的直连来源 → 定时任务的跨系统调用
→ 最后才问人:他们说的和日志不一致的地方,就是最危险的地方
6. 边界:什么时候可以压缩接管流程
| 场景 | 判断 | 压缩后的做法 |
|---|---|---|
| 系统即将下线 | 投入不可能回收 | 交接会 + 运行手册 + 明确"只维持不修" |
| 只有读流量 | 风险面小 | 保证可观测 + 权限收敛即可 |
| 代码量极小 | 通读一遍可行 | 一次通读 + 关键路径记录 |
| 边界清晰且有完整测试 | 重写窗口真的存在 | 对照旧行为做替换,而不是凭空重建 |
// 接管的四个度量:看趋势,不看单次值
部署成功率 ← 发布记录
平均恢复时长 ← 从发现问题到恢复的时长中位数
改动到上线时长 ← 提测到生效的时间
未知模块数 ← 逆向记录里标"未解问题"的模块数,只降不升说明接管在推进
一句话收口:接管的目标不是"读懂它",而是"能安全地改它"。读懂靠未知模块数下降来衡量,安全靠回滚演练过的次数来衡量——两者都不看代码读了多少页。
压缩也有底线:交接会加运行手册这个组合,手册里必须包含入口清单、启动与回滚命令、不可动清单、联系人及响应时限。少任何一项都不是"压缩流程",只是这件事没做。
自测
- 为什么"能回滚"要排在"能观测"前面?两者对调会付出什么代价?
- 判断一次回滚是否真实可用,只问哪一句话?
- 读遗留代码为什么推荐从流量入口开始,而不是从调用链开始?
- "这行代码为什么这么写"应该用什么方式回答?为什么猜出来的原因站不住?
- 重写最贵的代价是什么?它在验收上以什么形式暴露?
- 接管的验收判据只有一条,它是什么?