《深入剖析Kubernetes》总结二:学学容器基础

Linux容器实现手段:Linux Namespace 、Linux Cgroups ,基于 rootfs 的文件系统

Mac容器,Windows容器实现手段:基于虚拟化技术

Linux容器的实现手段

容器其实是一种沙盒技术,能够像一个集装箱一样,把你的应用“装”起来,使应用与应用之间因为有了边界而不至于相互干扰;
而被装进集装箱的应用,也可以被方便地搬来搬去;
容器的本质:进程

容器技术的核心功能,就是通过约束和修改进程的动态表现,从而为其创造出一个“边界”,Cgroups技术是用来制造约束的主要手段,Namespace 技术则是用来修改进程视图的主要方法。

  • Namespace

在创建容器进程时,指定了这个进程所需要启用的一组 Namespace 参数,比如clone()可以在参数中指定 CLONE_NEWPID参数:

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

这样容器就只能“看”到当前 Namespace 所限定的资源、文件、设备、状态,或者配置,比如Mount Namespace 里挂载的目录和文件, Network Namespace 里的网络设备等,而对于宿主机以及其他不相关的程序,它就完全看不到了。

Namespace 的隔离机制相比于虚拟化技术有很多优点,比如不需要运行一个完整的OS 才能执行用户的应用进程,相对更加高性能和敏捷,而不足之处则是隔离得不彻底:多个容器之间使用的是同一个 宿主机的操作系统内核,在 Windows 宿主机上运行 Linux 容器,或者在低版本的 Linux 宿主机上运行高版本的 Linux 容器,都是行不通的;其次,在 Linux 内核中,有很多资源和对象是不能被 Namespace 化的,最典型的例子就是时间,如果容器中的程序使用settimeofday系统调用修改了时间,整个宿主机的时间都会被随之修改

  • Cgroups

Cgroups则是 Linux 内核中用来为进程设置资源限制的一个重要功能,其最主要的作用,就是限制一个进程组能够 使用的资源上限,包括 CPU、内存、磁盘、网络带宽等等,也就是协调容器占用的资源(此外,Cgroups 还能够对进程进行优先级设置、审计,以及将进程挂起和恢复等操作)

在 Linux 中,Cgroups 给用户暴露出来的操作接口是文件系统/sys/fs/cgroup,其不同的子目录(子系统,如cpuset、cpu、memory)代表可以进行限制的资源类型,这些子目录下的文件就可以看到具体可以被限制的方法,可以通过修改这些文件的内容来设置限制

而对于 Docker来说,只需要在每个子系统下面为每个容器创建一个控制组(即创建一个新目录),然后在启动容器进程之后,把这个进程的PID填写到对应控制组的 tasks 文件中就可以了。

Cgroups 对资源的限制能力也有很多不完善的地方,比如/proc 文件系统的问题:
/proc 目录存储的是记录当前内核运行状态的一系列特殊文件,用户可以通过访问这些文件,查看系统以及当前正在运行的进程的信息,比如 CPU 使用情况、内存占用 率等,这些文件也是 top 指令查看系统信息的主要数据来源;
但是,如果在容器里执行 top 指令,就会发现它显示的信息是宿主机的 CPU 和内存数据,而不是当前容器的数据;
原因:/proc 文件系统并不知道用户通过 Cgroups 给这个容器做了什么样 的资源限制,即/proc 文件系统不了解 Cgroups 限制的存在;
修复:生产环境中,使用lxcfs来修复

容器镜像

  • Mount Namespace

首先看看Mount Namespace 跟其他 Namespace 的使用略有不同的地方:它对容器进程视图的改变,一定是伴随着挂载操作(mount)才能生效;
也就是说即使开启了 Mount Namespace,容器进程看到的文件系统也跟宿主机完全一 样;
因为Mount Namespace 修改的是容器进程对文件系统“挂载点”的认知,这也就意味着,只有在“挂载”这个操作发生之后,进程的视图才会被改变,而在此之前,新创建的容器会直接继承宿主机的各个挂载点;
因此创建新进程时,除了声明要启用 Mount Namespace 之外,还要告诉容器进程,有哪些目录需要重新挂载

如果想要在创建新容器时,容器进程看到的文件系统就是一个独立的隔离环境,而不是继承自宿主机的文件系统,则可以在容器进程启动之前重新挂载它的整个根目录“/”。而由于Mount Namespace的存在,这个挂载就对宿主机不可见了;
chroot命令可以实现这个功能,Mount Namespace 则是基于对 chroot 的不断改良才被发明出来的,它也是 Linux 操作系统里的第一个 Namespace;
为了能够让容器的这个根目录看起来更“真实”,一般会在这个容器的根目录下挂载 一个完整操作系统的文件系统,比如 Ubuntu16.04 的 ISO。这样,在容器启动之后,在容器里通过执行 “ls /” 查看根目录下的内容,就是 Ubuntu 16.04 的所有目录和文件。 而这个挂载在容器根目录上、用来为容器进程提供隔离后执行环境的文件系统,就是所谓的“容器镜像”,或者叫作:rootfs(根文件系统)。

到了这里,就可以总结Docker容器创建的流程了:

  1. 启用 Linux Namespace 配置;
  2. 设置指定的 Cgroups 参数;
  3. 切换进程的根目录(优先使用 pivot_root 系统调用,如果系统不支持,才会使用 chroot)

不过虽然有了独立的操作系统的文件、配置、目录,但是内核却是共享的,它对于该机器上的所有容器来说是一个“全局变量”,牵一发而动全身,也算是一个缺陷

  • 一致性

协同开发时,能以增量的方式去做这些修改,而不是每次都生成一个新的rootfs,避免碎片化,类似git

前提技术:联合文件系统将多个不同位置的目录联合挂载(union mount)到同一个目录下的能力

引入层(layer)得概念,用户制作镜像的每一步操作,都会生成一个层,也就是一个增量 rootfs。

例子:比如Ubuntu,拉一个镜像下来,假如由五个层组成,那么这五个层就是五个增量 rootfs,每一层都是 Ubuntu操作系统文件与目录的一部分,层的目录为 /var/lib/docker/aufs/diff
而在使用镜像时,Docker会把这些增量联合挂载在一个统一的挂载点 /var/lib/docker/aufs/mnt/上,这个目录里面正是一个完整的 Ubuntu 操作系统

组成:
在这里插入图片描述
第一部分,只读层。

它是这个容器的 rootfs 最下面的五层,对应的正是 ubuntu:latest 镜像的五层。

可以看到,它 们的挂载方式都是只读的(ro+wh,即 readonly+whiteout),这些层,都以增量的方式分别包含了 Ubuntu 操作系统的一部分。

第二部分,可读写层。

它是这个容器的 rootfs 最上面的一层(6e3be5d2ecccae7cc),它的挂载方式为:rw,即 read write。

在没有写入文件之前,这个目录是空的,而一旦在容器里做了写操作,你修改产生的内容就会以增量的方式出现在这个层中。

如果要删除只读层里的一个文件呢,AuFS 会在可读写层创建一个 whiteout 文件,把只读层里的文 件“遮挡”起来。 比如要删除只读层里一个名叫 foo 的文件,那么这个删除操作实际上是在可读写层创建了 一个名叫.wh.foo 的文件。这样,当这两个层被联合挂载之后,foo 文件就会被.wh.foo 文 件“遮挡”起来,“消失”了。这个功能,就是“ro+wh”的挂载方式,即只读 +whiteout 的 含义。

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

第三部分,Init 层。

它是一个以“-init”结尾的层,夹在只读层和读写层之间,是 Docker 项目单独生成的一 个内部层,专门用来存放 /etc/hosts、/etc/resolv.conf 等信息

需要这样一层的原因是,这些文件本来属于只读的 Ubuntu 镜像的一部分,但是用户往往需要 在启动容器时写入一些指定的值比如 hostname,所以就需要在可读写层对它们进行修改。 可是,这些修改往往只对当前的容器有效,我们并不希望执行 docker commit 时,把这些信息 连同可读写层一起提交掉。 所以,Docker 做法是,在修改了这些文件之后,以一个单独的层挂载了出来。而用户执行 docker commit 只会提交可读写层,所以是不包含这些内容的

最终,这 7 个层都被联合挂载到 /var/lib/docker/aufs/mnt 目录下,表现为一个完整的 Ubuntu 操作系统供容器使用。

容器的其他原理

docker exec:一个进程的每种 Linux Namespace,都在它对应的 /proc/[进程号]/ns 下有一个对应的虚拟文件,并且链接到一个真实的 Namespace 文件上,也就是说有一个可以得到所有 Linux Namespace 的文件,那么一个进程就可以通过选择加入到某个进程已有的 Namespace 当中,从而达到“进 入”这个进程所在容器的目的,此操作为setns()

docker commit:在容器运行起来后,把最上层的“可读写层”,加上原先容器镜像的只读层,打包组成了一个新的镜像,只读层在宿主机上是共享的,不会占用额外的空间;而由于使用了联合文件系统,在容器里对镜像 rootfs 所做的任何修改,都会被操作系统先复制到这个可读写层,然后再修改,这就是Copy-on-Write

Volume 机制:允许将宿主机上指定的目录或者文件,挂载到容器里面进行读取和修改操作;可以解决两个问题:容器里进程新建的文件,怎么才能让宿主机获取到? 宿主机上的文件和目录,怎么才能让容器里的进程访问到?;
工作流程:在 rootfs 准备好之后,在执行 chroot 之前(执行 chroot(或者 pivot_root)之前,容器进程一直可以看到宿主机上的整个文件系统),把 Volume 指定的宿主机目录 (比如 /home 目录),挂载到指定的容器目录(比如 /test 目录)在宿主机上对应的目录(即 /var/lib/docker/aufs/mnt/[可读写层 ID]/test)上,而且由于执行这个挂载操作时,“容器进程”已经创建了,也就意味着此时 Mount Namespace 已经开启了。所以,这个挂载事件只在这个容器里可见,宿主机上是看不见容器内部的这个挂载点的。这就保证了容器的隔离性不会被 Volume 打破;
挂载技术:为Linux 的绑定挂载(Bind Mount)机制,允许将一个目录或者文件,而不是整个设备,挂载到一个指定的目录上,并且在该挂载点上进行的任何操作,只是发生在被挂载的目录或者文件上,而原挂载点的内容则会被隐藏起来且不受影响;绑定挂载实际上是一个 inode 替换的过程(inode和dentry详情可见博文https://blog.csdn.net/qq_41594698/article/details/103155873,inode可以理解为存放文件内容的“对象”,而 dentry,也叫目录项,就是访问这个 inode 所使用的“指针”)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I5kQidMv-1593925327450)(https://i.loli.net/2020/07/05/sxXigWVqAP6kpJv.png)]

猜你喜欢

转载自blog.csdn.net/qq_41594698/article/details/107137691
今日推荐