以firejail sandbox解析Docker核心原理依赖的四件套
Posted 世纪顶点科技有限公司
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了以firejail sandbox解析Docker核心原理依赖的四件套相关的知识,希望对你有一定的参考价值。
本文并不会详细描述Docker的用法,本文的目的是解析Docker得以成型所依托的核心组件原理,以Linux平台为例,我们看看是什么内核特性成就了Docker这么一个伟大的东西。
总结为四件套,分别是Linux Namespace,Linux Cgroup,Linux OverlayFS,Linux虚拟网卡。
新的视角-firejail
和其它文章不同,本文采用另外一条相反的路线,通过另外一个东西,即firejail来解析这四件套,最后我们看看它们是如何合并成一个和Docker很类似的容器的。
之所以用firejail来描述,是因为它足够简单,没有那些外围的东西,比如守护进程,C/S模型,配置文件,DSL等等。它在Linux发行版上就是简单的一条命令,而我们的目的正是通过这么一条命令,构建一个和Docker类似的东西。
什么是firejail?
firejail简单到什么程度呢?你只需要在命令行敲入firejail,然后用某种debug hacker的精神去探究,你就能发现一切秘密。
Linux Namespace
首先,什么是Namespace?
任何基础设施都需要以某种方式被管理,系统往往需要对被管理组件进行编址,编址往往是全局唯一的,这里所谓的全局指的就是在一个Namespace(命名空间)内的全局。引用Wiki上的描述:
Linux内核目前支持以下几种Namespace:
接下来就分别解释一下这些个Namespace。
PID Namespace
我们知道,Linux进程管理是基于进程pid的,所有的进程均被分配一个PID Namespace内唯一的PID作为其标识。除此之外,关于PID的管理非常复杂,这涉及到UNIX/Linux的进程模型,详情可以参考下面的文章:
朴素的UNIX之-进程/线程模型:https://blog.csdn.net/dog250/article/details/40208219
这个文章里有一幅大图,详细解释了PID,TGID等ID的关系,本文为了简单起见,就假设只有进程这么一个PID,不再考虑线程和进程组。
我们来看上一个小节最后的那个实验,敲入firejail命令后,系统进入了一个新的PID Namespace,其中有自己的1号进程,此时如果我们另起一个终端,在该PID Namespace外部看,看看有没有什么发现,我们用pstree命令看一下层级关系:
我们发现自firejail衍生出来的一个bash,其PID是13393,然而在其独立的PID Namespace中,它的PID则是3,很容易确认它们是同一个进程:
我们发现它们的PID Namespace确实就是同一个“pid:[4026532135]”。这说明了PID Namespace的一个层级结构:
在内核代码中,这个层级关系体现在alloc_pid函数(不得已,这段代码非常简单地解释了为什么父PID Namespace能看到子PID Namespace的进程,比用语言描述要简单很多。):
pid->level = ns->level; # 只要在clone调用中有NEWPID标识,新的NS level便会在当前NS level的基础上加1,以记录层级关系。
和进程的父子关系类似,PID Namespace也维持这么一个关系,即子PID Namespace对父PID Namespace是可见的,每一个子PID Namespace在其所有上层的PID Namespace中均有唯一的PID编址。这一点是个其它别的Namespace不同的地方,值得注意。
父PID Namespace是可以操作子PID Namespace里面的进程的,当我们在外部父PID Namespace中renice子PID Namespace中的bash后,后者的优先级随即发生了变化:
首先看firejail中的进程优先级:
随机用外部父PID Namespace中的PID renice其bash的优先级:
然后再firejail里面看:
甚至可以kill掉进程,发信号等等。
因此,我们得知,PID Namespace在纵向上依然保留着管理关系,并没有完全隔离,在横向上则是完全隔离的,比如非直系继承的兄弟叔伯之间便无法使能这种管理动作。
以上便是PID Namespace的理论机制,我们已经知道firejail已经在新的PID Namespace中clone出了一个bash,那么此后在此bash中执行的所有的命令以及启动的所有的daemon均属于这个PID Namespace了,只要父PID Namespace不干预,它们和其它的PID Namespace中的进程就是隔离的了,互相不可见。这完成了容器隔离的第一步,Docker的实现在这一步与firejail完全一致。
在进入Net Namespace的分析之前,我们想一下父PID Namespace什么情况下会干预子PID Namespace中的进程呢?最为直观的答案似乎是,即在子PID Namespace占用了大量资源的时候,这个时候就不得不行使家长制权力了,为了避免这一点,Linux内核拥有新的机制来限制子PID Namespace的资源,即Cgroup机制,这个下文会说。
接下来看看Net Namespace。我们exit退出firejail,然后重新启动,这次携带一个参数:
似乎发生了一点变化。
Net Namespace
紧接着上节留下的悬念,我们check一下这个新的PID Namespace在哪个Net Namespace中:
显然“net:[4026531957] != net:[4026532141]”,嗯,它们没有在一个Net Namespace!
firejail的–net参数,含义就是另外新建一个Net Namespace。我们看看这意味着什么。
基本上Net Namespace就这些内容,和PID Namespace相比,它显得非常简单,就像下图一样:
它没有层级结构,没有父子关系,完全就是一个平坦隔离的模型,非常简单。值得注意的是,每一张网卡,不管是物理网卡还是虚拟网卡,均只能同时在一个Net Namespace中,也就是说,如果网卡A被Net Namespace1用了,那么Net Namespace2便无法看到这张网卡,这个问题促使了各种网卡虚拟化技术的发展,其中的veth这种简单的以及pf/vf这种复杂的自不必说,比较好玩的就是MACVLAN,IPVLAN等技术,即在不需要任何硬件机制支持的基础上将一张物理网卡虚拟成多张不同的虚拟网卡,这个下文会简述。
…等等其它Namespace
在PID和Net层面均实现了Namespace隔离之外,我们的firejail欲至何方?它离Docker还有多远?
所谓的Mount Namespace无非就是实现了隔离的文件系统视图,在更加简单的层面上,想用几乎所有人都懂的chroot来模拟这个,虽然不严谨,但是意思传达了。在下面的“合并为Docker”这一小节,将用chroot来完成最终的合并。
Docker确实使用了Mount Namespace,我们可以运行一个Docker容器以确认:
我们再看看容器外部的宿主机的1号进程对应的Namespace:
在接下来的内容中,绕过新建Mount Namespace这个过程,采用简化的chroot来描述firejail的对应过程,请注意不同点。
此外,其它的几个Namespace,与本文无关,便不说了。
Linux Cgroup
如果说Linux Namespace是从编址的角度对被管理实体进行了有效隔离的话,那么Linux Cgroup则是从资源配额的角度对被管理实体进行了另一个维度的隔离。
举一个例子,分属两个同一层级不同PID Namespace中的进程A和进程B互相不可见,但是它们却都依赖同一个资源池,比如CPU资源池,内存资源池…进程A如果恶意消耗资源,将会饿死进程B,如此一来虽然进程A无法直接打击进程B,但是可以通过这种恶意抢占物理资源的方式间接影响进程B,这也是我们所不希望的。Linux Cgroup可以解决这一问题,从资源池利用配额的实现不同Group的完美隔离。
Cgroup机制很简单,就是事先分配一系列的group,然后为每一个group分配一定量的某种资源的配额,然后把进程加入到这些group即可实现资源配额的隔离。
然而现实总是比理论上该如何要考虑更多,首先,我们要对资源进行分类,除了CPU资源外,还有内存,IO等资源,有的策略只想限制进程或者一组进程的CPU利用率,而另外的策略则要限制它们的内存利用率,不一而足,如何设计这个Cgroup的管理结构,就是一个问题了。
Linux内核采用了两层集合多对多的映射方式来解决这个问题,即Cgroup被分成了两类集合:
同类资源(一个subsystem)被分成Group
不同类资源限额被整合成Set
多个Group的元素和多个Set的元素之间形成了典型的组相联结构(大部分cache均具有的形式),最终只要把进程或者多个进程添加进一个Set,即可实现多种资源的配额控制了。相当于每一个Set从每一个Subsystem中调一个group出来,Subsystem只和Set打交道。具体如下图所示:
有关cgroup的设计:
一个资源管理系统的设计–基于cgroup机制:https://blog.csdn.net/dog250/article/details/5985710
一个资源管理系统的设计–解析linux的cgroup实现:https://blog.csdn.net/dog250/article/details/5991938
大多数的Linux发行版均已经为你配置好了Cgroup,Linux是通过标准的VFS接口来让用户操作Cgroup的,看看你的/etc/mtab文件里有没有类似下面的内容:
如果有,那么在/sys/fs/cgroup目录下便可以操作它了,如果没有,也可以手工mount它们到任何目录。
以cpu,cpuacct为例,我们来看一个实际的效果。首先准备以下的可执行文件:
以下是其top视图:
现在让我们按照下面的步骤配置这个a.out进程到一个单独的Set中:
此时再看看top统计:
完全符合预期,即便是无限循环,CPU利用率也是降到了指定的100%。此时试试再跑一个a.out,然后将其pid加入到/sys/fs/cgroup/cpu//sys/fs/cgroup/cpu,再看看top结果:
对于内存,IO等资源也是完全一样,cgroup甚至可以设置进程或者一组进程跑在固定的cpuset上,非常强大。
那么,也许你没有从上面的即视感操作中看到有那个多对多组相联的视图,事实上,如果一个进程或者一组进程受多种资源限制,就会体现了,比如希望一个进程的CPU利用率不超过10%,内存使用不超过100MB,那么我们不得不创建两个类型的group,在cpu这个type的cgroup mount目录下创建一个10_percent_cpu子目录,然后再在memory这个type的cgroup mount目下创建一个100MB_memory子目录,然后分别将受控进程的pid追加到这两个子目录的tasks文件中。此时的场景就是一个组相联的关系。
对于一个进程而言,它引用多个group中的单独元素,对于一个group而言,它被多个进程引用,为了组织上的高效,内核还使用css_set这个结构体对引用同一组group元素的task进程了聚合,通过hash表来索引,从而提高了内存的空间使用率:
不得已的时候我才会贴代码,但是上面的这个结构实在是太重要了。
Linux Cgroup在原理和观感上就是以上这些,Docker无疑使用它做了资源配额的隔离,同理,firejail也有一个cgroup的配置参数,它比Docker使用起来更加简单,如果你对Cgroup有足够的理解,那么即便是在启动Docker或者firejail的时候没有配置Cgroup,事后你也能手工将其限制在某些Cgroup subsystem的group中。
Linux Cgroup就这么多,接着,让我们来看套装中的第三件,OverlayFS!
Linux OverlayFS
OverlayFS必须是一个创举。我这里有几个需求,看看传统的FS如何破解:
把data1这个分区mount到了/mnt,后来又把data2分区mount到了/mnt,请问如何在不卸载data2(服务在运行,无法卸载)的情况下访问data1里的数据。
两个隔离的容器需要共享相同的几个目录。
…
仅仅上面第一个需求就够很多人头痛的,至于第二个需求,如果说把数据复制两份,但这是最好的方案吗?
OverlayFS可以解决所有的这些问题。
还记得LiveCD吗?可以在不安装映像的情况下先玩几个小时这个操作系统,这是怎么做到的?用OverlayFS的原理解释起来非常简单。
所谓的OverlayFS就是将既有的FS叠加而成的一种抽象的复合FS,它并不负责底层实际的分区布局和数据存储格式,它只负责将那些已经存在的FS组合起来。简单来讲,OverlayFS有4个部分组成:
lower层:随便的一个目录,在OverlayFS中它是只读的。如果对它进行写入,则会执行Copy-on-Write将其拷贝到upper层之后再写入。
upper层:随便的一个目录,在OverlayFS它是可读写的。
merge层:随便的一个空目录,在OverlayFS中它完成了lower层和upper层的并集,提供一个统一的操作视图。
worker空间:充当工作中的操作数据的空间。
就是上述这么4点关键,可以让OverlayFS任意组合,其中,最为关键的一点就是,lower层可以是OverlayFS的merge层本身,这便使得OverlayFS最终可以递归地任何组合成你想要的任何形式,关键是看你怎么使用了。
回过头来继续说LiveCD,我们知道光盘是只读的,而内存时可读写的,完全可以将光盘挂载的目录设置为OverlayFS的lower层,然后内存盘的某两个目录作为upper层和merge层,关机期间的临时读写的数据,全部在内存盘里。
值得注意的是,OverlayFS的lower层仅仅在OverlayFS本身的意义上是只读的,即你从merge层视图提供的目录是无法操作lower层的,这并不是lower层本身就一定要是一个只读的文件系统,比如光盘这种,它完全可以是ext4这种,你仍然可以从别的目录去读写lower层的数据。
以软文的形式来描述OverlayFS显得不够专业,那么我就用简单的小实验来获得一点即视感:
首先,我们创建4个目录并且随便放一些文件在lower目录:
然后我们来mount一个OverlayFS:
此时,我们来看下tree的结构:
确实将lower层的内容合并到merge层了,此时我们尝试修改它:
可以看到,这个lower层中的lowerfile被拷贝到upper层了,此时如果我们在merge视图下新建一个文件,预期的结果就是该文件会在upper层被创建:
符合预期。关于删除文件我就不再解释了,这个请参考其它的资料。接下来我们来构建一个嵌套的OverlayFS目录,即用我们刚才的merge层目录merge作为新的lower层来构建:
最终,你可以实现的是,一份数据源,任意方式共享,无需冗余化的复制数据,结合Copy-on-Write,使得资源利用率非常之高。
firejail可以因此获得多少收益呢?让我们重新开始,这次分析一下firejail使用文件系统的方式。
最初可能会有一个疑问,即如果仅仅执行了firejail命令,创建了新的Namespace,此时如果在这个firejail中执行“rm -rf /”的话,会发生什么呢?如果真的悲剧了的话,那岂不是firejail枉自称什么jail或者什么sandbox了吗?这么试了一下,发现根文件系统并未损坏,这是怎么做到的呢?
让我们执行firejail的时候加上–debug参数看个究竟:
为了防止误伤根文件系统,这么多的条条框框…现在当我们知道了有了OverlayFS这个文件系统后,一切防误伤的机制完全交给OverlayFS即可,我们只需要准备一个可以chroot的最小化映像目录,然后将其设置为其lower层即可。
总之就是,我们把不希望被完全修改的文件或者多个firejail用户共享的文件放进lower层,其它的比如私有文件放进upper层,然后mount到一个merge层,最终让firejail通过命令行参数chroot到那个merge目录就行了。
如果在这一小节说完了全部,那么本文后面的内容就显得空洞了,所以把合并成Docker样子的firejail最后的一点内容再往后拖拖。拖到“合并为Docker”那个小节。
Linux虚拟网卡
图解几个与Linux网络虚拟化相关的虚拟网卡-VETH/MACVLAN/MACVTAP/IPVLAN:https://blog.csdn.net/dog250/article/details/45788279
Linux虚拟网卡可以说是完成了Net Namespace网络协议栈隔离的下端的最后一小段。最终的网络协议栈隔离效果成了下面的形式:
也就是说,对于firejail而言,它的–net参数除了新开辟一个独立的Net Namespace之外,还会在参数指定的物理网卡上创建一个macvlan虚拟接口供该firejail使用。
合并为Docker
当安装好Docker的基础映像debian之后,看Docker基础映像的目录:
/var/lib/docker/overlay2/29ee7bc2bce7accafd0ad4442d04810f89f1c613d7b98c49ad632d36b2c53b06/diff/
这个目录里就是一个基础可chroot运行的根文件系统。这种小的基础根文件系统在一些小型嵌入式设备上非常之常见,比如家用路由器什么的。它可以作为OverlayFS的lower层就行。
接下来我们在任意目录下构建下面的结构:
这个就是容器目录了。接下来,我们来mount一个OverlayFS:
确认无误后,便可以firejail了。
Apache已经在firejail中运行了,此时,要停止它的运行,然后在接下来的某个时间再次运行这个已经配置好的Apache…或者说将它迁移到别的机器上运行,先停止:
此时我们看一下现在的upper目录,它已经被firejail里面的apt-get install填满了东西:
过了很久之后,我们再次运行firejail:
因此,只需要将upper目录打包迁移即可,安装配置的东西全部都在这个可读写的upper层,至于说lower层的/var/lib/docker/overlay2/29ee7bc2bce7accafd0ad4442d04810f89f1c613d7b98c49ad632d36b2c53b06/diff/,这个一般是公共的,共享的,它在一个公共的目录下可以被很容易获取到!
只需要把upper目录想个办法打包带走就好了,至于采用什么格式,是否压缩,那就是另外的问题了。总之,可以把upper目录看作是一个标准化的集装箱了,把它无论搬到哪里,你只需要下面的命令就能让里面已经配置好的服务开始运行:
1.准备Base_Image & 挂载OverlayFS
2.创建新的PID Namespace以及Net Namespace
cgroup,因为它比较简单:
只需要添加–cgroup参数即可,至于cgropu怎么创建,却是另一个话题了。
用一个简单的firejail命令行工具实现了一个可以随意带走任意发布的容器,但是这一切看起来或者用起来并不是特别方便。Docker的底层使用了几乎一模一样的机制,只是通过firejail而不是Docker本身阐释了Docker底层的支撑技术,即所谓的Linux内核容器四件套,也正如本文的题目中所指示的那般。
简单易操作,最终Docker似乎完成了所有的事,将环境,配置,二进制完全整合在一个映像里,然后发布,任意运行。非常不错,但是千万不要把它看作是集装箱那么高大上的东西。
Docker组网模型
说起组网模型,还记得VMWare虚拟机的组网模型吗?四种模式:
Bridge模式
Host only模式
NAT模式
Segment模式
具体可以参见:
深入浅出VMware的组网模式:https://blog.csdn.net/dog250/article/details/7363534
Docker的组网模型要远比VMWare还要花式。这里说几个有特点的:
Bridge模式
这是最简单的模式,请看下图:
MACVLAN模式
这个比较类似firejail的隔离模式,使用将物理网卡创建多个MACVLAN虚拟网卡来组网:
Container模式
Container模式比较有意思。它实际上是将整个Net Namespace和虚拟网卡作为一个组件来使用,这个完整的协议栈是可插拔的。
有的时候,我们会对多个容器配置完全相同的网络策略,比如完全相同的iptables,完全相同的路由表,这个时候,我们可以通过OverlayFS共享配置文件的方式轻松搞定,但有的时候,如果容器的网络策略需要和容器本身隔离的时候,应该怎么办呢?比如网络策略是具有独立权限的人事先配好在宿主机的某个Net Namespace中的时候,也就是说,它根本不属于容器。这个时候就需要容器的网络配置可以Attach其它的Net Namespace。
最重要的两件套
如果问Docker的运行依赖的东西中,哪些是必不可少的,回答无疑是两个,即Namespace和OverlayFS:
Namespace:提供了进程时间维度的隔离和复用;
OverlayFS:提供了空间维度的隔离(容器layer,即upper)和复用(基础映像,即lower)。
本来,容器这种东西就是要比虚拟机更加轻量的,但至于轻量到什么程度,Docker比较好的定义了一个下界,不能比这个更加轻量了。当然,不考虑Docker在应用部署,服务发布,应用编排,服务迁移上的那些优势,仅仅从底层的角度来讲,firejail定义的下界似乎更加合理,它连OverlayFS都没有强制使用,但是Docker却不行,毕竟从使用者的角度来讲,Docker之所以为Docker,之所以是Docker而不是firejail,原因无疑也就是它的那些应用部署,服务发布,应用编排,服务迁移上优势使然,而构建这些优势的根基,离不开OverlayFS!
至于另外两个,Cgroup以及Linux虚拟网卡,则是两个锦上添花的东西。
本文内容转载至dog250
https://blog.csdn.net/dog250/article/details/81025071
以上是关于以firejail sandbox解析Docker核心原理依赖的四件套的主要内容,如果未能解决你的问题,请参考以下文章
docker 入门(二):docker 和 沙盒、虚拟机以及 Kubernetes 的关系
sh [HDF 3.0 Docker容器启动脚本]使用此脚本启动HDF 3.0 docker容器#docker #hdf #sandbox
sh [HDP沙箱启动]使用此脚本启动HDP沙箱#docker #hdp #sandbox
无法解析远程名称:'api-3t.sandbox.paypal.com'
docker中编译android aosp源码,出现Build sandboxing disabled due to nsjail error