FDE Times 数据在招 FDE 职位 824▼37 近 7 天在招职位数的变化(同口径比较):不计入本周新开始追踪的公司的职位近 7 天新增 76在招公司 153支持远程 25%美国薪资中位数 $202k招聘最多 Databricks 104
ENVI

前线部署工程师(FDE)的专业媒体

技能指南 · 指南

改客户表结构,别直接动正在用的列

把破坏性变更拆成多次小部署,收缩阶段之前每步都能回滚,最后的清理也不能省。

分享微博

要点速览

  • 不要修改正在使用的列。先在旁边加上新结构,两边同时写入,然后才切换读取路径。
  • 每一步都是一次单独的部署,与上一步向后兼容,因此在收缩阶段之前随时可以回滚。
  • 跳过收缩阶段,系统可能比改动前还糟。
时间线图,展示扩展-收缩式表结构变更的六个步骤:扩展、双写、回填、影子读、切换和收缩。上方有一条蓝色色带,标注“可回退”,覆盖第 1 到第 5 步。第 6 步“收缩”隔开一段距离单独放置,用橙色标出,标注“无法回头”。
前五步都能回滚,但越往后,回退的代价越高。收缩放在最后,需要单独签字确认。来源:文中步骤表,依据 Martin Fowler、Pete Hodgson、Prisma 和 Stripe 的资料整理。

设想你正在客户现场。客户要把客户数据推送到一个新系统,而新系统要求姓和名分成两个字段。customers 表只有一个 name 列,读它的有主应用、一个每晚运行的报表任务,还有另一个团队负责的提取-转换-加载(ETL)数据管道。

最先想到的做法是写一个迁移:拆列、改名、改代码,一次部署完事。但滚动部署期间,旧版本的应用实例仍在运行,仍在找 name 列。今晚的报表任务也完全不知道你改了什么。

这种局面对前线部署工程师(FDE)来说尤其棘手。你在别人的数据库上干活,不清楚谁在读什么,也没法让整个公司停下来等你。应对这种局面的技术叫扩展-收缩。

核心思路:永远不改正在运行的东西

Martin Fowler 称之为并行变更(parallel change):一种安全引入向后不兼容接口变更的方法。数据库表结构也是一种接口,每一条对它执行的 SQL 查询都是它的客户端。

这个模式分三个阶段。扩展阶段,拓宽接口,让它同时支持新旧两个版本。迁移阶段,把仍在使用旧版本的客户端逐个迁到新版本。收缩阶段,移除旧版本。

Pete Hodgson 把好处说得很直白:原本是一次高风险部署,现在变成一串相互独立的部署,而且这串变更中的每一次都保持兼容。

由于每个阶段都与上一阶段向后兼容,系统的不同部分可以处在不同阶段。新旧版本的应用并行运行,互不破坏。

拆分 name 列,一次部署一步

Prisma 关于扩展-收缩的指南用的正是这个例子:需要把 name 拆成 first_name 和 last_name 时,应新增这两列,而不是修改 name。Stripe 在介绍如何大规模执行在线迁移时,把工作拆成四步。

先对新旧结构双写,再把所有读取路径迁到新结构,然后只写新结构,最后删除仍依赖旧模型的数据。

套用到客户的表上,第一步只动表结构:

-- 第 1 步:扩展。只新增,不修改也不删除。
ALTER TABLE customers ADD COLUMN first_name text NULL;
ALTER TABLE customers ADD COLUMN last_name  text NULL;

新列允许 NULL,旧应用可以照常插入。万一出问题,只要删掉新加的两列,谁都不受影响。

第二步修改写入代码。按 Prisma 的描述,这一阶段客户端同时写入原结构和新结构,读取路径仍走原结构:

def save_customer(conn, cid, full_name, first, last):
    conn.execute(
        """UPDATE customers
           SET name = %s, first_name = %s, last_name = %s
           WHERE id = %s""",
        (full_name, first, last, cid),
    )

双写只能让新记录保持同步。数百万条旧数据的 first_name 仍是空的。Stripe 的办法是回填(backfill):把新存储中缺失的部分补齐,让两边的信息完全一致。

-- 第 3 步(单独一次部署或单独一个任务):分批回填,重复执行直到没有剩余行
UPDATE customers
SET first_name = split_part(name, ' ', 1),
    last_name  = substr(name, length(split_part(name, ' ', 1)) + 2)
WHERE id IN (
  SELECT id FROM customers
  WHERE first_name IS NULL
  ORDER BY id LIMIT 5000
);

上面的查询假设第一个词是名,其余部分是姓。对于“Nguyen Van An”这样姓在前的越南语数据,这条规则得出的结果完全错误。这不是技术 bug,而是业务决策,做决定的必须是客户,而不是你。

切换读取路径之前先校验

读取路径最容易出错,因为这里的错误会直接暴露在用户面前。Stripe 在切换读取路径之前,用 GitHub 的 Scientist 库做校验。不一定要用这个库,用它的思路就行:两边都读,做比较,不一致时记日志,但仍返回旧结构的结果。

def display_name(row):
    old = row["name"]
    if row["first_name"] is None:
        return old  # 该行尚未回填:没有可比较的内容,避免误报不一致日志
    new = f'{row["first_name"]} {row["last_name"] or ""}'.strip()
    if old != new:
        log.warning("name_mismatch", extra={"id": row["id"]})
    return old  # 在日志干净之前,继续信任旧结构

要清楚这个函数的局限。把 first_name 和 last_name 拼起来,几乎能原样还原原字符串,所以它发现不了“Nguyen”被误放进名字段这类问题。它只能捕捉技术错误,比如字符丢失、空白不对,或者双写失败的行。

要捕捉业务错误,需要和客户认可的东西做比较:比如由客户员工手工拆分姓名的几百行样本,或者一条经过签字确认的姓氏拆分规则。与这份样本出现偏差,才是回填理解错了含义的真正信号。

在客户的真实流量上跑几天。等日志里不再出现不一致,或者每一处不一致都有了解释,再部署从 first_name 和 last_name 读取的版本。之后再做一次部署,切换为只写新列。

下表应放进交给客户的交接文档。它列出每一步在必要时如何回滚:

步骤 写入 读取 如何回滚
1. 扩展 name name 删除两个新列
2. 双写 两边 name 恢复为只写 name 的代码
3. 分批回填 两边 name 停止任务;旧数据未被改动
4. 比较读取 两边 name,不一致时记日志 关闭比较
5. 切换读取,再改为只写新列 两边,之后只写新列 新列 在保持双写的同时把读取指回 name
6. 收缩 新列 新列 没有低成本的退路

越往后走,回滚代价越高,到了收缩这一步几乎无路可退。因此收缩必须放在最后,而且需要单独审批。

收缩阶段:最常被遗忘的一步

一切运行顺畅之后,最大的诱惑是就此打住。Fowler 警告说,如果跳过收缩阶段,最终状态可能比开始时更糟:两列并存,代码写两遍,接手的人不知道该信哪一列。

-- 最后一步:收缩。只在没有任何客户端还在读 name 时执行。
ALTER TABLE customers DROP COLUMN name;

“没有客户端还在读”必须证明,不能靠猜。在客户环境里,要查看数据库查询日志,询问负责 ETL 的团队,检查各个看板。每晚的报表任务会是第二天早上第一个出问题的。

在客户环境里,一切都更慢

Hodgson 估计,一个随时能部署到生产环境的团队,熟练之后一两个小时就能走完一轮扩展-收缩。客户环境很少是这样:可能有固定的发布周期、变更评审委员会,还有另一个团队掌管着一半的客户端。整个过程会因此拉长很多。

所以在第一次工作会议上,就要问清客户多久能部署一次、由谁审批。答案决定你如何合并步骤。如果每月只有一个发布窗口,也许不得不把扩展和双写放进同一次发布,但收缩绝不能和其他任何步骤合并。

常见错误

最常见的错误是直接对正在使用的列执行 ALTER COLUMN 或 RENAME,完全跳过扩展阶段。其次是用一条 UPDATE 回填整张表,长时间持有锁,拖慢的恰恰是你本想不去惊动的系统。第三是回填还没完成就切换读取路径,让用户看到空名字。

还有两个错误往往后来才暴露。一是忘了那些不在你代码仓库里的客户端。二是放弃收缩阶段就离开项目,留给客户一套比你来之前更难理解的表结构。

对职业发展来说,这项技能值得在简历上写准确。不要写“执行过数据库迁移”,而要写:通过一系列向后兼容的部署修改了客户生产表的结构,每一步都有比较读取和回滚方案。

FDE 面试问到事故经历时,不要只讲救火的故事。也准备一个这样的例子:你如何规划一次变更,让它在不停机的情况下完成,以及每一步如何回滚。

本周练习:在手头项目里挑一个真实的列,按上表的六个步骤写一份方案,交给同事审读。如果对方指不出哪一步会让系统崩溃,这份方案就可以拿给客户了。

这篇文章对你有帮助吗?

4 个信源
路线图上的下一篇 · 阶段 5: 部署上线审计人员不读代码,只追溯一个 AI 决策一条日志若无法把审计人员引向测试集、人工审核人和漂移告警,上线日期就会推迟。