云栖TechDay活动第十八期中,阿里云容器服务团队的核心成员陈萌辉带来了题为《Docker插件机制详解》的分享,分享中,他结合阿里云容器服务实践介绍了Docker插件的基本原理、实现方法以及插件机制未来的演进。
幻灯片下载地址:https://yq.aliyun.com/attachment/download/?filename=bdefe06ba7a14d7604af5a63a4bcc4f3.pdf
以下为现场分享观点整理。
为什么需要Docker插件?
Docker之所以这么火并且有很多人愿意使用它,其中涉及到很多方面的因素,例如功能性以及隔离性等各种各样的原因。其中Docker的开箱即用功能是一个非常具有特色的优点,Docker安装后即可使用,无需再做其他的配置;同时Docker自包含的镜像提取出来就是一个完整的系统;此外,它自带网络、进程、文件系统的隔离,使用者可以很方便在一台机器上部署各种互相隔离的系统。因此,Docker的应用就非常简单、方便,但是它这种简单方便也带来一些问题。Docker把一些场景固化掉了,例如Docker的网络模型和存储,固化之后,当特定场景不符合Docker预期的要求时,Docker就无法满足要求,此时需要重新定制Docker。例如在你的网络环境里面,IP地址是预先分配好的,跟Docker预留的不匹配或者是你想自定义一个容器间互相互联一个网络,Docker预留的那些方式满足不了需求时,需要对它进行扩展。
Docker插件机制简介
Docker公司对Docker的扩展分成三个级别,从低到高分别是:User-facing API,该API是Docker提供用于串联出一些场景的API;第二层是插件(Plugins),也是今天的主要介绍内容,Plugins和Docker是两个相互独立的进程,它们之间通过一些预定的通讯协议进行功能扩展;第三层是叫Drivers,是Docker实现功能的一些驱动,例如一些文件存储的驱动。目前,大多数Drivers都是官方出的,当有特定需求或者是用到独特技术时,开发者可以自己写相应的驱动。
Docker的插件是增加Docker引擎功能的进程外扩展,也就是说它和Docker engine是两个独立分开的进程。插件运行在Docker daemon外,通过一些插件的发现机制和预定义协议进行交互通信,这样做的好处是:当更新插件时,无需重新重启更新Docker。
目前Docker官方提供的插件的类型有四种:
-
第一种是授权(authz)。可以写一个插件带对每一个请求做验证,例如,可以对公司的通信录或者权限系统绑定,然后给一些请求做授权,使得一部分人能够执行查看类的操作,另外一部分人可以执行创建删除类的操作,这样就可以将权限系统与Docker结合起来了。
-
第二种是数据卷(VolumeDriver)插件。用于提供不同的存储功能,是应用最多的一类插件。
-
第三种是网络(NetworkDriver)插件。该插件用于提供容器之间互联网路模型,这种插件也比较普遍,例如阿里云容器服务提供了针对阿里云的网络的网络插件,在VPC下,所有容器都是互相可以访问的,而且是没有性能损耗的。
-
第四种是IP地址管理(IpamDriver)插件,当需要对IP地址的分配进行保留时,可能需要该插件。
下面介绍一下Docker插件的发现机制。Docker规定了三种插件发现的机制,因为每一个插件其实它本身就是一个HTTP服务器,Docker是通过HTTP访问和插件进行通信的。那么Docker是如何知道Http服务器的地址呢?它规定了三种方式:第一个是.sock文件,在/run/docker/plugins下写一个unix socket文件,则Docker认为这是一个插件,而且该文件名就是插件的名字;第二类是.spec文件,它是一个纯文本文件,文件内容是插件访问地址;第三类.json文件,它包含的内容比.spec文件内容多,包括插件的Name、Addr、TLSConfig。
上文提到了插件和Docker 引擎是两个独立进行,但插件的生命周期是由Docker的官方文档所规定的,插件必须先于Docker daemon启动,后于Docker daemon停止,也就是插件的生命周期必须要长于Docker daemon。但在具体是实现时,由于第一次访问插件时才激活,例如一个存储插件,当第一次访问这一类存储或创建这一类储存卷时,才会真正激活插件,因此插件其实可以晚于Docker daemon启动,也可以在Docker结束之间结束,这也为之后将插件放到容器里面提供了很好的条件。
Docker官方提供了插件SDK,使用者所需的发现相关的功能,在SDK中大部分都自带,因此只需简单引用即可,将精力解放于如何实现问题上。
Docker数据卷与存储插件
下面结合数据卷来讲解一下存储的插件。提到数据卷就不得不提Docker的分层文件系统,Docker之所以风靡全球,分层文件系统可能贡献了其中三分之一的能量。如图所示,分层文件系统的最底层是Bootfs,是容器和数据集共享的一层;在其上是base Image层和image层,最上面才是可写入的container。对于一个容器来说,它里面所有的文件都是可能处于不同层内,当修改该文件时,其实是修改的拷贝文件。比如说,你修改了一个系统的文件,其实在磁盘上,原来操作系统的文件并没有发生改变,而是拷贝出来的另一份(容器可见的文件)才是被你修改了。但如果其他人引用了同样一个操作系统时,所看到的文件是没有被修改的文件。分层文件系统有一个很大的优点,它可以把软件的构建变成类似可视化的方式。但分层文件系统同时也带来一些问题:第一,它的系统性能差,因为在修改文件时,有一个拷贝过程,势必会导致性能的损耗,同时在读取时,存在一个回溯操作,也就是它要从下面往上找,找到对应文件所在的层,这也导致了它性能比原操作系统差。
另一个问题是它的生命周期和容器相同,也就是说启动一个容器,在容器被写入例如一些日志或应用相关数据,当容器一旦被删除,这些数据也随之消失,因此一个需要长期保留的数据是无法写入容器的。
为了解决上述问题,Docker提供了数据卷功能,数据卷是将主机文件夹挂在容器内部,绕过分层系统,相当于容器内部直接读写某主机上的某一文件夹。
这样一来它的性能和主机性能是一致的,生命周期也脱离了容器的生命周期,在容器挂靠之后,数据依然存在主机的磁盘中。上图形象地展示了这一过程,当用docker run -v建立一个数据卷时,其实是把这一文件夹从主机挂到容器内部,该文件夹其实已经存放在主机上了。
但Docker官方提供的数据卷也有一些缺点:第一,仅能够读写本地磁盘,当访问网络数据时,数据卷就无能为力了;另外,数据卷不能随容器迁移,因为它是读写本机的磁盘,如果容器从A机器迁移到B机器,它无法帮你把数据一块迁移过去,无法实现共享数据。
那为了解决上述问题,阿里云容器服务提供了两类网络存储数据卷:第一类是OSSFS数据卷,第二类是叫NAS数据卷。
存储插件的功能是管理数据卷的生命周期,它的主要工作是将第三方存储映射到Host本地文件系统中,以便同期使用该数据卷。Docker本身可以使用本地的数据卷,如果没有修改Docker是无法直接访问网络数据卷的,将网络数据卷映射到本地文件系统的过程如上图所示:通过命令行给Docker daemon发送创建数据卷的命令,然后该命令通过Plugin APIs访问到Volume Drive插件,然后该插件将第三方的存储映射到宿主机上面,就可以实现网络数据卷的访问。
Docker定义了一些存储插件API,是Docker下发给存储插件的,需要按照API的格式返回相应的数据,一共有七个API:
-
/VolumeDriver.Create:当需要创建数据 volume 时,调用该API
-
/VolumeDriver.Remove:当需要删除数据volume时,调用该API
-
/VolumeDriver.Mount:容器每次启动时都会调用该API一次
-
/VolumeDriver.Path:返回volume在主机上的实际位置
-
/VolumeDriver.Unmount:容器每次停止时,调用该API
-
/VolumeDriver.Get:docker volume inspect时,调用该API
-
/VolumeDriver.List:激活插件时,调用该API,用于询问当前已有的volume,防止重复创建。
从这些API可以看出,对存储插件而言,Docker daemon所做的事情很少,它就是将这些命令转到Volume Plugin,然后所有的工作,包括所有状态的存储都是由Volume Plugin自身完成。
数据的存储插件的需要有一些持久存储的东西。例如某些特定的参数持久化下来,当下次重启时,可以再次还原回来。
写一个存储插件时非常简单的,只需要把上面的七个请求实现即可,熟悉的人两天就可以写写完,不是很熟练二代开发者一周内也可完成。但是功能完成后,还有其他的事情需要考虑。
第一需要考虑是否可以在容器中运行插件。因为插件和Docker daemon是两个完全独立的进程,但如果一个插件是在主机上的一个进程时,是有一定的不便之处。因为它的生命周期中有Docker,Docker中没有方便的方法进行管理。另外插件执行时需要root权限,对于容器服务的产品,它是不适合的。
目前的设计方式是将存储插件跑到容器中,这样Docker就可以获得管理权限和root权限。但是插件在容器内运用存在一个令人头疼的额问题:容器的Mount namespace。上图比较简单地演示了该问题,上面是主机的文件系统,它真实对应了硬盘的文件系统。每一个容器都有自己的文件系统。容器的文件系统和主机文件系统是完全隔离的,在容器内做一些mount操作,完全不影响主机上的文件系统,主机上根本无法发现mount过的文件。如果存储插件运行在容器内时,无乱在容器内做任何mount操作,主机和其他容器都无法发现,这样做就没有任何意义。
突破mount space有两种方式:
第一种是nsenter,可以在容器中修改主句的mount点;第二种方式是Docker1.10开始提供的shared_mount,它的意思是,虽然处于不同的空间,但是一个变化时可以通知另一个,例如容器文件系统中,通过修改目录,当容器里面的插件由变化是,它会通知主机文件系统,使得主机能够看到该变化。
阿里云容器服务提供了三种数据卷:
-
OSSFS数据卷:基于FUSE,将OSS Bucket映射为本地文件系统。
-
NAS数据卷:基于NFS协议,按需扩容,高性能高可靠性。它的后台是一个高性能、高可靠性的服务。它默认数据是三份,不会丢数据。它是不同用户之间会做隔离的,这样的话A用户的请求在性能上不会影响B用户。
-
云盘数据卷:将云盘当成一个数据卷挂在容器中。
上图对比三中数据卷的优缺点和使用范围。
OSSFS数据卷的优点在于它能跨主机共享,同时OSS本身就是一个比较成熟的服务,所以很快能够使用起来。但是它的缺点同样明显:首先读写/IS性能低,读写性能低是由于每次修改文件时,都会导致文件的重写。IS性能低是因为在获取文件时,需要服务器将文件罗列出来,当文件较多时,往往会耗费10秒甚至更长的时间。OSSFS数据卷的特点决定了它只适合在小数据量、无修改的场景下使用。例如配置文件和附件上传。
NSA数据卷的优点也是跨主机共享的,同时它可以按需扩容,高性能、高可靠性,挂载速度高。NSA数据卷的缺点是成本略高。
它适应于一些共享数据的重IO应用,例如文件服务器等需要快速迁移的重IO应用。
云盘数据卷目前尚未开放,还在等待中,但应该很快就会开放出来。它的优点就是性能很高;它的缺点是不能跨主机共享,一个云盘只能挂在一台主机上,无法同时挂载不同主机上。此外它的挂率也是比较慢的。因为云盘数据卷适合如数据库、Redis等不需要共享数据的应用。
Docker插件系统的发展
Docker的插件的系统从Docker1.6版本引入,目前是Docker1.10版本。它存在一些历史问题,现在新版本中也在改进这些问题。
第一个问题是它无法控制启动顺序。现在提到的一容器化方式考虑存储、网络类的插件,很大的一个问题就是:插件必须在其他容器启动之间启动,这样才能使得其他容器正常运转;同时必须在其他容器停止之后才能停止,否则就会影响其他容器的使用。在以前的版本内,是没有办法很好控制这个事情,通过同Docker的修改,在阿里云容器服务中可以保证插件在用户逻辑前启动。
第二个问题是插件分发混乱,缺乏统一的渠道。例如开发者新写的插件想共享给别人,缺乏统一的方式和行为规范。
在Docker1.12中开发了一些特性。第一个是highly available services,它可以把插件独立于其他容器对待,使得插件和Docker engine同时启动停止。
同时,它还引入Docker store,使得插件以镜像的形式分发。
在Docker1.13以及以后,他们列出了一些后续开发的项目:
-
Per node plugin,保证plugin部署在每个节点上。
-
Swarm-deployed Plugins,可以在集群范围内部署plugin,集中管理插件。
- 提供编排插件,目前编排都是官方提供的,现在计划将该功能开放出来,使得调度可以引入第三方调度策略,完成特定需求定制。