原文地址: https://dramasamy.medium.com/life-of-a-packet-in-kubernetes-part-1-f9bc0909e051
Kubernetes 集群的网络可能让人有点感到疑惑, 即使对于具有虚拟网络和请求路由实践经验的工程师来说也是如此. 这篇文章将会帮助你理解 Kubernetes 的网络基础知识. 最初的想法是通过 HTTP 请求如何到具体的 Service 进行深入解释 Kubernetes 的网络复杂度. 然而, 数据包的 “生命周期” 如果缺失了 “命名空间” / CNI / calico. 将不是完整的, 所以我们将从 Linux 网络开始然后在之后逐步涵盖这些主题.
这篇文章已经很长了, 因此按照主题划分成几个部分.
Topics-Part 1
- Linux 命名空间
- 容器网络
- 什么是CNI
- Pod 网络空间
链接: 这篇文章
Topics-Part 2
- Calico CNI
链接: https://dramasamy.medium.com/life-of-a-packet-in-kubernetes-part-2-a07f5bf0ff14
Topics-Part 3
- Pod-to-Pod
- Pod-to-External
- Pod-to-Service
- External-to-Pod
- 外部流量策略
- Kube-Proxy
- iptable 规则处理流
- 网络规则基础
链接: https://dramasamy.medium.com/life-of-a-packet-in-kubernetes-part-3-dd881476da0f
Topics-Part 4
- Ingress Controller
- Ingress Resources Example
- Nginx
- Envoy+Contour
- Ingress with MetalLB
链接: https://dramasamy.medium.com/life-of-a-packet-in-kubernetes-part-4-4dbc5256050a
Topics — Part 5
- ISTIO service mesh
链接: https://dramasamy.medium.com/life-of-a-packet-in-istio-part-1-8221971d77de
Linux 命名空间
Linux命名空间成为了大多数现代容器实现的技术基础。从高层来看, 它们允许在独立进程之间隔离全局系统资源。例如,PID命名空间会隔离进程ID号空间。这意味着在同一主机上运行的两个进程可以拥有相同的PID!
这种隔离级别在容器的世界中显然非常有用。假设没有命名空间,容器 A 中的一个进程可能可以卸载容器 B 中的一个重要文件系统, 或者更改容器 C 的主机名, 或者从容器 D 中删除一个网络接口。通过为这些资源创建命名空间,容器 A 中的进程甚至不能感知到容器B、C和D中的进程存在。
- Mount — 隔离文件系统挂载点
- UTS — 隔离主机名和域名
- IPC — 隔离进程间通信(IPC)资源
- PID — 隔离PID号空间
- Network — 隔离网络接口
- User — 隔离UID/GID号空间
- Cgroup — 隔离cgroup根目录
大多数容器实现都利用上述命名空间来实现单独的容器进程之间高级别的隔离。但请注意,cgroup 命名空间比其他命名空间稍微晚一些,并且没有广泛使用。
容器网络(网络命名空间)
在我们开始理解 CNI 和 Docker 提供的各种能力之前, 让我们探索驱动容器网络的核心技术。Linux 内核开发了各种特性来为主机提供多租户 (multi-tenancy) 功能。命名空间提供了不同类型的隔离功能, 其中网络命名空间提供了网络隔离。
在任何 Linux 操作系统中使用 ip
命令创建网络命名空间非常容易。让我们创建两个不同的网络命名空间, 并将它们命名为 client 和 server。
1 | ip netns add client |
创建一对veth来连接这些网络命名空间。可以把veth对想象成一根两头有连接器的网络电缆。1
2
3
4ip link add veth-client type veth peer name veth-server
ip link list | grep veth
4: veth-server@veth-client: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
5: veth-client@veth-server: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
这对veth(电缆)存在于主机网络命名空间中。现在让我们把这对veth的两端移动到我们之前分别创建的网络命名空间中。
1 | ip link set veth-client netns client |
让我们验证 veth 的两端是否真的存在于各个命名空间中。我们先从 client
命名空间开始:
1 | ip netns exec client ip link |
现在让我们检查 server
命名空间
1 | ip netns exec server ip link |
现在让我们为他们分配 IP 地址, 并且启动他们
1 | ip netns exec client ip address add 10.0.0.11/24 dev veth-client |
使用 Ping 命令, 我们可以验证两个网络命名空间已经被连接, 并且可以访问
1 | ip netns exec client ping 10.0.0.12 |
如果我们想要创建更多的网络命名空间并把它们连接起来, 为每一组命名空间组合都创建一个 veth 对可能不是一个可扩展的解决方案。
取而代之的是, 可以创建一个 Linux 网桥, 并挂接这些网络命名空间到网桥上以获取连接。这正是在同一主机上运行的 Docker 容器之间建立网络的方式!
让我们创建命名空间并将其附加到网桥上。
1 | All in one |
使用ping命令,我们可以验证这两个网络命名空间已经连接并可以访问
1 | controlplane $ ip netns exec client1 ping 172.30.0.12 -c 5 |
让我们从命名空间中 ping HOST_IP
1 | controlplane $ ip netns exec client1 ping $HOST_IP -c 2 |
是的 网络不可达
因为新创建的网络并没有设置任何路由, 让我们添加一个默认路由
1 | controlplane $ ip netns exec client1 ip route add default via 172.30.0.1 |
现在, 访问外部网络的 “默认” 路由是通过网桥的,因此这些命名空间可以使用任何外部网络服务。
1 | controlplane $ ping 8.8.8.8 -c 2 |
如何从外部服务器访问私有网络?
正如你所见, 我们演示的机器上已经安装了Docker, 这就导致了 docker0 网桥的创建。
在网络命名空间上下文中运行 web 服务器并不容易, 因为所有的 Linux 命名空间需要互相配合来模拟该场景。
让我们使用Docker来模拟这个场景。
1 | docker0 Link encap:Ethernet HWaddr 02:42:e2:44:07:39 |
现在让我们启动一个 Nginx 容器并检视配置。
1 | controlplane $ docker run -d --name web --rm nginx |
由于Docker没有在默认位置创建 netns,ip netns list 没有显示这个网络命名空间。我们可以创建一个指向预期位置的符号链接来克服这个限制。1
2
3
4
5
6
7
8
9
10controlplane $ container_id=web
controlplane $ container_netns=$(docker inspect ${container_id} --format '{{ .NetworkSettings.SandboxKey }}')
controlplane $ mkdir -p /var/run/netns
controlplane $ rm -f /var/run/netns/${container_id}
controlplane $ ln -sv ${container_netns} /var/run/netns/${container_id}
'/var/run/netns/web' -> '/var/run/docker/netns/c009f2a4be71'
controlplane $ ip netns list
web (id: 3)
server1 (id: 1)
client1 (id: 0)
让我们检查 web
命名空间的 IP 地址
1 | controlplane $ ip netns exec web ip addr |
让我们检查 docker 容器的 IP 地址1
2
3controlplane $ WEB_IP=`docker inspect -f "{{ .NetworkSettings.IPAddress }}" web`
controlplane $ echo $WEB_IP
172.18.0.3
很明显 Docker 使用了所有的 Linux 命名空间用于将容器与主机隔离。让我们从主机服务器尝试访问在 web 网络命名空间中运行的 WebApp。
1 | controlplane $ curl $WEB_IP |
可以从外部访问 webserver 吗? 是的, 通过端口转发即可1
2
3controlplane $ iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination $WEB_IP:80
controlplane $ echo $HOST_IP
172.17.0.23
让我们尝试使用 HOST IP 访问 webserver1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25node01 $ curl 172.17.0.23
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
node01 $
CNI插件通过执行类似上面的命令(不完全一样)来设置 loopback 接口, eth0, 并为容器分配IP地址。
容器运行时(即Kubernetes, PodMan等)使用CNI来设置POD网络。我们在下一节进行讨论 CNI
什么是CNI?
一个 CNI 插件负责向容器网络命名空间中插入一个网络接口(例如veth对的一端),并在主机上进行必要的更改(例如将veth的另一端连接到网桥上)。它应该为接口分配IP并通过调用适当的IPAM插件,设置与IP地址管理部分一致的路由。
等等, 看起来很眼熟? 是的, 我们在 “容器网络(网络命名空间))” 一节见过。
CNI(容器网络接口), 一个云原生计算基金会项目, 包含一个规范和库, 用于编写Linux容器中配置网络接口的插件, 以及许多支持的插件。CNI只关心容器的网络连接性, 并在删除容器时释放已分配的资源。因为仅关注于此, CNI得到了广泛的支持, 其规范也很简单实现。
注意:运行时可以是任何东西——例如 Kubernetes、PodMan、cloud Foundry 等
在我第一次通读的时候, 我发现以下几点很有意思:
- 该规范将容器定义为一个 Linux 网络命名空间。我们应该对这个定义很熟悉,因为像Docker这样的容器运行时为每个容器创建一个新的网络命名空间。
- CNI的网络定义以JSON文件存储。
- 网络定义通过STDIN流传输给插件;也就是说,主机上没有放置网络配置的配置文件。
- 通过环境变量向插件传递其他参数。
- CNI插件被实现为一个可执行文件。
- CNI插件负责连接容器。即它需要完成所有工作以使容器联网。在Docker中,这将包括以某种方式将容器网络命名空间连接回主机。
- CNI插件负责IPAM,包括IP地址分配和安装所需的路由。
让我们尝试在没有 Kubernetes 的情况下手动模拟 Pod 创建,并通过 CNI 插件分配 IP,而不是繁多的 CLI 命令。完成本演示后,您将了解 Kubernetes 中的 Pod 是什么。
步骤一: 下载 CNI 插件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21controlplane $ mkdir cni
controlplane $ cd cni
controlplane $ curl -O -L https://github.com/containernetworking/cni/releases/download/v0.4.0/cni-amd64-v0.4.0.tgz
Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 644 100 644 0 0 1934 0 --:--:-- --:--:-- --:--:-- 1933
100 15.3M 100 15.3M 0 0 233k 0 0:01:07 0:01:07 --:--:-- 104k
controlplane $ tar -xvf cni-amd64-v0.4.0.tgz
./
./macvlan
./dhcp
./loopback
./ptp
./ipvlan
./bridge
./tuning
./noop
./host-local
./cnitool
./flannel
controlplane $
步骤二: 创建一个 JSON 格式的 CNI 配置文件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18cat > /tmp/00-demo.conf <<"EOF"
{
"cniVersion": "0.2.0",
"name": "demo_br",
"type": "bridge",
"bridge": "cni_net0",
"isGateway": true,
"ipMasq": true,
"ipam": {
"type": "host-local",
"subnet": "10.0.10.0/24",
"routes": [
{ "dst": "0.0.0.0/0" },
{ "dst": "1.1.1.1/32", "gw":"10.0.10.1"}
]
}
}
EOF
CNI配置参数:
- cniVersion: 定义版本兼容的CNI规范版本
- name:网络名称
- type:希望使用的插件的名称。在这种情况下,插件可执行文件的实际名称
- args: 可选的额外参数
- ipMasq: 为该网络配置出站伪装(源NAT)
- ipam:
- type: IPAM插件可执行文件的名称
- subnet: 要分配的子网(这实际上是IPAM插件的一部分)
- routes:
- dst:希望访问的子网
- gw: 到达dst的下一跳的IP地址。如果未指定,则假定子网的默认网关
- dns:
- nameservers: 希望与此网络一起使用的 DNS服务器 列表
- domain: 用于DNS请求的搜索域
- search: 搜索域列表
- options: 要传递给接收方的选项列表
步骤三: 创建一个具有 “none” 网络的容器,所以该容器没有任何可用的 IP 地址。可以使用任何镜像创建容器,不过我使用 “pause” 容器来模仿 Kubernetes。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24controlplane $ docker run --name pause_demo -d --rm --network none kubernetes/pause
Unable to find image 'kubernetes/pause:latest' locally
latest: Pulling from kubernetes/pause
4f4fb700ef54: Pull complete
b9c8ec465f6b: Pull complete
Digest: sha256:b31bfb4d0213f254d361e0079deaaebefa4f82ba7aa76ef82e90b4935ad5b105
Status: Downloaded newer image for kubernetes/pause:latest
763d3ef7d3e943907a1f01f01e13c7cb6c389b1a16857141e7eac0ac10a6fe82
controlplane $ container_id=pause_demo
controlplane $ container_netns=$(docker inspect ${container_id} --format '{{ .NetworkSettings.SandboxKey }}')
controlplane $ mkdir -p /var/run/netns
controlplane $ rm -f /var/run/netns/${container_id}
controlplane $ ln -sv ${container_netns} /var/run/netns/${container_id}
'/var/run/netns/pause_demo' -> '/var/run/docker/netns/0297681f79b5'
controlplane $ ip netns list
pause_demo
controlplane $ ip netns exec $container_id ifconfig
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
步骤四:使用 CNI 配置文件调用 CNI 插件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17controlplane $ CNI_CONTAINERID=$container_id CNI_IFNAME=eth10 CNI_COMMAND=ADD CNI_NETNS=/var/run/netns/$container_id CNI_PATH=`pwd` ./bridge </tmp/00-demo.conf
2020/10/17 17:32:37 Error retriving last reserved ip: Failed to retrieve last reserved ip: open /var/lib/cni/networks/demo_br/last_reserved_ip: no such file or directory
{
"ip4": {
"ip": "10.0.10.2/24",
"gateway": "10.0.10.1",
"routes": [
{
"dst": "0.0.0.0/0"
},
{
"dst": "1.1.1.1/32",
"gw": "10.0.10.1"
}
]
},
"dns": {}
- CNI_COMMAND=ADD — 操作(可用值:ADD/DEL/CHECK)
- CNI_CONTAINER=pause_demo — 我们告诉CNI我们要使用的网络命名空间叫“pause_demo”
- CNI_NETNS=/var/run/netns/pause_demo— 命名空间的路径
- CNI_IFNAME=eth10— 容器一侧连接的接口名称
- CNI_PATH=`pwd` — 我们需要告诉CNI插件可执行文件所在的位置。在这种情况下,由于我们已经在“cni”目录中,我们只引用pwd(当前工作目录)。需要在命令pwd加上撇号才是有效的。
我强烈建议您阅读CNI规范以获取有关插件及其功能的更多信息。您可以在同一个JSON文件中使用多个插件来链接操作;例如添加防火墙规则等。
步骤五: 运行上述命令会返回一些东西。首先——它返回一个错误, 因为IPAM驱动程序找不到它用于本地存储IP信息的文件。如果我们针对不同的命名空间再次运行, 我们不会收到这个错误, 因为文件是在我们第一次运行插件时创建的。我们得到的第二个信息是一个 JSON 返回, 指示插件配置的相关IP配置。在这种情况下, 网桥应该配置了 10.0.10.1/24 的 IP 地址, 命名空间接口会配置成10.0.10.2/24。它还添加了我们在网络配置 JSON 中定义的默认路由和 1.1.1.1/32 路由。让我们检查它所做的:
1 | controlplane $ ip netns exec pause_demo ifconfig |
CNI 创建一个网桥并根据配置对其进行配置,1
2
3
4
5
6
7
8
9controlplane $ ifconfig
cni_net0 Link encap:Ethernet HWaddr 0a:58:0a:00:0a:01
inet addr:10.0.10.1 Bcast:0.0.0.0 Mask:255.255.255.0
inet6 addr: fe80::c4a4:2dff:fe4b:aa1b/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:7 errors:0 dropped:0 overruns:0 frame:0
TX packets:20 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:1174 (1.1 KB) TX bytes:1545 (1.5 KB)
步骤六: 创建一个 webserver 并且共享 ·pause· 容器的命名空间
1 | controlplane $ docker run --name web_demo -d --rm --network container:$container_id nginx |
步骤七: 通过 pause 容器的 IP 地址浏览 webserver 页面1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24controlplane $ curl `cat /var/lib/cni/networks/demo_br/last_reserved_ip`
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
让我们看看 Pod 的定义
Pod网络命名空间
在Kubernetes中需要首先理解的是, POD实际上与容器不等价, 而是一组容器的集合。这些相同集合的所有容器共享一个网络栈。Kubernetes 通过在每个创建的pod上的暂停容器上设置网络来管理它。所有其他容器都附加到暂停容器的网络上, 后者本身除了提供网络之外什么也不做。因此, 一个容器也可以通过 localhost 与同一 pod 定义中的不同容器中的服务进行通信。
引用
- https://man7.org/linux/man-pages/man7/namespaces.7.html
- https://github.com/containernetworking/cni/blob/master/SPEC.md
- https://github.com/containernetworking/cni/tree/master/cnitool
- https://github.com/containernetworking/cni
- https://tldp.org/HOWTO/BRIDGE-STP-HOWTO/set-up-the-bridge.html
- https://kubernetes.io/
- https://www.dasblinkenlichten.com/