Docker 在当下很火,那么,当我们谈 Docker ,谈容器的时候,我们在谈什么?或者说,你对 Docker ,对容器了解吗?容器,到底是怎么一回事儿?
Linux 容器
这篇文章着重来讲一下 Linux 容器,为什么强调 Linux 容器,而不是 Docker ,是因为 Docker 是基于虚拟化技术来实现的,但是这篇文章涉及到 Linux 容器的核心实现方面,两者不同,所以着重强调一下。
容器其实是一种沙盒技术。顾名思义,沙盒就是能够像一个集装箱一样,把你的应用装起来。这样,应用与应用之间就有了边界而不会相互干扰;同时装在沙盒里面的应用,也可以很方便的被搬来搬去,这也是 PaaS 想要的最理想的状态。但是说起来容易,等到真正实现起来的时候,就会有难度。因为容器是运行在宿主机上面的,当它运行起来的时候,需要加载到内存中,需要 CPU 完成加法操作等等。也就是说,如果想要实现真正意义上的容器,就要解决容器和宿主机真正隔离这样的问题,但现实中这样的问题还没办法解决。
既然问题还没解决,那么我们所说的容器,是在说什么?容器的核心功能又什么?
容器核心功能
在上面已经说过,容器其实是一种沙盒技术,应用和应用之间有“边界”。所以容器的核心功能,就是通过约束和修改进程的动态表现,从而创造出一个"边界"。
这个官方语言可能会有点儿难懂,咱们换个说法。容器用英语来说就是 Container ,而 Container 的另一个意思是集装箱。提到集装箱的时候,你的脑海里第一反应是不是大船停靠在岸边,然后好多整齐划一的箱子可以运来运去。为什么这些集装箱可以很方便的运来运去呢?因为它们大小一致,而且是箱子,对吧?所以当我们使用 Container 来形容容器的时候,就是我们想要让容器达到一个可以打包,符合标准的状态。
基于以上,我觉得咱们可以达成一个共识,就是如果想要让容器帮助我们达到一个可以打包,符合标准的状态的话,首先要解决的是什么问题?就是将容器和容器之间隔离出来,这样我才能对这个容器统一做一个操作,对不对。对于 Docker 等大多数 Linux 容器来说,做到让容器和容器之间隔离,主要是两种技术:一种是看起来是隔离了的技术:Namespace 技术,它是用来修改进程视图的主要方法,也就是说每个 namespace 中的应用看到的是不同的 IP 地址、用户空间等;一种是用起来是隔离了的技术:Cgroups 技术,它是用来制造约束的主要手段,也就是说,我这台服务器总共有 8G 的内存,都给这一个应用的话,其他的应用怎么跑起来呢?所以 Cgroups 技术就是对容器来做一个限制。
Namespace
Namespace 就是命名空间的意思,如果编程使用的是,面向对象的程序设计语言,那对于这个词应该不是很陌生。一个团队在一起写代码,难免会有相同的类,此时编译就会冲突。如果每个功能都有自己的命名空间,那在不同的空间里面就算类名相同,也不会有啥冲突。写程序如此,在 Linux 上跑程序也是如此。当我们在一台 Linux 上跑多个进程时,进程有全局的进程 ID ,网络也有全局的路由表。如果多个进程使用不同的路由策略,可能会导致这些进程冲突,解决办法也很简单,将这些进程放在一个独立的 namespace 里面就可以了嘛。
说是这样说,但是有一点我希望你能明确知道,进程在静态状态下就是程序,它只是磁盘上的二进制文件罢了。只有当它运行起来时,才成为进程。所以,当我们开始运行程序时,操作系统都会为进程分配一个进程编号,这个编号就是进程的唯一标识。假设我们开始运行了一个程序,它的 PID=100 。也就是说这个程序是第 100 个进程,在它前面还有 99 个进程。而现在,如果我们通过 Docker 把这个程序运行在一个容器当中,那么 Docker 就会在第 100 个进程创建时,给它施一个"障眼法",让它永远看不到其他 99 个进程,这样这个程序就会误以为自己是第 1 个进程 这种机制,其实就是对被隔离应用的进程空间做了手脚,使得这些进程只能看到重新计算过的进程编号,比如上面的第 100 个进程,经过 Docker 的"障眼法"之后,误以为自己是第 1 个进程,但是实际上在宿主机的操作系统中,它还是原来的第 100 个进程。
容器限制( Cgroups )
Linux Cgroups 的全称是 Linux Control Group 。它最主要的作用,就是限制一个进程组能够使用的资源上限,包括 CPU ,内存,磁盘,网络带宽等。特别简单的一句话就是,你的电脑只有 8G 内存,你会允许一个进程占用你的内存到 7G 嘛?一般情况下应该是不会吧,那样的话,做其他事情不都卡的要死嘛,对不对。所以在 Linux 中,提供了一种技术,来控制进程组所能使用的资源。Cgroups 的有很多子系统,每一项子系统都有自己独有的资源限制能力,比如:
- blkio :为块设备设定 I/O 限制,一般用于磁盘等设备;
- cpuset :为进程分配单独的 CPU 核和对应的内存节点;
- memory :为进程设定内存使用的限制;
- cpu :使用调用程序为进程控制 CPU 的访问;Linux Cgroups 的设计还是比较易用的,它就是一个子系统目录加上一组资源限制文件的组合。对于 Docker 等 Linux 容器项目来说,它们只需要在每个子系统下面,为每个容器创建一个控制组(即创建一个新目录),然后在启动容器进程之后,把这个进程的 PID 填写到对应控制组的 tasks 文件中就可以了。至于在这些控制组下面的资源文件里填什么值,那就交给用户执行 docker run 时的参数来指定了。
经过以上分析,我们可以了解到,容器这个听起来玄而又玄的概念,实际上它就是操作系统上的一种特殊的进程而已。所以,容器本身并没有价值,有价值的是"容器编排"。当我们在谈容器的时候,其实我们在谈如何更好的去编排容器。这也是为什么当下 k8s 这么火的原因。
容器与虚拟机异同
看到这里,你会不会有疑问,容器和虚拟机之间有什么不同呢?你可能看到过下面这个图片:
在这张图的左边,画出了虚拟机的工作原理,其中 Hypervisor 的软件是虚拟机主要部分,它通过硬件虚拟化功能,将主机的 cpu ,内存, I/O 设备等虚拟出来,在这些虚拟的硬件上,安装了一个新的操作系统,也就是图中的 GuestOS 。此时,用户的应用进程就可以运行在这个虚拟的机器中,它能看到的也就只有 GuestOS 的文件和目录,使用的也是这个机器里面的虚拟设备。这就是为什么虚拟机能够将不同的应用进程相互隔离,因为它们所在的系统本来就不是同一个系统。
这张图的右边则是容器,它只由应用程序本身和它的环境依赖(库和其他应用程序)两部分组成,并且是直接在宿主机上运行的。当你想要启动容器的时候,根本不需要启动整个操作系统,因为它本来就是在这个操作系统上的。而且,因为容器直接在宿主机上,所有容器都共享这个底层操作系统,没有另外新装操作系统,这就使得容器不管是在体积上,还是启动速度方面,都会更快,开销更小,也更加容易迁移。
还记得讲容器的时候,介绍的 Namespace 技术嘛,虚拟机是真实存在的,你可以直接在自己的电脑上创建一个,但是容器不一样,它没有一个真正的“容器”运行在宿主机里面, Docker 项目帮助用户启动的,还是原来的应用进程,只是在创建这些进程时,加上了 Namespace 参数罢了,但是对于宿主机来说,本质还是进程罢了。