Unikernel不适合生产环境
最近我犯了个错,在推ter上语气激昂的问是否该讲讲为什么unikernel不适合用在生产环境。结果响应十分强烈:有的人感觉unikernel走错方向了,在寻找支持这种观点的细节;有的人是unikernel的支持者,也很想知道反对unikernel的观点是什么。显然人们都想听到反对在生产环境上使用unikernel的观点究竟是什么。
那么,unikernel的问题究竟是什么?让我们首先来看一下定义:unikernel是一个完全运行在微处理器的特权模式的应用程序(具体的术语可能不一样:在x86上这被称作运行在Ring 0)。即在unikernel中,完全没有传统意义上的应用程序;相反,应用程序的功能被拖拽到了操作系统内核中。(没有操作系统(“no os”)的想法会误导你;并不是没有操作系统,而是应用程序已经担负起了操作系统作为硬件接口的责任 - 这是一个完全的OS(“all os”),只不过是一个粗制的,一个贫血的操作系统。)在我们讨论这样做会面临哪些挑战的时候,有必要首先探索一下unikernel的背后的动机是什么,即使仅仅是因为那么一点点的好处...
选择在操作系统内核中实现功能的主要原因是性能:通过消除在用户-内核边界之间的上下文切换,那些需要依靠在这个边界进行切换来完成的操作就可以做到更快。对于unikernel,这些观点看起来貌似是正确的:一方面现代的平台运行时(runtime)的复杂度很高,一方面现代微处理器的性能很好。很少有人会留意到应用的性能受到了用户-内核态的上下文切换产生的开销的限制。并且这种观点还可能站不住脚的地方在于,unikernel很大程度上依赖硬件虚拟化来实现多租户等功能,这个事实也削弱了这些观点的说服力。正如我 在过去的一篇文章中展开的,在硬件层的虚拟化有无法逃避的性能税费:通过把可以看到硬件的系统(即hypervisor)与可以实际上看到应用程序的系统(借宿操作系统/guest OS)隔离开来,在硬件的利用率(比如DRAM,网卡,CPU、I/O等)方面的损失了效率,而且再多的意志力(willpower)和暴力(brute force)都无法弥补这种损失。但是,继续在性能这一点上继续纠缠下去没有什么好处,那就让我们让我们认为unikernel在性能论这一点上占了上风,但是存在一些有坚实基础的反面观点,让我们继续讨论。
另外一个unikernel的支持者给出的观点是unikernel更加安全,但实在不清楚这背后的思想基础是什么。是的,unikernel通常运行更少的软件(因而攻击面更小)- 但是unikernel并没有什么什么本质上的东西一定会导出软件更少的结果。是的,unikernel通常运行新开发的,与总不同的软件(因而不会受到 OpenSSL 每周漏洞列表中出现的漏洞的威胁 ),但是这种因为模糊晦涩就会安全的观点,可以套用到任何新出现的或者难以理解的系统上。这个安全观点同时也似乎忽视了unikernel很大程度上依赖的保护边界的事实:由底层hypervisor提供的在寄宿操作系统(guest OS)之间提供的保护边界。 Hypervisor的漏洞是的确存在的;不能一方面渲染Linux内核的漏洞是如何的一种潜在威胁,一方面觉得hypervisor的漏洞只会存在于想像中。相反,不让应用程序开发者使用用户保护边界的工具,破坏了 最小特权原则(Principle of least privilege):任何应用程序中的漏洞都会根植在unikernel中。在这个以容器为基础进行部署的世界中,这会造成一个很棘手的问题 - 秘密管理(secret management) - 并且会让其变得相当难以处理(并且有更高的风险)。在最好的情况下,uniknernel算得上是一个“安全戏剧”(注:security theater意思是那些旨在提升安全,或只想给人们制造一种更安全、一切尽在掌控之中的感觉,却实际上对于提升安全性并没有或者很少帮助的安全措施);在最糟的情况,这就是一个安全噩梦。
unikernel的支持者最后一个的观点是unikernel体积小 - 但是,unikernel并没有什么小的地方!拿我自己的经验来说,我在 小的系统和 大的系统上面做过实现内核的事情;你当然可以有一个精简的系统;而无需走上借助于unikernel来实现一种类似胃绕道手术一样的效果。(我自己也喜欢在Alpine Linux非常精简的用户态基础上来运行Linux程序和/或Docker容器。)并且根据unikernel没有包含多少代码的这种程度来说,这种感觉似乎更多是因为幼稚,而不是因为设计。但是仅仅通过代码来衡量unikernel的体积是错误的,并且这里unikernel的支持者也忽略了更大型的系统的细节:因为unikernel以一个借宿操作系统来运行,由hypervisor为它分配的DRAM会被完全占用,即使应用自身没有利用这些内存。因为内存耗尽仍然是导致应用崩溃的罪恶首要之一(特别是在动态环境中),受这个原因(需求)的驱使,内存大小调整很容易被过度工程化,通常采取的是盲目翻倍或者在相反的情况被吞并(slop up)。在unikernel的模型中,任何这种吞并的情况没有了 - 没有谁再能使用它,因为hypervisor不知道它有没有真正地被使用。(这跟在容器中的情况完全不同,在容器中没有被应用使用的内存可以被其他的容器使用,或者被系统自身所使用)。因此,当考虑了整个系统,unikernel的支持观点变得更加单薄(如果没有被完全否决掉的话)。
所以这些就是支持unikernel的原因:很大一部分是因为性能,然后一点安全戏剧,和一个软件崩溃食谱( software crash diet)。正如这些理由不冷不热所预示的,它们构成了unikernel的好消息的终点。事情从这里开始,都是坏消息:要获得这些好处必须付出成本,不管这些好处是多么多么的脆弱。
unikernel的缺点从应用程序自身的运行机制开始。当操作系统的边界被消除了,你可能也消除了应用程序和外部世界的接口,如网络和持久存储 - 但难道你就没有使用这些接口的需求,你就完全没这需要了?有些unikernel(如OSv和Rumprun)采用的是实现一个“POSIX类似”的接口来尽量减少对应用程序的影响。好消息是:app好像也可以工作。坏消息是:我们还未尝提到这些应用程序 需要被移植!这里希望你的应用的“POSIX相似性”不会设计到一些老的的观念,如创建一个进程:在unikernel中 没有进程,如果你的应用程序依靠这个基础的话,基本上你就是被囚禁了( 或者比囚禁了更糟)。
如果这种方法看起来比较边缘,对于那些特定语言的unikernel,事情会变得更加糟糕,如 MirageOS这种特定语言的unikernel深嵌了一个特定的语言运行时。一方面,只允许用某一个类型安全的语言的实现可以让一些严重的unikernel安全问题被解规避掉;但另一方面,保佑你所有你需要的都已经在OCam中有了!
因此要让你的应用运行起来会有一些问题,假定你都已经都解决了这些问题:或者是因为你的unikernel暴露的POSIX接口(注:原文是surface)对于你的应用或者平台来说足够了,或者它已经是用OCam或者Erlang或者Haskell或者其他什么语言实现的。假如你有了可以用unikernel来运行的应用程序,现在到了一个最深刻的为什么 unikernel 不适合生产环境的理由 - 并且这个理由当真要部署任何真正在运行生产中的时候,(至少对于我来说)简直直击unikernel的心脏: Unikernel是完全不可调试的。没有进程,因此也没有netstat,没有tcpdump,没有ping。并且这些还只是已经有几个世纪历史的工具,那任何现代的工具,如DTrace或者MDB就更不用说了。站在调试的角度上,要说这是最基本的工具都言轻了:这岂止是石器时代,这简直就是前寒武纪。我,作为一个全部职业生涯都在开发生产环境系统,和各种工具来调试这些系统的人,我发现这种隐含出的拒绝调试生产环境是让人感到愤怒的,似乎在那些unikernel支持者中有更深让人不爽的症状:完全没有对运维的同理心(empathy)。生产环境问题就被简单地挥挥衣袖撇开了 - 服务在不正常的时候只能被等着重启。这种态度,尽管即使只是被推论出来并不广为存在,对于任何曾经负责过运维一个系统的人来说很难是不让人不冒火的。(假如你认为我是一个局外人,听听再 我在DockerCon 2015的演讲中,在我强调系统出了问题后系统需要调试而不是仅仅被重启后台下的掌声吧)。如果需要把话明说出来,这种态度让人愤怒因为这是错误的:如果一个生产环境的应用开始工作不正常,原因在于一个非致命的情况,如如 监听降低(listen drops),重启应用是在可能的最遭时间(换句话说,在负载最高的情况下)中断了应用,却并没有朝着找出问题的根源(一个不够的未完成的工作)迈出任何一步。
那么,能在unikernel中实现一个生产环境的调试工具么?一个字,不可能。调试工具通常会跨越用户-内核边界,在利用命令行提供的特定工具进行查询是最有成效的,然而提供这种工具的器官已被特意地从uninkernel中摘除了,名曰减肥(注:上文中提到的胃绕道手术就是一种减肥的手术);任何提供足够复杂调试工具以供生产环境使用的unikernel都违反了自己的信条。unikerne不适合生产环境不仅仅因为其实现方式,同时也因为其被理解的方式:他们在生产环境工作不正常的时候会无法被理解 - 而且源于它们自己的坚持,它们永远也做不到这一点。
尽管以上所有这些,我确实发现了一些和unikernel支持者一样的观点:我同意容器的革命需要一个更加精简,更加安全,更加高效的runtime,而不仅仅是一个运行在虚拟硬件之上的一个共享的Linux 借宿操作系统(Linux guest OS) - 并且在 Joyent过去的几年 ,我们的焦点一样的在用SmartOS and Triton来达成这个目标。尽管我们和unikernel 的支持者看到了相似的问题,我们采取的方式有本质不同:与放弃在一个多租户的基础上运行容器的观念不同,我们拿已经安全的 zone做为基石并且为其 添加了原生执行Linux 二进制文件的能力。即,我们选择利用操作系统的进步成果,而不是忽视它们的存在,不但给Linux和Docker带了安全裸机之上(on-the-metal)的容器,同时也带来了操作系统的先进成果,如 ZFS和 Crossbow,也当然少不了DTrace。这些努力都值得最后一次重新强调:我们在生产环境上的努力体现在我们做的每件事情上,但特别应指出的是, 全面的工具来调试生产环境的操作系统 - 并且通过将这些工具链带入Linux容器这个更广阔的世界, Triton已经允许在生产环境中的调试,而这在之前是不敢想的。。
我认为,终有一日,unikernel最有成效的结果还是得益于它产生的负面效果:它会作为一个很好的示例,来证明它们的方法对于生产系统是不切实际的。照此,它们会跟 事务型内存( transactional memory ), m-to-n调度模型一起,成为曾风靡一时,但因现实无情的细节,而死掉的断气软件。但是没必要把我的话当真,正如我在tweet中提到的,不可调试的系统自身就如同种下一个恶果 - 只是自己受用就好,就不要加害其他芸芸众生了。
( 原文链接:Unikernels are unfit for production,翻译:钟最龙)