健康检查是绿的,订单却进不来
一句话总结
部署不是在“新版本已经跑起来”时结束,而是在确认“用新版本能把一笔业务走完”时才算结束。把版本拆成目录,用一个符号链接一次性切换,那么确认失败时,就能用同样的方法一次性回滚。
为什么需要它
把客户公司的订单接收 API 升级到 1.5.0 的那天,部署负责人看到健康检查是绿色,就汇报完成了。当晚,界面对每笔订单都显示“已受理”,仓库里却一笔都没有进来。原来新版本里漏掉了存储路径的配置。健康检查没有说谎。进程还活着,/health 也返回了 200。只是它问的是“活着吗”,而客户关心的是“订单进去了吗”。
FDE 经常要在客户主机上工作,而那里没有部署自动化,或者非常简陋。即使是在无法使用 Kubernetes 滚动更新(rolling update)或蓝绿切换的一台 VM 上,也必须能用文件系统和 shell 脚本实现同样的原理。
工作原理
结构很简单。
/root/site/
├── releases/
│ ├── 1.4.0/ (VERSION, app.py)
│ └── 1.5.0/
├── current -> releases/1.4.0
├── shared/ orders.jsonl · app.pid · app.log (판이 바뀌어도 남는 것)
└── deploy.log 배포 한 번에 JSON 한 줄
应用总是从 current/app.py 启动。更换版本不是覆盖文件,而是改变 current 所指向的位置,回滚也是同样的动作。旧版本的目录原封不动地在那里,所以回滚时不需要重新下载或解压。
关键在于“切换的那一瞬间”。rename(2) 在 newpath 已存在时会原子地替换它,保证其他进程不会看到这个名字不存在的瞬间。如果 newpath 是符号链接,被覆盖的是链接本身。所以在同一目录中创建一个临时链接,再 rename 到 current 之上即可。为什么必须在同一目录,同一份文档里也有说明:不同挂载点之间的 rename 会以 EXDEV 失败。在 Python 中,os.replace 做同样的事,文档写明成功时是原子的(POSIX 要求)。
那么常用的 ln -sfn 呢?GNU ln 手册把 -f 说明为“删除已有的目标”,但同时写明,只要不使用 --backup,就不会出现目标不存在的短暂瞬间。这不是很久以前的行为。coreutils NEWS 的 8.27(2017-03-08)条目说明,ln -f A B 不再先删除 B。在实验镜像(coreutils 9.4)上实测,当一个进程用 ln -sfn 切换 current 3,000 次、又用临时链接 + mv -T 切换 3,000 次的同时,另一个进程读取 readlink 约 1000 万次,两种方式都一次也没有看到“不存在”。所以差别在于:是依赖工具的版本,还是依赖系统调用的保证。如果不知道客户主机的 ln 是哪个版本,用 rename 更容易解释。
更常出事故的是 -n。手册把 -n 说明为“当最后一个参数是指向目录的符号链接时,不做特殊处理”。实测中,current 指向 releases/a 时执行 ln -sf releases/b current,current 保持不变,而在 releases/a 里面新生成了一个名为 b 的链接。命令以成功结束,所以没有人知道。
健康检查和冒烟测试的作用不同。
| 确认 | 问的是什么 | 本实验的方法 |
|---|---|---|
| 健康检查 | 新进程是否已启动并响应 | /health 返回 200,并且 version 是新版本 |
| 冒烟测试 | 一笔业务路径能否走完 | POST 一笔订单后用 GET 读回 |
在健康检查中确认 version 是有原因的。如果旧进程没能停掉,而新进程因端口冲突而崩溃,那么回应 /health 的就是旧版本。必须先确认这盏绿灯是不是新版本的。另外,新版本加载缓存可能花超过 2 秒,所以要等到上限,但如果进程已经死了,就不用再等。
在现场相遇的样子
回滚时最常见的错误是不留记录就回滚。第二天早上,客户会问:“听说昨晚部署了 1.5.0,为什么现在是 1.4.0?”必须有一行记录写明回滚了什么、为什么、回到了哪个版本,对话才能开始。失败版本的目录也不要删除,因为那是要调查的对象。
第二个是保留清理。为了省磁盘,在 cron 里挂上“只保留最新的 3 个”,回滚之后 current 就可能不是最新的。在 1.6.0 被回滚、回到 1.5.2 的状态下,如果 1.7.0、1.7.1 接连失败,按最新顺序的清理就会把当前正在运行的版本删掉。current,以及 current 出问题时要回去的上一个版本,无论日期如何都要保护好。
实际工作中真正重要的事
- 切换和回滚必须是同一个动作。如果回滚有单独的流程,紧急时就会出错。
- 健康检查要看到“是谁”回应的。冒烟测试要看客户付费的那条路径。
- 冒烟订单必须有标记(本实验使用 SMOKE- 前缀)。与客户数据混在一起,结算就会出错。
- 回滚不是失败,而是流程的一种结果。用退出码和记录把它区分开来留存。
下一项实验要做什么
手动解压构建包建立 current 之后,依次做出原子切换脚本、含健康检查和冒烟测试的部署脚本、回滚以及保留清理。评分器每次都会用不同的版本号,构建出正常的、只在冒烟测试中失败的、一启动就崩溃的、启动很慢的构建包来运行你的脚本,并重新测量实际响应的版本和符号链接。最后,亲自部署客户的 1.5.0,把它回滚,并留下事件记录。