2、Docker基础回顾:镜像、容器、Dockerfile核心指令、常用命令、数据卷与网络基础

在进入多模型推理服务的编排实战之前,必须先夯实Docker的核心知识体系。Docker并非一个单一工具,而是一套围绕“镜像(Image)—容器(Container)—仓库(Registry)”构建的交付与运行逻辑。理解镜像的只读分层机制、容器的可写层生命周期、数据卷的持久化边界以及网络模型的通信路径,是后续使用Docker Compose管理多个推理服务(如TensorRT、vLLM、Triton)的基石。本节将系统梳理这些核心概念,并重点剖析Dockerfile的编写规范与常用运维命令,为多容器协作场景打下坚实基础。

镜像层 容器实例 数据卷与网络 只读层:Ubuntu 22.04 只读层:CUDA 12.0 只读层:Python 3.10 只读层:推理框架 可写层:容器运行期修改 容器A:vLLM服务 端口映射 8000:8000 容器B:Triton服务 端口映射 8001:8001 容器C:模型预热 依赖卷挂载 数据卷 Volume /models 持久化 Bridge 网络 容器间DNS通信 Host 网络 共享宿主机IP docker run 挂载/连接

上图展示了Docker三大核心对象的关系:镜像层提供只读模板,容器实例在镜像之上增加可写层运行,而数据卷与网络则负责解决持久化和跨容器通信问题。下面我们将逐一深入每个模块。

2.1 镜像与容器:从模板到实例的运作机制

核心原理:联合文件系统与写时复制

Docker镜像由多个只读层叠加而成,每一层对应Dockerfile中的一条指令。当启动容器时,Docker会在镜像层顶部添加一个可写层(容器层)。所有对文件系统的修改(写入、删除、修改)都发生在可写层,底层镜像保持不变。这种设计称为“写时复制(Copy-on-Write)”,它带来的直接好处是:多个容器可以共享同一镜像底层,节省磁盘空间并加速启动。

关键理解:镜像是一个静态模板,容器是动态运行实例。删除容器不会影响镜像;但容器可写层中的数据会随容器删除而丢失,除非使用数据卷持久化。

判断方法:镜像与容器的生命周期管理

在实际操作中,需要清晰区分镜像命令与容器命令。镜像命令以 docker image 或 docker build 为主,容器命令以 docker run、docker start、docker exec 为主。判断一个对象是镜像还是容器,可以通过 docker ps -a 查看容器列表,通过 docker images 查看镜像列表。镜像名称通常包含仓库名和标签(如 nginx:1.25),容器则拥有唯一ID和随机名称。

对比维度镜像(Image)容器(Container)
本质只读模板可写运行实例
创建方式Dockerfile 构建或 pulldocker run / create
生命周期持久存在可启动、停止、删除
数据持久性不可变可写层易失,需卷持久化
典型命令docker build, docker pulldocker run, docker exec

实战场景:拉取并运行第一个推理基础镜像

假设我们需要一个包含CUDA环境的镜像来运行GPU推理服务。操作流程如下:

# 1. 拉取镜像
docker pull nvidia/cuda:12.0-base-ubuntu22.04

# 2. 查看镜像信息
docker image inspect nvidia/cuda:12.0-base-ubuntu22.04

# 3. 运行容器并进入交互式Shell
docker run -it --rm --gpus all nvidia/cuda:12.0-base-ubuntu22.04 bash

# 4. 在容器内验证GPU
nvidia-smi

这里 --rm 表示容器退出后自动删除,适合临时测试;--gpus all 将宿主机GPU暴露给容器。注意,如果宿主机没有NVIDIA驱动或nvidia-container-toolkit,此命令会失败。

风险提示:不要在生产环境中使用 --rm 运行需要持久化日志的服务。另外,镜像标签 latest 具有不确定性,建议始终指定精确版本号。

常见误区与复盘

  • 误区1:认为容器内修改的文件会永久保存。实际上,容器删除后所有未持久化的修改都会丢失。
  • 误区2:混淆镜像大小与容器磁盘占用。容器可写层会随运行时间增长,需定期清理。
  • 误区3:忽略 .dockerignore 文件,导致构建上下文过大,构建速度缓慢。

复盘建议:每次操作前先确认对象类型,使用 docker system df 查看磁盘占用分布,养成及时清理悬空镜像的习惯。

2.2 Dockerfile核心指令与构建优化

核心原理:指令即层,顺序即缓存

Dockerfile中的每一条指令都会生成一个新的镜像层。构建时,Docker会检查每一层是否已有缓存,如果指令和上下文未变化,则直接复用缓存层。因此,指令的顺序直接影响构建效率和镜像大小。常用的核心指令包括 FROM、RUN、COPY、ADD、ENV、WORKDIR、EXPOSE、CMD 和 ENTRYPOINT。

操作流程:编写一个高效的推理服务Dockerfile

以下是一个用于部署Python推理服务的Dockerfile示例,重点展示层缓存优化策略:

# 基础镜像
FROM python:3.10-slim

# 设置环境变量
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

# 设置工作目录
WORKDIR /app

# 先复制依赖文件,利用缓存
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 再复制业务代码
COPY ./src ./src

# 暴露端口
EXPOSE 8000

# 启动命令
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
优化要点:将 requirements.txt 的复制和安装放在业务代码之前,这样当业务代码变更时,Docker可以复用依赖层缓存,避免重复安装依赖,大幅提升构建速度。

案例拆解:多阶段构建减小镜像体积

对于包含编译步骤的推理服务(如需要编译C++扩展),多阶段构建是标准方案。以下案例展示如何将构建阶段与运行阶段分离:

# 阶段1:构建
FROM golang:1.20 AS builder
WORKDIR /src
COPY . .
RUN go build -o /app/inference-server

# 阶段2:运行
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y libgomp1 && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/inference-server /usr/local/bin/
EXPOSE 8080
CMD ["inference-server"]

最终镜像仅包含运行所需的二进制文件和动态库,体积从数GB降至数百MB。这种模式在部署TensorRT或ONNX Runtime等需要编译优化的推理引擎时尤为关键。

常见误区与总结

  • 误区1:在 RUN 指令中连续使用 && 时,如果某一步失败,整个层构建失败,但缓存可能残留部分状态。
  • 误区2:使用 ADD 指令自动解压tar包,但 ADD 还会从URL下载文件,这可能导致不可控的构建行为。推荐优先使用 COPY。
  • 误区3:忘记设置 WORKDIR,导致路径混乱,容器启动时找不到应用。

总结:Dockerfile的编写应遵循“层数最小化、缓存最大化、上下文最小化”三大原则。使用 docker history 可以查看镜像每一层的创建指令,帮助排查体积异常。

2.3 常用命令与数据卷管理

核心原理:命令分类与数据持久化边界

Docker命令按功能可分为生命周期管理、状态查询、资源清理和调试四类。数据卷(Volume)是Docker管理宿主机文件系统上特定目录的机制,它独立于容器生命周期,用于持久化数据库文件、模型权重、日志等关键数据。绑定挂载(Bind Mount)则直接将宿主机目录映射到容器,适合开发调试。

操作流程:常用命令速查与卷操作

命令用途示例
docker ps -a查看所有容器docker ps -a --format "table {{.Names}}\t{{.Status}}"
docker logs -f跟踪容器日志docker logs -f --tail 100 my-container
docker exec -it进入运行中容器docker exec -it my-container bash
docker volume create创建数据卷docker volume create model-weights
docker run -v挂载卷或目录docker run -v model-weights:/models my-image

数据卷的挂载语法为 -v 卷名:容器路径 或 -v 宿主机路径:容器路径。推荐使用命名卷,因为它由Docker管理,迁移和备份更方便。

实战场景:持久化模型文件与日志

假设我们要运行一个加载大模型的服务,模型文件存放在宿主机 /data/models 目录,日志需要持久化到 /data/logs:

# 创建命名卷
docker volume create model-store
docker volume create inference-logs

# 运行容器并挂载
docker run -d --name inference \
  -v model-store:/models \
  -v inference-logs:/var/log/inference \
  -p 8000:8000 \
  my-inference-image:latest

这样即使容器被删除,模型和日志依然保留在卷中。后续升级镜像时,只需重新运行容器并挂载相同卷即可。

风险说明:绑定挂载(如 -v /data/models:/models)在宿主机路径不存在时会自动创建目录,但权限可能异常。生产环境建议使用命名卷,并配合 docker volume inspect 查看挂载点。

常见误区与复盘

  • 误区1:使用 docker commit 保存容器状态作为镜像,这会导致镜像包含大量临时文件且不可复现。
  • 误区2:忽略日志轮转,导致容器日志文件无限增长,占满磁盘。应配置 --log-opt max-size=10m --log-opt max-file=3。
  • 误区3:在容器内直接修改挂载卷中的文件,可能引发权限冲突。建议在宿主机或通过专用管理容器操作。

复盘建议:为每个服务规划独立的命名卷,并定期使用 docker volume ls 检查未使用的卷,用 docker volume prune 清理。

2.4 网络基础:容器间通信与端口映射

核心原理:Docker网络驱动模型

Docker默认提供三种网络驱动:bridge(默认)、host 和 none。Bridge网络创建一个虚拟网桥,容器通过虚拟网卡连接,彼此可以通过IP或容器名通信。Host网络直接使用宿主机网络栈,性能更高但隔离性差。对于多模型推理服务,通常使用自定义Bridge网络,以便通过服务名进行DNS解析。

操作流程:创建网络并连接容器

# 创建自定义网络
docker network create --driver bridge inference-net

# 运行两个容器并连接到同一网络
docker run -d --name model-a --network inference-net my-model-a
docker run -d --name model-b --network inference-net my-model-b

# 在model-a中测试访问model-b
docker exec model-a ping model-b

自定义Bridge网络提供了内嵌DNS,容器可以直接使用名称互相访问,无需依赖IP地址。这在Compose编排中尤为重要,因为服务名即主机名。

案例拆解:多推理服务的端口规划

假设我们需要同时部署三个推理服务:vLLM(端口8000)、Triton(端口8001)、自定义预处理服务(端口8080)。使用Bridge网络时,端口映射策略如下:

服务容器端口宿主机映射网络别名
vllm-server80008000:8000vllm
triton-server80018001:8001triton
preprocess80808080:8080preprocess

在容器内部,服务间调用使用 http://vllm:8000 或 http://triton:8001,而外部客户端通过宿主机IP加映射端口访问。这种模式避免了容器IP变化带来的配置更新问题。

经验提醒:如果服务需要接收大量外部请求,建议使用 host 网络模式以减少NAT开销。但host模式不支持端口映射,且要求宿主机端口唯一,在多服务场景下容易冲突。

常见误区与总结

  • 误区1:使用 --link 参数连接容器,该机制已过时,应使用自定义网络。
  • 误区2:忘记指定 --network,导致容器运行在默认bridge网络,无法通过名称互相访问。
  • 误区3:暴露不必要的端口到宿主机,增加攻击面。仅映射需要外部访问的服务端口。

总结:网络配置的核心是“隔离与连通”的平衡。对于多模型推理服务,建议将所有相关容器放入同一自定义网络,仅将网关服务映射到宿主机端口,内部调用走容器网络。

本节核心回顾:镜像提供只读模板,容器是运行实例;Dockerfile的指令顺序决定缓存效率;数据卷解决持久化问题;自定义Bridge网络实现容器间安全通信。掌握这四块内容,即可顺利过渡到Docker Compose的多服务编排学习。