> For the complete documentation index, see [llms.txt](https://kerasnoone.gitbook.io/garnet/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://kerasnoone.gitbook.io/garnet/gong-cheng-zhan/docker-ge-li-yuan-li/docker-he-xin-gai-nian.md).

# docker核心概念

## 镜像, 容器与仓库的关系

### 镜像

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)的关系, 类似于程序中类和实例的关系. **镜像是静态的类, 容器是镜像运行时的实体**. 一个容器运行的本质是一个进程, 因此可以在对一个镜像创建多个容器, 相互之间按进程隔离, 互不干扰.

容器可以被创建, 启动, 停止, 删除, 暂停等.

### 关系

上面三者个关系可以总结如下图:

![](https://1942165044-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MI7KRyeBH5dlW-CkUtn%2Fsync%2F0552e880ed545f3528a08aee9781cd5519c290e6.jpg?generation=1620650098995375\&alt=media)

或者更详尽的, 带有docker指令的下图:

![](https://1942165044-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MI7KRyeBH5dlW-CkUtn%2Fsync%2F567ce9c6a9158df39551e7ea19746fa60735335c.png?generation=1620650099200879\&alt=media)

## 镜像, 容器, 与层

### 镜像与文件系统

从上面我们可以知道, 镜像是只读的, 静态的, 一旦被打包创建, 就是永久不会变的. Docker镜像本质就是一个**Linux文件系统**(Root File System), 这个文件系统里面包含可以运行在Linux内核的程序以及相应的数据.

实际上, Linux分为两个部分, Linux内核(Linux Kernel)和用户空间. 可以把Docker镜像看成是上面所说的用户空间, 而当Docker通过镜像创建一个容器时, 就是将镜像定义好的用户空间作为独立隔离的进程运行在宿主机的Linux内核之上.

### 镜像与层

我们在从远程仓库pull获取镜像, 或者打包容器生成镜像的过程中, 可以看到Docker镜像会被分层若干层. Docker镜像是**分层**的, 从最底层的**Base层**开始, 一层一层叠成, 前一层是后一层的基础, 如下图所示:

![](https://1942165044-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MI7KRyeBH5dlW-CkUtn%2Fsync%2Fe2590402d94e2a830e8d6c63f44486c053bb27f0.png?generation=1620660490401370\&alt=media)

#### 为什么要分层

一个镜像由多个中间层组成, 多个镜像可以共享同一个中间层, **可以通过在镜像添加多一层来生成一个新的镜像**. 这样带来了一个好处: **共享资源**.

对于每一层, 我们都可以认为从最底层到以这层为顶层, 以及之间的所有层, 共同组成了一个镜像. 每加一层就生成了一个新的镜像, 这样我们就在镜像和层之间建立了一种一一对应的映射关系.

我们可以将下层镜像看作是上层镜像的**父镜像**. 最底层镜像没有任何父镜像, 称之为**基础镜像(Base镜像)**. Base镜像类似于大楼的地基功能, 一般是各种Linux发行版本, 如Ubuntu, Debian, CentOS等, 在上图中也可以看出.

之后我们就可以在Base镜像上安装一个个软件, 相当于在Base镜像上一个个叠加层, 得到一个个镜像. 这样层就在不同镜像之间得到了共享, 节省了资源.

![](https://1942165044-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MI7KRyeBH5dlW-CkUtn%2Fsync%2F4cf87aeeb96bfbb55c677274afa9db3a3aa103cd.png?generation=1620660490538189\&alt=media)

不是说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)堆叠而成, 是只读层的统一视角.

![](https://1942165044-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MI7KRyeBH5dlW-CkUtn%2Fsync%2Ff609880110e7cf776e634cb6fae4ec119d9364c0.png?generation=1620660489637801\&alt=media)

在上图的左边, 我们看到了多个只读层, 除了最下面一层, 其它层都会有一个指针指向下一层, 这些层是Docker内部的实现细节, 并且能够在宿主机的文件系统上访问到. **统一文件系统**(Union File System)技术能够将不同的层整合成一个文件系统, 为这些层提供了一个统一的视角, 这样就隐藏了多层的存在, **在用户的角度看来, 只存在一个文件系统**, 就如上面的右图所示.

#### 镜像的只读性

需要特别强调镜像的只读性. 镜像构建时, 是一层一层构建的, 前一层是后一层的基础, **每一层构建完就不会再发生改变**, 后一层上的任何改变只发生在自己这一层.

例如我删除了前面某一层镜像的内容, 并不是在那一层镜像中直接修改的, 而是**在当前层标记了修改的内容**. 最终打包的镜像看起来是修改后的内容, 但实际在构建解析的过程中, 是一层进行了创建, 一层进行了修改.

### 容器与层

当从镜像中实例化出一个容器时, 实际上是在镜像原来的层之上, 又创建了一个**新层**作为顶层, 但这个层是**可写的**, 被称为是**容器层**, 对应的之前的层称为是**镜像层**.

由于容器层可以写, 因此我们可以在这一层上进行任意操作, **容器所有发生文件变更写都发生在这一层**.

因此可以理解为**容器 = 镜像 + 读写层**, 无论这个容器是否在运行, 容器对应的容器层都是存在的, 只是是否给这个容器分配了进程去运行的区别.

![](https://1942165044-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MI7KRyeBH5dlW-CkUtn%2Fsync%2F946fcd996ecc4f549acf155013981d9426f978f6.jfif?generation=1620660490147283\&alt=media)

![](https://1942165044-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MI7KRyeBH5dlW-CkUtn%2Fsync%2Fc48052cabcf674b32e1082ded1c633e08e777161.png?generation=1620660489877718\&alt=media)

#### 容器层的复制策略

Docker通过一个修改时复制策略来保证创建容器时, 依赖镜像的安全性, 以及更高的性能和空间利用率.

* **当容器需要读取文件的时候**, 从最上层的镜像层开始往下找, 找到后读取到内存中, 若已经在内存中, 可以直接使用. 换句话说, 运行在同一台机器上的Docker容器共享运行时相同的文件
* **当容器需要修改文件的时候**, 从上往下查找, 找到后复制到容器层, 对于容器来说, 可以看到的是容器层的这个文件, 看不到镜像层里的文件, 然后直接修改容器层的文件
* **当容器需要删除文件的时候**, 从上往下查找, 找到后在容器中记录删除, 并不是真正的删除, 而是软删除, 这导致镜像体积只会增加, 不会减少
* **当容器需要增加文件的时候**, 直接在最上层的容器可写层增加, 不会影响镜像层

这种策略被称为**copy-on-write**.

#### 容器层打包

容器层这种可读写的层是**动态的**, 是**没有被持久化存储的**. 当容器被remove, 或者宿主机物理机重启后, 容器层就没有了, 相应的容器也没有了. 因此在对镜像调整完毕后, 要对容器进行打包操作, 使当前的容器层转变为只读的镜像层, 保存在宿主机系统中, 得到了持久化, 同时也就生成了一个新的镜像.

## 参考资料

* [Docker基本概念和原理理解](https://zhuanlan.zhihu.com/p/39171987)
* [Image镜像](http://www.dockerinfo.net/image%e9%95%9c%e5%83%8f)
* [Docker的三大核心组件：镜像、容器与仓库](http://dockone.io/article/9249)
* [Docker镜像分层的原理](https://segmentfault.com/a/1190000021225614)
* [10张图带你深入理解Docker容器和镜像](http://dockone.io/article/783)
