- Linux 网络命名空间如何在 Pod 中工作">Linux 网络命名空间如何在 Pod 中工作
- 在 Pod 中,pause 容器创建了网络命名空间">在 Pod 中,pause 容器创建了网络命名空间
- 分配一个 IP 地址给 Pod">分配一个 IP 地址给 Pod
- 查看集群中 Pod 到 Pod 的流量">查看集群中 Pod 到 Pod 的流量
- Pod 网络命名空间连接到以太网桥">Pod 网络命名空间连接到以太网桥
- 跟踪在同一节点上 Pod 到 Pod 的流量">跟踪在同一节点上 Pod 到 Pod 的流量
- 跟踪不同节点上的 Pod 到 Pod 通信">跟踪不同节点上的 Pod 到 Pod 通信
- 按位运算的工作原理">按位运算的工作原理
- 容器网络接口 - CNI">容器网络接口 - CNI
- 检查 Pod 到 Service 的流量">检查 Pod 到 Service 的流量
- 使用 Netfilter 和 Iptables 拦截和重写流量">使用 Netfilter 和 Iptables 拦截和重写流量
- 检查来自服务的响应">检查来自服务的响应
- 总 结">总 结
- 在不使用网络地址转换 (NAT) 的情况下,集群中的 Pod 能够与任意其他 Pod 进行通信。
- 在不使用网络地址转换 (NAT) 的情况下,在集群节点上运行的程序能与同一节点上的任何 Pod 进行通信。
- 每个 Pod 都有自己的 IP 地址(IP-per-Pod),并且任意其他 Pod 都可以通过相同的这个地址访问它。
- 如何确保同一个 Pod 中的容器行为就像它们在同一个主机上一样?
- 集群中的 Pod 能否访问其他 Pod?
- Pod 可以访问服务吗?服务是负载均衡的吗?
- Pod 可以接收集群外部的流量吗?
Linux 网络命名空间如何在 Pod 中工作
来看一个运行应用的主容器和伴随一起的另一个容器。 在示例中,有一个带有 nginx 和 busybox 容器的 Pod:部署时,会发生以下事情:
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
- Pod 在节点上拥有独立的网络命名空间。
- 分配一个 IP 地址给 Pod ,两个容器之间共享端口。
- 两个容器共享相同的网络命名空间,并在本地彼此可见。
- 物理网络接口持有根网络命名空间。
- 可以使用 Linux 网络命名空间来创建独立的网络。每个网络都是独立的,除非进行配置,默认不会与其他网络互通。
<font style="color:rgb(30, 107, 184);">ip netns list</font>
可以列出主机上的命名空间。
需要注意的是,创建的网络命名空间会出现在 /var/run/netns 下面,但 Docker 并没有遵循这一规则。
例如,这是 Kubernetes 节点的一些命名空间:
注意 cni- 前缀;这意味着命名空间是由 CNI 插件创建的。 当创建一个 Pod,Pod 被分配给一个节点后,CNI 将:
$ 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)
- 分配 IP 地址。
- 将容器连接到网络。
- 当创建 Pod 时,容器运行时会给容器创建一个网络命名空间。
lsns 是一个用于列出主机上所有可用命名空间的命令。 请记住,Linux 中有多种命名空间类型。 Nginx 容器在哪里? 那些 pause 容器是什么?
$ 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
在 Pod 中,pause 容器创建了网络命名空间
先列出节点上的所有命名空间,看看能否找到 Nginx 容器:Nginx 容器在挂载 (mnt)、Unix time-sharing (uts) 和 PID (pid) 命名空间中,但不在网络命名空间 (net) 中。 不幸的是,lsns 只显示每个进程最小的 PID,可以根据这个进程 ID 进一步过滤。 使用以下命令,在所有命名空间中检索 Nginx 容器:
$ 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;
pause 进程再次出现,它劫持了网络命名空间。 这是怎么回事?
$ 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;
集群中的每个 Pod 都有一个额外的隐藏容器在后台运行,称为 pause 容器。
列出在节点上运行的容器并获取 pause 容器:可以看到,节点上的每一个 Pod 都会有一个对应的 pause 容器。 这个 pause 容器负责创建和维持网络命名空间。 底层容器运行时会完成网络命名空间的创建,通常是由 containerd 或 CRI-O 完成。 在部署 Pod 和创建容器之前,由运行时创建网络命名空间。 容器运行时会自动完成这些,不需要手工执行
$ docker ps | grep pause
fa9666c1d9c6 k8s.gcr.io/pause:3.4.1 "/pause" k8s_POD_kube-dns-599484b884-sv2js…
44218e010aeb k8s.gcr.io/pause:3.4.1 "/pause" k8s_POD_blackbox-exporter-55c457d…
5fb4b5942c66 k8s.gcr.io/pause:3.4.1 "/pause" k8s_POD_kube-dns-599484b884-cq99x…
8007db79dcf2 k8s.gcr.io/pause:3.4.1 "/pause" k8s_POD_konnectivity-agent-84f87c…
<font style="color:rgb(30, 107, 184);">ip netns</font>
创建命名空间。
话题回到 pause 容器。
它包含非常少的代码,并且在部署后立即进入睡眠状态。
但是,它是必不可少的,并且在 Kubernetes 生态系统中起着至关重要的作用。
- 创建 Pod 时,容器运行时会创建一个带有睡眠容器的网络命名空间。
- 使 busybox 容器加入之前的网络命名空间。
- 分配 IP 地址。
- 将容器连接到网络。
如果 Pod 中的一个容器崩溃了,剩下的仍然可以回复其他网络请求。
分配一个 IP 地址给 Pod
前面提到 Pod 和两个容器将具有同一个 IP 地址。 那是怎样配置的呢?在 Pod 网络命名空间内,创建了一个接口,并分配了一个 IP 地址。
验证一下。 首先,找到 Pod 的 IP 地址:接下来,找到相关的网络命名空间。 由于网络命名空间是从物理接口创建的,需要先访问集群节点。 如果运行的是 minikube,使用 minikube ssh 访问节点。如果在云厂中运行,那么应该有某种方法可以通过 SSH 访问节点。 进入后,找到最新创建的命名网络命名空间:
$ kubectl get Pod multi-container-Pod -o jsonpath={.status.PodIP}
10.244.4.40
在示例中,就是 cni-0f226515-e28b-df13-9f16-dd79456825ac。然后,可以在该命名空间内运行 exec 命令:
$ 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
这个 IP 就是 Pod 的 IP 地址!通过查找 @if12 中的 12 找到网络接口
$ ip netns exec cni-0f226515-e28b-df13-9f16-dd79456825ac ip a
# output truncated
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
还可以验证 Nginx 容器是否监听了来自该命名空间内的 HTTP 流量:
$ 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
如果无法通过 SSH 访问集群中的工作节点,可以使用
$ 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
<font style="color:rgb(30, 107, 184);">kubectl exec</font>
获取到 busybox 容器的 shell 并直接在内部使用 ip 和 netstat 命令。
刚刚介绍了容器之间的通信,再来看看如何建立 Pod 到 Pod 的通信吧。
查看集群中 Pod 到 Pod 的流量
Pod 到 Pod 的通信有两种可能的情况:- Pod 流量的目的地是同一节点上的 Pod。
- Pod 流量的目的地是在不同节点上的 Pod。
为了让一个 Pod 与其他 Pod 通信,它必须先访问节点的根命名空间。
通过虚拟以太网对来实现 Pod 和根命名空间的连接。 这些虚拟接口设备(veth 中的 v)连接并充当两个命名空间之间的隧道。 使用此 veth 设备,将一端连接到 Pod 的命名空间,另一端连接到根命名空间。现在 Pod 的命名空间有一个可以访问根命名空间的 隧道。 节点上,新建的每一个 Pod 都会设置这样的 veth 对。 一个是,创建接口对;另一个是为以太网设备分配地址并配置默认路由。 下面看看如何在 Pod 的命名空间中设置 veth1 接口:
$ ip link add veth1 netns Pod-namespace type veth peer veth2 netns root
在节点上,创建另一个 veth2 对:
$ 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
可以像前面一样检查现有的 veth 对。 在 Pod 的命名空间中,检索 eth0 接口的后缀。
$ ip addr add 169.254.132.141/16 dev veth2
$ ip link set veth2 up
在这种情况下,可以使用命令 grep -A1 ^12 查找(或滚动到目标所在处):
$ 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
也可以使用 ip -n cni-0f226515-e28b-df13-9f16-dd79456825ac link show type veth.命令 注意 3: eth0@if12和12: cali97e50e215bd@if3 接口上的符号。 从 Pod 命名空间,该 eth0 接口连接到根命名空间的 12 号接口,因此是 @if12. 在 veth 对的另一端,根命名空间连接到 Pod 命名空间的 3 号接口。 接下来是连接 veth 对两端的桥接器。
$ 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
Pod 网络命名空间连接到以太网桥
网桥会汇聚位于根命名空间中的每一个虚拟接口。这个网桥允许虚拟 pair 之间的流量,也允许穿过公共根命名空间的流量。 补充一下相关原理。 以太网桥位于 OSI 网络模型 的第 2 层。 可以将网桥视为接受来自不同命名空间和接口的连接的虚拟交换机。 以太网桥可以连接节点上的多个可用网络。 因此,可以使用网桥连接两个接口,即 Pod 命名空间的 veth 连接到同一节点上另一个 Pod 的 veth。跟踪在同一节点上 Pod 到 Pod 的流量
假设同一个节点上有两个 Pod,Pod-A 向 Pod-B 发送消息。- 由于访问目标不在同一个命名空间,Pod-A 将数据包发送到其默认接口 eth0。这个接口与 veth 对的一端绑定,作为隧道。这样,数据包会被转发到节点上的根命名空间。
跟踪不同节点上的 Pod 到 Pod 通信
对于跨节点 Pod 之间的通信,会经过额外的通信跳跃。- 前几个步骤保持不变,直到数据包到达根命名空间并需要发送到 Pod-B。
按位运算的工作原理
在确定数据包的转发位置时,源节点必须执行位运算 这也称为与操作。 复习一下,按位与运算的规则:除了 1 与 1 以外的都是 false。 如果源节点的 IP 为 192.168.1.1,子网掩码为 /24,目标 IP 为 172.16.1.1/16,则按位与运算将得知它们位于不同的网段上。 这意味着目标 IP 与数据包的源不在同一个网络上,数据包将通过默认网关转发。 数学时间。 必须从二进制的 32 位地址开始进行 AND 操作。 先找出源 IP 网络和目标 IP 网段。
0 AND 0 = 0
0 AND 1 = 0
1 AND 0 = 0
1 AND 1 = 1
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 |
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 |
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 |
- 现在,数据包路由到另一个节点的默认接口,称为 Node-B。
- 以相反的顺序。现在,数据包位于 Node-B 的根命名空间,并到达网桥,这里会进行 ARP 解析。
- 路由系统将返回与 Pod-B 相连的接口的 MAC 地址。
容器网络接口 - CNI
容器网络接口(CNI)主要关注的是当前节点中的网络。- Calico
- Cillium
- Flannel
- Weave Net
- 其他网络插件
- 创建接口。
- 创建 veth 对。
- 设置网络命名空间。
- 设置静态路由。
- 配置以太网桥。
- 分配 IP 地址。
- 创建 NAT 规则。
- 还有其他大量事情。
- ADD - 向网络添加一个容器。
- DEL - 从网络中删除一个容器。
- CHECK - 如果容器的网络出现问题,则返回错误。
- VERSION - 显示插件的版本。
但是,Kubelet 以 JSON 格式指定配置并发送至 CNI 插件。
可以进入节点上的 /etc/cni/net.d 文件夹,使用以下命令查看当前的 CNI 配置文件:每个 CNI 插件都会使用不同类型的网络配置。 例如,Calico 使用基于 BGP 的三层网络连接 Pod Cilium 从三层到七层使用的是基于 eBPF 的 overlay 网络 与 Calico 一样,Cilium 也支持通过配置网络策略来限制流量。 那么应该使用哪一个呢?主要有两类 CNI。 在第一类中,使用基本网络设置(也称为平面网络),从集群的 IP 池为 Pod 分配 IP 地址的 CNI。 这种方式可能很快耗尽 IP 地址,而成为负担。 相反,另一类是使用 overlay 网络。 简单来说,overlay 网络是主(底层)网络之上的重建网络。 overlay 网络通过封装来自底层网络的数据包工作,这些数据包被发送到另一个节点上的 Pod。 overlay 网络的一种流行技术是 VXLAN,它可以在 L3 网络上建立 L2 域的隧道。 那么哪个更好呢? 没有单一的答案,这取决于需求。 是否正在构建具有数万个节点的大型集群? 也许 overlay 网络更好。 是否在意更简单的配置和审查网络流量,而不会愿意在复杂网络中丢失这种能力? 扁平网络更适合。 现在讨论完了 CNI,接着来看看 Pod 到服务的通信是如何连接的。
$ cat 10-calico.conflist
{
"name": "k8s-Pod-network",
"cniVersion": "0.3.1",
"plugins": [
{
"type": "calico",
"datastore_type": "kubernetes",
"mtu": 0,
"nodename_file_optional": false,
"log_level": "Info",
"log_file_path": "/var/log/calico/cni/cni.log",
"ipam": { "type": "calico-ipam", "assign_ipv4" : "true", "assign_ipv6" : "false"},
"container_settings": {
"allow_ip_forwarding": false
},
"policy": {
"type": "k8s"
},
"kubernetes": {
"k8s_api_root":"https://10.96.0.1:443",
"kubeconfig": "/etc/cni/net.d/calico-kubeconfig"
}
},
{
"type": "bandwidth",
"capabilities": {"bandwidth": true}
},
{"type": "portmap", "snat": true, "capabilities": {"portMappings": true}}
]
}
检查 Pod 到 Service 的流量
由于 Pod 在 Kubernetes 中是动态的,分配给 Pod 的 IP 地址不是静态的。 Pod 的 IP 是短暂的,每次创建或删除 Pod 时都会发生变化。 Kubernetes 中的 Service 解决了这个问题,为连接一组 Pod 提供了可靠的机制。使用 Netfilter 和 Iptables 拦截和重写流量
Kubernetes 中的 Service 是基于 Linux 内核中的两个组件构建的:- 网络过滤器
- iptables
Netfilter 是一个可以配置数据包过滤、创建 NAT 、端口转发规则以及管理网络中流量的框架
此外,它可以屏蔽和禁止未经同意的访问。 另一方面,iptables 是一个用户态程序,可以用来配置 Linux 内核防火墙的 IP 数据包过滤规则。 iptables 是作为不同的 Netfilter 模块实现的。 可以使用 iptables CLI 即时修改过滤规则,并将它们插入 netfilters 挂载点。 过滤器配置在不同的表中,其中包含用于处理网络流量数据包的链。 不同的协议使用不同的内核模块和程序。 当提到 iptables 时,通常指的是 IPv4。对于 IPv6 ,终端工具是 ip6tables。 iptables 有五种链,每一种链都直接映射到 Netfilter 的钩子上。 从 iptables 的角度来看,它们是:- PRE_ROUTING
- INPUT
- FORWARD
- OUTPUT
- POST_ROUTING
- NF_IP_PRE_ROUTING
- NF_IP_LOCAL_IN
- NF_IP_FORWARD
- NF_IP_LOCAL_OUT
- NF_IP_POST_ROUTING
还可以使用这个工具来可视化节点上的 iptables 链。 这是来自 GKE 节点上的可视化 iptables 链的示例图:
$ iptables-save
检查来自服务的响应
Pod-B 发送响应,将其 IP 地址设置为源地址,并将 Pod-A 的 IP 地址设置为目标地址。总 结
一起回顾下本文相关要点- 容器如何在本地或 Pod 内通信。
- 在相同节点和不同节点上的 Pod 如何通信。
- Pod-to-Service - Pod 如何将流量发送到 Kubernetes 中服务后面的 Pod 时。
- 什么是命名空间、veth、iptables、chains、conntrack、Netfilter、CNI、overlay 网络,以及 Kubernetes 网络工具箱中所需的一切。