跳到主要内容

启动脚本

onstart 是一段 bash 脚本,在实例第一次启动时运行一次。你在这里安装依赖包、拉取权重、克隆代码仓库或预热缓存——也就是那些否则每次部署后都要手动重做一遍的准备工作。

把它粘贴到模板表单的启动脚本里,或者通过 API 设置 onstart

它在哪里运行,在哪里不运行

启动模式行为
ssh在 Superheat 基础镜像上,会在 sshd 和 Jupyter 启动之后运行一次。在第三方镜像上,只是导出,不执行
jupyterssh 相同
vm由 cloud-init 写进客户机,在首次启动时运行一次
args我们这边只导出、不执行 —— 以 SUPERHEAT_ONSTART 交付,只有镜像自己去读它才会被运行

args 模式下,镜像自己的入口点掌管着进程,而这是刻意的:在那里换上 Superheat 的入口点会把你的命令丢掉,而那条命令正是一个 args 模板的全部内容。所以脚本会以 SUPERHEAT_ONSTART 交给容器,除此之外我们什么都不做。

决定它跑不跑的是镜像,不是模式。Superheat 的基础镜像会在自己的入口点里读 SUPERHEAT_ONSTART,把脚本运行一次,然后 exec 你的命令 —— 所以一个跑在我们镜像上的 args 模板确实会执行它。第三方镜像从来没听说过这个变量,会忽略它,在 ssh 模式下和在 args 模式下一样忽略。如果你的脚本没有运行,就把模板换到一个 Superheat 基础镜像上、把准备工作构建进你自己的镜像,或者让它成为你的命令做的第一件事。

它以环境变量的形式到达

脚本内容是以 SUPERHEAT_ONSTART 环境变量的形式交给容器的。它绝不是从主机挂载进去的文件。基础镜像入口点读取这个变量,把内容以 0700 权限写入 /var/lib/superheat/onstart.sh,然后运行它。为了兼容已经在读取 ONSTART 的镜像,裸名字 ONSTART 也被接受为别名。

由此引出四件事,而这四件事都会让以为是文件挂载的人吃亏。

脚本内容原样送达,所以你不需要为 Superheat 做任何转义。 引号、反斜杠、换行、heredoc 和 $ 全都会按你敲下的样子抵达容器。像在编辑器里那样写脚本,而不是像在 bash -c "…" 参数里那样写。变量展开发生在容器内部,在 bash 运行这个文件的时候,针对的是容器的环境——而不是在此之前。

脚本由 bash 运行,所以 shebang 只是装饰。 开头写 #!/usr/bin/env python3 不会让它变成 Python 脚本;它只是让第一行成为注释的 bash 脚本,而下一行就是语法错误。要运行 bash 之外的东西,就让脚本先把内容写出来,再调用正确的解释器。

长度是有上限的。 模板字段把 onstart 限制在 65,536 个字符以内,这里不是放大段内容的地方。如果你的准备工作超过一两屏,就把它放进代码仓库,让启动脚本去拉取并运行——这样也顺带给了变动最频繁的那部分版本控制。

它在你的 shell 里读不到。 入口点有意把 SUPERHEAT_ONSTART 连同 Jupyter 令牌和 SSH 密钥变量一起,排除在导出给登录 shell 的环境之外。通过 SSH 执行 echo $SUPERHEAT_ONSTART 什么都不会返回。如果你需要看到究竟运行了什么,去读 /var/lib/superheat/onstart.sh

运行一次就是只运行一次

无论成功还是失败,入口点都会在 /var/lib/superheat/.onstart-done 写下一个哨兵文件,并且在那个容器上再也不会运行这个脚本。停止再启动实例会重新运行入口点,但哨兵文件会让它跳过启动脚本内容,这样一次只做了一半的准备工作就不会被无声地叠加重试。

如果你的脚本失败,容器仍会保持运行。失败会被记录下来,但不是致命的,正是为了让你能 SSH 进去手动修复。有两个日志位置很重要:

cat /var/log/superheat/onstart.log # your script's own output

入口点的汇总行(包含退出码)也会进入控制台里的实例日志。参见日志

因为 sshd 是在启动脚本之前启动的,你可以在它还在跑的时候连上去看着它:

tail -f /var/log/superheat/onstart.log

修好之后想重新运行,删掉哨兵文件并自己执行脚本:

rm -f /var/lib/superheat/.onstart-done
bash /var/lib/superheat/onstart.sh

一个完整示例

这个脚本安装依赖、用作为私密环境变量提供的令牌拉取模型,并留下一个标记文件,让你一眼就能看出它有没有跑完。

set -euo pipefail

echo "onstart: $(date -u +%FT%TZ) on ${GPU_COUNT} GPU(s)"

pip install --no-cache-dir -r /workspace/requirements.txt

# HF_TOKEN comes from a template env entry marked secret, or from
# env_overrides at deploy time. Never hard-code it here.
if [ -n "${HF_TOKEN:-}" ]; then
huggingface-cli download meta-llama/Llama-3-8B \
--local-dir /workspace/models/llama-3-8b
else
echo "HF_TOKEN unset — skipping model download" >&2
fi

date -u +%FT%TZ > /workspace/.setup-complete
echo "onstart: done"

值得照搬的几点:set -euo pipefail,让某一步失败时脚本直接停下,而不是留下一个半成品环境;${HF_TOKEN:-},让未设置的变量不会在 set -u 下中止脚本;以及把所有东西都写进 /workspace,这样它能在停止再启动后依然存在。

绝不要把凭据写进启动脚本

公开模板的启动脚本是每一位 Superheat 用户都能读到的,而分享出去的配方会原样带上它。请把令牌放进标记为私密的环境变量条目,并像上面那样从脚本里读取。参见环境变量

之后编辑它

onstart 是启动配方的一部分,所以修改它会重新生成模板的 hash_id。已经在运行的实例保留它们启动时的脚本——修改只会影响此后部署的实例。