本节前半部分“Docker是什么”、“机器学习中的Docker”和“Docker中的基本概念”介绍了一些Docker的基础知识。如果想快速构建自己的镜像,可以直接阅读“自定义构建镜像”和“上传镜像到Registry”部分。
Docker是什么
Docker是一个开放源代码软件项目,让应用程序布署在软件容器下的工作可以自动化进行,借此在Linux操作系统上,提供一个额外的软件抽象层,以及操作系统层虚拟化的自动管理机制。Docker利用Linux核心中的资源分离机制,例如cgroups,以及Linux核心命名空间(name space),来建立独立的软件容器(containers)。这可以在单一Linux实体下运作,避免启动一个虚拟机器造成的额外负担。——摘自维基百科
这个定义真是学院派的不能再学院派了,一般人通过这个充满专业名词的定义,能模糊地了解docker是种虚拟化工具,可以实现简化部署之类功能,但是并不能清楚地理解它到底牛逼在什么地方。
「Docker」字面意思是「码头工人」,这个名字应该是官方深思熟虑之后的结果,本身就带有很强的隐喻性质:借用了一个在真实世界中已经成熟的体系——全球物流系统,来映射docker在软件领域中起到的作用。在全球物流系统中,一个非常重要的发明就是集装箱。
为了理解这件事情,可以先考察一下集装箱出现之前的物流情况:货物从工厂生产出来之后装箱,然后一箱箱的搬到卡车上,然后再一箱箱卸下来,一箱箱送上火车,运送到码头附近的火车站,再一箱箱卸下来,装上卡车,拉到货轮上,再一箱一箱的装上去…
可以看出在整个流程中,大量的时间,人力 ,物力浪费在了中间的装卸上。在物流系统里,由于路程和运输工具速度的限制,货物真正在路上的时间是一定的,在交通技术得到改善之前,这个时间也很难去缩短,于是「货物装卸时间」就成了物流系统中的瓶颈。
这个瓶颈在集装箱出现之后得到了很大的改善。集装箱重要在它提供了一种通用的封装货物的标准规格(尺寸,外形符合统一标准),这样就产生了一些巨大的优点:只需要在运输前一次性封装,集装箱就可以放上火车,卡车,拉到码头,直接放在货船上;卸船之后直接再放上火车,卡车,运送到目的地。而且由于集装箱符合统一标准,整个流程非常容易机械化,这引发了以集装箱为中心的整个全球物流的标准化进程,进而节省了大量的资源,物流成本迅速下降,促进了全球资源的流动与重新配置。
Docker就像往集装箱里装货物的码头工人那样,它把应用打包成具有某种标准规格的集装箱,用计算机领域的语言来说,这种按照一定规格封装的集装箱叫「镜像」。其实就是将你原来的代码添加点额外的内容,格式之类的,生产出来的一个符合某种标准的东西。
Docker在部署过程中,将安装,配置等重复的部分自动化完成。只需要在第一次部署时,构建完可用的Docker镜像(装好集装箱),在以后使用中,短短的几行命令就可以直接拉取镜像,根据这个镜像创建出一个容器,把服务跑起来了。所需要的仅仅是安装了Docker的服务器,一个Dockerfile文件(装箱清单),以及比较流畅的网络而已,真可谓『一次构建,到处部署』。
Docker不像传统的软件交付方式那样,只把代码以及说明文档之类的给你就完了,而是直接给你一个像集装箱那样的标准Docker货件,这个标准件包括了代码运行需要的OS等整体依赖环境。
机器学习中的Docker
在机器学习开发流程中的某个阶段,我们会遇到以下难题:在具有大型数据集的大型模型上进行训练,但仅在一台服务器上运行无法在合理的时间内获得结果。这是我们可以将单个模型分布在集群上以实现更快的训练。但是集群上运行的操作系统和内核版本、GPU、驱动程序和运行时以及软件依赖项可能与我们调试代码的服务器有所不同。机器学习依赖于复杂且不断发展的开源机器学习框架和工具包以及同样复杂且不断发展的硬件生态系统。由于开源机器学习软件堆栈的高度复杂性,在将代码迁移至集群环境时,会引出许多问题。conda 和 virtualenv 之类的虚拟环境方法只能解决部分问题,但是存在某些非 Python 依赖项不由这些解决方案管理。由于机器学习软件找十分复杂,因此很大一部分框架依赖项(例如硬件库)都在虚拟环境范围之外。
Docker容器技术可以封装整个依赖项甚至硬件运行时库。我们可以通过其得到一个一致且可移植的机器学习开发环境。通过Docker容器技术,在大规模计算集群上提交任务作业都会变得更加简单。我们只需将您的容器镜像推送到公共镜像仓库(Dockerhub)中,就可以在全球任何位置拉取该镜像重现实验结果。
Docker中的基本概念
Docker 镜像:Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。
分层存储:因为镜像包含操作系统完整的文件系统,其体积往往是庞大的,因此在 Docker 设计时,就充分利用 Union FS 的技术,将其设计为分层存储的架构。所以严格来说,镜像并非是像一个 ISO 那样的打包文件,镜像只是一个虚拟的概念,其由多层文件系统联合组成。
镜像构建时,会一层层构建,前一层是后一层的基础。每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。比如,删除前一层文件的操作,实际不是真的删除前一层的文件,而是仅在当前层标记为该文件已删除。在最终容器运行的时候,虽然不会看到这个文件,但是实际上该文件会一直跟随镜像。因此,在构建镜像的时候,需要额外小心,每一层尽量只包含该层需要添加的东西,任何额外的东西应该在该层构建结束前清理掉。分层存储的特征还使得镜像的复用、定制变的更为容易。甚至可以用之前构建好的镜像作为基础层,然后进一步添加新的层,以定制自己所需的内容,构建新的镜像。
Docker容器:镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的 类 和 实例 一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的 命名空间。因此容器可以拥有自己的 root 文件系统、自己的网络配置、自己的进程空间,甚至自己的用户 ID 空间。容器内的进程是运行在一个隔离的环境里,使用起来,就好像是在一个独立于宿主的系统下操作一样。这种特性使得容器封装的应用比直接在宿主运行更加安全。也因为这种隔离的特性,很多人初学 Docker 时常常会混淆容器和虚拟机。
仓库:镜像构建完成后,可以很容易的在当前宿主机上运行,但是,如果需要在其它服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,Docker Registry 就是这样的服务。
一个 Docker Registry 中可以包含多个 仓库(Repository);每个仓库可以包含多个 标签(Tag);每个标签对应一个镜像。
通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本。我们可以通过 <仓库名>:<标签>
的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以 latest 作为默认标签。
以 Ubuntu 镜像 为例, ubuntu
是仓库的名字,其内包含有不同的版本标签,如, 16.04
, 18.04
。我们可以通过 ubuntu:16.04
,或者 ubuntu:18.04
来具体指定所需哪个版本的镜像。如果忽略了标签,比如 ubuntu ,那将视为
ubuntu:latest。<br />仓库名经常以 **两段式路径** 形式出现,比如
jwilder/nginx-proxy` ,前者往往意味着 Docker Registry 多用户环境下的用户名,后者则往往是对应的软件名。但这并非绝对,取决于所使用的具体 Docker Registry 的软件或服务。
Docker Registry 公开服务:Docker Registry 公开服务是开放给用户使用、允许用户管理镜像的 Registry 服务。一般这类公开服务允许用户免费上传、下载公开的镜像,并可能提供收费服务供用户管理私有镜像。
最常使用的 Registry 公开服务是官方的 Docker Hub,这也是默认的 Registry,并拥有大量的高质量的官方镜像。除此以外,还有 Red Hat 的 Quay.io;Google 的 Google Container Registry,Kubernetes 的镜像使用的就是这个服务。
由于某些原因,在国内访问这些服务可能会比较慢。国内的一些云服务商提供了针对 Docker Hub 的镜像服务(Registry Mirror
),这些镜像服务被称为 加速器。常见的有 阿里云加速器、DaoCloud 加速器 等。使用加速器会直接从国内的地址下载 Docker Hub 的镜像,比直接从 Docker Hub 下载速度会提高很多。在 安装 Docker 一节中有详细的配置方法。
国内也有一些云服务商提供类似于 Docker Hub 的公开服务。比如 网易云镜像服务、DaoCloud 镜像市场、阿里云镜像库 等。
私有 Docker Registry:除了使用公开服务外,用户还可以在本地搭建私有 Docker Registry。Docker 官方提供了 Docker Registry 镜像,可以直接使用做为私有 Registry 服务。
自定义构建镜像
镜像的定制实际上就是定制每一层所添加的配置、文件。如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么之前提及的无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是 Dockerfile。Dockerfile 是一个文本文件,其内包含了一条条的 指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。深度学习镜像的构建可以参考deepo仓库,这个仓库上有大量的例子可以参考。下面将给出一个完整可用例子,并逐步分析该例子:
FROM nvidia/cuda:10.1-runtime-ubuntu18.04
ENV LANG=C.UTF-8 LC_ALL=C.UTF-8
ENV PATH /opt/conda/bin:$PATH
RUN APT_INSTALL="apt-get install -y --no-install-recommends" && \
GIT_CLONE="git clone --depth 10" && \
echo 'deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic main restricted universe multiverse' > /etc/apt/sources.list && \
echo 'deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-updates main restricted universe multiverse' >> /etc/apt/sources.list && \
echo 'deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-backports main restricted universe multiverse' >> /etc/apt/sources.list && \
echo 'deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-security main restricted universe multiverse' >> /etc/apt/sources.list && \
cat /etc/apt/sources.list && \
rm -rf /var/lib/apt/lists/* \
/etc/apt/sources.list.d/cuda.list \
/etc/apt/sources.list.d/nvidia-ml.list && \
apt-get update && \
DEBIAN_FRONTEND=noninteractive $APT_INSTALL wget bzip2 graphviz git openssh-server vim && \
apt-get clean
RUN wget --quiet https://mirrors.tuna.tsinghua.edu.cn/anaconda/miniconda/Miniconda3-py38_4.8.2-Linux-x86_64.sh -O ~/miniconda.sh && \
/bin/bash ~/miniconda.sh -b -p /opt/conda && \
rm ~/miniconda.sh && \
ln -s /opt/conda/etc/profile.d/conda.sh /etc/profile.d/conda.sh && \
echo ". /opt/conda/etc/profile.d/conda.sh" >> ~/.bashrc && \
echo "conda activate base" >> ~/.bashrc && \
find /opt/conda/ -follow -type f -name '*.a' -delete && \
find /opt/conda/ -follow -type f -name '*.js.map' -delete && \
/opt/conda/bin/conda clean -afy
RUN PIP_INSTALL="/opt/conda/bin/pip --no-cache-dir install --upgrade" && \
/opt/conda/bin/pip config set global.index-url https://mirrors.cloud.tencent.com/pypi/simple && \
$PIP_INSTALL ipdb tb-nightly ipython graphviz scipy numpy scikit-learn pandas matplotlib jupyter && \
$PIP_INSTALL torch==1.6.0+cu101 torchvision==0.7.0+cu101 -f https://download.pytorch.org/whl/torch_stable.html && \
$PIP_INSTALL --extra-index-url https://developer.download.nvidia.com/compute/redist nvidia-dali-cuda100
所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。基础镜像是必须指定的。而 FROM
就是指定 基础镜像,因此一个 Dockerfile
中 FROM
是必备的指令,并且必须是第一条指令。上述代码指定的基础镜像为 nvidia/cuda:10.1-runtime-ubuntu18.04
,更多CUDA的镜像可以在这里查看。选择CUDA镜像时注意两点:1)注意CUDA镜像的版本,本例中的CUDA的版本为10.1。CUDA的版本和服务器的NVIDIA GPU驱动版本有关,具体的对应关系可以查看CUDA兼容性中的表1。在兼容GPU驱动的前提下一般CUDA的版本越新越好。2)注意CUDA镜像的类型,其类型一般分为 runtime
和 devel
两种,本例中是runtime
。两者的区别是devel
版本带NVCC编译器,可以编译CUDA程序,所以其镜像体积一般会大很多。RUN
指令是用来执行命令行命令的。由于命令行的强大能力,RUN
指令在定制镜像时是最常用的指令之一。RUN <命令>
,就像直接在命令行中输入的命令一样。本例写的 Dockerfile 中的 RUN
指令就是这种格式。每个RUN
指令都会添加一个新的存储层,本例中有3个RUN
指令,会添加3个新的存储层。为了节约存储空间,每个RUN
指令伴随着一些清理的命令,如 apt-get clean
、 /opt/conda/bin/conda clean -afy
和 /opt/conda/bin/pip --no-cache-dir
等。
上述Dockerfile是一个很好的例子,只要稍加改变就能快速构建自己的镜像:
A. 在第1行,更改CUDA镜像的版本到合适的版本,更多CUDA的镜像可以在这里查看;
B. 在第16行,更改希望安装的apt包;
C. 在第19行,更改希望安装的miniconda包,更多版本的包可以到这里查看;
D. 在第31-33行,更改希望安装的python包。
在完成对Dockerfile的改动后,新建一个空文件夹,例如/home/zhangsan/mydockerfile。然后将上述代码保存在这里空文件夹中,命名为 Dockerfile
(注意没有后缀名,就是Dockerfile
)。然后在该文件夹中,我们使用 docker build
命令进行构建:
docker build -t zhangsan/pytorch:1.6-cu101 .
在这里我们指定了最终镜像的名称 -t zhangsan/pytorch:1.6-cu101
,构建成功后,我们可以通过名字 zhangsan/pytorch:1.6-cu101
那样来运行这个镜像。其中zhangsan
是用户名,pytorch
是镜像名称,1.6-cu101
是镜像版本,这些都是可以自定义的。如果注意,会看到 docker build
命令最后有一个 .
。这是.
在指定 上下文路径,更多关于上下文路径的知识可以参考《Docker —— 从入门到实践》。
上传镜像到Registry
Docker Registry分为公有的和私有的,公有Registry如Docker Hub是全球可以访问的,但是访问速度较慢;私有的Registry只能在局域网内访问,但是访问速度极快。在集群中使用建议上传到私有的Registry,这样可以保证快速载入镜像进行实验。无论是公有的Registry还是私有的Registry,其上传步骤是基本相同的,下面将会分别进行讲解。
上传到公有Registry(Docker Hub)
首先要在Docker Registry建立自己的账号,官方的 Docker Hub 直接按照指引注册用户就行了。注意用户名和镜像的用户名需要相同,如镜像zhangsan/pytorch:1.6-cu101
的用户名为zhangsan。
第二步如果名称不符合上述条件,那么我们需要将镜像名按照以下命令重命名:
docker tag wrongname/pytorch:1.6-cu101 zhangsan/pytorch:1.6-cu101
第三步是在命令行中登陆到Docker Registry,使用 docker login
命令:
docker login
按照指引输入用户名和密码即可。
第四步是上传镜像,使用 docker push
命令:
docker push zhangsan/pytorch:1.6-cu101
上传到私有Registry
首先要在Docker Registry建立自己的账号,私有的Registry一般需要联系管理员。
第二步假设私有Registry的网址为registry.example.com
,那么我们需要将镜像名按照以下命令重命名:
docker tag zhangsan/pytorch:1.6-cu101 registry.example.com/zhangsan/pytorch:1.6-cu101
第三步是在命令行中登陆到私有Registry,使用 docker login
命令:
docker login registry.example.com
按照指引输入用户名和密码即可。
第四步是上传镜像,使用 docker push
命令:
docker push registry.example.com/zhangsan/pytorch:1.6-cu101
参考
- Docker 有什么优势? - 小狐濡尾的回答 - 知乎
- 为什么使用 Docker 容器进行机器学习开发? | 亚马逊AWS官方博客
- 《Docker —— 从入门到实践》