这篇文章将试图解释在k8s集群中多个网络层的运行机制。
Kubernetes是一个强大的平台,有着很多巧妙并且智能的设计,但讨论网络的交互模式可能会让人感到迷惑:pod网络、服务网络、集群IP、容器端口、主机端口、节点端口......一瞬间看到太多概念,感觉一些人眼睛都发直了。而我们大多在工作中谈论这些东西,一次就跨越所有的层次,理解上很难一次性到位。如果你每次都把它作为一个部分,并逐渐清楚地了解每一层的工作原理,你便能够以一种循序渐进的方式去理解它。
回到网络这个重点,我将把这篇文章分成三部分:
- 第一部分:容器和pod(也就是本篇)
- 第二部分:k8s service,它是pod的抽象层。
- 最后一篇文章将探讨
ingress
和集群外到pod的流量流向过程。
不过这篇文章不打算给容器、kubernetes以及pod做基本介绍。要了解更多关于容器的工作原理,请看 Docker 。关于 k8s 使用上的概述可以在 这里,关于pod的具体概述在 这里。最后,也需要你对网络和IP地址空间有基本了解。
额,如果不会的话,看后续有没有时间出一些关于上述的文章。
Pod
什么是 Pod 呢?(前提是你已经看过k8s官网的那篇关于 Pod 的文章了)
一个pod由一个或多个容器组成,它们位于同一主机上,并被配置为共享网络空间和其他资源,如文件卷(volumes)。
Pod是 k8s 应用程序的基本单元。而上面画重点说的: "共享网络空间" 到底是什么意思?
在实践中,它意味着一个pod中的所有容器都可以在localhost上互联。如果我有一个运行在80端口的 nginx
容器和另一个运行 scrapyd
的容器,第二个容器可以以 http://localhost:80
的方式连接到第一个容器。但这内部是怎么工作的呢?让我们看看一个经典情况:当我们在本地机器上启动一个docker容器。
我们从上往下看,有一个网络接口:eth0。与之相连的是一个网桥:docker0,而与之相连的是另一个虚拟网络接口:veth0。
注意,docker0 和 veth0 都在同一个网络上,本例中是:172.17.0.0/24。在这个网络中,docker0 被分配到172.17.0.1,并且是 veth0 的默认网关,后者被分配到172.17.0.2。
由于网络namespace(具体看docker原理的 namaspace隔离)的配置方式,当容器启动时,它内部的进程只能看到veth0,并通过docker0和eth0与外部世界通信。现在我们来启动第二个容器:
如上图所示,第二个容器得到了一个新的虚拟网络接口 veth1,连接到同一个 docker0 网桥。这个接口被分配了 172.17.0.3,所以它与网桥以及第一个容器在同一个网络上。那么两个容器只要能以某种方式可以发现另一个容器的 IP 地址,就可以通过网桥进行通信。
上面解释了docker容器底层通行的逻辑。但它并没有让我们看到 k8s pod的 "共享网络空间"。幸运的是,namespace的设计是灵活的。**Docker可以启动一个容器,但是不为它创建一个新的虚拟网络接口,而是指定它共享一个现有的接口(也就是加入现有的 namespace 体系中)。**在这种情况下,上面的图看起来就有点不同:
现在第二个容器看到的是 veth0,而不是像前面的例子中那样可以有自己的 veth1。
这有几个含义:首先,两个容器都可以从连接的 172.17.0.2 上寻址,而在里面,每个容器都可以连接另一个容器在 localhost 上打开的端口。这也意味着两个容器不能打开相同的端口,这是一个限制,但与在一台主机上运行多个进程时的情况没有区别。 通过这种方式,一组进程可以充分利用容器的解耦和隔离,同时在最简单的网络环境中一起互联工作。
k8s 通过给每个pod创建一个特殊的容器(infra)来实现这一网络模型,就是为其他容器提供一个网络接口。如果你ssh到一个有pod的kubernetes集群节点,运行 docker ps
,你会看到至少有一个容器是用 pause
命令启动的。
pause命令会暂停当前进程,直到收到信号为止,所以这些容器除了 sleep 之外什么都不做,直到k8s 向它们发出 SIGTERM
。尽管不处于活跃状态,但 "pause" 容器是pod的核心,它提供了虚拟的网络接口,所有其他的容器都会用它来与对方和外部世界通信。因此,在一个假设的 pod-like 结构中,网络模型有点像这样:
Pod NetWork
很NB,但就算是一个满是容器的pod,内部容器可以相互交谈,也不能让我们得到一个庞大的系统(因为现实世界不可能只有一个pod)。在下一篇讨论 service 的文章中,我们可以更清楚地看到,k8s设计的核心是要求pod能够与其他pod进行通信,无论它们是运行在同一台本地主机上还是运行在不同的主机上。
为了研究这种情况如何通信,我们需要上升一个台阶,来看看集群中的节点。
本节将包含一些关于网络路由不恰当的解释,但求在解释的时候会尽可能通俗易懂。像找到一个简单明了并简短的关于IP路由的教程有点困难,但是如果你想要一个可靠的回答,维基百科上关于 这个主题的文章 并不难。
一个k8s集群由一个或多个节点组成。一个节点就是一个主机,无论是物理的还是虚拟的,都有一个容器运行时和它的容器依赖(不过目前大部分是docker)以及几个k8s系统组件,它被连接到一个网络,允许它到达集群中的其他节点。简单来个图,一个由两个节点组成的简单集群看起来像这样:
如果你在AWS这样的云平台上运行您的集群,那么对于单个项目环境,上面的图就非常接近默认的网络架构。为了便于说明,我在本例中使用了专用网络 10.100.0.0/24,所以路由器是10.100.0.1,两个实例分别是 10.100.0.2 和 10.100.0.3。
在这种设置下,每个实例都可以在eth0上与其他实例通信,这符合我们上面docker容器中说明的那样,但请记住上面我们看到的pod并不在这个专用网络上:它挂在一个完全不同的网桥上,因为这个网络是虚拟的,只存在于一个特定的节点上。为了更清楚,让我们把 pod 的东西标回去:
左边的主机有一个地址为 10.100.0.2 的eth0接口,其默认网关是位于 10.100.0.1 的路由器上。与该接口相连的是地址为 172.17.0.1 的网桥 docker0,与之相连的是地址为172.17.0.2 的接口 veth0。
veth0接口 是和 pause容器 一起创建的,并且通过共享网络空间在所有三个容器中都可见。由于在网桥创建时建立了本地路由规则,任何到达 eth0 且目的地地址为 172.17.0.2 的包将被转发到网桥,然后网桥将它发送到 veth0,到目前为止听起来还不错。
如果我们知道在这台主机上有一个 172.17.0.2 的pod,我们可以在路由器中添加规则,将该地址的下一跳设置为 10.100.0.2,它们将从那里转发到veth0。crazying! 现在让我们看看另一个主机。
右边的主机也有eth0,其地址为10.100.0.3,使用相同的默认网关10.100.0.1,eth0连接到的也是 docker0网桥,地址为172.17.0.1。
好的,是不是发现了一个问题?这个地址应该和host1上的另一个网桥不一样。而我在这里设计的是一样的,原因是我想设计一个最坏的情况。如果你安装docker然后让它默认开始,它很可能会地以这种方式工作。但是即使选择的网络不同,这也突出了一个更根本的问题:
即一个节点通常不知道分配给另一个节点上的网桥私有地址空间是什么
但是如果我们要向它发送数据包并让它们到达正确的地方,就需要知道这一点。显然需要额外结构。
k8s 通过两种方式提供这种结构。
- 首先,它为每个节点上的网桥分配了一个总体地址空间,然后根据网桥所构建的节点,分配该空间内的网桥地址。
- 其次,它向位于 10.100.0.1 的网关添加路由规则,告诉它前往每个网桥的数据包应该如何被路由,即:网桥可以通过哪个节点的 eth0 到达。
这种虚拟网络接口、网桥和路由规则的组合通常被称为:overlay网络。在谈论 k8s 的时候,我通常把这个网络称为 "pod网络",首先它是一个overlay网络,允许pod在任何节点上互联通信。
下面这张图显示了整个路由表的构建:
应该注意的一点是:我已经将桥的名称从"docker0"更改为"cbr0"。k8s 并不使用标准的docker桥接设备,事实上"cbr"是"自定义桥接"的缩写。对于它的一些定制目前我不是很清楚,但这是docker运行在k8s和默认安装之间的一个重要区别。
另一件要注意的事情是:本例中分配给网桥的地址空间是 10.0.0.0/14。这是从我在Cloud中的一个集群中获取的,对于我是一个真实的示例。但是你的的集群可能被分配一个完全不同的地址范围。但是目前还没有办法使用 kubectl 来公开它。
总结
不过话说回来:一般来说,你不需要考虑这个网络是如何工作的(观众:我白看了?🥱)。
当一个pod与另一个pod通信时,它通常是通过 service 的抽象来进行的。service 是一种软件层面定义的代理,这将是本系列下一篇文章的主题。但是pod的网络地址会在日志中和调试时出现,和在某些场景中,你可能还是需要显式路由到这个网络。
例如,离开k8s pod到10.0.0.0/8范围内任何地址,默认情况下不会被NAT。所以如果你与该范围内的另一个私有网络上的服务通信,你可能需要设置某种规则将数据包路由回pod。
最后希望本文可以帮助到你。
本文正在参与 “网络协议必知必会”征文活动