一文搞懂 Kubernetes Pod 网络原理

Kubernetes网络需求

在深入研究数据包在Kubernetes集群内部如何流动之前,让我们首先明确Kubernetes网络的需求。

Kubernetes网络模型定义了一组基本规则:

  1. 集群中的Pod应该能够自由地与任何其他Pod通信,而无需使用网络地址转换(NAT)。
  2. 在集群节点上运行的任何程序都应该能够与同一节点上的任何Pod通信,而无需使用NAT。
  3. 每个Pod都有自己的IP地址(每个Pod一个IP),每个其他Pod都可以通过该地址访问它。

这些要求并不限制实现为单一解决方案。

相反,它们以一般性的术语描述了集群网络的属性。

在满足这些约束时,你将不得不解决以下挑战:

  1. 如何确保同一Pod中的容器的行为就像它们在同一主机上一样?
  2. Pod是否能够访问集群中的其他Pod?

Linux网络命名空间(namespace)在Pod中的工作原理

让我们考虑一个主容器承载应用程序和另一个与其一起运行的容器。

在这个例子中,你有一个Pod包含了Nginx的容器和busybox的容器。

yaml 复制代码
apiVersion: v1
kind: Pod
metadata:
  name: multi-container-pod
spec:
  containers:
    - name: container-1
      image: busybox
      command: ['/bin/sh', '-c', 'sleep 1d']
    - name: container-2
      image: nginx

在部署时,发生了以下事情:

  1. Pod在节点上获得了自己的网络命名空间(network namespace)。
  2. 为Pod分配了一个IP地址,端口在两个容器之间共享。
  3. 两个容器共享相同的网络命名空间,并且可以在localhost上看到彼此。

网络配置在后台以极快的速度发生。

然而,让我们退一步,尝试理解为什么容器需要上述配置才能运行。

在Linux中,网络命名空间是独立的、隔离的、逻辑的空间。

我们可以将网络命名空间视为将物理网络接口切割成更小的独立部分

每个部分都可以单独配置,并具有自己的网络规则和资源,这些资源包括防火墙规则、interfaces(虚拟或物理)、路由以及与网络相关的所有其他内容。

物理接口最终必须处理所有真实的数据包,因此所有虚拟接口都是从物理接口创建的。

veth(Virtual Ethernet) 是 Linux 提供的另外一种虚拟网卡接口,每个veth被赋予 IP 地址,并参与三层网络路由的过程,被创建出来的虚拟网卡会成对出现,从一张网卡发出的数据可以直接出现在对应的网卡上,所以可以使用它把一个网络命名空间连接到外部的默认命名空间或者global命名空间,而物理网络就存在这些命名空间中,从而实现物理上的联通。

网络命名空间可以由 ip-netns 管理工具管理,你可以使用 ip netns list 来列出主机上的命名空间。

注意:当创建网络命名空间时,它将存在于 /var/run/netns 下,但Docker并不总是遵循这一点,docker 默认情况下不会讲容器的 network namespace 添加到Linux 运行时数据中 (/var/run),所以要查看网络命名空间可以使用 nsenter ,后面如果需要查看网络命名空间并且是使用Docker容器,则用该方法。

例如,以下是来自Kubernetes节点的命名空间:

bash 复制代码
$ ip netns list
cni-0f226515-e28b-df13-9f16-dd79456825ac (id: 3)
cni-4e4dfaac-89a6-2034-6098-dd8b2ee51dcd (id: 4)
cni-7e94f0cc-9ee8-6a46-178a-55c73ce58f2e (id: 2)
cni-7619c818-5b66-5d45-91c1-1c516f559291 (id: 1)
cni-3004ec2c-9ac2-2928-b556-82c7fb37a4d8 (id: 0)

注意 cni- 前缀;这意味着命名空间的创建已由 CNI 负责。

当您创建 pod 并将该 pod 分配给节点时,CNI 将:

  1. 分配 IP 地址。
  2. 将容器连接到网络。

如果 Pod 包含多个容器(如上面所示),则这两个容器将放置在同一命名空间中。

那么,当列出节点上的容器时会发生什么?

可以通过 SSH 连接到 Kubernetes 节点并探索命名空间:

bash 复制代码
$ lsns -t net
        NS TYPE NPROCS   PID USER     NETNSID NSFS                           COMMAND
4026531992 net     171     1 root  unassigned /run/docker/netns/default      /sbin/init noembed norestore
4026532286 net       2  4808 65535          0 /run/docker/netns/56c020051c3b /pause
4026532414 net       5  5489 65535          1 /run/docker/netns/7db647b9b187 /pause

其中 lsns 是用于列出主机上所有可用命名空间的命令。

请记住,Linux 中有多种命名空间类型。

Nginx 容器在哪里?

那些 pause 容器是什么?

pause 容器在 Pod 中创建网络命名空间

让我们列出节点上的所有进程,并检查是否可以找到 Nginx 容器:

yaml 复制代码
$ lsns
        NS TYPE   NPROCS   PID USER            COMMAND
# truncated output
4026532414 net         5  5489 65535           /pause
4026532513 mnt         1  5599 root            sleep 1d
4026532514 uts         1  5599 root            sleep 1d
4026532515 pid         1  5599 root            sleep 1d
4026532516 mnt         3  5777 root            nginx: master process nginx -g daemon off;
4026532517 uts         3  5777 root            nginx: master process nginx -g daemon off;
4026532518 pid         3  5777 root            nginx: master process nginx -g daemon off;

容器在mount(mnt)、Unix time-sharing(uts)和 PID(pid)命名空间中列出,但不在网络(net)命名空间中。

不幸的是,lsns 只显示每个进程的最低 PID,但可以根据进程ID进一步筛选。

你可以使用以下命令检索 Nginx 容器的所有命名空间:

css 复制代码
$ sudo lsns -p 5777
       NS TYPE   NPROCS   PID USER  COMMAND
4026531835 cgroup    178     1 root  /sbin/init noembed norestore
4026531837 user      178     1 root  /sbin/init noembed norestore
4026532411 ipc         5  5489 65535 /pause
4026532414 net         5  5489 65535 /pause
4026532516 mnt         3  5777 root  nginx: master process nginx -g daemon off;
4026532517 uts         3  5777 root  nginx: master process nginx -g daemon off;
4026532518 pid         3  5777 root  nginx: master process nginx -g daemon off;

再次看到 pause 进程,这次它在托管网络命名空间。

那是什么?

集群中的每个 Pod 都有一个后台运行的额外隐藏容器,称为 pause

如果列出在节点上运行的容器并获取 pause 容器:

bash 复制代码
docker ps | grep pause
fa9666c1d9c6   registry.k8s.io/pause:3.4.1  "/pause"  k8s_POD_kube-dns-599484b884-sv2js...
44218e010aeb   registry.k8s.io/pause:3.4.1  "/pause"  k8s_POD_blackbox-exporter-55c457d...
5fb4b5942c66   registry.k8s.io/pause:3.4.1  "/pause"  k8s_POD_kube-dns-599484b884-cq99x...
8007db79dcf2   registry.k8s.io/pause:3.4.1  "/pause"  k8s_POD_konnectivity-agent-84f87c...

你会看到为 Node 上的每个分配的 Pod,都会自动与之配对一个 pause 容器。

这个 pause 容器负责创建并持有网络命名空间。

网络命名空间的创建由底层容器运行时完成,通常是 containerdCRI-O

在 Pod 部署和容器创建之前,(除其他事项外)容器运行时有责任创建网络命名空间。

实现共享命名空间后容器间就可以通过 localhost 进行相互访问。

与手动运行 ip netns 并手动创建网络命名空间不同,容器运行时会自动执行这个操作。

回到 pause 容器。

它包含很少的代码,并在部署后立即进入睡眠状态。

然而,它是不可或缺的,在 Kubernetes 生态系统中发挥着至关重要的作用

这里我截取了它关键的代码:

C 复制代码
static void sigdown(int signo) {
  psignal(signo, "Shutting down, got signal");
  exit(0);
}

static void sigreap(int signo) {
  while (waitpid(-1, NULL, WNOHANG) > 0)
    ;
}

int main(int argc, char **argv) {
  int i;
  for (i = 1; i < argc; ++i) {
    if (!strcasecmp(argv[i], "-v")) {
      printf("pause.c %s\n", VERSION_STRING(VERSION));
      return 0;
    }
  }

	// 当前进程的进程ID不等于1,它会输出一条警告信息,指出应该将该程序作为第一个进程运行。
  if (getpid() != 1)
    /* Not an error because pause sees use outside of infra containers. */
    fprintf(stderr, "Warning: pause should be the first process\n");

	// 通过调用 sigaction 函数
	// 该程序捕获了 SIGINT、SIGTERM 和 SIGCHLD 信号,并分别指定了相应的处理函数。
	// SIGINT 和 SIGTERM 信号将触发程序的正常退出
	// SIGCHLD 信号用于处理子进程的退出。
  if (sigaction(SIGINT, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
    return 1;
  if (sigaction(SIGTERM, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
    return 2;
  if (sigaction(SIGCHLD, &(struct sigaction){.sa_handler = sigreap,
                                             .sa_flags = SA_NOCLDSTOP},
                NULL) < 0)
    return 3;

  for (;;)
    pause(); //会让目前的进程暂停进入休眠状态,直到被信号中断
  fprintf(stderr, "Error: infinite loop terminated\n");
  return 42;
}

pause 不只是sleep。它执行另一项重要功能。它假定 PID 1 的角色,并且当它们被父进程孤立时,将通过调用 wait 来捕获任何僵尸进程(sigreap)。这样我们就不会在 Kubernetes Pod 的 PID 命名空间中堆积僵尸进程。

一个进入睡眠状态的容器有什么用处?

为了理解它的实用性,让我们想象一下有一个像前面的示例一样有两个容器的 Pod,但没有 pause 容器。

一旦容器启动,CNI:

  1. 使 busybox 容器加入之前的网络命名空间。
  2. 分配 IP 地址。
  3. 将容器连接到网络。

如果 Nginx 崩溃了会发生什么?

CNI 将必须再次执行所有步骤,并且两个容器的网络都将中断。

由于睡眠容器不太可能有任何错误,因此创建网络命名空间通常是更安全、更可靠的选择。

如果 Pod 内的一个容器崩溃,其余容器仍然可以回复任何网络请求。

并且Nginx也不是用来设计来收集僵尸进程的,所以 pause 容器不可或缺。

Pod 被分配了一个单独的 IP 地址

我提到了 Pod 和两个容器都接收相同的 IP。

这是如何配置的呢?

在 Pod 的网络命名空间内,创建了一个接口,并分配了一个 IP 地址。

让我们验证一下。

首先,找到 Pod 的 IP 地址:

ini 复制代码
$ kubectl get pod multi-container-pod -o jsonpath={.status.podIP}
10.244.4.40

接下来,找到相关的网络命名空间。

由于网络命名空间是从物理接口创建的,您将需要访问集群节点。

如果您正在运行 minikube,可以尝试使用 minikube ssh 来访问节点。如果您在云提供商上运行,应该有一些通过 SSH 访问节点的方式。

一旦进入节点,让我们找到最近创建的命名网络命名空间:

diff 复制代码
$ ls -lt /var/run/netns
total 0
-r--r--r-- 1 root root 0 Sep 25 13:34 cni-0f226515-e28b-df13-9f16-dd79456825ac
-r--r--r-- 1 root root 0 Sep 24 09:39 cni-4e4dfaac-89a6-2034-6098-dd8b2ee51dcd
-r--r--r-- 1 root root 0 Sep 24 09:39 cni-7e94f0cc-9ee8-6a46-178a-55c73ce58f2e
-r--r--r-- 1 root root 0 Sep 24 09:39 cni-7619c818-5b66-5d45-91c1-1c516f559291
-r--r--r-- 1 root root 0 Sep 24 09:39 cni-3004ec2c-9ac2-2928-b556-82c7fb37a4d8

在这种情况下,它是 cni-0f226515-e28b-df13-9f16-dd79456825ac

现在,您可以在该命名空间内运行 exec 命令:

perl 复制代码
ip netns exec cni-0f226515-e28b-df13-9f16-dd79456825ac ip a
# 输出被截断
3: eth0@if12: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default
    link/ether 16:a4:f8:4f:56:77 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.244.4.40/32 brd 10.244.4.40 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::14a4:f8ff:fe4f:5677/64 scope link
       valid_lft forever preferred_lft forever

这就是 Pod 的 IP 地址!

让我们通过 grep 找到该接口的另一端,以获取 @if12 部分:

perl 复制代码
ip link | grep -A1 ^12
12: vethweplb3f36a0@if16: mtu 1376 qdisc noqueue master weave state UP mode DEFAULT group default
    link/ether 72:1c:73:d9:d9:f6 brd ff:ff:ff:ff:ff:ff link-netnsid 1

您还可以验证 Nginx 容器在该命名空间内监听来自 HTTP 流量的情况:

css 复制代码
$ ip netns exec cni-0f226515-e28b-df13-9f16-dd79456825ac netstat -lnp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      692698/nginx: master
tcp6       0      0 :::80                   :::*                    LISTEN      692698/nginx: master

如果无法通过 SSH 访问集群中的工作节点,可以使用 kubectl exec 获取到 busybox 容器的 shell,并直接在其中使用 ipnetstat 命令。

现在,我们已经涵盖了容器之间的通信,让我们看看如何建立 Pod 与 Pod 之间的通信。

检查集群中 Pod 到 Pod 的流量

当 Pod 到 Pod 的通信出现问题时,有两种可能的情况:

  1. Pod 流量发往同一节点上的 Pod。
  2. Pod 流量发往驻留在不同节点上的 Pod。

为了使整个设置正常工作,我们需要我们已经讨论过的虚拟接口对和以太网桥。在继续讨论之前,我们先讨论一下它们的功能以及为什么它们是必要的。

为了使 Pod 与其他 Pod 通信,它必须首先有权访问节点的根命名空间。

这是通过连接两个命名空间(pod 和 root)的虚拟以太网对来实现的,这些虚拟接口设备(veth)连接并充当两个命名空间之间的隧道。

CNI 会为您执行此操作,但您也可以通过以下方式手动执行此操作:

shell 复制代码
$ ip link add veth1 netns pod-namespace type veth peer veth2 netns root

现在,您的 Pod 的命名空间具有到根命名空间的访问"隧道",节点上每个新创建的 pod 都将设置一个像这样的 veth 对。

创建接口对是其中的一部分,另一个是为以太网设备分配地址并创建默认路由,让我们探讨一下如何在 pod 的命名空间中设置 veth1 接口:

shell 复制代码
$ ip netns exec cni-0f226515-e28b-df13-9f16-dd79456825ac ip addr add 10.244.4.40/24 dev veth1
$ ip netns exec cni-0f226515-e28b-df13-9f16-dd79456825ac ip link set veth1 up
$ ip netns exec cni-0f226515-e28b-df13-9f16-dd79456825ac ip route add default via 10.244.4.40

在节点端,我们创建另一个 veth2 对:

shell 复制代码
$ ip addr add 169.254.132.141/16 dev veth2
$ ip link set veth2 up

您可以像之前一样检查现有的 veth 对。

在 Pod 的命名空间中,检索 eth0 接口的后缀。

shell 复制代码
$ ip netns exec cni-0f226515-e28b-df13-9f16-dd79456825ac ip link show type veth
3: eth0@if12: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP mode DEFAULT group default
    link/ether 16:a4:f8:4f:56:77 brd ff:ff:ff:ff:ff:ff link-netnsid 0

在这种情况下,您可以 通过grep 查看 grep -A1 ^12 (或者只是滚动浏览输出):

shell 复制代码
$ ip link show type veth
# output truncated
12: cali97e50e215bd@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP mode DEFAULT group default
    link/ether ee:ee:ee:ee:ee:ee brd ff:ff:ff:ff:ff:ff link-netns cni-0f226515-e28b-df13-9f16-dd79456825ac

注意 3: eth0@if1212: cali97e50e215bd@if3 接口上的符号。

从 pod 命名空间,eth0 接口连接到根命名空间中的 12 号接口。

在 veth 对的另一端,根命名空间连接到 pod 命名空间接口编号 3。

接下来是连接 veth 对两端的桥。

Pod 网络命名空间连接到以太网桥

桥接器(ethernet bridge)会将位于根命名空间(root namespace)中的虚拟接口的每一端"捆绑"在一起。该桥(bridge)将允许流量在虚拟对之间流动并穿过公共根命名空间(root namespace)。

以太网桥位于 OSI 网络模型的第 2 层,可以将网桥视为接受来自不同名称空间和接口的连接的虚拟交换机。

以太网桥允许您在同一节点上连接多个可用网络。

因此,您可以使用此设置并桥接两个接口,即从 pod 命名空间的 veth 到节点上的另一个 pod veth。

让我们看看以太网桥和 veth 对的实际应用。

跟踪同一节点上 Pod 到 Pod 的流量

假设同一节点上有两个 pod,Pod-A 想要向 Pod-B 发送消息。

至此,Pod-A和Pod-B之间的通信已经成功。

跟踪不同节点上 Pod 到 Pod 的通信

对于需要跨不同节点进行通信的 Pod,需要在网络通信中增加一跳。

这次ARP 解析不会发生,因为源 IP 和目标 IP 位于不同的网络。检查是使用按位运算完成的。 当目的IP不在当前网络时,转发到节点的默认网关。

按位运算的工作原理

源节点在确定数据包应转发到何处时必须执行按位运算。 此操作也称为 AND 运算。 回顾一下,按位 AND 运算产生以下结果:

ini 复制代码
0 AND 0 = 0
0 AND 1 = 0
1 AND 0 = 0
1 AND 1 = 1

除了 1 和 1 之外的任何内容都是错误的。 如果源节点的 IP 为 192.168.1.1,子网掩码为 /24,目标 IP 为 172.16.1.1/16,则按位与运算将表明它们确实位于不同的网络。 这意味着目标 IP 与数据包源不在同一网络上,因此数据包将通过默认网关转发。 我们必须从二进制的 32 位地址开始进行 AND 运算。 我们首先找出源IP网络和目标IP网络。

css 复制代码
| Type             | Binary                              | Converted          |
| ---------------- | ----------------------------------- | ------------------ |
| Src. IP Address  | 11000000.10101000.00000001.00000001 | 192.168.1.1        |
| Src. Subnet Mask | 11111111.11111111.11111111.00000000 | 255.255.255.0(/24) |
| Src. Network     | 11000000.10101000.00000001.00000000 | 192.168.1.0        |
|                  |                                     |                    |
| Dst. IP Address  | 10101100.00010000.00000001.00000001 | 172.16.1.1         |
| Dst. Subnet Mask | 11111111.11111111.00000000.00000000 | 255.255.0.0(/16)   |
| Dst. Network     | 10101100.00010000.00000000.00000000 | 172.16.0.0         |

对于按位运算,需要将目标 IP 与数据包源自节点的源子网进行比较。

css 复制代码
| Type             | Binary                              | Converted          |
| ---------------- | ----------------------------------- | ------------------ |
| Dst. IP Address  | 10101100.00010000.00000001.00000001 | 172.16.1.1         |
| Src. Subnet Mask | 11111111.11111111.11111111.00000000 | 255.255.255.0(/24) |
| Network  Result  | 10101100.00010000.00000001.00000000 | 172.16.1.0         |

正如我们所看到的,AND 运算网络结果为 172.16.1.0,它不等于 192.168.1.0(来自源节点的网络) 。 由此,我们确认源 IP 地址和目标 IP 地址不在同一网络上。 例如,如果目标 IP 为 192.168.1.2,即与发送 IP 在同一子网中,则 AND 运算将产生该节点的本地网络。

css 复制代码
| Type             | Binary                              | Converted          |
| ---------------- | ----------------------------------- | ------------------ |
| Dst. IP Address  | 11000000.10101000.00000001.00000010 | 192.168.1.2        |
| Src. Subnet Mask | 11111111.11111111.11111111.00000000 | 255.255.255.0(/24) |
| Network          | 11000000.10101000.00000001.00000000 | 192.168.1.0        |

按位比较后,ARP 将检查其查找表以查找默认网关的 MAC 地址。 如果有条目,它将立即转发数据包。 否则,它将首先进行广播以确定网关的MAC地址。

快速回顾

  1. Pod之内的网络流动 ,是通过网络命名空间进行隔离,所以能够直接用 localhost 进行网络的访问
  2. 同个Node不同Pod的网络流动 ,是通过网桥(bridge)来将网络进行连接,从而通过 PodIP 进行访问。
  3. 不同Node不同Pod的网络流动,是在网络上增加了转发,到网桥后没能找到具体的 PodIP ,则传输到集群网络内进行查找,到达特定的节点后仍然与同节点的通信相同。

至此我们学习了Kuberntes Pod的网络通信原理。

Refrence

  1. arthurchiao.art/blog/what-h...
  2. 使用网络命名空间和虚拟交换机来隔离服务器
  3. www.tigera.io/learn/guide...
  4. learnk8s.io/kubernetes-...
相关推荐
荣光波比12 小时前
K8S(一)—— 云原生与Kubernetes(K8S)从入门到实践:基础概念与操作全解析
云原生·容器·kubernetes
伞啊伞12 小时前
K8s概念基础(一)
云原生·容器·kubernetes
hello_25014 小时前
k8s基础监控promql
云原生·容器·kubernetes
静谧之心16 小时前
在 K8s 上可靠运行 PD 分离推理:RBG 的设计与实现
云原生·容器·golang·kubernetes·开源·pd分离
1024find20 小时前
Spark on k8s部署
大数据·运维·容器·spark·kubernetes
能不能别报错1 天前
K8s学习笔记(十六) 探针(Probe)
笔记·学习·kubernetes
能不能别报错1 天前
K8s学习笔记(十四) DaemonSet
笔记·学习·kubernetes
火星MARK1 天前
k8s面试题
容器·面试·kubernetes
赵渝强老师2 天前
【赵渝强老师】Docker容器的资源管理机制
linux·docker·容器·kubernetes
能不能别报错2 天前
K8s学习笔记(十五) pause容器与init容器
笔记·学习·kubernetes