本文已超过一年。较旧的文章可能包含过时的内容。请检查页面中的信息自发布以来是否已不再正确。

为什么 Kubernetes 不使用 libnetwork

Kubernetes 早在 1.0 版本发布之前就已经有了非常基本的网络插件形式——大约与 Docker 的 libnetwork 和容器网络模型 (CNM) 引入的同时。与 libnetwork 不同,Kubernetes 插件系统仍然保留着“内测版 (alpha)”的标记。既然 Docker 的网络插件支持已经发布并获得支持,我们收到的一个明显问题是:为什么 Kubernetes 还没有采用它?毕竟,供应商几乎肯定会为 Docker 编写插件——我们都使用相同的驱动程序会更好,对吧?

在深入探讨之前,重要的是要记住 Kubernetes 是一个支持多种容器运行时(runtime)的系统,而 Docker 只是其中之一。配置网络是每个运行时的一个方面,所以当人们问“Kubernetes 会支持 CNM 吗?”时,他们真正的意思是“Kubernetes 会支持 Docker 运行时中的 CNM 驱动程序吗?” 如果我们可以实现跨运行时的通用网络支持,那将是很棒的,但这并不是一个明确的目标。

事实上,Kubernetes 并没有为 Docker 运行时采用 CNM/libnetwork。事实上,我们一直在研究 CoreOS 提出的另一种容器网络接口 (CNI) 模型,该模型是 App Container (appc) 规范的一部分。为什么?原因有很多,包括技术和非技术方面的。

首先也是最重要的是,Docker 网络驱动程序的设计中存在一些基本假设,这些假设给我们带来了问题。

Docker 有“本地(local)”和“全局(global)”驱动程序的概念。本地驱动程序(如“bridge”)是面向机器的,不做任何跨节点协调。全局驱动程序(如“overlay”)依赖 libkv(一种键值存储抽象)来进行跨机器协调。这种键值存储是另一种插件接口,并且非常底层(只有键和值,没有语义)。要在 Kubernetes 集群中运行类似 Docker 的 overlay 驱动程序,我们需要集群管理员运行一个完全不同的 consuletcdzookeeper 实例(参见多主机网络),或者我们必须提供自己的由 Kubernetes 支持的 libkv 实现。

后者听起来很有吸引力,我们也尝试实现过,但 libkv 接口非常底层,并且 Schema 是 Docker 内部定义的。我们必须要么直接暴露底层的键值存储,要么提供键值语义(在我们自己的结构化 API 之上,而这个 API 本身也是在键值系统上实现的)。出于性能、可伸缩性和安全性的考虑,这两种选择都不太有吸引力。最终结果是整个系统将变得更加复杂,而使用 Docker 网络的目标本应是简化事情。

对于愿意并能够运行必要基础设施以满足 Docker 全局驱动程序要求并自行配置 Docker 的用户来说,Docker 网络应该“开箱即用”(just work)。Kubernetes 不会妨碍这种设置,无论项目走向如何,这个选项都应该可用。但是,对于默认安装来说,实际结论是这对用户来说是一个不应有的负担,因此我们无法使用 Docker 的全局驱动程序(包括“overlay”),这消除了使用 Docker 插件的大部分价值。

Docker 的网络模型做了许多对 Kubernetes 无效的假设。在 Docker 1.8 和 1.9 版本中,它包含了一个有根本性缺陷的“发现”实现,导致容器中的 /etc/hosts 文件损坏(docker #17190)——而且这不容易关闭。在 1.10 版本中,Docker 计划捆绑一个新的 DNS 服务器,目前尚不清楚这是否可以关闭。容器级别的命名不是 Kubernetes 合适的抽象——我们已经有了自己的 Service 命名、发现和绑定概念,我们也有自己的 DNS Schema 和服务器(基于成熟的 SkyDNS)。捆绑的解决方案不足以满足我们的需求,而且无法禁用。

与本地/全局划分正交的是,Docker 既有进程内(in-process)插件,也有进程外(“远程”)插件。我们研究过是否可以绕过 libnetwork(从而跳过上述问题)并直接驱动 Docker 远程插件。不幸的是,这意味着我们无法使用任何 Docker 进程内插件,特别是“bridge”和“overlay”,这再次消除了 libnetwork 的大部分实用性。

另一方面,CNI 在理念上与 Kubernetes 更契合。它比 CNM 简单得多,不需要守护进程(daemons),而且至少在一定程度上是跨平台的(CoreOS 的 rkt 容器运行时支持它)。跨平台意味着有机会实现可在不同运行时(例如 Docker、Rocket、Hyper)上以相同方式工作的网络配置。它遵循了 Unix 的“做好一件事”的哲学。

此外,封装一个 CNI 插件并生成一个更定制化的 CNI 插件是轻而易举的——一个简单的 shell 脚本就可以完成。CNM 在这方面要复杂得多。这使得 CNI 成为快速开发和迭代的吸引人选项。早期原型已经证明,将 kubelet 中几乎 100% 当前硬编码的网络逻辑剥离到一个插件中是可能的。

我们研究过为 Docker 编写一个可以运行 CNI 驱动程序的“bridge” CNM 驱动程序。结果发现这非常复杂。首先,CNM 和 CNI 模型非常不同,所以没有“方法”能对得上。我们仍然面临上面讨论的全局与本地以及键值问题。假设这个驱动程序将自己声明为本地的,我们必须从 Kubernetes 获取关于逻辑网络的信息。

不幸的是,Docker 驱动程序很难映射到像 Kubernetes 这样的其他控制平面。具体来说,驱动程序并不知道容器正在连接到的网络的名称——只知道 Docker 内部分配的一个 ID。这使得驱动程序很难映射回其他系统中存在的任何网络概念。

网络供应商已经向 Docker 开发者提出了这些以及其他问题,但通常都被作为“按设计工作”而关闭(libnetwork #139, libnetwork #486, libnetwork #514, libnetwork #865, docker #18864),尽管这些问题使得非 Docker 第三方系统更难以集成。在整个调查过程中,Docker 已经明确表示他们不太接受偏离其当前路线或委托控制权的想法。这让我们非常担忧,因为 Kubernetes 补充了 Docker 并增加了许多功能,但它本身存在于 Docker 之外。

由于所有这些原因,我们选择投入 CNI 作为 Kubernetes 的插件模型。这将带来一些不幸的副作用。其中大多数相对较小(例如,docker inspect 将不显示 IP 地址),但有些则很重要。特别是,通过 docker run 启动的容器可能无法与通过 Kubernetes 启动的容器通信,而且网络集成商如果想完全集成到 Kubernetes,将不得不提供 CNI 驱动程序。另一方面,Kubernetes 将变得更简单、更灵活,并且早期引导(例如配置 Docker 使用我们的 bridge)中的许多麻烦之处将不复存在。

当我们沿着这条路前进时,我们肯定会密切关注更好的集成和简化方法。如果你对我们如何做到这一点有想法,我们非常乐意倾听——请在 slack 或我们的网络 SIG 邮件列表上找到我们。