镜像, 容器与仓库的关系
镜像
Docker镜像可以理解为一个模版, 将运行应用程序依赖的环境打包成只读的, 静态的模版, 即镜像. 这样在另外一台物理机上去运行这个应用程序, 就无须再配置一遍环境, 直接使用打包好的这个镜像就可以了.
仓库
打包好的镜像是以文件的形式存在, 如果只能通过拷贝的方式在物理机之间转移, 就太麻烦了. 而且一些基础的使用环境, 如tensorflow-gpu
等, 是如此多, 显然是需要一种共享机制的, 直接从远端下载即可, 无需个人重新配置.
可以参考Git的机制, Github
作为公共托管代码的平台, 每个人都可以创建多个仓库, 每个仓库记录单独的代码. Docker也有类似的机制. 类比于Git:
类比于
Github
,Docker Hub
是托管镜像的公共位置, 称为Docker Registry. 除了像Docker Hub
这种公共的镜像托管Registry, 也可以自己搭建, 一般公司都会有类似的资源Registry 中包含多个仓库, 可以把仓库理解为一个软件. 例如
tensorflow-gpu
这个仓库就是一个提供了tensorflow-gpu包, 以及它正常运行所需的依赖环境, 如Python
,cuda
,cudnn
等一个仓库可以包含多个镜像. 例如我们知道
tensorflow-gpu
包有很多版本, 从早期的1.x
,1.1x
到现在的2.x
, 它们都是tensorflow-gpu
这一种软件, 只是有很多版本, 每个版本所依赖的环境也不同, 需要打包成不同的镜像, 供用户按需使用. 这些镜像都属于tensorflow-gpu
这个仓库, 以标签(Tag)的形式区分版本我们可以使用
<仓库名>:<标签>
这种格式, 来具体制定需要这个软件哪个版本的镜像如果从远程仓库获取镜像时不指定tag, 默认是
latest
, 下载最新的镜像
容器
镜像(Image)和容器(Container)的关系, 类似于程序中类和实例的关系. 镜像是静态的类, 容器是镜像运行时的实体. 一个容器运行的本质是一个进程, 因此可以在对一个镜像创建多个容器, 相互之间按进程隔离, 互不干扰.
容器可以被创建, 启动, 停止, 删除, 暂停等.
关系
上面三者个关系可以总结如下图:
或者更详尽的, 带有docker指令的下图:
镜像, 容器, 与层
镜像与文件系统
从上面我们可以知道, 镜像是只读的, 静态的, 一旦被打包创建, 就是永久不会变的. Docker镜像本质就是一个Linux文件系统(Root File System), 这个文件系统里面包含可以运行在Linux内核的程序以及相应的数据.
实际上, Linux分为两个部分, Linux内核(Linux Kernel)和用户空间. 可以把Docker镜像看成是上面所说的用户空间, 而当Docker通过镜像创建一个容器时, 就是将镜像定义好的用户空间作为独立隔离的进程运行在宿主机的Linux内核之上.
镜像与层
我们在从远程仓库pull获取镜像, 或者打包容器生成镜像的过程中, 可以看到Docker镜像会被分层若干层. Docker镜像是分层的, 从最底层的Base层开始, 一层一层叠成, 前一层是后一层的基础, 如下图所示:
为什么要分层
一个镜像由多个中间层组成, 多个镜像可以共享同一个中间层, 可以通过在镜像添加多一层来生成一个新的镜像. 这样带来了一个好处: 共享资源.
对于每一层, 我们都可以认为从最底层到以这层为顶层, 以及之间的所有层, 共同组成了一个镜像. 每加一层就生成了一个新的镜像, 这样我们就在镜像和层之间建立了一种一一对应的映射关系.
我们可以将下层镜像看作是上层镜像的父镜像. 最底层镜像没有任何父镜像, 称之为基础镜像(Base镜像). Base镜像类似于大楼的地基功能, 一般是各种Linux发行版本, 如Ubuntu, Debian, CentOS等, 在上图中也可以看出.
之后我们就可以在Base镜像上安装一个个软件, 相当于在Base镜像上一个个叠加层, 得到一个个镜像. 这样层就在不同镜像之间得到了共享, 节省了资源.
不是说Docker镜像实例化为容器运行时, 只是进程的隔离, 是共享宿主机内核的吗? 这里为什么Base镜像还是Linux操作系统?
因为Base镜像里的操作系统指的不是内核, 上面也提到, Linux操作系统由用户空间和内核空间构成, 内核空间是kernel, 用户空间是rootfs, 不同发行版的区别主要是rootfs. 比如Ubuntu使用upstart管理服务, apt管理软件包; 而CentOS使用systemd和yum. 这些都是用户空间的不同, Kernel差别不大.
所以Docker可以同时支持多种Linux镜像, 模拟出不同的操作系统环境. Base镜像只是用户空间和发行版本一致, 内核空间使用的是Docker宿主机器的Kernel.
Docker怎么管理分层
上面说过Docker镜像本质是一个文件系统, 每个镜像都可以看做是一堆只读层(read-only layer)堆叠而成, 是只读层的统一视角.
在上图的左边, 我们看到了多个只读层, 除了最下面一层, 其它层都会有一个指针指向下一层, 这些层是Docker内部的实现细节, 并且能够在宿主机的文件系统上访问到. 统一文件系统(Union File System)技术能够将不同的层整合成一个文件系统, 为这些层提供了一个统一的视角, 这样就隐藏了多层的存在, 在用户的角度看来, 只存在一个文件系统, 就如上面的右图所示.
镜像的只读性
需要特别强调镜像的只读性. 镜像构建时, 是一层一层构建的, 前一层是后一层的基础, 每一层构建完就不会再发生改变, 后一层上的任何改变只发生在自己这一层.
例如我删除了前面某一层镜像的内容, 并不是在那一层镜像中直接修改的, 而是在当前层标记了修改的内容. 最终打包的镜像看起来是修改后的内容, 但实际在构建解析的过程中, 是一层进行了创建, 一层进行了修改.
容器与层
当从镜像中实例化出一个容器时, 实际上是在镜像原来的层之上, 又创建了一个新层作为顶层, 但这个层是可写的, 被称为是容器层, 对应的之前的层称为是镜像层.
由于容器层可以写, 因此我们可以在这一层上进行任意操作, 容器所有发生文件变更写都发生在这一层.
因此可以理解为容器 = 镜像 + 读写层, 无论这个容器是否在运行, 容器对应的容器层都是存在的, 只是是否给这个容器分配了进程去运行的区别.
容器层的复制策略
Docker通过一个修改时复制策略来保证创建容器时, 依赖镜像的安全性, 以及更高的性能和空间利用率.
当容器需要读取文件的时候, 从最上层的镜像层开始往下找, 找到后读取到内存中, 若已经在内存中, 可以直接使用. 换句话说, 运行在同一台机器上的Docker容器共享运行时相同的文件
当容器需要修改文件的时候, 从上往下查找, 找到后复制到容器层, 对于容器来说, 可以看到的是容器层的这个文件, 看不到镜像层里的文件, 然后直接修改容器层的文件
当容器需要删除文件的时候, 从上往下查找, 找到后在容器中记录删除, 并不是真正的删除, 而是软删除, 这导致镜像体积只会增加, 不会减少
当容器需要增加文件的时候, 直接在最上层的容器可写层增加, 不会影响镜像层
这种策略被称为copy-on-write.
容器层打包
容器层这种可读写的层是动态的, 是没有被持久化存储的. 当容器被remove, 或者宿主机物理机重启后, 容器层就没有了, 相应的容器也没有了. 因此在对镜像调整完毕后, 要对容器进行打包操作, 使当前的容器层转变为只读的镜像层, 保存在宿主机系统中, 得到了持久化, 同时也就生成了一个新的镜像.
参考资料
最后更新于