SkyPilot API 服务器无感升级测试指南:用 Helm 滚动更新验证 Kubernetes 上的可用性
2026/9/16 15:17:41 网站建设 项目流程

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 launchsky statussky 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 launchsky status等命令就会报错,直接影响团队的工作。

SkyPilot 的官方文档 api-server-upgrade.rst 中提到,只要满足以下条件,API 服务器就可以进行优雅升级(graceful upgrade)

  • 使用 Helm 部署 API 服务器;
  • 升级前后的版本在 API 兼容性范围内(SkyPilot 从0.10.0开始保证相邻 minor 版本之间的 API 兼容性)。

在优雅升级过程中,API 服务器对不同请求的处理策略是:

  • 关键请求(如启动集群):等待其完成,超时后再中断;
  • 非关键请求(如日志尾随):取消并返回错误,提示客户端重试;
  • 新请求:返回错误提示重试,待新版本 API 服务器就绪后继续服务。

tests/kubernetes/upgrade/下的测试脚本,正是为了自动化验证上述行为而存在的。它模拟了一个真实的滚动升级场景:

  1. 启动一个长时间运行的sky launch任务并尾随其日志;
  2. 触发helm upgrade滚动更新 API 服务器;
  3. 在升级期间同时发起多个不同类型的请求(sky statussky launch --dryrunsky jobs queuesky launch);
  4. 验证这些请求是否都能成功,以及日志尾随是否能在升级后恢复。

这与 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所描述的全部安装步骤。这包括:

  1. 安装 Helm 并添加 SkyPilot 的 Helm 仓库;
  2. 通过helm installhelm upgrade --install部署skypilot/skypilot-nightlyChart;
  3. 确保 API 服务器 Pod 正常运行,且 ingress 已暴露外部访问地址。

此外,还需要:

  • 本机已安装并配置好 SkyPilot CLIsky命令可用),且已通过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$1API 服务器的访问地址,如http://your-api-server.com
RELEASE_NAME$2skypilotHelm release 的名称
NAMESPACE$3skypilotHelm 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_URLNAMESPACERELEASE_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.dbConnectionSecretNameapiService.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: 1count: 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 中两种策略的对比表完全一致:

维度RecreateRollingUpdate
可用性升级期间短暂停机零停机
请求处理新请求等待升级完成新请求由可用副本持续服务
数据库要求可使用本地存储(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=300

6. 滚动更新在代码层的配套支持

测试脚本验证的行为背后,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_mountsworkdir时,

会提示用户这些本地路径在滚动更新后可能丢失,建议改用云存储桶、卷、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 startsky launch任务 120 秒内未输出count: 1检查集群资源是否充足、sky status中集群是否 UP
Incorrect log tailing, refer to $log_file for details日志中count: 1count: 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 --watch

8. 手动复现:把测试脚本变成日常演练

如果你不想直接运行脚本,也可以按以下步骤手动完成一次滚动升级演练(与脚本逻辑一一对应):

# 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 -y

9. 总结

tests/kubernetes/upgrade/中的 Graceful Upgrade Test 是 SkyPilot 工程实践中一个精巧的验证工具:

  • 它以真实流量验证了"优雅升级"承诺:在滚动更新期间,同时发起短请求(sky statussky jobs queue、dryrun)、关键请求(sky launch)和长请求(日志尾随),并严格校验全部成功、日志恰好完整(count: 1count: 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询