细谈容器化技术实现原理--以Docker为例

目录

一、Docker解决了什么

二、容器的发展过程

三、容器基础

3.1. 容器实现的原理:

⚠️原理详解:

3.1.1. Namespace

3.1.2. Cgroups

3.1.3. chroot

四、Volume

4.1. Docker是如何做到把一个宿主机上的目录或者文件,挂载到容器里面去的?

⚠️4.1.1.  挂载技术 bind mount

五、容器化应用的常见问题

六、layer与联合文件系统

6.1. 联合文件系统(Union File System)

layer

总结:容器的实质:


一、Docker解决了什么

最简单的例子就是日常开发中,我们clone下来一个代码想直接运行,会发现很多问题,即本地环境跟远端环境不一致。

docker能将应用打包起来,搬到哪里都可以直接使用;

1. docker build [镜像]

  • 用当前操作系统文件与目录制作一个压缩包
  • 这个包包含了这个应用运行所需的所有依赖
  • 从而保证了本地环境和云端环境的高度一致

2. docker run [镜像]

  • 创建一个“沙盒”来解压这个镜像,然后在“沙盒”中运行自己的应用
  • 这个沙盒就是 Cgroups和Namespace (下文会讲)

二、容器的发展过程

了解docker得结合其诞生的背景

  1. 首先有一大批物理服务器,想要租给用户使用,因此搭建了一个物理集群,向用户售卖计算资源 --> 这就是 IaaS (Infrastructure as a Service) :它提供了基础设施(硬件)作为服务,让用户能够在云端租用虚拟化的计算资源,如虚拟机、存储、网络等
  2. 用户有了云端虚拟机,需要在虚拟机上部署自己的应用。但是本地的开发环境和购买的虚拟机之间有很大不一样,调试、部署很麻烦,应用之间没有隔离。--> Paas(Platform as a Service)出现了提供了一个平台来让开发人员构建、测试、部署和管理他们的应用程序
  3. Paas提供了大规模部署应用的能力,提供了“沙盒”来应对隔离,但是用户发现打包过程很繁琐需要投入大量人力时间与云端Paas进行适配 --> Docker用镜像将整个应用运行所需的环境和依赖打包实现了本地环境和云端高度一致。
  4. 为了对多个容器应用的自动部署、负载均衡、弹性伸缩、服务发现、健康检查、容器间通信如容器A管理B等等 --> 容器编排技术:docker swarm compose、Kubernetes

三、容器基础

3.1. 容器实现的原理

  • 启用 Linux Namespace 配置;
  • 设置指定的 Cgroups 参数;
  • 切换进程的根目录(Change Root)

详细讲解其必要性

⚠️原理详解

3.1.1. Namespace

  • Namespace是Linux内核中的另一种机制,它可以创建一个独立的进程环境,包括文件系统、网络、进程ID、用户ID等等。
  • Namespace可以用于隔离容器内的应用程序与主机系统的环境,从而实现容器的隔离和安全性。
  • 通过使用Namespace,容器内的应用程序可以在一个独立的命名空间中运行,而不会影响主机系统或其他容器。

  • 在使用 Docker 的时候,并没有一个真正的“Docker 容器”运行在宿主机里面。Docker 项目帮助用户启动的,还是原来的应用进程,只不过在创建这些进程时,Docker 为它们加上了各种各样的 Namespace 参数。这时,这些进程就会觉得自己是各自 PID Namespace 里的第 1 号进程,只能看到各自 Mount Namespace 里挂载的目录和文件,只能访问到各自 Network Namespace 里的网络设备,就仿佛运行在一个个“容器”里面,与世隔绝。不过,相信你此刻已经会心一笑:这些不过都是“障眼法”罢了。

  • 虽然容器内的第 1 号进程在“障眼法”的干扰下只能看到容器里的情况,但是宿主机上,它作为第 100 号进程与其他所有进程之间依然是平等的竞争关系。这就意味着,虽然第 100 号进程表面上被隔离了起来,但是它所能够使用到的资源(比如 CPU、内存),却是可以随时被宿主机上的其他进程(或者其他容器)占用的。当然,这个 100 号进程自己也可能把所有资源吃光。这些情况,显然都不是一个“沙盒”应该表现出来的合理行为。

3.1.2. Cgroups

  • Cgroups是Linux内核中的一种机制,它可以将进程分组并为每个组分配特定的资源限制,例如CPU、内存、磁盘IO和网络带宽等。

  • 通过Cgroups,系统管理员可以控制进程对资源的访问和使用,从而避免出现资源竞争和滥用的情况。Cgroups可以用于容器化应用程序,以限制容器中的进程对主机资源的访问。

3.1.3. chroot

  • 为容器提供执行环境和依赖的文件系统是如何实现:

  • Mount Namespace
  • 只隔离增量,不隔离存量:原来有的文件不会被隔离,新增的才会
  • 因此开启了隔离还不行,还需要重新进行挂载才会生效。
  1. 使用Mount Namespace在容器进程启动之前重新挂载它的整个根目录“/”(挂在对宿主机不可见)
  2. 使用chroot改变进程的根目录到指定位置
  3. 为了能够让容器的这个根目录看起来更“真实”,我们一般会在这个容器的根目录下挂载一个完整操作系统的文件系统
  4. 挂载在容器根目录上、用来为容器进程提供隔离后执行环境的文件系统,就是所谓的“容器镜像”。它还有一个更为专业的名字,叫作:rootfs(根文件系统)

注意⚠️

一个正在运行的 Docker 容器,其实就是一个启用了多个 Linux Namespace 的应用进程,而这个进程能够使用的资源量,则受 Cgroups 配置的限制。这也是容器技术中一个非常重要的概念,即:容器是一个“单进程”模型。

rootfs 只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。所以一台机器上的所有容器共享宿主机操作系统的内核

四、Volume

通过volume机制,可以在容器里面访问宿主机的文件,也可以在宿主机上访问容器里面的文件

4.1. Docker是如何做到把一个宿主机上的目录或者文件,挂载到容器里面去的?

⚠️看懂数据卷挂载可以帮助你加深对容器化尤其是docker的理解,篇幅很长,都是精华:

1. 尽管开启了 Mount Namespace,但是在它执行 chroot(或者 pivot_root)之前,容器进程一直可以看到宿主机上的整个文件系统。

2. 而宿主机上的文件系统,也自然包括了我们要使用的容器镜像。这个镜像的各个层,保存在 /var/lib/docker/aufs/diff 目录下,在容器进程启动后,它们会被联合挂载在 /var/lib/docker/aufs/mnt/ 目录中,这样容器所需的 rootfs 就准备好了。

3. 所以,我们只需要在 rootfs 准备好之后,在执行 chroot 之前,把 Volume 指定的宿主机目录(比如 /home 目录),挂载到指定的容器目录(比如 /test 目录)在宿主机上对应的目录(即 /var/lib/docker/aufs/mnt/[可读写层 ID]/test)上,这个 Volume 的挂载工作就完成了。

4. “容器进程”已经创建了,也就意味着此时 Mount Namespace 已经开启了。所以,这个挂载事件只在这个容器里可见。你在宿主机上,是看不见容器内部的这个挂载点的。这就保证了容器的隔离性不会被 Volume 打破。

⚠️4.1.1.  挂载技术 bind mount

使用到的挂载技术,就是 Linux 的绑定挂载(bind mount)机制。它的主要作用就是,允许你将一个目录或者文件,挂载到一个指定的目录上。并且,这时你在该挂载点上(/test)进行的任何操作,只是发生在被挂载的目录或者文件上(/home),而原挂载点(/test)的内容则会被隐藏起来且不受影响。

绑定挂载实际上是一个 inode 替换的过程

在 Linux 操作系统中,inode 可以理解为存放文件内容的“对象”,而 dentry,也叫目录项,就是访问这个 inode 所使用的“指针”。

细谈容器化技术实现原理--以Docker为例_第1张图片

 

正如上图所示,mount --bind /home /test,会将 /home 挂载到 /test 上。其实相当于将 /test 的 dentry,重定向到了 /home 的 inode。这样当我们修改 /test 目录时,实际修改的是 /home 目录的 inode。这也就是为何,一旦执行 umount 命令,/test 目录原先的内容就会恢复:因为修改真正发生在的,是 /home 目录里。

因此进程在容器里对这个 /test 目录进行的所有操作,都实际发生在宿主机的对应目录(比如,/home,或者 /var/lib/docker/volumes/[VOLUME_ID]/_data)里,而不会影响容器镜像的内容。

$ docker run -v /test ...(没有显式地声明要映射的宿主机目录,那么docker在宿主机上创建的映射的临时目录的路径为/var/lib/docker/volumes/[VOLUME_ID]/_data)
$ docker run -v /home:/test ... (对应的就是/home)

❓疑问: 

这个 /test 目录里的内容,既然挂载在容器 rootfs 的可读写层,它会不会被 docker commit 提交掉呢?

不会,由于moutn namespace的作用挂载对于宿主机并不可见,在宿主机的眼里 /test的 inode就是原本没有重定向的inode,你所有的修改都不在这个文件下

五、容器化应用的常见问题

/proc Linux 下的 /proc 目录存储的是记录当前内核运行状态的一系列特殊文件

用户可以通过访问这些文件,查看系统以及当前正在运行的进程的信息,比如 CPU 使用情况、内存占用率等,这些文件也是 top 指令查看系统信息的主要数据来源。

但是,如果在容器里执行 top 指令,就会发现,它显示的信息居然是宿主机的 CPU 和内存数据,而不是当前容器的数据。

造成这个问题的原因就是,/proc 文件系统并不知道用户通过 Cgroups 给这个容器做了什么样的资源限制,即:/proc 文件系统不了解 Cgroups 限制的存在。在生产环境中,这个问题必须进行修正,否则应用程序在容器里读取到的 CPU 核数、可用内存等信息都是宿主机上的数据,这会给应用的运行带来非常大的困惑和风险

这也是在企业中,容器化应用碰到的一个常见问题,也是容器相较于虚拟机另一个不尽如人意的地方。

扩展命令:

docker exec :

        一个进程的每种 Linux Namespace,都在它对应的 /proc/[进程号]/ns 下有一个对应的虚拟文件,并且链接到一个真实的 Namespace 文件上。

        docker exec的原理就是使用linux系统指令setns将进程加入到一个容器进程的namespace,从而“进入”容器;

docker volume ls

        查看docker所有挂载的数据卷

docker inspect

        命令查看容器的 IP 地址

lxcfs

六、layer与联合文件系统

Docker 在镜像的设计中,引入了层(layer)的概念。

也就是说,用户制作镜像的每一步操作,都会生成一个层,也就是一个增量 rootfs。

6.1. 联合文件系统(Union File System)

层的概念用到了一种叫作联合文件系统(Union File System)的能力。

最主要的功能是将多个不同位置的目录联合挂载(union mount)到同一个目录下(数据卷挂载的实现原理。比如,我现在有两个目录 A 和 B,它们分别有两个文件:

$ tree
.
├── A
│ ├── a
│ └── x
└── B
├── b
└── x

然后,我使用联合挂载的方式,将这两个目录挂载到一个公共的目录 C 上:

$ mkdir C
$ mount -t aufs -o dirs=./A:./B none ./C

这时,我再查看目录 C 的内容,就能看到目录 A 和 B 下的文件被合并到了一起:

$ tree ./C
./C
├── a
├── b
└── x
在这个合并后的目录 C 里,有 a、b、x 三个文件,并且 x 文件只有一份。这,就是“合并”的含义。此外,如果你在目录 C 里对 a、b、x 文件做修改,这些修改也会在对应的目录 A、B 中生效。(数据卷挂载的实现原理

layer

一个完整的docker镜像是由多个层组成的,docker用ufs将多个层联合挂载到一个目录中

分层可以方便用户制作自己定制的镜像。

其中以ubuntu镜像为例包含如下:

细谈容器化技术实现原理--以Docker为例_第2张图片

  • 第一部分,只读层。它是这个容器的 rootfs 最下面的五层,对应的正是 ubuntu:latest 镜像的五层。可以看到,它们的挂载方式都是只读的(ro+wh,即 readonly+whiteout,至于什么是 whiteout,我下面马上会讲到)。

  • 第二部分,可读写层。它是这个容器的 rootfs 最上面的一层(6e3be5d2ecccae7cc),它的挂载方式为:rw,即 read write。在没有写入文件之前,这个目录是空的。而一旦在容器里做了写操作,你修改产生的内容就会以增量的方式出现在这个层中

    • 1. 删除只读层的文件呢?AuFS 会在可读写层创建一个 whiteout 文件,把只读层里的文件“遮挡”起来。(所以删除文件还会增加镜像的文件大小)

    • 2. 可读写层的作用,就是专门用来存放你修改 rootfs 后产生的增量,无论是增、删、改,都发生在这里。而当我们使用完了这个被修改过的容器之后,还可以使用 docker commit 和 push 指令,保存这个被修改过的可读写层,并上传到 Docker Hub 上,供其他人使用;而与此同时,原先的只读层里的内容则不会有任何变化。这,就是增量 rootfs 的好处。

  • 第三部分,Init 层。用户拉取镜像到本地需要在启动容器时写入一些指定的值比如 hostname,这些修改往往只对当前的容器有效,因此将存放 /etc/hosts、/etc/resolv.conf 等信息的单独提取出一个层,在用户执行commit时不包含该层。

总结:容器的实质:

  • 一个“容器”,实际上是一个由 Linux Namespace、Linux Cgroups 和 rootfs 三种技术构建出来的进程的隔离环境
  • 一个正在运行的 Linux 容器,其实可以被“一分为二”地看待:一组联合挂载在 /var/lib/docker/aufs/mnt 上的 rootfs,这一部分我们称为“容器镜像”(Container Image),是容器的静态视图;
  • 一个由 Namespace+Cgroups 构成的隔离环境,这一部分我们称为“容器运行时”(Container Runtime),是容器的动态视图。

你可能感兴趣的:(docker,java,容器)