2、Docker基础回顾:镜像、容器、Dockerfile核心指令、常用命令、数据卷与网络基础
在进入多模型推理服务的编排实战之前,必须先夯实Docker的核心知识体系。Docker并非一个单一工具,而是一套围绕“镜像(Image)—容器(Container)—仓库(Registry)”构建的交付与运行逻辑。理解镜像的只读分层机制、容器的可写层生命周期、数据卷的持久化边界以及网络模型的通信路径,是后续使用Docker Compose管理多个推理服务(如TensorRT、vLLM、Triton)的基石。本节将系统梳理这些核心概念,并重点剖析Dockerfile的编写规范与常用运维命令,为多容器协作场景打下坚实基础。
上图展示了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 构建或 pull | docker run / create |
| 生命周期 | 持久存在 | 可启动、停止、删除 |
| 数据持久性 | 不可变 | 可写层易失,需卷持久化 |
| 典型命令 | docker build, docker pull | docker 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-server | 8000 | 8000:8000 | vllm |
| triton-server | 8001 | 8001:8001 | triton |
| preprocess | 8080 | 8080:8080 | preprocess |
在容器内部,服务间调用使用 http://vllm:8000 或 http://triton:8001,而外部客户端通过宿主机IP加映射端口访问。这种模式避免了容器IP变化带来的配置更新问题。
host 网络模式以减少NAT开销。但host模式不支持端口映射,且要求宿主机端口唯一,在多服务场景下容易冲突。
常见误区与总结
- 误区1:使用
--link参数连接容器,该机制已过时,应使用自定义网络。 - 误区2:忘记指定
--network,导致容器运行在默认bridge网络,无法通过名称互相访问。 - 误区3:暴露不必要的端口到宿主机,增加攻击面。仅映射需要外部访问的服务端口。
总结:网络配置的核心是“隔离与连通”的平衡。对于多模型推理服务,建议将所有相关容器放入同一自定义网络,仅将网关服务映射到宿主机端口,内部调用走容器网络。