RETURN_TO_INDEX

RESEARCH_ENTRY // 后端

容器化一个可观测的 AI 服务

从镜像分层、健康检查到链路追踪,记录 AI API 服务的容器化实践。

不只是写一个 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 用量、重试次数和错误类型,但不要记录完整提示词中的隐私信息。

建议同时观察:

基础检查清单

  1. 锁定依赖版本,并使用多阶段构建。
  2. 将模型缓存与应用镜像分离。
  3. 提供存活与就绪两类健康检查。
  4. 为请求注入 trace id,串联模型与工具调用日志。
  5. 设置明确的并发、超时和资源上限。
  6. 使用非 root 用户运行,密钥只在运行时注入。
  7. 验证 SIGTERM、依赖故障和磁盘写满时的行为。

真正可靠的容器镜像,不只是体积小,而是它的依赖、状态、权限和失败方式都清楚可见。