优化推理服务启动命令
本文介绍如何编写可靠的推理服务启动命令。阅读本文后,您可以避免容器秒退、实现优雅停机,并掌握排障技巧。
准备工作
阅读本文前,请确保您理解以下核心概念。
启动命令决定容器生命周期
在平台上,启动命令就是推理服务的全部定义:
- 命令正在运行,容器保持存活
- 命令执行结束,容器立即销毁
与物理机或虚拟机不同,容器启动后不会进入"待机"状态等待您 SSH 登录。您在创建推理服务页面填写的启动命令会立即执行,执行完毕后容器就会销毁。对于推理服务,启动命令必须是一个持续运行的前台进程。
您是否遇到了这些问题?
| 现象 | 原因 | 解决方案 |
|---|---|---|
| 容器启动后立即退出 | 使用 & 或 nohup 将服务放入后台 | 保持服务在前台运行。参阅方案一:极简模式 |
| 容器反复重启 | 启动命令执行完毕后退出 | 使用阻塞式命令。参阅方案一:极简模式 |
| 停止服务时请求被切断 | 服务未能接收 SIGTERM 信号 | 使用 exec 或 trap 转发信号。参阅方案三:自定义生命周期模式 |
| 无法查看启动失败的日志 | 容器秒退,来不及排查 | 失败时挂起容器保留现场。参阅方案四:排错调试模式 |
如果您只需要快速启动服务,请直接参阅方案一:极简模式。
设计原则
编写推理服务启动命令时,遵循以下原则:
- 进程常驻前台:容器的生命周期与 PID 1 进程绑定。服务进程不能放入后台。
- 优雅响应信号:平台扩缩容或更新时会发送
SIGTERM信号。服务必须能捕获该信号并完成当前请求。 - 实时日志:禁用 Python 输出缓冲,防止日志卡在缓冲区,导致报错时看不到关键信息。
- 失败保留现场:开发调试阶段,服务启动失败时容器应保持运行,允许用户进入排查。
方案一 极简模式
这是最轻量级的写法,核心原则是 使用 exec 启动推理相关进程,保证其可正常接收信号。日志直接输出到标准输出(Stdout),由平台收集,在推理服务日志页面展示。
以 vLLM 为例,在启动命令输入框中填写:
# 1. 减少 Python 日志缓冲 (确保报错能立刻看到)
export PYTHONUNBUFFERED=1
# 2. 使用 exec 启动服务
# 原理:exec 让 vLLM 进程直接替换 Shell 成为 PID 1。
# 优势:K8s 的停止信号 (SIGTERM) 能直达 vLLM,实现优雅停机,不丢请求。
exec vllm serve \
--model /mnt/models/Qwen3-0.6B \
--port 8000 \
--host 0.0.0.0 \
--tensor-parallel-size 1 \
--gpu-memory-utilization 0.95注意
为什么加 exec?
- 不加
exec不影响推理服务正常启动和运行,只不支持优化停机。 - 不加
exec,Shell 会成为主进程。当您停止服务时,Shell 会拦截信号,导致推理进程无法接收停止指令(SIGTERM),最终被系统暴力强杀(SIGKILL),导致推理请求中断。
方案二 日志双写模式
适用于需要将日志同时输出到屏幕和文件(用于持久化保存或下载分析)。使用了高级的 IO 重定向技术,既保留了 PID 1 的信号传递能力,又实现了日志双写。
#!/bin/bash
set -e
# ========================================================
# 1. 配置全局日志管道
# ========================================================
# 技巧:将当前 Shell 的所有输出(stdout/stderr)重定向给 tee
# 效果:屏幕能看到日志,同时写入 /mnt/logs/service.log
exec > >(tee -a /mnt/logs/service.log) 2>&1
export PYTHONUNBUFFERED=1
# ========================================================
# 2. 启动服务
# ========================================================
echo "正在启动服务 (PID: $$)..."
# 依然使用 exec
# vLLM 会继承上面配置好的日志管道,同时保持 PID 1 的身份
exec vllm serve \
--model /mnt/models/Qwen3-0.6B \
--port 8000 \
--host 0.0.0.0方案三 自定义生命周期模式(Trap 信号转发)
您可以使用 trap 将信号转发给推理相关进程。在服务停止后执行一些清理工作(如上传日志、清理缓存、通知回调)。
#!/bin/bash
# 1. 定义信号处理函数 (Wrapper Pattern)
cleanup() {
echo "收到停止信号,正在通知 vLLM 退出..."
# 转发信号给后台的 vLLM 进程
if [ -n "$CHILD_PID" ]; then
kill -TERM "$CHILD_PID"
wait "$CHILD_PID" # 等待 vLLM 优雅结束
fi
# === 在这里执行您的后置清理逻辑 ===
echo "正在上传日志..."
# aws s3 cp ...
echo "清理完成,容器退出。"
exit 0
}
# 2. 注册信号捕获器
trap cleanup SIGTERM SIGINT
# 3. 启动服务 (必须放入后台 &)
export PYTHONUNBUFFERED=1
echo "启动 vLLM..."
vllm serve \
--model /mnt/models/Qwen3-0.6B \
--port 8000 \
--host 0.0.0.0 &
# 4. 获取 PID 并挂起脚本
CHILD_PID=$!
wait "$CHILD_PID"注意
- 因为
vllm &是后台运行,如果没有 wait,脚本会继续执行到最后一行并立即退出,导致 PID 1 结束,容器会被 K8s 瞬间销毁(秒退)。wait还确保 Shell 能被信号立即中断(唤醒)从而执行trap转发逻辑。 - 如果将启动逻辑写入脚本文件(如 bash
run-vllm.sh),请自行在run-vllm.sh内实现信号转发,实现优雅停机。
方案四:排错调试模式
在开发调试阶段,如果服务启动秒退(如 OOM、路径错误),导致容器反复重启,无法查看日志。建议出错后强制挂起容器,给您留出时间进入 Web Terminal 排查。
export PYTHONUNBUFFERED=1
# 尝试启动服务
# 逻辑:如果 vLLM 成功启动,就一直运行;
# 如果 vLLM 失败退出 (||),则打印错误并 sleep 1 小时。
vllm serve \
--model /mnt/models/Qwen3-0.6B \
--port 8000 \
--host 0.0.0.0 \
> >(tee /mnt/logs/debug.log) 2>&1 \
|| { echo "服务启动失败!系统保留现场 1 小时..."; sleep 3600; exit 1; }注意
在保留现场的情况下,如果服务异常,容器状态依然是运行中,可以进入推理服务详情页,找到「实例信息」,点击登录进入容器 Web Terminal。
错误排查
本节列出启动命令的常见错误模式。这些错误通常导致:
- 服务无法保持前台运行(容器立即退出)
- 调试时容器瞬间退出,无法排查 OOM 或路径错误
- 推理进程无法接收 SIGTERM,导致无法优雅停机
问题一 推理服务跑在后台,导致容器立即退出
vllm serve /mnt/models/Qwen3-0.6B &
echo "started"& 让服务进入后台,shell 很快执行完启动命令,导致容器退出,推理服务随即进行异常状态。
正确操作是保持推理服务在前台运行,以下示例参考了方案一 极简模式。
exec vllm serve --model /mnt/models/Qwen3-0.6B问题二 启动命令立即执行成功导致容器退出
vllm serve ... 2>&1 | tee vllm.logtee 总是执行成功,导致容器退出码立即为 0。平台发现推理服务容器退出,导致异常状态。
正确写法建议参考方案一 极简模式。如果需要写日志文件,可参考其他方案。
问题三 关于推理进程的退出与优雅关闭
如果推理进程在后台运行,或者您希望推理相关进程优雅关闭(例如等待正在进行的请求完成),您可能需要处理以下项目:
- 让推理相关进程在收到 SIGTERM 时(在控制台停止推理服务时)自己完成收尾工作
- 保留日志、等待 pending 请求、执行自己的 shutdown handler
可以参考 方案三 自定义生命周期模式(trap-信号转发),捕获主脚本退出事件,向推理进程发送一个可处理的信号(如 SIGTERM),等待它自然退出。