Docker的发展势头一天比一天强劲,它显然在试图解决实际的问题。然而,对如今许多的生产环境用户来说,没有出现优点压倒缺点的局面。在开发、测试和持续性集成等环境下,Docker在让容器吸引广大开发人员方面确实有上佳的表现,不过它还没有颠覆生产环境。按照DockerCon 2015的“生产环境下的Docker”这一主题,我想公开讨论Docker想在生产环境使用场合下得到广泛采用还没有克服的种种挑战。这里提到的问题没有一个是新问题,它们都以某种形式出现在GitHub上。大多数问题我已经在大会演讲中或与Docker团队交流中讨论过。本文倒不是要明确指出什么不再是问题:比如说,新注册中心(registry)克服了旧注册中心的许多不足。本文并没有提到仍然问题重重的许多方面,不过我认为下面这些问题是近期内需要解决的最重的问题;只有解决了这些问题,更多的企业组织才能够迈出一大步,在生产环境中运行容器。我在电子商务公司Shopify运行Docker的经历对本文有很大的影响;一年多来,我们一直在容器上大规模运行核心平台。由于像Docker这样发展这么迅猛的技术,不可能一切都保持现状。如果你发现不正确之处,务必联系我。
映像构建
为大型应用程序构建容器映像依然是个挑战。如果我们要依赖容器映像用于测试、持续性集成和紧急部署,就需要在不到1分钟的时间内将映像准备就绪。Docker文件(Dockerfile)让这对大型应用程序来说几乎不可能。虽然Docker文件易于使用,但是位于过高的抽象层,无法支持复杂的使用场合:
- 带外缓存,面向特别错综复杂的、针对特定应用程序的依赖项;
- 在构建时访问密文(密码、密钥和相关内容),又不将它们提交给映像
- 全面控制最终映像中的层
- 并行处理构建层
大多数人并不需要这些功能,但是对大型应用程序而言,其中许多功能是快速构建映像的先决条件。Chef和Puppet等配置管理软件使用广泛,但是让人觉得用于构建映像过于笨拙。我打赌,在今后十年内,现有形式的这类系统会因容器而逐渐退出历史舞台。然而,许多应用程序依赖它们来配置、部署和编排。Docker文件无法真实地记录下现在由配置管理系统管理的复杂性,但这种复杂性需要在某个地方加以管理。在Shopify,我们最后使用docker commit API,从头开始构建了自己的系统。这个过程很麻烦,我希望这一幕不会出现在任何人身上,我很想摆脱这种局面,但是我们又不得不扫除障碍。很少有人会花这么大的力气去管理用于生产环境的容器。
这个领域会出现什么情况不得而知;目前在这个领域,开展的研究工作并不多(一个例子是dockramp,这是另一种打包器)。Docker引擎会在将来有所改进,将构建基本步骤(添加文件和设置入口点等)与客户端(Docker文件)分开来。为版本1.8所做的合并工作已经让这变得更容易,为配置管理工具厂商、业余爱好者和公司进行试验尝试创造了条件。考虑到配置系统的历史还很短,认为一种标准有望搞定这个问题(就像运行时标准那样)是不切实际的。什么时候可以实现可扩展的映像构建,相当不明朗。据我所知,没人在积极迭代,很遗憾这种现状已维持一年多了。
垃圾收集
每个部署的重大Docker系统到头来要编写垃圾收集器,以便清除主机上的旧映像。使用了各种启发式方法,比如清除超过X天的旧映像,在主机上最多执行Y个映像。Spotify最近开放了其系统的源代码。我们还在很久以前就编写自己的垃圾收集器。我能明白为此设计一种易预测的用户界面(UI)有多难,但是这又是核心中绝对需要的。当生产环境的机器嚷着要存储空间时,大多数人无意中发现要求收集垃圾。最后,你会遇到同一映像,Docker注册中心因庞大映像而溢出,不过这个问题已列在了发行版路线图上(详见https://github.com/docker/distribution/blob/master/ROADMAP.md#deletes)。
迭代速度和核心状态
Docker引擎致力于1.x版本的稳定性。在版本1.5之前,降低准入门槛以便在生产环境得到采用方面所做的工作不多。开发容器的公众心理模式对Docker的成功而言必不可少,Docker害怕破坏这种模式是有其道理的。如果用户体验(UX)方面的每个变化经历的过程时间过长,迭代速度难免受到影响。自版本1.7起,Docker开始发布试验性版本,以网络和存储插件带头。这些功能特性被明确标为“未准备用于生产环境”,可能会从核心中取出或者随时经历重大变化。对于早已看好Docker的公司来说,下面这个是好消息:它让核心开发团队可以更快速地迭代开发新功能,不用担心本着最佳设计的精神而破坏次要版本之间的向后兼容性。公司仍然很难改动Docker核心,因为它需要分支――这是导致最终失败的行为和维护负担,或者需要得到上流接受;对于值得关注的补丁来说,这常常很耗费人力。自版本1.7起,宣布插件后,解决这个问题的策略就很明确:让每一个固执己见的的组件都可以插入,最后显示了“带电池而且可以更换”这种理念的成果,这种理念最早是在2014年的DockerCon欧洲大会上提出来的(不过相当模糊)。在6月份的DockerCon大会上,很高兴听到这归入到“管道”(Plumbing)这个大主题来探讨,作为核心开发团队的重中之重。虽然未来终于大有希望,但是这在今天仍然是个痛点,就跟过去两年一样。
日志
日志是表明有望得益于之前变化的一个方面的例子。这并不是引起强烈关注的问题,却是普遍性的问题。目前没有理想的、普通的解决方案。日志到处都是:尾部日志文件、容器里面的日志、通过挂载发送到主机的日志、发送到主机syslog的日志,通过fluentd(开源数据采集器)等工具来暴露日志,从应用程序直接发送到网络的日志,或者发送到文件的日志,让另一个进程将日志发送到Kafka。在版本1.6中,支持日志驱动程序的功能已并入到核心中(https://blog.docker.com/2015/04/docker-release-1-6/);然而,驱动程序在核心中必须得到接受(这并非易事)。在版本1.7中,已并入了试验性支持进程外插件的功能,但是让我失望的是,它并不随带日志驱动程序。我认为,版本1.8会计划添加这项功能,但是在官方记录中找不到这项。到那时,厂商们就能够编写自己的日志驱动程序。社区内部的共享将轻而易举,大型应用程序再也不必求助于设计定制的解决方案。
密文
迁移到容器的人大多数依赖配置管理,在机器上安全地配置密文;然而,继续沿着配置管理这种老路子配置容器中的密文很笨拙。另一个办法就是将密文与映像一同分发,但是这带来了安全风险,而且很难在开发、持续性集成和生产环境之间安全地回收映像。最纯粹的解决办法就是通过网络访问密文,让容器的文件系统保持无状态。就在不久前,这方面还没有任何面向容器的机制;不过最近,两家颇令人关注的密文代理系统Valut(https://vaultproject.io)和Keywhiz(https://github.com/square/keywhiz)开放了源代码。在Shopify,我们一年半前开发了ejson(ejson是一种简单的库,用嵌入在JSON文件中的公钥加密该文件中的所有值,详见https://www.shopify.com/technology/26892292-secrets-at-shopify-introducing-ejson),以解决这个问题,从而管理JSON文件中非对称加密的密文文件;然而,它就所运行的环境有一些假设,因而让它与密文代理系统相比,不是很理想的一般性解决方案(如果你很好奇,可以参阅这篇文章https://www.shopify.com/technology/26892292-secrets-at-shopify-introducing-ejson)。
#p#
文件系统
Docker依赖来自文件系统的写时拷贝机制(CoW)。这是为了确保如果有100个容器是从一个映像运行的,你就不需要100倍的磁盘空间。相反,每个容器在映像上面创建一个CoW层,只有利用原始映像创建文件时,才使用磁盘空间。容器的“规范市民”对容器里面的文件系统带来的影响极小,因为这类变化意味着容器具有了状态,这是绝对禁止的。这类状态应该存储在映射到主机或的卷上或通过网络来存储。此外,层次技术节省了部署之间的存储空间,因为映像常常相似,有共同的层。在Linux上支持CoW的文件系统存在的问题是,它们都有点新。我们Shopify在几百个负载相当大的主机上遇到过几种文件系统:
AUFS。看到整个分区在我们要重新挂载的地方锁起来。速度缓慢,耗用大量内存。代码库很庞大,难以读取,这可能就是为什么它没有被接受、进入到上游,因而需要自定义内核。
BTRFS。面临学习曲线,需要学用一套新的工具,因为du和ls不管用。与AUFS一样,我们看到分区冻结,内核锁隹,尽管玩猫捉老鼠的游戏,希望内核版本保持是最新版本。临近磁盘空间极限时,BTRFS的行为捉摸不定,如果你有1000个这样的CoW层(用BTRFS的术语来说是子卷),也是如此。BTRFS耗用大量内存。
OverlayFS。这在3.18版本中已并入到Linux内核,对我们来说已相当稳定、快速。它耗用的内存要少得多,因为它设法在索引节点(inode)之间共享页面缓存。遗憾的是,它需要你运行未被大多数发行版采用的较新内核,这常常意味着构建自己的内核。
幸好对Docker来说,Overlay文件系统很快就会无所不在,不过在我们看来,运行大量节点时,AUFS这一默认文件系统对生产环境来说仍然很不安全。不过很难说在这里该如何是好,因为大多数发行版也并不随带已准备支持Overlay的内核(有人提议Overlay作为默认文件系统,但由于这个原因遭到驳斥),不过这绝对是这个领域的发展方向。看来我们除了等待别无他法。
依赖处于前沿的内核功能
正如Docker依赖最前沿的文件系统那样,它还充分利用最近为内核添加的大量功能特性,也就是命名空间以及不是太新,又不常使用的控制组(cgroup)。这些特性(尤其是命名空间)还没有在业界得到广泛采用,因而还没有经过考验。我们偶尔会遇到这些特性存在的不明显错误。我们碰到网络命名空间在生产环境下被禁用的情况,那是由于我们遇到过相当多的软死锁,事后查明这些软死锁与实施有关,却又没有资源从上流解决问题。内存控制组耗用相当多的内存,我听到过外头反映不可靠的情况。随着容器得到越来越广泛的使用,大公司可能会率先做好这项稳定工作。
我们在生产环境中碰到的需要加固的一个例子就是僵尸进程(https://blog.phusion.nl/2015/01/20/docker-and-the-pid-1-zombie-reaping-problem/)。容器在PID命名空间里面运行,这就意味着容器里面的第一个进程有pid 1。容器里面的init需要执行确认已死的子进程这一特殊任务。某个进程死后,并不立即从内核进程数据结构中消失,而是成为一个僵尸进程。这确保了父进程可以通过wait(2)检测到子进程已死。然而,如果子进程成为孤儿,其父进程就被设成init。该进程随后死后,init的任务就是通过wait(2)确认子进程已死――不然,僵尸进程就会永远存在。这样一来,内核进程数据结构会被僵尸进程耗尽,之后你就得自己想办法。对基于进程的master/worker模式来说,这种场景相当常见。如果worker进程逃出外壳,并花了很长时间,master进程就会使用SIGKILL终结等待外壳命令的worker进程(除非你使用进程组,一下子终结整个进程组)。逃出外壳的派生进程然后被init继承。等该进程终于完成后,init需要对它执行wait(2)操作。Docker引擎可以解决这个问题,通过Docker引擎使用设置PR_SET_CHILD_SUBREAPER确认容器里面的僵尸进程,https://github.com/docker/docker/issues/11529有所描述。
安全
运行时安全对容器来说仍要打个问号;想针对生产环境进行加固是经典的先有鸡还是先有蛋的安全问题。以我们Shopify为例,我们并不依赖容器提供任何额外的安全保障。然而,许多使用场合却依赖容器提供额外保障。由于这个原因,大多数厂商仍在虚拟机中运行容器,而虚拟机的安全久经考验。由于操作系统虚拟机胜出,我希望看到虚拟机在今后十年内消失,因为有人曾在Linux邮件列表上说过:“我曾经听到虚拟机管理程序是活生生的证据,证明了操作系统的无能。”容器在虚拟机(硬件层虚拟化)和PaaS之间(应用程序层)提供了完美的中间体。我知道,运行时安全方面在做更多的工作,比如说能够将系统调用列入黑名单。映像方面的安全一向是问题的根源,但是Docker正在借助libtrust和notary――它们将是新的发行层(https://github.com/docker/distribution)的一部分的,积极改进这方面。
映像层和传输
- 第一个版本的Docker为映像的构建、传输和运行时环境采取了一条巧妙的捷径。它选择了适用于所有情况的工具:文件系统层,而不是为每个问题选择一种合适的工具。这种抽象机制一直作用于在生产环境中运行容器。这是完全可以接受的最小可行产品实用主义,但是每个问题都能极其高效地得到解决。
- 映像构建可以表示为有向工作图。这样可以弄清楚缓存和并行处理,以便迅速地构建可预测的映像。
- 映像传输而不是使用映像层,它就可以执行二进制差异化(binary diffing)。这个话题已研究了几十年。分发层和运行层越来越分离开来,为这种优化创造了条件。
- 运行时环境应该就实施单单一个CoW层,而不是再次使用随意的映像层抽象。如果你在第一次读取时使用AUFS之类的统一文件系统,就遍历链接文件列表来汇编最终文件。这很缓慢,而且完全没有必要。
层模式对映像传输以及对构建来说是个问题。这意味着,你必须极其小心对待映像每个层中的东西,因为不然你很可能到头来为大型应用程序传输100MB的数据。如果你在自己的数据中心里面有大型链接,这个问题不大,但是如果你想使用Docker Hub之类的注册中心服务,这就会通过公开的互联网来传输。映像分发目前正在积极开发之中。Docker公司有足够的动机将这方面做得可靠、安全而快速。至于构建,我希望这为插件创造条件,以便一种优秀的解决方案浮出水面。
结束语
另外许多话题有意没有探讨,比如存储、网络、多租户、编排和服务发现。如今Docker需要的是更多的人将容器部署到生产环境,而且是大批人在部署。遗憾的是,许多公司一开始就对PaaS寄以厚望,利用当前的架构过度补偿。如果你规模很小,或者规划将Docker部署到全新的环境,这种方法才可行。想在生产环境下得到更广泛的使用,我们就需要解决上述一些问题,以便让Docker的优点明显压倒缺点。
Docker将自己置于令人兴奋的位置,充当PaaS的接口,无论是发现、网络还是服务发现,应用程序没必要关心底层基础设施。这是好消息,因为正如Solomon所说,Docker的最大优点就是,它让人们达成了共识。我们终于开始就映像和运行时环境之外的其他方面达成共识。
我与Docker公司的人员深入探讨过所有上述问题。在一定程度上存在所有这些问题的GitHub Issues。我在这里的目的是,仅仅是提供个人之见,阐明哪几个方面对降低Docker的准入门槛最为重要。我对未来满怀激情,但是我们仍有大量工作要做,才能让Docker应用到更多的生产环境。
原文标题:Why Docker is Not Yet Succeeding Widely in Production