通过阅读文献,对docker有了一个全新的认识,下面就来谈谈Docker的核心
首先要牢记一句话:容器,就是一个特殊的进程。进程,顾名思义就是一个程序运行起来后的计算机执行环境的总和。对于进程来说,它的静态表现就是程序,平常都安安静静地待在磁盘上;而一旦运行起来,它就变成了计算机里的数据和状态的总和,这就是它的动态表现。而容器技术的核心功能,就是通过约束和修改进程的动态表现,从而为其创造出一个“边界”。对于 Docker 等大多数 Linux 容器来说,Cgroups 技术是用来制造约束的主要手段,而 Namespace 技术则是用来修改进程视图的主要方法。让我们启动一个容器来看看:
docker run -it busybox /bin/sh
docker exec -it busybox /bin/sh
/ # ps
PID USER TIME COMMAND
1 root 0:00 /bin/sh
10 root 0:00 ps
可以看到,容器内有一个1号进程/bin/sh,下面则有一个10号进程,这是我们刚刚打的命令。有没有想过,为啥这个容器就像与世隔绝的一样呢,为啥就不能ps出宿主机的进程呢?、
这种技术,就是 Linux 里面的 Namespace 机制。而 Namespace 的使用方式也非常有意思:它其实只是 Linux 创建新进程的一个可选参数。我们知道,在 Linux 系统中创建进程的系统调用是 clone(),而当我们用 clone() 系统调用创建一个新进程时,就可以在参数中指定 CLONE_NEWPID 参数,比如:
int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL);
其实namespace就是里面的一个可选参数而已,而每个 Namespace 里的应用进程,都会认为自己是当前容器里的第 1 号进程,它们既看不到宿主机里真正的进程空间,也看不到其他 PID Namespace 里的具体情况。所以容器,其实就是操作系统在启动进程时通过设置一些参数实现了隔离不相关资源后的一个特殊进程。很多人喜欢把容器和虚拟机相提并论,当了解到这里时,自然而然也不会这样做了吧。
在介绍完容器的“隔离”技术之后,我们再来研究一下容器的“限制”问题。为什么要为容器做限制呢?虽然容器内的第 1 号进程在“障眼法”的干扰下只能看到容器里的情况,但是宿主机上,它作为第 100 号进程与其他所有进程之间依然是平等的竞争关系。这就意味着,虽然第 100 号进程表面上被隔离了起来,但是它所能够使用到的资源(比如 CPU、内存),却是可以随时被宿主机上的其他进程(或者其他容器)占用的。当然,这个 100 号进程自己也可能把所有资源吃光。这可不是容器使用者想看到的。而 Linux Cgroups 就是 Linux 内核中用来为进程设置资源限制的一个重要功能。Linux Cgroups 的全称是 Linux Control Group。它最主要的作用,就是限制一个进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等等。可以在操作系统上看看:
可以看到,在 /sys/fs/cgroup 下面有很多诸如 cpuset、cpu、 memory 这样的子目录,也叫子系统。这些都是我这台机器当前可以被 Cgroups 进行限制的资源种类。而在子系统对应的资源种类下,你就可以看到该类资源具体可以被限制的方法。
在当前的子系统下创建一个目录,比如在/sys/fs/cgroup/cpu下创建一个my_limit目录,这个目录就称为一个“控制组”。你会发现,操作系统会在你新创建的my_limit目录下,自动生成该子系统对应的资源限制文件。举个例子,我们可以在my_limit目录下的cpu.cfs_quota_us文件写入echo 20000,因为cpu.cfs_period_us默认是100000(us),它意味着在每 100 ms 的时间里,被该控制组限制的进程只能使用 20 ms 的 CPU 时间,也就是说这个进程只能使用到 20% 的 CPU 带宽。然后再把需要的进程号写入tasks文件中,用top命令时你会发现,该进程最多也就能占用百分之20的cpu资源了。对于Cgroup来说,简单粗暴地理解呢,它就是一个子系统目录加上一组资源限制文件的组合。所以在创建容器时我们可以这样些:
docker run -it --cpu-period=100000 --cpu-quota=20000 ubuntu /bin/bash
通过以上讲述,现在应该能够理解,一个正在运行的 Docker 容器,其实就是一个启用了多个 Linux Namespace 的应用进程,而这个进程能够使用的资源量,则受 Cgroups 配置的限制。这也能更好的理解了容器是单进程模型的概念,只有 PID=1 的进程才会被 Docker控制,即 pid=1 的进程挂了 Docker 能够感知到,但是其它的进程却不受 Docker 的管理,所以这也是docker官方说的,建议一个容器只运行一个应用程序。
还有一个问题不知道大家有没有仔细思考过:这个房间四周虽然有了墙,但是如果容器进程低头一看地面,又是怎样一副景象呢?换句话说,容器里的进程看到的文件系统又是什么样子的呢?这里又要引入Mount Namespace了,Mount Namespace 修改的,是容器进程对文件系统“挂载点”的认知。但是,这也就意味着,只有在“挂载”这个操作发生之后,进程的视图才会被改变。而在此之前,新创建的容器会直接继承宿主机的各个挂载点。在 Linux 操作系统里,有一个名为 chroot 的命令可以帮助你在 shell 中方便地完成这个工作。顾名思义,它的作用就是帮你“change root file system”,即改变进程的根目录到你指定的位置。为了能够让容器的这个根目录看起来更“真实”,我们一般会在这个容器的根目录下挂载一个完整操作系统的文件系统,比如 Ubuntu16.04 的 ISO。这样,在容器启动之后,我们在容器里通过执行 "ls /" 查看根目录下的内容,就是 Ubuntu 16.04 的所有目录和文件。而这个挂载在容器根目录上、用来为容器进程提供隔离后执行环境的文件系统,就是所谓的“容器镜像”。它还有一个更为专业的名字,叫作:rootfs(根文件系统)。注意:当容器进程被创建之后,尽管开启了 Mount Namespace,但是在它执行 chroot(或者 pivot_root)之前,容器进程一直可以看到宿主机上的整个文件系统。所以容器的volume挂载一定是在 Mount Namespace后,chroot前的!!
rootfs分为只读层,可读写层,init层。上面的读写层通常也称为容器层,下面的只读层称为镜像层,所有的增删查改操作都只会作用在容器层,相同的文件上层会覆盖掉下层,这也是copy-on-write的概念。init 层是 Docker 项目单独生成的一个内部层,专门用来存放 /etc/hosts、/etc/resolv.conf 等信息。需要这样一层的原因是,这些文件本来属于只读的 镜像的一部分,但是用户往往需要在启动容器时写入一些指定的值比如 hostname,所以就需要在可读写层对它们进行修改。可是,这些修改往往只对当前的容器有效,我们并不希望执行 docker commit 时,把这些信息连同可读写层一起提交掉。所以,Docker 做法是,在修改了这些文件之后,以一个单独的层挂载了出来。而用户执行 docker commit 只会提交可读写层,所以是不包含这些内容的。
而我们需要的不只是单个的容器,我们需要的是容器间的通信连接,这又涉及到了容器的部署和与之相关的一系列应用场景,这时k8s随之诞生......