探讨 Docker 在机器学习(ML)场景中的使用。本文是系列文章的第一篇,将介绍一些 Docker 的基础知识,这些知识适用于 ML 应用程序。
Docker 可以在多个平台上运行,包括适用于 Windows 和 macOS 的 Docker Desktop,以及适用于各种 Linux 发行版的 Docker Server,无论是在 Intel/AMD 还是 ARM 处理器上。可以在 Docker 官网找到适合平台的安装指南。
Docker 是一种轻量级技术,用于在隔离环境中打包和执行软件组件。这样的包被称为“容器镜像”(或简称“镜像”),而镜像代码执行的环境被称为“容器”。
Docker 技术确保每个容器的 Python 和系统依赖关系完全隔离,这比虚拟环境提供的隔离程度更高。同时,这项技术允许在运行时环境中实现最大的可移植性。在许多情况下,同一个容器可以在本地工作站或服务器上运行,无论是在本地还是云端。
为了更好地理解在后续文章中将做出的一些设计选择,值得花时间了解 Docker 的基础知识。
Docker 镜像由多个只读层组成。每一层只包含当前层与前一层之间的差异。构建镜像时,只有发生变化的层以及随后的层会被刷新。这就是为什么定义 Docker 层时,应该按照从最静态到最“动态”的顺序排列,这可以大大减少构建镜像所需的时间。
层的数量会影响镜像大小(和构建时间),因此建议使用单个 RUN
语句执行多个 Linux 命令(单个 RUN
= 单个层)。
Dockerfile 定义了镜像。让考虑一个非常简单的例子:
FROM python:3.7-slim
RUN apt-get update & amp; apt-get install python3-numpy
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt
COPY app /app
WORKDIR app
ENTRYPOINT ["python", "app.py"]
CMD ["--input", "1234"]
上述语句的含义如下:
FROM
定义基础镜像,格式为 RUN
执行 Linux 命令。COPY
将本地文件从主机上下文复制到镜像中。WORKDIR
切换当前文件夹(如果需要,则创建它)。ENTRYPOINT
和 CMD
定义每次容器启动时执行的命令。如果这两个语句一起使用,ENTRYPOINT
反映命令的常量部分,而 CMD
则是其参数。以下语句用于构建镜像:
$ docker build -t : .
注意,末尾的 "."(点)表示当前文件夹作为 Docker 上下文(简而言之,是复制到容器的文件和 Dockerfile 的根文件夹位置)。
默认情况下,如果未明确提供标签,Docker 会使用“最新”标签作为基础镜像的标签。类似于为 Pip 或 Conda 定义 Python 依赖时指定包版本,应该始终为基镜像添加一个预定的标签,而不是使用“最新”默认标签。这可以在生产环境中节省很多麻烦。虽然这并不能保证每次构建的镜像100%相同,但它显著降低了引入有害更改的风险。
Docker 根据宿主操作系统略有不同。在内部,Docker 依赖于四个核心 Linux 特性:联合文件系统、Linux 进程、命名空间和 cgroups。
联合文件系统(UnionFS)是一种用于处理镜像层的技术。在 Linux 上的 Docker Server 的情况下,所有容器与宿主共享单个内核。
剩下的三个特性——进程、命名空间和 cgroups——确保了容器的正确隔离。在 Windows 和 Mac 上的 Docker Desktop 的情况下,会在宿主机器上安装 Linux VM,所有运行的容器共享其内核。
考虑到上述情况,很容易理解为什么在 Linux 服务器上使用默认的 root 用户运行容器是一个非常糟糕的主意。如果只有单个容器在单个专用机器上运行(这通常是云部署的情况),问题稍微不那么危险。
无论如何,即使在使用 Docker Desktop 的情况下,也不应该以 root 用户身份运行容器代码。
容器执行速度极快的秘诀在于,当它启动时,不会复制任何数据。只会在只读镜像层的堆栈上添加一个单一的(最初非常薄的)可读写容器层。容器执行期间对文件的所有更改都存储在这个新层中,作为“增量”使用联合文件系统。
当容器被移除时,这个可读写层也会随之被删除。这就是为什么应该始终将容器数据视为临时的。如果关心容器处理的数据,需要一个卷。根据需求和宿主环境,它可能由 Docker 实例持久化,或者映射到本地或云文件夹。
Docker 不是魔法——它依赖于运行它的硬件。这意味着可能仍然需要为 Intel/AMD 和 ARM 处理器使用略有不同的镜像。