
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/skypilotSkyPilot 的 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 TestSkyPilot 的 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 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$1是—API 服务器的访问地址如http://your-api-server.comRELEASE_NAME$2否skypilotHelm release 的名称NAMESPACE$3否skypilotHelm release 所在的命名空间注意脚本中参数顺序为RELEASE_NAME在前、NAMESPACE在后与 README 中的示例一致实际脚本源码里二者的赋值顺序与此相同仅传参顺序易混淆建议始终显式指定。3.3 示例./test-upgrade.sh http://your-api-server.com skypilot skypilot该命令表示API 服务器地址为http://your-api-server.comHelm 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.upgradeStrategyRollingUpdate这是整个测试的关键一步将 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_NAMEtest-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 21 tail_pid$! echo Launch and tailing log to $log_file, PID: $tail_pidsky 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 等待任务开始输出timeout120 elapsed0 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.restartat$timestamp这是第二次helm upgrade用于触发滚动更新通过--set apiService.annotations.restartat$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 21 sky_pids($!) sky launch --infra kubernetes --dryrun -y /tmp/sky_launch_dryrun.log 21 sky_pids($!) sky jobs queue /tmp/sky_jobs_queue.log 21 sky_pids($!) sky launch --infra kubernetes --cpus 1 echo hello -y /tmp/sky_launch.log 21 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_jobs0 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 中两种策略的对比表完全一致维度RecreateRollingUpdate可用性升级期间短暂停机零停机请求处理新请求等待升级完成新请求由可用副本持续服务数据库要求可使用本地存储SQLite必须使用外部持久化数据库升级期间资源使用先删旧 Pod再启新 Pod先启新 Pod再删旧 Pod适用场景开发环境、简单部署生产环境、高可用要求5.1 为什么必须配置外部数据库api-deployment.yaml 中的模板校验清晰地说明了这一点RollingUpdate下新旧两个 Pod 会同时运行而 SkyPilot API 服务器的状态默认存储在 SQLite 中位于 PVC 上。两个进程同时写同一个 SQLite 文件是不安全的因此必须将状态存储迁移到外部 PostgreSQL。5.2 为什么存储模式必须是 ReadWriteManyvalues.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.enabledtrue且 accessMode 为ReadWriteOnceapi-deployment.yaml 会直接failLocal 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.enabledfalse时的警告If storage.enabledfalse 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.terminationGracePeriodSeconds3006. 滚动更新在代码层的配套支持测试脚本验证的行为背后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 startsky launch任务 120 秒内未输出count: 1检查集群资源是否充足、sky status中集群是否 UPIncorrect 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 appskypilot-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.upgradeStrategyRollingUpdate \ --set apiService.annotations.restartat$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.rstHelm 部署指南docs/source/reference/api-server/api-server-admin-deploy.rstChart 值定义charts/skypilot/values.yamlDeployment 模板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),仅供参考