目录
一、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
总结:容器的实质:
最简单的例子就是日常开发中,我们clone下来一个代码想直接运行,会发现很多问题,即本地环境跟远端环境不一致。
docker能将应用打包起来,搬到哪里都可以直接使用;
1. docker build [镜像]
2. docker run [镜像]
了解docker得结合其诞生的背景
详细讲解其必要性
在使用 Docker 的时候,并没有一个真正的“Docker 容器”运行在宿主机里面。Docker 项目帮助用户启动的,还是原来的应用进程,只不过在创建这些进程时,Docker 为它们加上了各种各样的 Namespace 参数。这时,这些进程就会觉得自己是各自 PID Namespace 里的第 1 号进程,只能看到各自 Mount Namespace 里挂载的目录和文件,只能访问到各自 Network Namespace 里的网络设备,就仿佛运行在一个个“容器”里面,与世隔绝。不过,相信你此刻已经会心一笑:这些不过都是“障眼法”罢了。
虽然容器内的第 1 号进程在“障眼法”的干扰下只能看到容器里的情况,但是宿主机上,它作为第 100 号进程与其他所有进程之间依然是平等的竞争关系。这就意味着,虽然第 100 号进程表面上被隔离了起来,但是它所能够使用到的资源(比如 CPU、内存),却是可以随时被宿主机上的其他进程(或者其他容器)占用的。当然,这个 100 号进程自己也可能把所有资源吃光。这些情况,显然都不是一个“沙盒”应该表现出来的合理行为。
Cgroups是Linux内核中的一种机制,它可以将进程分组并为每个组分配特定的资源限制,例如CPU、内存、磁盘IO和网络带宽等。
通过Cgroups,系统管理员可以控制进程对资源的访问和使用,从而避免出现资源竞争和滥用的情况。Cgroups可以用于容器化应用程序,以限制容器中的进程对主机资源的访问。
为容器提供执行环境和依赖的文件系统是如何实现:
注意⚠️
一个正在运行的 Docker 容器,其实就是一个启用了多个 Linux Namespace 的应用进程,而这个进程能够使用的资源量,则受 Cgroups 配置的限制。这也是容器技术中一个非常重要的概念,即:容器是一个“单进程”模型。
rootfs 只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。所以一台机器上的所有容器共享宿主机操作系统的内核。
通过volume机制,可以在容器里面访问宿主机的文件,也可以在宿主机上访问容器里面的文件
⚠️看懂数据卷挂载可以帮助你加深对容器化尤其是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 打破。
使用到的挂载技术,就是 Linux 的绑定挂载(bind mount)机制。它的主要作用就是,允许你将一个目录或者文件,挂载到一个指定的目录上。并且,这时你在该挂载点上(/test)进行的任何操作,只是发生在被挂载的目录或者文件上(/home),而原挂载点(/test)的内容则会被隐藏起来且不受影响。
绑定挂载实际上是一个 inode 替换的过程。
在 Linux 操作系统中,inode 可以理解为存放文件内容的“对象”,而 dentry,也叫目录项,就是访问这个 inode 所使用的“指针”。
正如上图所示,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
Docker 在镜像的设计中,引入了层(layer)的概念。
也就是说,用户制作镜像的每一步操作,都会生成一个层,也就是一个增量 rootfs。
层的概念用到了一种叫作联合文件系统(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 中生效。(数据卷挂载的实现原理)
一个完整的docker镜像是由多个层组成的,docker用ufs将多个层联合挂载到一个目录中。
分层可以方便用户制作自己定制的镜像。
其中以ubuntu镜像为例包含如下:
第一部分,只读层。它是这个容器的 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时不包含该层。