从我上一次对Docker进行评价到现在已然经过了一年有余,想当初我在文章里对这套容器技术方案的架构设计缺陷与糟糕的用户体验做出了严厉的批判。不过在这段时间当中,Docker项目也开始逐步走向成熟,迎来自己的1.0版本并在Amazon的推动下声名大噪,但同时用户挫败感、过度宣传引发的指责甚至因漏洞遭利用而引发主机感染越来越多。当然,Docker Hub中私有库的引入让用户不必再为了托管部署而运行自有Registry系统,再配合webhook以及同GitHub的紧密集成, Docker看起来有一个良好的前途。
有鉴于此,我决定再给Docker一次机会,并以六个月为周期将其引入生产环境。结果非常糟糕,Docker性能极差,而旁门左道的解决方案加上以用户体验为代表的种种短板简直令人抓狂。实际上,Docker的性能表现实在太差,禁用缓存功能竟然能够加快build速度。
(感兴趣的朋友可以查看reddit与ycombinator网站上与该主题相关的讨论内容)。
Dockerfile
Dockerfile存在着一系列问题,它令人讨厌、充满局限、不伦不类而且包含根本性缺陷。假如要构建一个库的多个镜像,例如第二个镜像包含内容调试工具,但两个镜像拥有同样的基础运行要求。Docker不支持这种做法(详见9198号问题),我们无法扩展Dockerfile(详见735号问题),使用子目录会破坏构建上下文并导致用户无法使用ADD/COPY(详见2224号问题)或者“管道(piping)”(详见2112号问题),我们也不能在构建过程中通过环境变量实现有条件的指令变更(详见2637号问题)。
我们给出的解决方案是创建一个基础镜像,两个特定环境的镜像以及其它一些包括重命名以及sed替换功能的Makefile自动化。除此之外,Docker中还有一些意想不到的“功能”有可能导致环境变量$HOME消失,进而产生无用的错误信息。太令人厌恶了。
Docker缓存/层
Docker有能力利用COW(即写入时复制)文件系统实现缓存Dockerfile指令,这一点与LVM快照机制相似,而且直到最近都只支持AuFS,而后者还存在大量问题。之后,0.7版本引入了多种不同的COW实现方式以改进稳定性与性能,感兴趣的朋友可以点击此处了解更多细节信息。
然而,这套缓存系统不智能,它不能阻止单一指缓存(详见1996号问题),产生了一些意料之外的副作用。它的运行速度也极为缓慢,如果禁用缓存并避免使用层,其构建速度甚至能够得到提升。而Docker Hub缓慢的上传/下载速度则让情况进一步恶化,这个问题我们将在下文作进一步评述。
这些问题均源自Docker整体所采用的糟糕的架构设计,这直接导致它即使是在完全不适用的情况下,依然会强制执行线性指令(详见2439号问题)。作为构建缓慢的解决方案,可以使用支持异步执行的第三方工具,例如Salt Stack、Puppet甚至bash,它们完全能够达成层的目的而使层变得没用。
Docker Hub
Docker鼓励用户通过Docker Hub进行社会化合作。用户可以在上面发布Dockerfile——包括公开与私有文件。其他用户可以通过FROM指令而不是复制/粘贴来继承并使用这些Dockerfile。该生态系统类似于AWS市场以及Vagrant Boxes中的AMI,从理论上讲还是非常有用的。
然而由于一些原因,Docker Hub的实现存在缺陷。Dockerfile不支持多FROM指令(详见3378号、5714号以及5726号问题),这意味着只能继承单个镜像。此外,它没有版本强制。举例来说,dockerfile/ubuntu:14.04的作者可以替换该标签的内容,这相当于允许用户使用软件包管理器但又不没有版本强制机制。而且正如下文所提到,Docker Hub在这方面存在着令人沮丧的速度缓慢的限制。
Docker Hub还拥有一套自动化构建系统,能够检测到库中新提交内容并触发容器构建。因为许多原因,这项功能也是完全没用。由于几乎不能定制,构建配置受到了极大限制,甚至无法支持最基本的脚本执行前/后的钩子。Docker Hub采用一套特殊的项目结构,一个项目下只能有一个Dockerfile,这破坏了我们先前提到的构建解决方案,而且构建速度极为缓慢。
我们的解决方案是使用CircleCI,它是一个优秀的托管CI平台,能够从Makefile触发Docker构建并推送到Docker Hub。虽然这种方式无法解决速度慢的问题,但唯一的可选方案是使用我们自己的Docker Registry,其复杂程度都到了荒唐的地步。
安全性
Docker最初使用LXC作为默认执行环境,但现在,0.9版本默认使用libcontainer。这使得用户可以调整命名空间功能、权限,并且可以在使用合适的exec-driver时使用自定义的LXC配置文件。
这需要一直在主机上运行一个root守护进程,而且Docker一直存在着若干安全漏洞,例如CVE-2014-6407以及CVE-2014-6408。坦率地讲,这些问题起初就不应该存在。甚至Gartner公司也在其追踪报告中给出了糟糕评价,并表达了对Docker的不成熟和安全性问题的担忧。
按照设计,Docker对于命名空间功能给予充分信任,这就导致其攻击面要比其它典型的虚拟机管理程序更宽。Xen拥有129项CVE,相比之下Linux则拥有1279项。在某些情况下上述问题并非不可接受,例如在Travis CI当中进行公开构建,但这对于私有、多用户环境来说无疑是危险的。
容器与虚拟机并不是一回事
命名空间与cgroups功能极为强大,允许一个进程及其子进程拥有一个共享内核资源——例如网络堆栈以及进程表——的私有视图。这种细粒度控制与隔离机制配合上chroot jailing与grsec,能够提供非常出色的保护层。一些应用程序,如uWSGI,可以在没有Docker的情况下直接利用这些特性的优点,而不支持命名空间的应用程序则可以利用firejail实现沙箱化处理。如果您有冒险精神,可以将这种支持直接添加到自己的容器化项目的代码中,例如LXC以及Dokcer,从而在单一内核空间中利用这些特性的有点高效地运行多套发行版。相较于虚拟机管理程序,这种作法有时候会有降低内存使用率和减少启动时间的好处,但其代价就是降低安全性、稳定性以及兼容性。举个与Linux Kernel Interface相关的最糟糕的极端案例,在内核及用户空间中运行不兼容或者未经测试的glibc组合版本很可能引发意料之外的行为。
早在2008年LXC尚处于构思阶段时,硬件辅助虚拟化也仅仅诞生了几年时间,许多虚拟机管理程序都存在着性能以及稳定性问题。因此,虚拟化技术并没有得到广泛应用,而面对成本以及物理基础设施占用减少等优势,上述问题是可以接受的。不过如今我们的虚拟机管理程序在性能表现方面几乎与裸机设备不相上下,而且有趣的是,在某些情况下速度更快。另外,托管的、按需分配的虚拟机速度越来越快,成本越来越低,DigitalOcean在性能与成本方面都要远远胜过EC2,这也使应用程序与虚拟机之间进行一对一映射从经济角度讲成为可能。
[编辑意见]正如Bryan Cantrill提出的观点,虚拟化技术的性能将受到工作负载类型的显著影响。例如,IO任务繁重的应用程序会导致性能降低。
在某些特定的应用场景中,容器化确实是恰当的解决方案,不过除非能够明确说明为什么在你的应用场景中选择此类处理方式,否则你可能应用使用虚拟机管理程序代替。而且即使使用虚拟化技术方案,你仍然应该利用命名空间的优点,而在应用程序没有对这些特性提供原生支持的情况下,像firejail这样的工具可以提供帮助。
Docker并不是必须的
Docker增加了一个复杂的侵入层,使开发、故障排查以及调试工作的难度大幅上升,它带来的问题往往多于它能够解决的问题。它在部署上也没有任何优势,因为你仍然需要利用快照实现响应式自动扩展。更糟糕的是,如果大家并没有使用快照机制,那么生产环境的扩展就会依赖于Docker Hub的稳定性。
目前有不少项目都在滥用容器化技术,例如baseimage-docker,该镜像旨在通过运行作为入口的init.d简化检查、调试和兼容,甚至还提供一个可选的SSH服务器,实际上是将容器当作虚拟机对待,虽然作者本人以无甚说服力的言词反对这种观点。
总结
如果开发流程合乎情理,那么你已经明白Docker不是必须的。Docker中号称能够带来助益的全部功能要么完全无用,要么实现很差,而其最大的优势直接使用命名空间就很容易实现。如果放在8年前,Docker是一个有趣的概念,但以现在的眼光来看,它几乎是没用的。
修正/改进
从表面上看,Docker还有很多事情要做。它的生态系统鼓励开发人员倾向于“不可变部署(immutable deployment)”这样一种理念,新项目能够更快更轻松的地完成,在这一点上其实用性确实得到了许多数人的肯定。然而需要注意的是,这篇文章的主旨在于探讨Docker的日常使用和长远使用,包括在本地及生产环境中。
尽管前面提到的大部分问题都清晰易懂,但文章并没有提及Docker如何才能做得更好。有许多可选的方案可以替代Docker,每种方案中都有自己的优点和不足,而我将在接下来的文章中进行详细阐述。
感兴趣的读者可以查看a-ko和markbnj的进一步讨论,前者讨论了容器化的长远影响,后者从技术上进行了反驳,你可能会觉得它们都非常有用。
我要对在百忙中抽出时间给出个人反馈意见的每位朋友表示由衷的感谢。看到文章受到大家的关注确实令人倍感振奋,而且在读过多位工程技术大牛——其中包括许多年来都一直给我启发的那些人——的回应后,我也颇有种诚惶诚恐之感。
查看英文原文:Lets review.. Docker (again)