发布失败时,正常运行的后端永远不会被删除
9 月 25 日,一次失败的重新发布删除了某个应用的后端。这个后端之前已经稳定运行了大约两个半小时。撤销失败发布的代码,把这次发布涉及的每一个服务器应用都删掉了,包括发布开始前就已经在提供服务的应用。现在,只有发布本身创建的应用才会被删除;原本就存在的应用,会被恢复到它运行前的确切版本。同一天的另外两个修复,确保了当托管平台自己的部署工具失败时,撤销流程也会执行;并且,一个只是处于休眠状态的健康应用,不会因此被判定为失败。
后端是如何发布的
当你的 Mythex 应用包含后端(即 API 或服务器,而不仅仅是静态文件)时,发布过程会用 Dockerfile(把应用打包成镜像的配方)构建后端,并把这个镜像部署到我们托管平台上的机器中。接着,健康检查会等待新版本真正启动并保持稳定运行,之后才会把这次发布标记为已上线。
如果过程中任何一步失败,就会运行回滚来撤销这些改动。回滚必须做对,这比发布中的几乎任何其他环节都重要,因为它恰恰是在出问题的时候运行的。
问题一:回滚删掉了本该恢复的东西
回滚会记录下一次发布部署到了哪些服务器应用,一旦失败,就把它们全部销毁。对于首次发布来说,这样做是对的:应用是刚刚创建的,没有任何东西依赖它,删掉它就等于回到了发布之前的状态。
但对于重新发布来说,这恰恰是错的。应用早已存在,并且正在为真实的访问者提供服务。9 月 25 日,一个已经上线约两个半小时的应用 API,就被一次失败的重新发布删掉了。
同样的代码里还藏着第二种故障模式。如果删除没有成功,那么有问题的新镜像就会留在机器上,而我们却告诉应用所有者,他们当前的网站仍然在线。健康检查失败时,新镜像早已部署到了每一台机器上。不做任何事,并不等于撤销。
修复方案:只销毁自己创建的,其余全部恢复
现在,每次发布都会为它涉及的每个服务器应用记录两项信息:
- 这次发布是否创建了它,以及
- 如果不是,它在发布前运行的是哪个镜像。
失败时,每个应用会按以下三种方式之一处理:
- 由本次发布创建: 像以前一样销毁。
- 已存在,且知道之前的镜像: 用相同的配置重新部署之前的镜像。无需构建,只需把镜像切换回去。
- 已存在,但之前的镜像未知: 保持不动。已存在的应用绝不会被删除,即使我们无法恢复它。
这些规则是一个很小的纯函数,并配有自己的测试,因此这个决策不会在不知不觉中退回到“全部销毁”,只要有测试失败就能及时发现。
问题二:部署工具可能在我们记下撤销方法之前就失败了
修复之后不久,我们发现其中还有一个缺口。托管平台的部署工具会运行自己的冒烟检查:如果新机器崩溃,它会以错误退出。但这一步发生在新镜像已经部署到机器上之后。
我们的代码只有在工具成功返回后,才会把应用登记到回滚列表中。当工具自己导致部署失败时,应用还没有进入回滚列表,因此什么也没有恢复。在我们的预发布环境中,一次失败的重新发布就让有问题的镜像继续提供服务。
修复方案:在开始之前先登记撤销方法
现在,应用会在准备完成、尚未部署任何内容之前,就登记到回滚列表中,并同时记录它是否为新建以及它运行的是什么镜像。无论之后哪一步失败,回滚都知道该恢复什么。我们在本地做了验证:一次失败的重新发布在部署工具自己的冒烟检查中被拦下,之前的版本依然正常响应。
问题三:处于休眠的应用被误判为“从未启动”
Mythex 上发布的应用在闲置时会休眠,以节省你的积分。重新发布会让休眠中的机器保持停止状态,因此健康检查必须先把它唤醒。过去,它只会请求平台启动机器一次,而且忽略了平台的回应。
9 月 25 日,那一次启动请求没有起作用。一个健康的 API 就一直处于停止状态,直到五分钟的健康检查时间耗尽,最终被判定为失败,报错信息是“1 台机器从未启动(最后状态:stopped)”。
平台有时确实会拒绝启动请求。我们在复现问题时,它甚至在没有任何诱因的情况下返回了“lease currently held”冲突;而当机器正在更新时,它也会拒绝启动。
修复方案:持续请求,并主动敲门
现在,健康检查会:
- 在等待期间,每 20 秒再次请求启动一次已停止的机器,
- 记录平台拒绝启动时的回应内容,
- 向应用发送一个普通请求,就像访问者那样——平台的代理会在请求到达时唤醒休眠的机器。
我们使用一个首次启动会被拒绝的临时应用,在真实平台上进行了复现。修复之前,发布在 91 秒后失败;修复之后,20 秒内就发布成功。而确实会在启动时崩溃的应用,依然会按预期失败。
我们从中学到的
- 回滚必须能区分新建与已存在。 对于你创建的东西,“撤销”就是删除;对于本来就存在的东西,“撤销”就是恢复。把两者等同对待,正是安全机制删掉正常运行应用的原因。
- 在行动之前先写下如何撤销。 如果只在某一步成功之后才记录撤销方法,那么一个中途失败的步骤,就没有可用于撤销的依据。
- 绝不忽略你所依赖的请求的回应。 一次被忽略的“不行”,就让一个健康的应用变成了发布失败。
同一天的另一项相关修复是:捕获发布后立即崩溃的应用。如果发布在你这边失败,请参阅文档中的发布故障排查。