你想要的 Docker 理论知识全在这了

400 阅读9分钟

一、序言

对于技术更新迭代迅速的计算机领域,我们时常会显得手足无措,这么多东西怎么学的完啊,我还是老老实实切图去吧😭,但是作为卷王的我怎么能放弃,这周末看就打算好好补补Docker(虽然不是什么新技术),废话不多说,开干。

二、虚拟化技术的发展史

1、物理机时代

在没有虚拟化技术的物理机时代,我们是怎么去部署我们的应用的呢?首先我们肯定需要一台物理机,然后在这台物理机上装上我们需要的操作系统,其次需要安装我们应用所需要的依赖,最后就可以在我们的操作系统上跑我们的应用了。

image.png

那这种应用部署方案有什么缺陷呢?

  1. 部署过程复杂且慢 从上述的流程可以看出,整个应用的部署流程是非常复杂而且低效的。
  2. 成本高 这里的成本主要就是物理机的成本,就算是一些小的应用也需要一台物理机,同时每台物理机只能安装一个操作系统,我如果我们要换一个系统进行部署又要重新去采购新的物理机。
  3. 空闲的资源难以得到复用

2、虚拟化时代

为了解决物理机出现的问题,就衍生出了虚拟化技术,说到虚拟化技术立马想到的就是虚拟机了,下面展示了虚拟机的工作原理。

image.png

虚拟机比物理机多了一层虚拟化层(Hypervisor),这是一个常用的硬件虚拟化软件,可以把底层的操作系统抽象出多个底层的硬件接口。在 Hypervisor 层上面是三个并行的虚拟机,而且每一个虚拟机与物理机相比又多了一层 用户操作系统(Guest OS),也就是说每个虚拟机都有自己的操作系统。 总的来说虚拟机技术就是使用 Hypervisor 模拟出了运行一个操作系统所需要的硬件(CPU、内存、I/O等),然后在这个基础上安装了一个新的操作系统,这样用户的应用就能运行在这个虚拟的机器中,这样也达到了隔离的效果。

现在虚拟机已经改物理机的基础上有了很大的改进,那他还有什么缺陷吗?

  1. 每台虚拟机都需要占用宿主机的资源,同时多台虚拟机还会出现竞争资源的情况,这会严重影响到系统的响应。
  2. 每次创建一个新的虚拟机都需要重新配置一边环境,使得开发者的开发效率变低
  3. 内存占用大,每台虚拟机都需要占用100 ~ 200MB内存

3、Docker时代

什么是Docker呢?下面是百度百科的介绍:

image.png

简单来说Docker就是一个容器,开发者可以将应用装入这个容器然后发布,容器和容器之间互相不会影响达到了隔离的效果。

往深讲Docker其实是属于 Linux 容器的一种封装,而 Linux 容器是 Linux 发展出的一种虚拟化技术,Linux 容器他不是像虚拟机一样模拟出完整的一个操作系统,而是对进程进行了隔离,相当于在进程外面套了一层沙盒,而在对于容器中的进程来说,他所看到的东西都是虚拟出来的,下面展示的是它的结构图:

image.png

他和虚拟机的结构很相似,区别就是每个容器没有虚拟出来的操作系统,同时使用了 Docker 引擎替代 Hypervisor。

三、Docker的实现原理

1、隔离技术

上面我们讲到 Docker 容器其实就是一个被包装的进程,这里大家可能有些疑惑为什么是进程呢?其实理由也很简单,进程就是我们程序运行的一个状态,它里面包含了一个程序运行所需要的所有资源(内存、CPU、I/O设备等),而容器就是将这一整个流程包装起来,与其他应用进行隔离。

这里说到的隔离技术就是我们接下来要讲的,Docker 的隔离技术其实就是利用了 Linux 里面的Namespace 机制,而 Namespace 就是 Linux 创建进程的一个可选的参数,比如 CLONE_NEWPID 、Mount、UTS、IPC 等参数,每个参数都有自己的功能,Mount Namespace 用于让被隔离进程只看到当前 Namespace 里的挂载点信息;Network Namespace 用于让被隔离进程看到当前 Namespace 里的网络设备和配置等:

int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL); 

但是这种隔离方式也有它的缺陷,首先我们知道容器只是宿主机器下面的一个进程,那么容器所使用的操作系统内核还是宿主机器的操作系统内核,这会带来什么问题呢?如果我们想在 Windows 宿主机器上运行 Linux 容器这肯定是行不通的。其次在 Linux 内核中有很多的资源是不能被 Namespace 化的,比如时间,这就意味着假如我在某一个容器中修改了时间,那么宿主机器上的时间也会被改动,这肯定是不能被允许的。正是由于这些问题导致我们的应用很容易被攻击。

2、Cgroups

上面我们说道通过 Namespace 字段可以给容器做一些限制,容他只能看到容器里的情况,但是作为进程来说他和宿主机器中的其他进程仍然是平等的关系,这就意味着其他进程能够和容器竞争宿主的资源(CPU、内存等),往两个极端方向讲其他进程能够把宿主机器上的资源全都占用,同时容器也能够把宿主机器上的资源也全都吃光,这显然不是合理的。

所以 Linux 内核就设计出 Cgroup 来为进程设置资源的限制,它能够限制进程能够使用资源的上线,包括CPU、内存、磁盘、网络带宽等。

3、镜像

对于容器来说,他里面的应用进程应该看到的是一份完全独立的文件系统,这样的话他就可以在自己的目录下进行操作,这既不会对宿主环境造成影响也不会受宿主机器和其他容器的影响,但是在默认情况下新创建的容器会直接继承宿主机器的各个挂载点。也就是说容器中的文件内容和宿主机器中的是一样的,那我们怎么去修改呢?

主要有两个点:

  1. 在创建进程时将 Mount Namespace 这个参数带上,上面也介绍过,这个参数修改的是容器进程对文件系统挂载点的认知。
  2. 在创建进程时我们需要告诉进程有哪些目录需要重新挂载,因为 Mount Namespace 只有在目录重新挂载之后才会生效。
mount("none", "/tmp", "tmpfs", 0, "");

比如这行代码,这就告诉了容器以 tmpfs 格式重新挂载 /tmp 目录,因为这里只是去介绍一些理论知识所以具体的实现就不做了。

那这些和我们要讲的镜像有什么关系呢?其实我们的容器镜像就是用来为容器进程提供隔离后执行环境的文件系统,它里面会包含/bin、/etc、/proc等目录和文件。

这里有一点要注意了,镜像知识一个操作系统所包含的文件、依赖配置和目录,它并不包含操作系统的内核,我们容器中所使用的内核都是共享宿主机上的操作系统内核。

有了镜像之后我们无论在本地、云端还是其他任何一台机器,用户只要解压打包好的镜像,那么就能获得应用执行所需要的完整环境,比如我们可以使用 Ubuntu 操作系统的 ISO 做一个镜像。

除此之外镜像还有一个非常重要的概念就是分层,镜像使用分层就是为了解决复用问题,一般我们镜像层都放置在/var/lib/docker/aufs/diff 目录下,下面就看一个例子:

/var/lib/docker/aufs/diff/6e3be5d2ecccae7cc...=rw
/var/lib/docker/aufs/diff/6e3be5d2ecccae7cc...-init=ro+wh
/var/lib/docker/aufs/diff/32e8e20064858c0f2...=ro+wh
/var/lib/docker/aufs/diff/2b8858809bce62e62...=ro+wh
/var/lib/docker/aufs/diff/20707dce8efc0d267...=ro+wh
/var/lib/docker/aufs/diff/72b0744e06247c7d0...=ro+wh
/var/lib/docker/aufs/diff/a524a729adadedb90...=ro+wh

这是/var/lib/docker/aufs/diff 文件下镜像每一层的信息,从结构来看可以划分为三个部分:

image.png

可读写层

这一层也可以称为容器层,它主要是用来承载用户的增、删、改操作,用户所有的修改操作都只会作用于这一层,相同的文件上层会覆盖掉下层,比如说只读层里面有一个 A 文件,然后我需要对这个文件进行一些修改,那么就会从上向下去寻找这个文件,然后将这个文件复制到容器层,修改之后会将结果作用到下层的文件。

init层

init层用于挂载一些仅对当前容器生效的文件,这些文件在执行 docker commit 时是不会被提交的,只会提交可读写层。

只读层

只读层就是这个镜像的一个公共部分,开发者只需要在这个的基础上进行增量的修改,之后也只需要去维护相对`只读层修改的增量的内容。