## Mesos 容器引擎的架构设计和实现解析
文/张乾
>提到容器,大家第一时间都会想到 Docker,毕竟 Docker 是目前最为流行的容器开源项目,它实现了一个容器引擎(Docker engine),并且为容器的创建和管理、容器镜像的生成、分发和下载提供一套非常便利的工具链,而它的容器镜像格式几乎就是业界的事实标准。但其实除了 Docker 之外,在容器的开源生态圈中还有其他一些项目也在做自己的容器引擎,这样的项目一般也被称作为容器运行时(container runtime),比如:CoreOS 的 rkt 和 Mesos 的容器引擎(Mesos containerizer)。在本文中,笔者将对 Mesos 容器引擎进行全面介绍,解释在 Docker 如此流行的情况下 Mesos 为什么还要坚持做自己的容器引擎,介绍 Mesos 容器引擎的总体架构和各核心组件,以及它对容器各相关标准规范的采纳和支持。
### 容器和容器引擎的定义
首先我们来了解一下什么是容器。笔者对容器的定义是:一个或一组使用了 Cgroups 做资源限定,使用了 namespace 做资源隔离,且使用了镜像文件做根文件系统的进程。如图1所示。
图1 容器3要素
由此可见,实现容器的三大核心技术分别是:
- Cgroups(Control Cgroups,控制群组):Linux 中的 Cgroups 包含多个不同的子系统,如:CPU、memory、device 等。通过这些子系统就可以对容器能够使用的各种资源进行限定,比如:通过 CPU 子系统可以限定容器使用 CPU 资源的相对权重和单位时间内能够使用的的 CPU 时间。
- Namespace(命名空间): Linux 同样支持多个namespace,如:mount、network、pid 等。通过这些 namespace 可以对容器进行不同维度的资源隔离,比如:通过 mount namespace 可以让容器具有自己独立的挂载空间,在主机或别的容器中发生的挂载事件对该容器就不可见,反之亦然。通过 network namespace 可以让容器具有自己独立的网络协议栈,而不必和其所在主机共用同一个网络协议栈。
- Layered filesystem(分层文件系统):Linux 中的 layered filesystem 有多种不同的实现,如:AUFS、overlayfs 等。通过这些 layered filesystem 配合 mount namespace 就可以快速部署出容器自己独立的根文件系统。而且,基于同一个镜像文件创建出来的多个容器可以共享该镜像文件中相同的只读分层,以达到节省主机磁盘空间的效果。
上面这三种技术都是在 Linux 系统中存在已久且相对成熟的技术,但让终端用户直接使用它们来创建和管理容器显然并不方便。所以,容器引擎就应运而生了,它所做的主要工作就是将这三种技术在其内部有机地结合和利用起来以实现创建容器和管理容器的生命周期,并对外提供友好的接口让用户能够方便的创建和管理容器。Cgroups、namespace 和 layered filesystem 的详细介绍我就不再本文中赘述了,感兴趣的读者可以查阅 Linux 中这三种技术的相关文档。
需要指出的是,容器引擎对这三种技术的使用往往是有选择且可定制的,比如:用户可以通过容器引擎创建一个使用 cgroups memory 子系统但不使用 CPU 子系统的容器,这样的容器对内存资源的使用就会受到相应的限定,但对 CPU 资源的使用则不受任何限定。用户也可以创建一个使用 mount namespace 但不使用 network namespace 的容器,这样的容器就会有自己独立的挂载空间,但和主机共用一个网络协议栈。Mesos 容器引擎在这方面的可定制化进行得非常彻底,除了上面所说的对 cgroups 子系统和 namespace 的定制之外,Mesos 容器引擎还能够支持无镜像文件创建容器,这是其它容器引擎所不具备的。
### Mesos 容器引擎产生的背景
在 Docker 如此流行的情况下,Mesos 为什么还要坚持做自己的容器引擎呢?其实 Mesos 在很早期的版本就和 Docker 进行了集成,用户可以通过 Mesos 创建一个 Docker 容器,在内部实现上,Mesos agent 会调用 Docker 的命令行和 Docker engine 通信,以让其创建 Docker 容器。这也就是意味着 Mesos 对容器的管理严重依赖于 Docker engine,而这种做法的问题是:
- 稳定性不足:Mesos 常常会被用来管理几千甚至上万节点的生产环境,而在如此大规模的生产环境中,稳定性极其重要。而在这样的环境中,通过实测我们发现 Docker engine 的稳定性的确有所不足,有时会出现停止响应甚至一些莫名其妙的 Bug,而这样的问题反映到 Docker 社区中后有时又无法及时得到解决。这就促使了 Mesos 的开发者开始设计和实现自己的容器引擎。
- 难于扩展:Mesos 的用户常常会提出一些和容器相关的新需求(比如:让容器能够使用 GPU 资源,通过 CNI 配置容器的网络,等等),而这些需求都受限于 Docker engine 的实现,如果 Docker 社区拒绝采纳这些需求,或有完全不同的实现方式,那 Mesos 作为 Docker engine 之上的调用方也无计可施。
众所周知,Mesos 的定位是数据中心操作系统,它是一个非常好的通用资源管理和资源调度系统,一开始就是一个“大脑级”的存在,但如果只有“大脑”没有“四肢”(对容器的支持就是“四肢”的一种),或“四肢”掌握在别人手中,那 Mesos 本身和其生态圈的可持续发展显然是受限的。所以,发展自己的“四肢”是 Mesos 逐步发展壮大的必然选择。
基于上述这些原因,Mesos 社区决定要做自己的容器引擎,这个容器引擎完全不依赖于 Docker engine(即:和 Docker engine 没有任何交互),但同时它又完美兼容 Docker 镜像文件。这也就意味着,用户可以通过 Mesos 在一台没有安装运行 Docker engine 的主机上,基于任意 Docker 镜像创建出容器。
### Mesos 容器引擎的总体架构和核心组件
首先我们来看一下 Mesos 的总体架构,以及容器引擎在其中的位置。
图2 总体架构
图2中蓝色部分是 Mesos 自身的组件,可以看到 Mesos 是典型的 master + agent 架构。Master 运行在 Mesos 集群中的管理节点上,可以有多个(一般设置为三个、五个等奇数个),它们相互之间通过 ZooKeeper 来进行 leader 选举,被选举成 leader 的 master 对外提供服务,而其他 master 则作为 leader master 的备份随时待命。Master 之上可以运行多个计算框架,它们会调用 master 提供的 API 发起创建任务的请求。Master 之下可以管理任意多个计算节点,每个计算节点上都运行一个 agent,它负责向 master 上报本节点上的计算资源,并接受 master 下发的创建任务的请求,将任务作为容器运行起来。而 agent 内部的一个组件 containerizer 就是用来管理容器的生命周期的(包括:容器的创建/更新/监控/销毁),它就是 Mesos 的容器引擎。
我们再来进一步看一下 Mesos 容器引擎内部的总体架构和核心组件。
如图3所示,Meoss 容器引擎内部包含了多个组件,主要有:launcher、provisioner(其内部又包含了 store 和 backend 两个组件)和 isolator。下面来逐一介绍这些组件的主要功能和用途。
图3 Mesos 容器引擎内部的总体架构和核心组件
##### Launcher
Launcher 主要负责创建和销毁容器,由于容器的本质其实就是主机上的进程,所以 launcher 在其内部实现上,主要就是在需要创建容器时,调用 Linux 系统调用 fork()/clone() 来创建容器的主进程,在需要销毁容器时,调用 Linux 系统调用 kill() 来杀掉容器中的进程。Launcher 在调用 clone() 时根据需要把容器创建在其自己的 namespace中,比如:如果容器需要自己的网络协议栈,那 launcher 在调用 clone() 时就会加入 CLONE _ NEWNET 的参数来为容器创建一个自己的 network namespace。
Launcher 目前在 Mesos 中有两种不同的实现: Linux launcher 和 Posix launcher,上面提到的就是 Linux launcher 的实现方式,Posix launcher 适用于兼容 Posix 标准的任何环境,它主要是通过进程组(process group)和会话(session)来实现容器。在 Linux 环境中, Mesos agent 会默认启用 Linux launcher。
##### Provisioner
为了支持基于镜像文件创建容器,Mesos 为其容器引擎引入了 provisioner。这个组件完成的主要工作是通过 store 组件来下载和缓存指定的镜像文件,通过 backend 组件基于指定的镜像文件来部署容器的根文件系统。
##### Store
Mesos 容器引擎中目前已经实现了 Appc store 和 Docker store,分别用来支持 Appc 格式的镜像和 Docker 镜像,此外,Mesos 社区正在实现 OCI store 以支持符合 OCI(Open Container Initiative)标准格式的镜像。所以基本上,针对每种不同的镜像文件格式, Mesos 都会去实现一个对应的store。而以后当 OCI 镜像格式成为业界容器镜像文件的标准格式时,我们可能会考虑在 Mesos 容器引擎中只保留一个 OCI store。
Store 下载的镜像会被作为输入传给 backend 来去部署容器的根文件系统,且该镜像会被缓存在 agent 本地的工作目录中,当用户基于同一镜像再次创建一个容器时,Store 就不会重复下载该镜像,而是直接把缓存中的镜像传给 backend。
##### Backend
Backend 的主要工作就是基于指定镜像来部署容器的根文件系统。目前 Mesos 容器引擎中实现了四个不同的 backend。
- AUFS:AUFS 是 Linux 中的一种分层文件系统。AUFS backend 就是借助这种分层文件系统把指定镜像文件中的多个分层挂载组合成一个完整的根文件系统给容器使用。基于同一镜像文件创建出来的多个容器共享相同的只读层,且各自拥有独立的可写层。
- Overlay:Overlay backend 和 AUFS backend 非常类似,它是借助 Overlayfs 来挂载组合容器的根文件系统。Overlayfs 也是一种分层文件系统,它的原理和 AUFS 类似,所以,AUFS backend 所具备的优点 Overlay backend 都有,但不同的是 Overlayfs 已经被 Linux 标准内核所接受,所以,它在 Linux 平台上有更好的支持。
- Copy:Copy backend 的做法非常简单,它就是简单地把指定镜像文件的所有分层都拷贝到一个指定目录中作为容器的根文件系统。它的缺点显而易见:基于同一镜像创建的多个容器之间无法共享任何分层,这对计算节点的磁盘空间造成了一定的浪费,且在拷贝镜像分层时也会消耗较大的磁盘 I/O。
- Bind:Bind backend 比较特殊,它只能够支持单分层的镜像。它是利用 bind mount 这一技术把单分层的镜像以只读的方式挂载到指定目录下作为容器的根文件系统。相对于 Copy backend,Bind backend 的速度更快(几乎不需要消耗磁盘 I/O),且多个容器可以共享同一镜像,但缺点就是只能支持单分层镜像,且根文件系统是只读的,如果容器在运行时需要写入数据,就只能通过额外挂载外部卷的方式来实现。
在用户没有指定使用某种 backend 的情况下,Mesos agent 在启动时会以如下逻辑来自动启用一种 backend。
- 如果本节点支持 Overlayfs,启用 Overlay backend。
- 如果本节点不支持 Overlayfs 但支持 AUFS,启用 AUFS backend。
- 如果 OverlayFS 和 AUFS 都不支持,启用 Copy backend。
由于 Bind backend 的局限性较大,Mesos agent 并不会自动启用它。
##### Isolator
Isolator 负责根据用户创建容器时提出的需求,为每个容器进行指定的资源限定,且指引 launcher 为容器进行相应的资源隔离。目前 Mesos 内部已经实现了十多个不同的 isolator,比如:cgroups isolator 通过 Linux cgroups 来对容器进行 CPU、memory 等资源的限定,而 CNI isolator 会指引 launcher 为每个容器创建一个独立 network namespace 以实现网络隔离,并调用 CNI 插件为容器配置网络(如:IP 地址、DNS 等)。
Isolator 在 Mesos 中有着非常好的模块化设计,且定义了一套非常清晰的 API 接口让每个 isolator 可以根据自己所需要完成的功能去有选择地实现。这套 isolator 接口贯穿容器的整个生命周期,Mesos 容器引擎会在容器生命周期的不同阶段通过对应的接口来调用 isolator 完成不同的功能。下面是一些主要的 isolator 接口:
- prepare():这个接口在容器被创建之前被调用,用来完成一些准备性的工作。比如:cgroups isolator 在这个接口中会为容器创建对应的 cgroups。
- isolate():这个接口在容器刚刚被创建出来但还未运行时被调用,用来进行一些资源隔离性的工作,比如:cgroups isolator 在这个接口中会把容器的主进程放入上一步创建的 cgroups 中以实现资源隔离。
- update():当容器所申请的资源发生变化时(如:容器在运行时申请使用更多的 CPU 资源),这个接口会被调用,它用来在运行时动态调整对容器的资源限定。
- cleanup():当容器被销毁时这个接口会被调用到,用来进行相关的清理工作。比如:cgroups isolator 会把之前为容器创建的 cgroups 删除。
借助于这种模块化和接口标准化的设计,用户可以很方便地根据自己的需求去实现一个自己的 isolator,然后将其插入到 Mesos agent 中去完成特定资源的限定和隔离。
### Mesos 容器引擎对容器标准规范的支持
容器这项技术在近几年来飞速发展,实现了爆炸式的增长。但就像任何一种成熟的技术一样,在度过了“野蛮生长期”后,都需要制定相应的规范来对其进行标准化,让它能够持续稳定地发展。容器技术也不例外,目前已知的容器相关的标准规范有:
- OCI(Open Container Initiative):专注于容器镜像文件格式和容器生命周期管理的规范,目前已经发布了1.0的版本。
- CNI(Container Network Interface):专注于容器网络支持的规范,目前已经发布了0.6.0版本。
- CSI(Container Storage Interface):专注于容器存储支持的规范,目前还在草案阶段。
标准规范带来的益处是显而易见的:
- 对于终端用户:标准化的容器镜像和容器运行时能够让用户不必担心自己被某个容器引擎所“绑架”,可以更加专注于对自身业务和应用的容器化,更加自由地去选择容器引擎。
- 对于网络/存储提供商:标准规范中定义的网络/存储集成接口把容器引擎的内部实现和不同的网络/存储解决方案隔离开来,让各提供商只需实现一套标准化接口就可以把自己的解决方案方便地集成到各个容器引擎中去,而不必费时费力地去逐个适配不同的容器引擎。
Mesos 容器引擎在设计和实现之初,就持有拥抱和采纳业界标准规范的态度,是最早支持 CNI 的容器引擎之一,且目前正在积极实现对 OCI 镜像规范的支持。CSI 目前正在由 Mesos 社区和 Kubernets 社区一起主导并逐步完善,待 CSI 逐渐成熟后,Mesos 容器引擎自然会第一时间支持。日后,作为同时支持三大标准规范的容器引擎,Mesos 容器引擎自然会更好地服务上层应用框架和终端用户,同时更加完善地支持各种主流网络/存储解决方案。