SkyPilot API 服务器无感升级测试指南:用 Helm 滚动更新验证 Kubernetes 上的可用性
【免费下载链接】skypilotThe AI Compute Platform for frontier teams. SkyPilot turns fragmented AI compute into one AI supercomputer, so frontier AI teams build custom intelligence faster.项目地址: https://gitcode.com/GitHub_Trending/sk/skypilot
SkyPilot 的 API 服务器是无状态部署在 Kubernetes 上的,负责接收sky launch、sky status、sky jobs queue等客户端请求,并将任务调度到集群内的 Pod 中。当通过 Helm 对 API 服务器进行升级时,默认的Recreate策略会造成短暂的中断;而RollingUpdate策略则能在零停机的前提下完成版本切换,但这也意味着旧 Pod 被替换的瞬间,正在进行的请求可能被中断。为了验证这一升级过程的可靠性,SkyPilot 提供了一个名为Graceful Upgrade Test的测试脚本。
本篇文章将围绕tests/kubernetes/upgrade/README.md及其配套的tests/kubernetes/upgrade/test-upgrade.sh展开,详细介绍:
- 该测试的目的与原理:如何在滚动升级期间验证 API 服务器是否还能持续处理客户端请求。
- 测试的完整步骤:从前置条件、脚本用法到参数说明。
- 脚本背后的实现细节:每个 CLI 命令的作用、超时与校验逻辑。
- 升级策略的底层机制:为什么
RollingUpdate需要外部数据库、为什么存储需要ReadWriteMany、以及terminationGracePeriodSeconds对升级窗口的影响。 - 测试结果的解读与常见问题:如何判断升级是否成功、失败时如何排查。
读完本文,你将能够自己搭建一个可重复的 Kubernetes 升级演练环境,验证 SkyPilot API 服务器在滚动更新过程中的请求可用性,并理解该测试在 SkyPilot 工程实践中的价值。
1. 测试概述:为什么需要 Graceful Upgrade Test
SkyPilot 的 API 服务器(API Server)是团队使用 SkyPilot 的入口,它通过 REST API 接收所有客户端命令。当需要升级 API 服务器(例如升级到 nightly 版本或新的 minor 版本)时,如果升级过程导致 API 服务器不可用,用户的sky launch、sky status等命令就会报错,直接影响团队的工作。
SkyPilot 的官方文档 api-server-upgrade.rst 中提到,只要满足以下条件,API 服务器就可以进行优雅升级(graceful upgrade):
- 使用 Helm 部署 API 服务器;
- 升级前后的版本在 API 兼容性范围内(SkyPilot 从
0.10.0开始保证相邻 minor 版本之间的 API 兼容性)。
在优雅升级过程中,API 服务器对不同请求的处理策略是:
- 关键请求(如启动集群):等待其完成,超时后再中断;
- 非关键请求(如日志尾随):取消并返回错误,提示客户端重试;
- 新请求:返回错误提示重试,待新版本 API 服务器就绪后继续服务。
而tests/kubernetes/upgrade/下的测试脚本,正是为了自动化验证上述行为而存在的。它模拟了一个真实的滚动升级场景:
- 启动一个长时间运行的
sky launch任务并尾随其日志; - 触发
helm upgrade滚动更新 API 服务器; - 在升级期间同时发起多个不同类型的请求(
sky status、sky launch --dryrun、sky jobs queue、sky launch); - 验证这些请求是否都能成功,以及日志尾随是否能在升级后恢复。
这与 api-server-upgrade.rst 中描述的"API 服务器被升级时,CLI 会自动重试请求直到新版本就绪"是一致的。
2. 前置条件
根据 tests/kubernetes/upgrade/README.md,运行该测试之前需要完成以下前置工作:
Complete the helm installation guide in https://docs.skypilot.co/en/latest/reference/api-server/api-server-admin-deploy.html#step-1-deploy-the-api-server-helm-chart
即在 api-server-admin-deploy.rst 中第 1 步:部署 API 服务器 Helm Chart所描述的全部安装步骤。这包括:
- 安装 Helm 并添加 SkyPilot 的 Helm 仓库;
- 通过
helm install或helm upgrade --install部署skypilot/skypilot-nightlyChart; - 确保 API 服务器 Pod 正常运行,且 ingress 已暴露外部访问地址。
此外,还需要:
- 本机已安装并配置好 SkyPilot CLI(
sky命令可用),且已通过sky api login -e <SERVER_URL>登录到远程 API 服务器; - 本机已配置好 Kubernetes 集群(
kubectl可用,且能访问目标集群); - Helm已安装且能访问到本地
charts/skypilot(脚本中直接引用了相对路径charts/skypilot)。
3. 脚本用法与参数说明
3.1 基本用法
./test-upgrade.sh <SERVER_URL> [RELEASE_NAME] [NAMESPACE]3.2 参数详解
| 参数 | 位置 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
SERVER_URL | $1 | 是 | — | API 服务器的访问地址,如http://your-api-server.com |
RELEASE_NAME | $2 | 否 | skypilot | Helm release 的名称 |
NAMESPACE | $3 | 否 | skypilot | Helm release 所在的命名空间 |
注意:脚本中参数顺序为
RELEASE_NAME在前、NAMESPACE在后(与 README 中的示例一致),实际脚本源码里二者的赋值顺序与此相同,仅传参顺序易混淆,建议始终显式指定。
3.3 示例
./test-upgrade.sh http://your-api-server.com skypilot skypilot该命令表示:
- API 服务器地址为
http://your-api-server.com; - Helm release 名为
skypilot; - 命名空间为
skypilot。
运行前请确保脚本具有执行权限(chmod +x test-upgrade.sh,如需赋予权限,请手动执行后运行)。
4. 脚本执行流程逐步拆解
tests/kubernetes/upgrade/test-upgrade.sh是一个约 80 行的 Bash 脚本,set -euo pipefail确保了任何命令失败都会立即退出。下面按执行顺序逐步拆解其逻辑。
4.1 参数校验与 release 检查
SERVER_URL=${1} NAMESPACE=${2:-skypilot} RELEASE_NAME=${3:-skypilot} if [ -z "$SERVER_URL" ]; then echo "Server URL not provided" exit 1 fi helm ls -n $NAMESPACE | grep $RELEASE_NAME || (echo "Release $RELEASE_NAME not found in namespace $NAMESPACE" && exit 1)- 首先从命令行参数解析
SERVER_URL、NAMESPACE、RELEASE_NAME; - 若未提供
SERVER_URL直接报错退出; - 用
helm ls -n <NAMESPACE> | grep <RELEASE_NAME>确认目标 release 已安装,若不存在则提示并退出。
4.2 确保使用 RollingUpdate 升级策略
echo "Running upgrade test with server URL: $SERVER_URL" # Ensure the upgrade strategy is RollingUpdate, upgrade will error out if postgres is not configured previously. helm upgrade $RELEASE_NAME charts/skypilot \ --namespace $NAMESPACE \ --reuse-values \ --set apiService.upgradeStrategy="RollingUpdate"这是整个测试的关键一步:将 API 服务器的 Deployment 升级策略强制设置为RollingUpdate。
脚本注释明确指出:如果之前没有配置 PostgreSQL,这一步的升级会直接报错退出。这正是 api-deployment.yaml 中的模板校验逻辑:
{{- if eq .Values.apiService.upgradeStrategy "RollingUpdate" }} {{- if and (not .Values.apiService.dbConnectionSecretName) (not .Values.apiService.dbConnectionString) }} {{- fail "External database must be configured via .apiService.dbConnectionSecretName or .apiService.dbConnectionString when using RollingUpdate strategy" }} {{- end }}即:使用RollingUpdate策略时,必须通过apiService.dbConnectionSecretName或apiService.dbConnectionString配置外部数据库,否则 Helm 模板渲染阶段就会 fail。这背后的原因是:滚动更新期间新旧两个 Pod 会同时运行,SQLite 本地存储无法被两个进程安全共享,因此必须将状态存储迁移到外部 PostgreSQL。
4.3 登录 API 服务器并启动长任务
CLUSTER_NAME="test-upgrade" sky api login -e $SERVER_URL log_file=$(mktemp) sky launch -c $CLUSTER_NAME -y --cpus 1+ 'for i in {1..100}; do echo "count: $i" && sleep 1; done' --infra kubernetes > $log_file 2>&1 & tail_pid=$! echo "Launch and tailing log to $log_file, PID: $tail_pid"sky api login -e $SERVER_URL:登录远程 API 服务器,之后所有sky命令都通过该服务器执行;sky launch -c test-upgrade -y --cpus 1+ '...' --infra kubernetes:后台启动一个运行约 100 秒的任务(每 1 秒输出一次count: N),并通过--infra kubernetes指定在 Kubernetes 基础设施上运行;- 任务的 stdout 被重定向到临时文件
$log_file,该后台进程的 PID 记录为tail_pid——这模拟了"正在进行的长时间任务 + 日志尾随"场景。
4.4 等待任务开始输出
timeout=120 elapsed=0 while [ $elapsed -lt $timeout ]; do if grep -q "count: 1" "$log_file"; then break fi sleep 1 elapsed=$((elapsed + 1)) done if [ $elapsed -ge $timeout ]; then echo "Timeout wait the log tailing start" exit 1 fi- 轮询等待最多120 秒,直到日志中出现
count: 1(表示任务已开始输出日志); - 若超时,则报错退出——说明任务未能正常启动,测试无意义。
4.5 触发滚动更新
echo "Triggering rolling update" timestamp=$(date +%s) helm upgrade $RELEASE_NAME charts/skypilot \ --namespace $NAMESPACE \ --reuse-values \ --set apiService.annotations.restart="at$timestamp"这是第二次helm upgrade,用于触发滚动更新:
- 通过
--set apiService.annotations.restart="at$timestamp"注入一个每次运行都不同的注解值(时间戳); - 该注解会被渲染到 Deployment 的 Pod template 中,改变 Pod template 的 metadata 会强制 Kubernetes 触发一次滚动更新,而镜像本身并未变化——这是一种常见的"重启 Deployment 而无须更换镜像"的技巧。
这一机制与 api-deployment.yaml 中的注解渲染逻辑一致:
{{- if .Values.apiService.annotations }} {{- toYaml .Values.apiService.annotations | nindent 8 }} {{- end }}4.6 升级期间并发发起多种请求
sky_pids=($tail_pid) sky status $CLUSTER_NAME > /tmp/sky_status.log 2>&1 & sky_pids+=($!) sky launch --infra kubernetes --dryrun -y > /tmp/sky_launch_dryrun.log 2>&1 & sky_pids+=($!) sky jobs queue > /tmp/sky_jobs_queue.log 2>&1 & sky_pids+=($!) sky launch --infra kubernetes --cpus 1+ 'echo hello' -y > /tmp/sky_launch.log 2>&1 & sky_pids+=($!)在滚动更新进行期间,脚本并发发起以下请求,模拟真实用户在升级期间的访问:
| 命令 | 作用 | 是否阻塞 |
|---|---|---|
sky status $CLUSTER_NAME | 查询集群状态 | 短请求 |
sky launch --infra kubernetes --dryrun -y | 预演启动(不实际创建) | 短请求 |
sky jobs queue | 查询作业队列 | 短请求 |
sky launch --infra kubernetes --cpus 1+ 'echo hello' -y | 实际启动一个新任务 | 长请求(关键请求) |
最初的tail_pid(日志尾随) | 持续输出日志 | 长请求 |
每个请求都在后台运行,PID 被收集到sky_pids数组中,随后统一等待结果。
4.7 校验所有请求是否成功
failed_jobs=0 for pid in "${sky_pids[@]}"; do if wait $pid; then echo "Command with PID $pid completed successfully" else echo "Command with PID $pid failed with exit code $?" failed_jobs=$((failed_jobs + 1)) fi done- 依次
wait每个后台进程; - 若任一命令非零退出,则
failed_jobs计数加 1。
4.8 校验日志尾随是否完整
cat $log_file | grep "count: 1$" | wc -l | grep -q 1 || (echo "Incorrect log tailing, refer to $log_file for details" && exit 1) cat $log_file | grep "count: 100$" | wc -l | grep -q 1 || (echo "Incorrect log tailing, refer to $log_file for details" && exit 1)- 校验日志中恰好出现一次
count: 1和一次count: 100(严格匹配行尾); - 这意味着:升级期间日志尾随虽然可能被中断,但 SkyPilot CLI 会自动重连/恢复,最终完整收到第 1 条到第 100 条输出——这正是 api-server-upgrade.rst 中所述"正在进行的请求(如日志尾随)会自动恢复"的验证。
注意:如果日志中出现了多余的
count: 1或count: 100(即任务被重复启动或日志被重复尾随),也会因为wc -l结果不为 1 而报错。这体现了脚本对"恰好一次"的严格校验。
4.9 清理并汇总结果
sky down $CLUSTER_NAME -y if [ $failed_jobs -gt 0 ]; then echo "Failed jobs: $failed_jobs" exit 1 fi- 无论测试成败,最后都用
sky down $CLUSTER_NAME -y清理测试集群; - 若期间有任何请求失败,脚本以非零状态退出,表示滚动升级测试未通过。
5. 升级策略的底层机制:为什么 RollingUpdate 需要这些前置条件
test-upgrade.sh强制使用RollingUpdate策略是有充分依据的,这与 SkyPilot 官方文档 api-server-upgrade.rst 中两种策略的对比表完全一致:
| 维度 | Recreate | RollingUpdate |
|---|---|---|
| 可用性 | 升级期间短暂停机 | 零停机 |
| 请求处理 | 新请求等待升级完成 | 新请求由可用副本持续服务 |
| 数据库要求 | 可使用本地存储(SQLite) | 必须使用外部持久化数据库 |
| 升级期间资源使用 | 先删旧 Pod,再启新 Pod | 先启新 Pod,再删旧 Pod |
| 适用场景 | 开发环境、简单部署 | 生产环境、高可用要求 |
5.1 为什么必须配置外部数据库
api-deployment.yaml 中的模板校验清晰地说明了这一点:RollingUpdate下新旧两个 Pod 会同时运行,而 SkyPilot API 服务器的状态默认存储在 SQLite 中(位于 PVC 上)。两个进程同时写同一个 SQLite 文件是不安全的,因此必须将状态存储迁移到外部 PostgreSQL。
5.2 为什么存储模式必须是 ReadWriteMany
values.yaml 中明确说明:
IMPORTANT: When using RollingUpdate upgrade strategy:
- ReadWriteOnce (RWO): NOT supported - the PVC cannot be mounted by both old and new pods during rolling update.
- ReadWriteMany (RWX): Supported - requires an RWX-capable storage class (e.g., NFS-backed storage like Google Filestore, AWS EFS, Azure Files, or an NFS provisioner).
如果storage.enabled=true且 accessMode 为ReadWriteOnce,api-deployment.yaml 会直接fail:
Local storage with ReadWriteOnce access mode is not supported when using RollingUpdate strategy. Either use Recreate upgrade strategy, set storage.enabled to false, or use ReadWriteMany access mode with a compatible storage class (e.g., NFS-backed storage like Google Filestore).5.3 滚动更新期间的存储布局变化
从 api-deployment.yaml 可以看到,当RollingUpdate+ 持久化存储同时启用时,~/.sky目录被挂载到emptyDir(临时卷),只有api_server/clients子目录持久化到 state-volume:
{{- if and $statePersist (eq .Values.apiService.upgradeStrategy "RollingUpdate") }} # For RollingUpdate with storage enabled, use emptyDir for ~/.sky to avoid # running SQLite on NFS. Only persist the clients directory for file mounts. - name: sky-ephemeral mountPath: /root/.sky - name: state-volume mountPath: /root/.sky/api_server/clients subPath: {{ .Values.storage.clientsSubPath | default "api_server/clients" | quote }} {{- else }} - name: state-volume mountPath: /root/.sky subPath: .sky {{- end }}这解释了 values.yaml 中关于storage.enabled=false时的警告:
If storage.enabled=false with RollingUpdate, file mounts and logs will be lost on pod restart; consider configuring 'jobs.bucket' in the SkyPilot config to persist file mounts to cloud storage.
5.4 优雅终止窗口:terminationGracePeriodSeconds
滚动更新期间,旧 Pod 被删除前 Kubernetes 会发送SIGTERM并等待宽限期。SkyPilot API 服务器利用这段时间让正在处理的关键请求(如集群启动)完成。
values.yaml 中定义了默认值:
# The number of seconds to wait for the API server to finish processing the request before shutting down. # If the API server is not able to finish processing the request within the grace period, the request will be aborted. # The default value is 60 seconds. terminationGracePeriodSeconds: 60该值通过环境变量SKYPILOT_GRACE_PERIOD_SECONDS注入到 API 服务器容器(见 api-deployment.yaml),并在服务端用于等待关键请求完成。官方文档建议根据实际工作负载调整,例如:
helm upgrade -n $NAMESPACE $RELEASE_NAME skypilot/skypilot-nightly --devel --reuse-values \ --set apiService.terminationGracePeriodSeconds=3006. 滚动更新在代码层的配套支持
测试脚本验证的行为背后,SkyPilot 源码提供了一系列配套实现:
6.1 滚动更新模式的环境变量
api-deployment.yaml 在RollingUpdate模式下注入两个环境变量:
{{- if eq .Values.apiService.upgradeStrategy "RollingUpdate" }} - name: SKYPILOT_APISERVER_UUID valueFrom: fieldRef: fieldPath: metadata.uid - name: SKYPILOT_ROLLING_UPDATE_ENABLED value: "true" {{- end }}其中SKYPILOT_ROLLING_UPDATE_ENABLED在 sky/skylet/constants.py 中定义,并被 sky/jobs/server/core.py 读取,用于在滚动更新模式下警告本地 file_mounts 与 workdir 的丢失风险(详见 6.3 节)。
6.2 就绪探针的耐心阈值
api-deployment.yaml 中,滚动更新模式下就绪探针的successThreshold被调整为 3:
{{- if eq $.Values.apiService.upgradeStrategy "RollingUpdate" }} # When using RollingUpdate strategy, be more patient with the new # API server to avoid flaky serving where one of the server process # returns ready of the healthz check endpoint while others may still # be starting up. successThreshold: 3 {{- else }} successThreshold: 1 {{- end }}目的:新 Pod 加入 Service 后端前需要连续 3 次健康检查通过,避免出现"进程已就绪但尚未完全启动"的抖动期,从而保证滚动更新期间的服务质量。
6.3 本地文件挂载丢失警告
sky/jobs/server/core.py 中的_warn_file_mounts_rolling_update函数专门处理滚动更新场景下的文件挂载风险:
- 当
SKYPILOT_ROLLING_UPDATE_ENABLED环境变量存在(即启用滚动更新); - 且持久化存储未启用(
SKYPILOT_API_SERVER_STORAGE_ENABLED不为'true'); - 且启用了 consolidation 模式;
- 且未配置
jobs.bucket; - 且任务中确实包含本地
file_mounts或workdir时,
会提示用户这些本地路径在滚动更新后可能丢失,建议改用云存储桶、卷、git 或配置jobs.bucket。这正是文档 api-server-upgrade.rst 中警告部分的代码级实现。
7. 测试结果解读与排障
7.1 预期输出
测试成功时,脚本的典型输出包括:
Running upgrade test with server URL: http://your-api-server.com Launch and tailing log to /tmp/tmp.XXXX, PID: 12345 Triggering rolling update Command with PID 12345 completed successfully Command with PID 12346 completed successfully ...最终以退出码 0 结束,并已清理测试集群test-upgrade。
7.2 常见失败场景
| 现象 | 原因 | 处理建议 |
|---|---|---|
Release skypilot not found in namespace skypilot | 未正确安装 Helm release 或参数传错 | 用helm ls -A确认 release 名称与命名空间 |
helm upgrade报错提示必须配置外部数据库 | 使用RollingUpdate前未配置dbConnectionSecretName/dbConnectionString | 按 api-server-admin-deploy.rst 配置 PostgreSQL,或在未配置数据库时保持Recreate策略 |
Timeout wait the log tailing start | sky launch任务 120 秒内未输出count: 1 | 检查集群资源是否充足、sky status中集群是否 UP |
Incorrect log tailing, refer to $log_file for details | 日志中count: 1或count: 100出现次数不为 1(重复尾随/重复启动,或日志不完整) | 查看$log_file与/tmp/sky_*.log定位具体请求失败原因 |
Failed jobs: N | 升级期间有 CLI 请求未能成功 | 结合/tmp/sky_status.log、/tmp/sky_launch_dryrun.log、/tmp/sky_jobs_queue.log、/tmp/sky_launch.log排查;若新版本不兼容,则不是测试问题而是 API 兼容性问题 |
排查时建议同时观察 API 服务器 Pod 的滚动状态:
kubectl get pod --namespace skypilot -l app=skypilot-api --watch8. 手动复现:把测试脚本变成日常演练
如果你不想直接运行脚本,也可以按以下步骤手动完成一次滚动升级演练(与脚本逻辑一一对应):
# 1. 登录 API 服务器 sky api login -e http://your-api-server.com # 2. 启动一个长任务(约 100 秒) sky launch -c test-upgrade -y --cpus 1+ 'for i in {1..100}; do echo "count: $i" && sleep 1; done' --infra kubernetes # 3. 另开终端,观察日志输出 sky status test-upgrade # 或使用 sky jobs logs 跟踪作业日志 # 4. 触发滚动更新(注入时间戳注解以强制重建 Pod) timestamp=$(date +%s) helm upgrade skypilot charts/skypilot \ --namespace skypilot \ --reuse-values \ --set apiService.upgradeStrategy="RollingUpdate" \ --set apiService.annotations.restart="at$timestamp" # 5. 升级期间观察集群状态 sky status sky jobs queue # 6. 确认日志完整(应看到 count: 1 到 count: 100) # 7. 清理 sky down test-upgrade -y9. 总结
tests/kubernetes/upgrade/中的 Graceful Upgrade Test 是 SkyPilot 工程实践中一个精巧的验证工具:
- 它以真实流量验证了"优雅升级"承诺:在滚动更新期间,同时发起短请求(
sky status、sky jobs queue、dryrun)、关键请求(sky launch)和长请求(日志尾随),并严格校验全部成功、日志恰好完整(count: 1与count: 100各出现一次); - 它反向验证了 Helm Chart 的前置校验:一旦未配置外部数据库或存储模式不满足 RWX 要求,
helm upgrade会在模板渲染阶段直接失败,从而保证升级过程的安全边界; - 它与官方文档和源码形成闭环:升级策略对比、
terminationGracePeriodSeconds调整、successThreshold: 3的就绪探针、SKYPILOT_ROLLING_UPDATE_ENABLED环境变量及其引发的文件挂载警告,都可以在 api-server-upgrade.rst、api-deployment.yaml 和 sky/jobs/server/core.py 中逐一找到依据。
对于在生产环境以 Helm 方式部署 SkyPilot API 服务器、并期望零停机升级的团队而言,这套测试脚本既是升级前的体检工具,也是理解 SkyPilot 升级机制的最佳入门教材。
参考文件索引
- 测试文档:tests/kubernetes/upgrade/README.md
- 测试脚本:tests/kubernetes/upgrade/test-upgrade.sh
- 官方升级指南:docs/source/reference/api-server/api-server-upgrade.rst
- Helm 部署指南:docs/source/reference/api-server/api-server-admin-deploy.rst
- Chart 值定义:charts/skypilot/values.yaml
- Deployment 模板:charts/skypilot/templates/api-deployment.yaml
- 滚动更新警告逻辑:sky/jobs/server/core.py
- 常量定义:sky/skylet/constants.py
【免费下载链接】skypilotThe AI Compute Platform for frontier teams. SkyPilot turns fragmented AI compute into one AI supercomputer, so frontier AI teams build custom intelligence faster.项目地址: https://gitcode.com/GitHub_Trending/sk/skypilot
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考