Linux容器技术的实现原理

网站建设4年前发布
32 0 0

作者 | 中国移动云能力中心PaaS产品部 赵慧慧,近些年来,容器技术迅速席卷全球,颠覆了应用的开发、交付和运行模式。容器技术作为云原生技术领域的技术基石,也是现今最热门的一种服务器端技术。容器以及容器编排技术成为基础设施领域最炙手可热的关键词,随着容器及周边生态技术的蓬勃发展,容器社区当仁不让成为开源社区最活跃的生态圈之一。同时,以容器技术为核心的容器生态圈在云计算、互联网等领域得到了广泛应用。,那么,容器到底是怎么一回事?它有什么样的特性?又是如何运行起来的呢?,下面我们简单介绍一下以Docker容器为主的Linux容器的实现原理。,容器最初是为了解决PaaS项目应用的托管问题,其目的是提供一个便利的打包机制和包运行技术,来解决应用在部署过程中云端环境和本地环境不一致的问题,从而实现应用一次打包随处部署的便捷功能。,容器通过提供一个容器镜像,对应用程序运行所需的整个环境(包括操作系统)进行打包;同时在运行时,将压缩包放入“沙盒”中解压并在其中运行,利用虚拟化的“沙盒”技术实现在同一操作系统上不同应用程序之间无感知的运行,仿佛应用程序自己独占整个操作系统的资源。,容器具有便携性、隔离性、可配额和安全性四个特性。,利用容器技术,用户部署应用只需要两步,第一步制作应用的容器镜像,第二步是利用容器命令创建一个”沙盒“,在沙盒中运行自己的应用。,那么,容器的镜像是如何制作的?容器所谓的“沙盒”的实现原理又是通过什么技术来实现的?,容器镜像是利用Linux的Union FS技术,将应用程序所依赖的运行操作系统、工具包、依赖库、配置文件、运行脚本等各种环境信息以分层的方式联合挂载到同一个目录下,作为镜像的根目录。,沙盒技术,顾名思义,就是在应用程序运行时为程序设计一个边界,使得不同应用之间可以独立运行而不互相干扰。其实这个沙盒技术并不是什么黑科技,其本质就是利用Linux的Namespace和Cgroups来实现应用隔离和资源限制,它并不是一个真正存在的边界。,综上所述,容器本质上是基于Linux内核的Namespace、Cgroups和Union FS等技术对进程进行封装隔离的操作系统层面的虚拟化技术。,接下来,我们展开介绍下,容器是如何利用沙盒技术实现进程的隔离与限制的,从而实现容器应用进程无感知运行的。依据前面的介绍,我们知道,该功能是基于Namespace和Cgroups实现的。,进程的隔离是指对进程视图的隔离,从进程的角度出发,能够看到的系统进程、网络信息、主机信息和用户信息等资源。容器对资源的隔离主要是通过Namespace技术实现的。Namespace是Linux Kernel提供的一种资源隔离方案,不同的Namespace下的资源是独立的,系统可以为容器进程分配不同的Namespace,从而保证各进程对彼此是透明的。,直接讲概念比较抽象,接下来,我们以进程资源为例,来介绍Namespace是如何实现容器进程间PID的隔离的。,我们利用Docker启动一个busybox容器并进入,运行ps指令:,我们再启动一个ubuntu容器并进入,运行ps指令:,我们在宿主机上运行ps指令:,可以看到,busybox、ubuntu和宿主机上的进程,看到的PID都是不一样的,这就是所谓的进程资源的视图隔离;而且我们发现,busybox和ubuntu两个容器进程的初始PID都是1,但是在宿主机进程中,我们发现他们真正的PID分别是1447和1752。这种机制其实就是对被隔离应用的进程空间动了手脚,使得这些进程只能”看到“重新计算过的PID(如PID=1),而实际在宿主机的操作系统中,他们的进程号还是原来的PID。,这种容器进程的PID的不同视图就是通过Linux的Namespace机制实现的。具体的实现方法就是,在Linux创建新进程的时候,给进程创建方法clone指定一个可选参数CLONE_NEWPID:,这时,新创建的进程将会进入一个全新的进程空间。在这个进程空间里,只能看到自己这个Namespace下的进程。,另外,除了PID Namespace之外,Linux Kernel还提供了IPC、Network、Mount、UTS和USER其他五种类型的隔离,用来对各种进程上下文进行资源隔离。如Network Namespace用于让被隔离进程只“看到”当前Namespace下的网络设备和配置;我们在刚刚启动的busybox容器中查看网络,查看容器的ip信息:,在宿主机上查看ip信息:,由此可以发现,容器进程实际上就是通过在创建进程时指定所需要启用的一组Namespace参数来为进程开辟一系列Namespace,使得容器进程拥有自己独特的文件系统、网络信息、用户管理、主机名等,保证当前进程只能“看到”自己的资源,从而实现容器进程的资源隔离。,我们可以在/proc目录下找到刚刚启动的busybox进程中的所有Namespace:,前面我们介绍了容器进程实际上就是一个Linux进程,虽然它通过Namespace修改了进程的视图,使进程只能看到该Namespace下的各种资源,但Namespace只是限定了进程的“视线”;但这些被隔离进程与普通进程没有太大区别,它与其他普通进程一样共享宿主机上的所有资源,这意味着虽然它所能够使用的资源(如CPU、内存)随时可以被宿主机上的其他进程占用,当然它也可能用光所有的资源。“沙盒”除了对进程实现封闭的视图限制外,还需要是对进程进行资源限制。,容器技术对资源的限制主要是通过Linux Cgroups技术实现的。Cgroups是Linux下用于对一个或一组进程进行资源(CPU、内存、磁盘I/O、网络带宽)控制和监控的机制,简单点说它的主要作用就是限制一个进程组能够使用cpu、内存、磁盘、带宽等资源的上限。不同资源的具体管理工作由相应的Cgroups子系统来实现,在Ubuntu机器里,可以使用mount 指令看到所有的子系统:,可以看到,Cgroups有十几个子系统,系统通过这些子系统用来控制每种类型的资源,如cpu用来限制进程组可以访问的cpu使用率,cpuset用来在多核cpu系统中为任务分配单独的cpu和内存, memory用来限制进程组的内存使用上限及生成内存资源报告等。这些子系统是一系列文件系统目录,他们均在/sys/fs/cgroup目录路径下,Cgroups通过Hierarchy层级树的方式对资源进行管理,层级树把Cgroups串成一个树型结构,使Cgroups可以做到继承。,如图所示,cgroup1 中限制了使用 cpu 及 内存资源,它将控制子节点的 cpu 和内存分配(即限制 cgroup2、cgroup3、cgroup4 中的cpu及内存资源分配)。cgroup2 中限制了内存资源,但是没有限制cpu的资源。结果是 cgroup3 和 cgroup4 的内存资源受 cgroup2内存的限制,同时cgroup3 和 cgroup4 受 cgroup1 的cpu的限制。,针对不同的资源限制,只要将限制策略在不同的子系统目录中进行配置即可。每种资源具体被限制的方法,可以在子系统目录下看到。如在cpu子系统中,可以看到如下文件:,可以看到对cpu的限制方法有很多。其中,cpu.shares表示可出让的能够获得的CPU使用时间的相对值,cpu.cfs_period_us用来配置时间周期长度,cpu.cfs_quota_us配置当前Cgroup在cpu.cfs_period_us时间内最多可以使用的CPU时间数。所以我们可以通过cpu.cfs_period_us和cpu.cfs_quota_us组合设置,来限制进程组的CPU使用率。,以一个例子来说明下具体控制过程:,首先建立一个用于限制cpu的控制组,在/sys/fs/cgroup/cpu目录下创建一个子目录test,可以发现,在这个新建的test目录下,自动生成了子系统对应的资源限制文件。,现在看下cpu.cfs_quota_us cpu.cfs_period_us 两个文件的内容:,cpu.cfs_quota_us的初始值为-1,表示还没有任何cpu时间的限制。,第二步,在后台执行这样一个脚本程序:while : ;do : ; done &,它执行了一个死循环,可以把系统CPU占满;我们用top指令查看CPU占用情况,输出显示,CPU的使用率已经到100%了。,现在,我们修改test目录下的文件,来对test控制组设置cpu最大使用上限。,首先向cpu.cfs_quota_us文件写入20ms,这意味着每100ms(pu.cfs_period_us文件设置)的时间里,被test控制组限制的进程只能使用20ms(cpu.cfs_quota_us)的CPU时间。,其次,将刚刚创建的死循环进程PID写入tasks文件,使得test控制组可以控制该进程:,通过这两步设置,该进程最大可使用的CPU带宽就被控制到了20%。我们运行top指令来验证下结果:,可以看到,该进程的CPU使用率立即降到了20%以下。,通过上述例子我们了解到Cgroups控制进程的方法,简单而言,它就是一个子系统目录(如test)加上一组资源限制文件(如/test目录下的各种限制方法文件)的组合。对于Linux容器来说,他们只需要在每个子系统下面为每个容器创建一个控制组,然后在启动容器进程之后,把这个进程的PID填写到对应控制组的tasks文件中即可。,以Docker项目为例,在安装完Docker之后,Docker会自动在Cgroups每个子系统中创建docker控制组作为Docker容器进程的根控制组。,以后,每启动一个Docker容器,Docker都会在每个子系统的docker控制组中以该容器进程的container id为名称新建一个子控制组,同时将该进程的PID添加到子控制组的tasks文件中,从而完成对新运行Docker容器进程的各种资源的限制。,知道了容器是怎么运行的,回过头来,我们再看下容器的镜像是如何生成的。前面我们说了,容器是利用Union FS对应用程序及其依赖的环境打包的,那具体是如何是对应用程序打包的呢?在讲解打包过程之前,我们先简单介绍下打包机制依赖的底层技术Union FS。,Union FS(Union File System,联合文件系统),是一种分层、轻量级并且高性能的文件系统,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下,简单来说就是将不同位置的目录联合挂载到同一个目录下。,Linux常用的UnionFS 有AUFS、OverlayFS 和 Btrfs 等,我们以实现相对简单的 OverlayFS 为例介绍下联合挂载机制。,OverlayFS 文件系统主要有三层,lower层、upper层和merged。lower层是只读层,用户不能修改这个层的文件;upper层是可读写层,用户能够修改这个层的文件;而 merged是合并层,把 lower 层和 upper层的文件合并展示,合并时上层目录文件覆盖。具体的合并原则是:不同层的文件会合并到一起,相同文件upper层覆盖lower层;同时支持目录级别的合并,同名同级目录按照前述规则递归合并。,比如,有两个目录lower和upper,他们分别有两个文件:,通过联合挂载的方式将这两个目录挂载到一个公共的目录merged上:,此时查看merged目录的内容,就能看到lower和upper目录的文件被合并到了一起,同时合并后的in_both.txt文件的内容来源于upper目录中in_both.txt。,容器镜像就是通过这种分层和联合挂载机制进行制作的。一个容器镜像包含应用程序可用的文件系统和其他元数据,如运行工具包、代理、可执行文件等,这些镜像信息都是以不同的层级堆叠到一起,这些增量的层最后合并生成一个容器镜像。,我们来看下一个简单的nginx镜像(Docker版本是20.10.7):,从上面可以看到,该nginx基础镜像由6个镜像层组成,它利用overlay2联合文件系统,合并/var/lib/docker/overlay2/43901fba***等lower层的目录和/var/lib/docker/overlay2/eadef61b8***等upper层的目录,构建出的nginx文件系统目录。,为了便捷镜像的制作过程和减少镜像的存储空间,容器镜像可以基于已经制作好的一些基础镜像(如系统文件镜像)进行构建,基础镜像以一个基础层(Layer)的形式内置于镜像层中;同时容器的镜像层可以在多个镜像之间共享和征用,如果某个已经被下载的容器已经包含了后面下载的镜像的某些层,那么后面下载的镜像就无须再下载这些层,两个镜像共享这些公共的镜像层,这种分层共享的方法大大提升了镜像在网络上的分发效率。,但是容器运行的时候,可能会修改这些基础层的文件内容,一旦基础层的文件系统被修改,那它就无法以一个纯净的标准状态被其他镜像引用。如何在基础层不被修改的前提下,实现容器修改的需求,从而保证基础镜像的一致性呢?,容器技术设计基础镜像层为只读层,这些基础镜像层的挂载方式都是只读的,不允许被修改。同时在基础镜像之上又增加了一个可读写层,挂载方式为rw(即read write),专门用于满足容器修改文件系统内容的需求。容器在对文件修改时,如果该文件已经在基础层,则会先将此文件复制到可读写层然后再应用进程的修改到文件,因此进程修改的是该文件在可读写层的拷贝,依据UnionFS合并原理,上层文件覆盖下层文件,因此用户进程看到的是修改后的可读写层的拷贝文件,这样就实现了对基础文件的“修改”功能。初始情况下,可读写层是空的,只有在容器进行写操作时,这些修改才会以增量的方式出现在该层,这种修改方式称为写时复制(Copy-on-Write)。当我们使用完了这个修改后的容器之后,还可以使用commit和push来保存这个修改过的可读写层,并上传到公共的镜像仓库供他人使用。,最终,在容器运行时,这三类层被联合挂载到容器的根目录下,表现为一个包含操作系统的应用程序环境供容器使用。当然,这个镜像只是操作系统的文件和目录,并不包含操作系统内核,容器真正运行还是基于宿主机的系统内核。,以上,我们介绍了Linux容器技术的实现方式,主要有镜像实现和运行时实现两个方面。容器镜像以UnionFS为技术载体通过分层镜像的设计来组件增量式的容器镜像,在镜像制作完成之后,Linux系统基于该镜像系统文件启动容器,并通过Namespace技术对容器的视图做限制,同时利用Cgroups控制进程最大可使用的各类系统资源。

© 版权声明

相关文章