Kubernetes 在 1.0 版本之前就已经有了最初的网络插件。与此同时 Docker 也引入了 libnetwork 和 Container Network Model (CNM)。现在 Docker 已经发布并支持了网络插件 libnetwork,然而 Kubernetes 的插件却还停留在 alpha 阶段。 那么一个显而易见的问题是为什么 Kubernetes 还没有采用 libnetwork。毕竟大部分的供应商都肯定会支持 Docker 的插件,Kubernetes 也理所应当采用它。
在开始讨论之前,我们首先需要知道的一点是: Kubernetes 是一个支持多种容器运行环境的系统,Docker 只是其中之一。配置网络对于每个运行环境都是同样重要的。所以当人们问到“ Kubernetes 支持不支持 CNM 时”,他们其实是在问“Kubernetes 会不会在 Docker 运行环境下支持 CNM”。我们当然希望利用同样一个网络插件来支持所有的运行环境,但是这并不是一个绝对的目标。
然而,Kubernetes 并没有在其 Docker 运行环境中采用 CNM/libnetwork。最近一段时间,我们一直在研究探讨能否使用 CoreOS 推出的 App Container (appc) 中的网络模型 Container Network Interface (CNI) 去替代它。为什么?这里面有各种技术上和非技术上的原因。
首先,Docker 的网络驱动设计做了一些兼容性不佳的基本假设,这给我们带来了不少困难。
比如 Docker 里面有本地和全局驱动的概念。本地驱动(比如 “bridge”)固定于一台机器而不能做跨机器的远程协调。全局驱动(比如 “overlay”)依赖于 libkv 库去做跨机器间的协调。该库定义了一个 key-value 存储的接口,并且接口非常的底层。为了让 Docker 的全局驱动在 Kubernetes 集群上跑起来,我们还得需要系统管理员去运行 etcd, ZooKeeper, Consul 的实例(具体看 Docker 的 multi-host network 文档); 或者我们得在 Kubernetes 里面提供另一套 libkv 的实现。
不过相比之下,第二种方案(全局驱动)看起来更灵活,更好,所以我们尝试去实现它。但是 libkv 的接口非常底层,它的模式也是为了 Docker 运行环境自身设计的。对于 Kubernetes 来说,我们要么直接暴露出我们的底层 key-value 接口,要么提供一个 key-value 的语义接口(也就是在一个 key-value 系统上实现结构存储的 API)。就性能、伸缩性和安全角度而言,这两个选择对我们来说并不是非常合适。如果我们这样实现的话,整个系统会变明显地变得复杂。这和我们希望利用 Docker 的网络模型来简化实现所冲突。
对于想要运行 Docker 的全局驱动并能有能力配置 Docker 的用户来说,Docker 的网络应该会运行得很好。对 Kubernetes 来说,我们不希望介入或影响 Docker 的配置步骤;并且不论 Kubernetes 这个项目今后如何发展,这一点应该不会改变。甚至,我们会努力兼容更多的选项给用户。但我们在实践过程中得到的结论是:Docker 的全局驱动是对用户和开发者来说增加了多余的负担,并且我们不会用它作为默认的网络选项——这也意味着使用 Docker 插件的价值很大程度上被排除在外。
与此同时,Docker 的网络模型在设计上还做了很多和 Kubernetes 不兼容的假设。比如说在 Docker 1.8 和 1.9 的版本中,它在实现“服务发现(Service discovery)”时有一个根本性的设计缺陷,结果导致了容器中的 /etc/hosts 文件被随意改写甚至破坏( docker #17190 ) ——而且我们还不能轻易关闭“服务发现”这个功能。在 1.10 的版本中,Docker 还计划增加捆绑一个新 DNS 服务器 的功能,而我们现在还不不清楚这个功能能否被关闭。对 Kubernetes 来说,把命名寻址绑定在容器层面,并不是一个正确的设计——我们已经自己定义了一套 Service 命名、寻址、绑定的概念和规则,并且我们也有了我们自己的 DNS 架构和服务(构建在非常成熟的 SkyDNS 上)。所以,捆绑一个 DNS 服务器这样的方案并不能满足我们的需求,而且它还有可能无法被关闭。
除开所谓“本地”,“全局”驱动这样的区分,Docker 还定义了“进程内”和“进程外”(“远程”)插件。我们还研究了下是否可以绕过 libnework 本身(这样就能避开之前所述的那些问题)来直接使用”远程“插件。不幸的是,这意味着我们同时也失去了使用 Docker 的那些“进程内”插件的可能,特别是像网桥(bridge)和覆盖网络(overlay)这样的插件。这令使用 libnetwork 这件事本身失去了很大一部分意义。
另一方面,CNI 和 Kubernetes 在设计哲学上就非常一致。它远比 CNM 简单,不需要守护进程,并且至少是跨平台的(CoreOS 的 rkt 容器支持 CNI)。跨平台意味着同一个网络配置可以在多个运行环境下使用(例如 Docker, rkt 和 Hyper)。这也非常符合 Unix 的设计哲学:把一件事情做好。
另外,包装 CNI 模块来实现一个更定制的模块是非常简单的- 写一个简单的 shell 脚本就可以完成。相反 CNM 就复杂多了。因此我们认为 CNI 更适合快速开发和迭代。早期的实验尝试证明我们可以利用 CNI 插件来替代几乎所有 kubelet 中硬编码的网络逻辑。
我们也尝试为 Docker 实现了一个网桥(bridge)CNM 驱动来使用 CNI 的驱动。结果表明这更加复杂了问题。首先 CNM 和 CNI 的模型非常不同,没有一个“方法”能把它们很好的兼容起来。其次,我们还是有之前提到的“全局”和“本地”以及 key-value 的问题存在。假设这是个本地驱动,那么我们仍旧要从 Kubernetes 中去得到相应的逻辑网络的信息。
不幸的是,对于像 Kubernetes 这样的管理平台而言,Docker 的驱动非常难以接入。特别是这些驱动使用了 Docker 内部分配的一个 ID 而不是一个通用的网络名字来指向一个容器所属的网络。这样的设计导致一个被定义在外部系统中(例如 Kubernetes )的网络很难被驱动所理解识别。
我们和其它网络提供商将这些问题和其它一些相关问题都汇报给了 Docker 的开发人员。尽管这些问题给非 Docker 的第三方系统带来了很多困扰,它们通常被以“设计就是如此”的理由所关闭( libnetwork #139 , libnetwork #486 , libnetwork #514 , libnetwork #865 , docker #18864 )。通过这些举动,我们观察到 Docker 清楚的表明了他们对于有些建议的态度不够开放,因为这些建议可能会分散其一些主要精力、或者降低其对项目的控制。这一点让我们很着急,因为 Kubernetes 一直支持 Docker,并且为其增加了如此多的功能,但同时 Kubernetes 也是一个独立于 Docker 之外的项目。
种种原因致使我们选择了 CNI 作为 Kubernetes 的网络模型。这将会带来一些令人遗憾的副作用,虽然绝大部分都是一些小问题,比如 Docker 的 inspect 命令显示不了网络地址。不过也会有一些显著的问题,特别是被 Docker 所启动的容器可能不能和被 Kubernetes 启动的容器沟通,以及网络整合工程师必须提供 CNI 驱动来把网络完全整合到 Kubernetes 中。但重要的是,Kubernetes 会变得更简单、灵活并且不需要进行提前配置(比如配置 Docker 使用我们的网桥)。
随着我们越走越远,我们会毋庸置疑地汲取更多更好的整合、简化的方法。如果您对此有啥想法,我们洗耳恭听——可以通过 Slack ( http://slack.k8s.io/ ) 和 network SIG 邮件列表联系我们。
评论