迁移到更大的工作区,又不让任何人等待
每个 Mythex 项目都有自己的工作区:一台小型云端计算机,智能体在上面编写代码,你的应用预览也在上面运行。我们已经把新工作区的内存从 1 GB 提升到了 2 GB,但旧的工作区仍然停留在 1 GB——在我们检查的那一天,大约有四分之一是这样。我们的第一个修复方案是在项目下次被打开时,把它迁移到更大的工作区。但在它上线的第一天,就让一个用户等了七分钟。于是我们为它设置了硬性时间上限,之后又干脆把迁移从打开项目这一操作中拿掉了。
为什么旧的工作区仍然很小
我们的沙盒提供商在工作区创建时就设定了它的大小,并在每次暂停和恢复时都保持这一设置。修改模板——也就是新工作区所基于的配方——只会影响修改之后创建的工作区,已经存在的工作区不会被调整大小。
这一点很关键。其中一个小工作区在预览启动时执行 git push 时内存耗尽,并且无响应了 11 分钟。一次发布因此失败。
第一次尝试:在唤醒时迁移项目
工作区在没有人使用时会暂停,在项目被打开时会被唤醒。唤醒的时候似乎是迁移项目的最佳时机,因为此时还没有人在里面工作。
迁移流程如下:
- 检查大小。 如果工作区小于 2 GB,就迁移。如果无法获知大小,则什么都不做。
- 不干扰正在进行的发布。 发布会读取工作区中的文件,因此如果有发布正在进行,就跳过迁移。
- 打包项目。 将项目中的所有内容打包成归档,包括它的 git 历史——也就是该项目拥有的每一个检查点——以及属于所有者的应用
.env文件,但不包括已安装的依赖(node_modules),因为它们可以重新安装。 - 直接复制过去。 将归档从旧工作区以流的形式传输到一个全新的 2 GB 工作区,这样它就不必完整地放进我们自己服务器的内存中。
- 切换前先比对。 在两个工作区中分别统计文件数量,读取最新的检查点,并统计检查点的总数。只有三者全部一致,才会切换。
- 任何失败都保留旧工作区。 任何错误或任何不一致,都会丢弃新的工作区,并像以前一样在旧工作区中打开项目。
比对中有一个细节,比看起来更重要。这项检查以管理员账户运行,而 git 会拒绝读取属于其他用户的仓库。两边都没有输出任何内容——而“没有输出”与“没有输出”相匹配,于是检查通过了,实际上并没有比较任何历史记录。我们告诉 git 信任该目录,并添加了一个测试,确保这种情况不会再次出现。
第一天出了什么问题
在它上线的第一天,打包其中一个项目时触发了五分钟的命令超时——而这一切都发生在唤醒过程中。项目所有者等了七分钟,他的工作区才打开。而且由于迁移失败,项目仍然是小工作区,下一次打开时还会再次尝试,让他们再次等待。
在用户打开自己的项目时让他们等待的迁移,是一笔糟糕的交易。把迁移放在唤醒时执行,本意是让人们根本察觉不到它的存在。
第二次尝试:设定硬性时间预算
同一天,我们改动了四处:
- 整个迁移只有一分钟。 每个步骤——检查大小、打包、创建新工作区、复制、解包、比对——共享同一个 60 秒的预算。预算耗尽时,项目就在旧工作区中打开。
- 停止工作本身,而不仅仅是等待。 如果我们放弃等待,但归档仍在项目所有者即将使用的工作区内运行,那就毫无意义。现在,每个归档命令都会在预算结束前不久,于工作区内部被直接终止。如果新工作区在我们放弃之后才到达,它会被丢弃。
- 不让用户等待清理。 删除临时归档文件的工作现在在后台完成。
- 不在每次打开时都重试。 无法迁移的项目会被搁置 24 小时,因此一次失败最多只造成每天一次延迟,而不是每次打开都延迟一次。
第三次尝试:把迁移从唤醒中移除
这样一来,最坏的情况是一分钟。但一分钟仍然是一分钟,而且我们意识到,迁移根本不需要出现在任何人的必经之路上。
现在已经不会再创建 1 GB 的工作区了。因此,这不是一个持续进行的过程,而是一次性的任务,针对的是一份固定的工作区清单。在唤醒过程中执行迁移,意味着每一次缓慢的迁移都由真实的人来承担代价,而我们了解到某个工作区无法迁移的唯一方式,就是有人刚刚为此等了很久。
因此,我们把迁移从唤醒过程中彻底移除。打开项目的行为与之前完全一致。剩下的小工作区则改为作为一个受监督的批处理任务来处理:没有人在等待它,而无法迁移的工作区会出现在批处理的报告中,我们可以在那里进行查看,而不是出现在项目所有者面前。
我们的收获
- 并非总能调整大小。 当平台在创建时就固定了大小,提高默认值只对新的东西有用。对于旧的东西,要另做打算。
- “反正还没人在用”,并不代表没有人在等待。 在用户进入产品的途中所做的工作,是有人在旁边看着的。
- 为用户路径中的任何操作设置预算,并在预算耗尽时停止工作。 在我们这边超时,而命令仍在别处继续运行,只是把问题转移了。
- 一次性的迁移应当放在一次性的任务中。 把它藏在日常操作里,编写时更省事,运行时却代价更高。
- 测试你的安全检查。 一个什么都没比较的比对,看起来与一个真正通过的比对完全一样。
如果你在 Mythex 上构建应用,无需做任何事:目标只是减少内存耗尽的工作区,同时让打开项目的速度保持不变。关于工作区如何暂停和唤醒,请参阅文档中的私有工作区。另一个关于内存耗尽的故事,请参阅隐藏在进度标签中的内存溢出崩溃。