不只是写一个 Dockerfile
AI 服务通常包含较大的依赖、模型缓存和外部推理端点。合理的镜像分层可以显著降低发布耗时,而健康检查与优雅退出决定服务滚动更新时是否稳定。
容器化的目标不是“在我电脑上能启动”,而是得到一个可重复构建、可以被调度器判断状态、出现问题能够定位的运行单元。模型权重、Python 依赖、系统库和业务代码的变化频率不同,应该分别考虑缓存与发布策略。
一个可维护的镜像
先固定 Python 基础版本,只复制依赖清单完成安装,再复制业务代码。这样修改应用代码时不需要重新安装全部依赖。
FROM python:3.12-slim AS runtime
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1
WORKDIR /app
RUN groupadd --system app && useradd --system --gid app app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY src ./src
USER app
EXPOSE 8000
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
同时使用 .dockerignore 排除虚拟环境、Git 历史、测试缓存、密钥文件和本地模型缓存。不要把 .env 或云服务凭证复制进镜像,生产环境应通过编排平台的 Secret 注入。
存活与就绪不是一回事
/livez 只回答进程是否仍能处理请求,不访问数据库和外部模型;/readyz 才检查当前实例是否具备接流量的条件。若把所有依赖都塞进存活检查,外部 API 短暂抖动可能让整批容器不断重启。
@app.get('/livez')
async def livez():
return {'status': 'ok'}
@app.get('/readyz')
async def readyz():
checks = await dependency_status()
if not checks.ready:
raise HTTPException(status_code=503, detail=checks.errors)
return {'status': 'ready'}
服务还要正确处理 SIGTERM:停止接收新请求,等待正在执行的推理到达合理超时,刷新日志后退出。终止宽限期应大于常见请求时长,但不能无限等待。
模型与缓存如何放置
大型权重通常不适合跟每次业务发布一起构建。可以把模型放在只读数据卷、对象存储缓存或独立模型服务中,并用模型版本或内容哈希校验。启动时下载模型要设置超时和断点续传,否则扩容会被网络带宽拖垮。
如果服务只调用外部 LLM API,容器本身仍需限制并发。一个请求可能占用几十秒连接和大量内存;无限并发只会把超时从入口扩散到每个下游。
可观测性从请求入口开始
为请求生成或透传 trace_id,并把它带到检索、模型、工具和数据库调用。日志使用结构化 JSON,至少记录路由、状态码、耗时、模型名、Token 用量、重试次数和错误类型,但不要记录完整提示词中的隐私信息。
建议同时观察:
- HTTP 请求延迟与错误率;
- 排队时间、运行中请求数和超时数;
- 模型首 Token 延迟、总耗时和 Token 成本;
- 工具调用成功率与外部依赖熔断状态;
- CPU、内存、GPU 显存和容器重启次数。
基础检查清单
- 锁定依赖版本,并使用多阶段构建。
- 将模型缓存与应用镜像分离。
- 提供存活与就绪两类健康检查。
- 为请求注入 trace id,串联模型与工具调用日志。
- 设置明确的并发、超时和资源上限。
- 使用非 root 用户运行,密钥只在运行时注入。
- 验证
SIGTERM、依赖故障和磁盘写满时的行为。
真正可靠的容器镜像,不只是体积小,而是它的依赖、状态、权限和失败方式都清楚可见。