客户问的不是「要多久」,而是「那两小时我们能不能干活」。
一、客户问「搬的时候我们还能用吗」
温以宁接下第三个从竞品整体搬过来的客户:1,240 行任务、14 列、46 个附件。电话会议最后,对方的运营负责人问:「周六上午搬的时候,我们还能用吗?」他脱口而出「能,我们不停机」,挂了电话才发现自己答的是「服务能打开」,不是「能写」。
以下情境为教学假设,人物、引语和数字不代表经核实的真实案例。
他把上一次迁移的时间线翻出来,从头到尾只有三行:开始、跑完、发邮件。没有窗口的起止时刻,没有谁在窗口里点过保存,也没有人记下那两小时里有多少次写入被拒绝。
「能用」至少有两种含义。能登录、能看、能导出,是读;能新建任务、能改状态、能传附件,是写。只读窗口里读没问题,写会失败。客户那边几个人在同一个工作区里干活,周六上午恰恰是他们补上周进度的时段。
第二次开会,他把回答改成一句可以被检验的话:周六 02:00–04:00(Asia/Shanghai)只读,期间的写入进入排队,04:00 解禁后按提交顺序回放;如果回放失败,窗口延长不超过 30 分钟,并逐条通知到提交人。对方接受了,因为这句话里有时间、有影响面、有兜底。
回滚是同一句话的另一半。他手上有一份演练记录:快照 90 秒、反向批次 150 秒,合计 4 分钟。但那次演练是在测试库上做的,真实的撤销只发生过 1 次,还是因为状态映射写错。
窗口的长度也不是拍出来的。23 次迁移里每次留 2 小时只读,一年 46 小时;这个数字要能和客户的支持时间对上,而不是和他自己「应该够用」的感觉对上。
这一篇讲三件事:三种切换方式各自适合谁、两小时窗口里到底发生什么、以及一次 4 分钟的撤销需要提前准备什么。
二、三种切换:十七次一次性、四次双写与两次灰度
切换方式有三种。本组 23 次里,一次性 17 次、双写 4 次、灰度 2 次。选择依据不是技术先进程度,而是客户能不能停写、数据里有没有跨对象引用、以及他们愿不愿意在迁移期改一次操作习惯。
一次性切换最直接:窗口内停写、全量写入、校验、解禁。它适合数据量在千行级、客户能接受一个凌晨窗口的场景。17 次里绝大多数是这种形态,窗口 2 小时,实际执行 72 分钟,余下的时间给校验与回放。
双写适合不能停写的客户。4 次双写都是客户有夜间批量任务,停 2 小时会打断第二天的报表。做法是旧系统与新系统同时写 7 天,第 8 天切读。代价是写入成本 +18%,以及要处理两边冲突:同一条任务在旧系统被改、在新系统也被改,谁赢必须提前定。
灰度适合多团队客户。2 次灰度都是先搬一个项目组的工作区,跑一周再搬其余。它把「一次搬错」变成「一部分搬错」,但边界最难划:跨项目引用的任务、共享的标签、评论里 @ 到另一个组的人。
| 方式 | 次数 | 适用 | 代价 | 失败样例 |
|---|---|---|---|---|
| 一次性 | 17 | 千行级、可停写 2 小时 | 风险集中在窗口内 | 状态映射错,整批回滚 |
| 双写 | 4 | 有夜间批次、不能停写 | 写入 +18%,冲突要定规则 | 两边同改,后写覆盖先写 |
| 灰度 | 2 | 多团队、可分批 | 边界与共享对象单独处理 | 跨组引用的任务搬过去成孤儿 |
三种方式不是互斥的。有一次是灰度加双写:先搬一个组,双写 7 天,再搬其余。混用会增加状态组合,所以只做过 2 次,且都在客户明确要求时才做。
判据要写成表,不能留在脑子里。客户问「能不能不停」,先看有没有夜间批次;问「能不能只搬一部分」,先看有没有跨组引用。两个问题十分钟内能问完,答案直接决定用哪种方式。
最容易被忽略的成本不在迁移当天。双写的 7 天里,任何一次写入都要写两遍、每天对一次账;这 7 天的支持量明显高于正常日,不能按迁移日的一次性投入估。
一次性切换也不等于粗糙。它把风险集中在一个有起止时间的窗口里,用快照和反向批次兜底;双写把风险摊到 7 天,靠每日对账兜底。两种都要回滚方案,只是触发点不同:前者在窗口内触发,后者在对账差额出现时触发。
无论哪种方式,窗口都要有一个人负责说「开始」和「结束」。17 次一次性里,有一次是客户自己在群里说「可以开始了」,而他正在另一个电话上;那天校验时间被压缩,最后一项对账拖到第二天才补。
三、两小时只读:三封邮件与一条写入队列
只读窗口的准备从 T-7 开始。三封邮件各有明确目的,不是为了「通知到位」这种无法验收的目标。
第一封在 T-7:确认窗口、影响面、回滚方案,并请客户指定一个人在窗口内值班。邮件里给两个时间让客户选,而不是通知一个时间;客户选了周六凌晨,因为那是他们唯一的低峰。
第二封在 T-1:列出只读期间不能做的事。这封要能被转发给客户的普通成员,所以不写内部术语,只写「新建任务、修改状态、上传附件会暂时不可用,你的操作会自动排队」。
第三封在 T-0,实际长这样:
主题:周六 02:00–04:00 迁移窗口(只读)
影响:新建、修改、上传进入排队,04:00 后自动提交
值班:贵方 1 人 + 我方 1 人,窗口内电话在线
回滚:校验不通过则 4 分钟内回到迁移前状态
窗口内的时间线按分钟排,每一段都有失败时的动作,而不只是「预计耗时」。
| 时刻 | 动作 | 失败时 |
|---|---|---|
| 02:00 | 切只读,确认没有长事务 | 有长事务则等其结束,窗口顺延 |
| 02:05 | 打快照,记录时间戳 | 快照失败则取消,不进入写入 |
| 02:10 | 五批写入,每批 248 行 | 单批失败重试,三次仍失败则回滚 |
| 03:30 | 行数、空值、重复三项校验 | 校验不过则回滚,不进入解禁 |
| 03:50 | 解只读,回放排队的写入 | 回放失败逐条重试并通知提交人 |
| 04:00 | 发结束邮件,附三个数字 | 超时 30 分钟必须提前通知 |
只读要真的只读。应用层把写接口挂起还不够,数据库连接也要切到只读事务,否则某个后台任务仍会写进去。那次窗口开始没多久就冒出一行 ERROR: cannot execute INSERT in a read-only transaction,来源是一个每分钟跑一次的提醒任务。
写入排队是这个承诺的另一半。1,240 行按一天 24 小时摊,2 小时里大约 103 次写入被拒;这些不能直接报错给用户,要落进队列,解禁后按提交顺序回放。队列条目必须带幂等键,否则用户重试一次就会写两条。
队列还要有上限。超过上限就把前端从「已排队」改成「请 04:00 后再试」;不设上限,解禁瞬间的回放会把数据库打满,比拒绝一次写入更糟。
只读窗口是承诺,不是建议。承诺包含三件事:起止时间、期间不能做什么、做不了的时候怎么办。缺第三件,客户会在窗口里反复点保存,然后收到一串看不懂的报错。
23 次 × 2 小时 = 46 小时/年,这是客户侧的感受成本,不是他的工时。他的工时在准备与校验里;窗口本身只占 72 分钟里执行的那 20 分钟。
四、四分钟撤销与百分之十八的写入成本
那一次撤销发生在 03:40。客户的值班人抽查了一页任务,发现 in_progress 全变成了 todo。原因不是映射表写错,而是清洗脚本把状态列的小写化放在了映射之后,in_progress 在查表时已经变成 in progress,落到默认值。
撤销分两步,不是一个按钮。第一步是快照恢复:把 02:05 的快照恢复到另一个库,90 秒,不覆盖主库;第二步是反向批次:按 external_id 删掉本次写入的 1,154 行,150 秒。合计 4 分钟。
为什么两步都要。快照是「回到窗口前」的完整证据,反向批次是「让主库干净」的动作。只做反向批次,万一删除条件写错就没有第二份可回;只做快照恢复,主库里那批错行还在,客户一刷新就看见两套数据。
反向批次必须幂等。按 external_id 删,不按时间范围删;重试第二次删 0 行才算对。按时间删会把客户在窗口后新建的任务一起删掉,这是撤销里最贵的一种错。
演练记录比方案更重要。他只演练过 1 次,演练时故意让反向批次在第二段中断,确认重跑之后仍然是 0 行。真实撤销那天两步都一次成功,因为脚本在演练里已经改过两次。
双写的成本要单独记。4 次 × 7 天,每次写入成本 +18%;这 18% 不是钱,是每条写入多一次网络往返与一次冲突检查。7 天里如果客户还想做批量导入,成本会继续叠加,所以双写期不安排别的数据操作。
回滚有时间上限。窗口 2 小时、撤销 4 分钟,看起来余量很大;但撤销之后要重新执行一次导入,72 分钟,仍然在窗口内。如果第二次还错,就不再有第三次,改为退回旧系统、另约窗口。
复算:窗口、切换方式、写入拒绝与撤销四笔账要能互相对上。
只读 2 h × 23 次 = 46 h/年
切换:一次性 17 + 双写 4 + 灰度 2 = 23
双写 4 次 × 7 天,写入成本 +18%
只读窗口写入 1,240 ÷ 24 × 2 ≈ 103 次被拒(改排队)
撤销 1 次 4 分钟:快照 90 秒 + 反向批次 150 秒
46 小时与 4 分钟不是一回事:46 小时是客户 23 次只读的累计感受,4 分钟是 1 次真实撤销的实际耗时。把两个数放在一张表里比较,会得出「回滚很便宜」的错误结论——便宜的是执行,贵的是演练与准备。
五、行动清单与复算验收
把窗口、邮件与回滚收进同一份运行手册,再用八条检查确认它经得起一次真实的撤销。
- 给每次迁移一个窗口编号,写清起止时刻与 IANA 时区
- 按客户能否停写选切换方式:一次性、双写或灰度,并记下理由
- T-7、T-1、T-0 各发一封邮件,第三封附写入、跳过、回放三个数字
- 窗口开始前确认没有长事务,再打快照并记录时间戳
- 只读同时落在应用层与数据库连接,避免后台任务绕过
- 写入排队带幂等键与上限,解禁后按提交顺序回放
- 反向批次按 external_id 删除,重跑第二次必须删 0 行
- 演练一次撤销并故意中断,记录快照与反向批次的真实耗时
验收标准:随便挑一次迁移,能报出窗口的起止时刻、期间被拒的写入条数与回放失败条数;再挑一次撤销,能报出快照与反向批次各自的秒数,并证明重跑第二次删 0 行。
使用边界与实操验收
边界一:本篇不做 schema 迁移。 加列、回填、切读属于架构组的范围,参见数据模型与迁移:三十四张表与一次只能动一处的改动 ;本篇只讨论窗口内怎么停写、怎么排队、怎么退回去。
边界二:只读不等于停机。 23 次迁移的停机时间都是 0,只读 2 小时只断写不断读;对客户要分开说清这两件事,否则「不停机」会被理解成「什么都能干」。
边界三:快照不是备份策略。 窗口前的快照只覆盖这一次迁移,保留到校验通过为止;日常备份与恢复演练由资产组负责,参见数据与备份:3-2-1、加密、离线副本与恢复演练 。
边界四:回滚退不掉外部副作用。 快照能把库退回去,退不掉已经发出的通知邮件、已经触发的 webhook,也退不掉客户在窗口里被拒绝的那 103 次操作;这些要单独补一封说明,而不是假装没发生。
边界五:窗口内的数据仍受隐私约束。 排队内容、失败清单与快照都可能含个人信息,留存与删除按隐私组口径处理,参见数据主体请求:一年十二件、三十天与一套导出脚本 ;本篇不讨论法定导出与删除流程。
关键参考与核验范围
- RFC 3339 :用于核对窗口起止时刻与快照时间戳的时间格式,避免本地时间与 UTC 混写。
- IANA 时区数据库
:用于核对三封邮件里
Asia/Shanghai这类时区名的写法与夏令时边界。 - PostgreSQL INSERT ON CONFLICT :用于核对双写与反向批次的冲突处理,保证重跑第二次删 0 行。
- Stripe 幂等请求 :用于核对排队回放与撤销请求的幂等键约定。
以上链接于 2026-10-26 实测可公开访问;未核验版本、价格与配额。核验日期:2026-10-26。
范围声明:本文的窗口时长、邮件节奏、队列行为与撤销耗时为教学假设,落地时以实际数据量、数据库版本与客户支持时间为准;不构成对任何具体产品可用性或恢复能力的承诺。
系列导航
- 回到本组目录:一个人的数据搬家:导入、去重、迁移与退出
- 上一篇:人工代导入:四小时半、六步与自动化的门槛
- 下一篇:附件与限额:四十六个文件、三百一十二兆与五个跳过
- 相关:数据与备份:3-2-1、加密、离线副本与恢复演练
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。