深度探索Docker、K8S的网络如何运行

能看到这篇文章,那你肯定是对容器、docker、k8s等有兴趣,那么本文将非常适合你,本文从网络这个角度去分析容器化网络用到的一些技术与内容,先使用命令模拟一个docker的环境,了解一下容器化网络的一些基础内容,然后深入分析k8s中容器通信以及Service的各种实现方式,最后简述一下dns的一些使用。希望对你有所帮助,文章较长,最好关注收藏,后面会推出k8s的后续的相关内容。

利用命令模拟Docker网络

Namespace(命名空间)

在 Linux 容器化技术中,namespace(命名空间)是实现进程隔离的核心技术之一,容器化需要使用 namespace 来提供不同容器之间的隔离和资源独立。wiki。Linux支持多种命名空间,本文重点关注网络部分,关于网络命名空间有以下描述

Network namespaces virtualize the network stack. On creation, a network namespace contains only a loopback interface. Each network interface (physical or virtual) is present in exactly 1 namespace and can be moved between namespaces.
Each namespace will have a private set of IP addresses, its own routing table, socket listing, connection tracking table, firewall, and other network-related resources.

每个网络命名空间,都有单独的IP地址,路由表,socket列表,防火墙等。

  • 网络命名空间使用命令
NS_NAME=MyNS
ip netns add $NS_NAME        //添加一个网络命名空间
ip netns list                //查看命名空间
ip netns delete $NS_NAME    //删除命名空间

有了命名空间之后,他们是隔离了,如果他们之间需要通信怎么办呢?好办,拉根线呗,接在一起。

veth pair(Virtual Ethernet)

先创建了namespace进行隔离,在用veth pair进行连接,有点像linux的pipe,将两个进程连接在一起。pair意味着它有两端,我个人觉得可以认为是两头各有一张网卡的网线。下面我们使用veth和namespace来模拟。

使用vnet直接连通两个命名空间
  • 添加一个veth,一头名字是veth0,一头是veth1,先看一下图 在这里插入图片描述
> ip link add veth0 type veth peer name veth1 

> ip link list 查看
...
141: veth1@veth0: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 2a:49:05:d5:83:0c brd ff:ff:ff:ff:ff:ff
142: veth0@veth1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 9e:c1:82:e5:e3:10 brd ff:ff:ff:ff:ff:ff

请注意此时会多了两个网口,一个名为veth1@veth0,一个名为veth0@veth1,状态都为DOWN

  • 创建两个namespace
ip netns add my-ns
ip netns add my-ns-peer
  • 分别将veth的两头放置到不同的namespace下
ip link set veth0 netns my-ns
ip link set veth1 netns my-ns-peer

查看一下:

> ip netns exec my-ns ip link list 
142: veth0@if141: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 9e:c1:82:e5:e3:10 brd ff:ff:ff:ff:ff:ff link-netnsid 1

> ip netns exec my-ns-peer ip link list 
141: veth1@if142: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 2a:49:05:d5:83:0c brd ff:ff:ff:ff:ff:ff link-netnsid 0

这个时候两个网卡的状态还是为DOWN

  • 设置IP地址并激活网卡
设置veth0
ip netns exec my-ns ip addr add 10.10.1.1/24 dev veth0
ip netns exec my-ns ip link set dev veth0 up

设置veth1
ip netns exec my-ns-peer ip addr add 10.10.1.2/24 dev veth1
ip netns exec my-ns-peer ip link set dev veth1 up

查看一下:

> ip netns exec my-ns ifconfig
veth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 10.10.1.1  netmask 255.255.255.0  broadcast 0.0.0.0
        inet6 fe80::9cc1:82ff:fee5:e310  prefixlen 64  scopeid 0x20<link>
        ether 9e:c1:82:e5:e3:10  txqueuelen 1000  (Ethernet)
        RX packets 8  bytes 648 (648.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 8  bytes 648 (648.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

> ip netns exec my-ns-peer ifconfig
veth1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 10.10.1.2  netmask 255.255.255.0  broadcast 0.0.0.0
        inet6 fe80::2849:5ff:fed5:830c  prefixlen 64  scopeid 0x20<link>
        ether 2a:49:05:d5:83:0c  txqueuelen 1000  (Ethernet)
        RX packets 8  bytes 648 (648.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 8  bytes 648 (648.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

两张网卡都已激活并正确的分配了IP地址

  • 测试一下连通性
从my-ns空间ping my-ns-peer
> ip netns exec my-ns ping 10.10.1.2
64 bytes from 10.10.1.2: icmp_seq=1 ttl=64 time=0.050 ms

从my-ns-peer空间ping my-ns
> ip netns exec my-ns-peer ping 10.10.1.1
64 bytes from 10.10.1.1: icmp_seq=1 ttl=64 time=0.034 ms

这个时候我们就使用一根线将两个namespace连通了。

但是这个时候外部是无法访问10.10.1.1和10.10.1.2的

> ping 10.10.1.1
2 packets transmitted, 0 received, 100% packet loss, time 1000ms
  • 如果你已经完全操作结束了,可以使用如下命令清理现场
ip netns exec my-ns ip link list
ip netns delete my-ns
ip netns delete my-ns-peer
多个namespace以及与宿主机通信

使用veth直连的方式只能解决两个namespace之间通信的问题,但是如果有多个namespace相互通信,veth就力不从心了。我们需要使用新的虚拟设备,虚拟网桥,下面我们做一下连通的实验,
先看一下总体的结构图,在这里插入图片描述

  • 创建一个虚拟网桥
ip link add br0 type bridge
ip link set br0 up
  • 创建3个命名空间
ip netns add my-ns-0
ip netns add my-ns-1
ip netns add my-ns-2
  • 创建三个veth
ip link add veth0 type veth peer name br-veth0
ip link add veth1 type veth peer name br-veth1
ip link add veth2 type veth peer name br-veth2
  • 将三个虚拟设备的一端放入分别的命名空间
ip link set veth0 netns my-ns-0
ip link set veth1 netns my-ns-1
ip link set veth2 netns my-ns-2
  • 将三个虚拟设备另一端连接到虚拟网桥并启动
ip link set br-veth0 master br0
ip link set br-veth0 up

ip link set br-veth1 master br0
ip link set br-veth1 up

ip link set br-veth2 master br0
ip link set br-veth2 up
  • 设置不同命名空间中设备的ip地址
ip netns exec my-ns-0 ip addr add 10.1.1.2/24 dev veth0
ip netns exec my-ns-0 ip link set veth0 up

ip netns exec my-ns-1 ip addr add 10.1.1.3/24 dev veth1
ip netns exec my-ns-1 ip link set veth1 up

ip netns exec my-ns-2 ip addr add 10.1.1.4/24 dev veth2
ip netns exec my-ns-2 ip link set veth2 up
  • 测试连通性,如果没有错误的话,这三个命名空间就连接到一起了,但是宿主机无法与三个命名空间通信,因为网桥目前没有IP地址
ip netns exec my-ns-0 ping 10.1.1.3
ip netns exec my-ns-1 ping 10.1.1.4
ip netns exec my-ns-2 ping 10.1.1.2
> ping 10.1.1.3 
2 packets transmitted, 0 received, 100% packet loss, time 1000ms
  • 给网桥设置IP地址
ip addr add ip 10.1.1.1/24 dev br0
> ifconfig 
br0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 10.1.1.1  netmask 255.255.255.0  broadcast 0.0.0.0
  • 测试联通性,如果没有错误的话,现在宿主机就和三个命名空间连通了
从my-ns-0测试到网桥的连通性
> ip netns exec my-ns-0 ping 10.1.1.1 
  PING 10.1.1.1 (10.1.1.1) 56(84) bytes of data.
  64 bytes from 10.1.1.1: icmp_seq=1 ttl=64 time=0.060 ms

从宿主机测试到my-ns-0的连通性
> ping 10.1.1.2
  PING 10.1.1.2 (10.1.1.2) 56(84) bytes of data.
  64 bytes from 10.1.1.2: icmp_seq=1 ttl=64 time=0.057 ms

  • 这个时候虽然之间连通了,但是还是无法访问外网,继续搞,添加网桥的192.168.1.1作为网关
ip netns exec my-ns-0 route add default gw 10.1.1.1
ip netns exec my-ns-1 route add default gw 10.1.1.1
ip netns exec my-ns-2 route add default gw 10.1.1.1
  • 还是不通,因为我的宿主机的IP是10.10.30.71,这个时候数据包能够出去,但是无法回来(可以在br0和eth0抓包观察),因为10.1.1.0是个私有网段,不在上层网络的路由规则中,这个需要加一条,这里有个iptables的插图,理解POSTROUTING的位置(iptables如果不太熟悉可以先简单记住就是在数据包离开主机之前所有来自10.1.1.0/24的数据ip都需要被修改
iptables -t nat -A POSTROUTING -s 10.1.1.0/24 -d 0.0.0.0/0 -j MASQUERADE

这个时候我在my-ns-0中执行ip netns exec my-ns-0 ping 8.8.8.8,分别在三个网口上进行转包,距离由近到远

  • 在veth的对端抓包
> tcpdump -n -nn -i br-veth0 dst host 8.8.8.8    
  tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
  listening on br0, link-type EN10MB (Ethernet), capture size 262144 bytes
  20:13:00.965858 IP 10.1.1.2 > 8.8.8.8: ICMP echo request, id 23016, seq 38, length 64
  20:13:01.966316 IP 10.1.1.2 > 8.8.8.8: ICMP echo request, id 23016, seq 39, length 64
  • 在网桥上抓包,源IP保持不变
> tcpdump -n -nn -i br0 dst host 8.8.8.8    
  tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
  listening on br0, link-type EN10MB (Ethernet), capture size 262144 bytes
  20:13:00.965858 IP 10.1.1.2 > 8.8.8.8: ICMP echo request, id 23016, seq 38, length 64
  20:13:01.966316 IP 10.1.1.2 > 8.8.8.8: ICMP echo request, id 23016, seq 39, length 64
  • 宿主机eth0转抓包,可以看到所有的源IP都转为了宿主机的10.10.30.71
> tcpdump -n -nn -i eth0 dst host 8.8.8.8
  tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
  listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
  20:12:32.956569 IP 10.10.30.71 > 8.8.8.8: ICMP echo request, id 23016, seq 10, length 64
  20:12:33.956712 IP 10.10.30.71 > 8.8.8.8: ICMP echo request, id 23016, seq 11, length 64

这样就可以连通外网了。

  • 最后一步,端口映射,在my-ns-0空间启动一个端口8000的http服务
> ip netns exec my-ns-0   python3 -m http.server 8000
> netstat -an | grep 8000 在宿主机上无法看到

因为网络是独立的,需要添加转发规则

iptables -t nat -A PREROUTING -p tcp --dport 9090 -j DNAT --to-destination 10.1.1.2:8000
iptables -t nat -nvL | grep 9090 查看规则是否添加成功

将宿主机上对9090端口的请求改为对10.1.1.2:8000请求,这个时候访问宿主机的http://10.10.30.71:9090(我的机器为10.10.30.71)就可以正常访问python启动的http服务了

  • 至此为止,我们就将多个namespace与宿主机连通了,能够互相访问,能够外网访问,能够启动端口对外提供服务。docker的网络的默认网桥模式基于以上原理实现的。

查看Docker具体的使用方式

  • 以我的机器为例子,我有一个docker0的网桥,ip地址为172.17.0.1
> ifconfig
docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.17.0.1  netmask 255.255.0.0  broadcast 172.17.255.255
> brctl show
  bridge name     bridge id               STP enabled     interfaces
  docker0         8000.0242b38b3dee       no              vetha0fa6b7
  • 在一个containerId为9f740ba587fe的容器,执行docker exec -it 9f740ba587fe /bin/sh
> ifconfig
eth0      Link encap:Ethernet  HWaddr 02:42:AC:11:00:03  
          inet addr:172.17.0.3  Bcast:172.17.255.255  Mask:255.255.0.0
> route -n
/ # route -n
  Kernel IP routing table
  Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
  0.0.0.0         172.17.0.1      0.0.0.0         UG    0      0        0 eth0

ip地址为172.17.0.3,网关为172.17.0.1就是上面的docker0网桥的地址

  • 查看容器的命名空间
lsns | grep `docker inspect -f {{.State.Pid}} 9f740ba587fe`
  • 查看地址伪装
> iptables -t nat -nvL  | grep 172.17
0     0 MASQUERADE  all  --  *      *       172.17.0.0/16        0.0.0.0/0   
  • 查看端口映射,该容器将8080端口映射到了容器中的80端口
> docker ps 
9f740ba587fe   tranglolab/kafka-connector-board   "nginx -g 'daemon of…"   7 weeks ago   Up 7 weeks   0.0.0.0:8080->80/tcp, :::8080->80/tcp   hopeful_brattain
> iptables -t nat -nvL  | grep 8080
4   208 DNAT       tcp  --  !docker0 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:8080 to:172.17.0.3:80

172.17.0.3就是上面看到的容器的ip地址,将8080端口映射到了172.17.0.3:80端口

  • 我们利用namespace、veth、iptables三者集合起来,就模拟和验证了docker网络组织的基本方式,这个其实是docker的网桥模式,还有host网络模式,这里就不展开了。

Kubernets网络

Docker的网络相对比较简单,但是这个是理解k8s网络的基础。K8S的网络可以说涉及到了计算机网络的各个方面,IP分配、节点连通、网关、DNS,路由等等,我们将k8s的容器网络与真实世界网络对比理解,也许效果更好。

提出问题

K8S的官网上提出了集群网络系统的四个问题

  • 高度耦合的容器间通信:这个已经被 Pod 和 localhost 通信解决了。
  • Pod 间通信
  • Pod 与 Service 间通信
  • 外部与 Service 间通信
    也给出了网络模型

先有一个K8S环境

  • 我部署了一个K0S,用的版本是k0s-v1.32.1+k0s.0-amd64,部署了3个机器,一个controller,两个worker,结构和IP如下
    在这里插入图片描述

  • 网络通信这块的内容比较多,我们分为两种情况讨论,节点内通信和跨节点通信。

节点内部通信

我们先看看单个Pod网络连接和设备对应情况
  • 启动一个BusyBox
apiVersion: v1
kind: Pod
metadata:
  name: busybox-pod
  labels:
    app: busybox-app
spec:
  containers:
  - name: busybox-container
    image: capnexus-registry.capstonedev.cn/library/busybox:latest
    command: ["sh", "-c", "while true; do echo 'Busybox is running'; sleep 10; done"]

运行得到以下信息的Pod

Name: busybox-pod
Namespace: default
Running Node: 192-168-40-35
Pod IP: 10.244.0.28

所以busybox-podd的宿主机为192.168.40.35

  • 192.168.40.35执行: kubectrl exec -n default busybox-pod -i -t -- ip link show
  ...
  2: eth0@if8: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue 
      link/ether 76:7e:4c:45:05:77 brd ff:ff:ff:ff:ff:ff

这里看到eth0@if8

  • 192.168.40.35中执行ip link
  ...
  8: veth9750d64d@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master kube-bridge state UP mode DEFAULT group default 
      link/ether 86:bc:22:8a:3d:83 brd ff:ff:ff:ff:ff:ff link-netnsid 3

我们看到序号为8的设备是在link-netnsid 3

  • 192.168.40.35上执行ip netns
  cni-9932435c-22c0-31fc-f51b-ee38cee99b01 (id: 3)
  cni-62099dba-a8a9-3628-d605-1ed794d5b220 (id: 1)
  cni-8fffbde1-4bf4-d317-fea0-16b91a20d73d (id: 2)
  cni-a0edeb62-71d7-f5e2-230b-2fea58467baa (id: 0)

找到id:3的网络命名空间

  • 192.168.40.35执行
ip netns exec cni-9932435c-22c0-31fc-f51b-ee38cee99b01 ip address

这个时候我们应该能够看到和kubectrl exec -n default busybox-pod -i -t -- ip link show一样的结果

  • 192.168.40.35上执行brctl show,此处需要安装brctrl工具yum install bridge-utils
kube-bridge             8000.cac33cb58442       no              veth01e0bb28
                                                        veth70c81cf8
                                                        veth9750d64d
                                                        vetha2ac3686
  • 我们可以看到有4个设备连接到了kube-bridge,然后从ip link中都可以看到kube-bridge和连接到四个interface的设备
  • 总结:busybox-pod通过veth veth9750d64d连接到kube-bridge上,在 cni-9932435c-22c0-31fc-f51b-ee38cee99b01这个网络命名空间中,使用的是IP是10.244.0.28
Pod内部的容器通信
  • 安装一个包含两个容器的Pod,使用busybox和node,其中Node启动了一个3000的HTTP端口
apiVersion: v1
kind: Pod
metadata:
  name: multi-container-pod
  labels:
    app: multi-container-example
spec:
  containers:
  - name: busybox-container
    image: busybox:latest
    command: ["sh", "-c", "while true; do echo 'Busybox is running'; sleep 10; done"]
  - name: node-container
    image: node:latest
    command: ["node", "-e", "require('http').createServer((req, res) => { res.end('Hello from Docker!'); }).listen(3000, '0.0.0.0', () => { conso
le.log('Server running at http://0.0.0.0:3000'); });"]
    ports:
    - containerPort: 3000
  • 使用这个命令kubectrl exec -n default multi-container-pod -c busybox-container -- telnet 127.0.0.1 3000,从busybox-container连接3000端口输出为
Connected to 127.0.0.1

说明在容器内部使用127.0.0.1即可通信

相同机器上不同Pod的容器通信
  • 部署一个新的busybox,名字为busybox-pod2
apiVersion: v1
kind: Pod
metadata:
  name: busybox-pod2
  labels:
    app: busybox-app
spec:
  containers:
  - name: busybox-container
    image: capnexus-registry.capstonedev.cn/library/busybox:latest
    command: ["sh", "-c", "while true; do echo 'Busybox is running'; sleep 10; done"]

和前文中的busybox-pod一样都运行在宿主机192.168.40.35下(运气好),IP地址为10.244.0.29,形成了如下结构
在这里插入图片描述

  • 两个Pod的IP分别为10.244.0.2810.244.0.29,他们通过kube-bridge在同一个网段中,可以通过ARP协议获取MAC地址进行通信。使用kubectrl exec -n default busybox-pod -i -t -- arp -an查看会发现
? (10.244.0.29) at 82:bf:18:88:ca:89 [ether]  on eth0
  • 我们运行kubectrl exec -n default busybox-pod -i -t -- ping 10.244.0.29的同时启动tcpdump -i kube-bridge -ent arp可以看到不定时的
76:7e:4c:45:05:77 > 82:bf:18:88:ca:89, ethertype ARP (0x0806), length 42: Request who-has 10.244.0.29 tell 10.244.0.28, length 28
82:bf:18:88:ca:89 > 76:7e:4c:45:05:77, ethertype ARP (0x0806), length 42: Request who-has 10.244.0.28 tell 10.244.0.29, length 28 

而使用tcpdump -i eth0 -ent arp是看不到,也就是说ARP协议包都是通过kube-bridge进行的。

分析不同机器的Pod的容器通信到底存在什么问题

我们创建了一个名为multi-container-pod的Pod,它运行在192.168.40.36,我们尝试连通multi-container-podbusybox-pod(运行在192.168.40.35),multi-container-pod的IP为10.244.1.4busybox-pod的IP为10.244.0.28。如下结构
在这里插入图片描述

  • 执行kubectrl exec -n default busybox-pod -i -t -- ip address
...
    inet 10.244.0.28/24 brd 10.244.0.255 scope global eth0
...

可以看到它10.244.0.28是24位的地址,和10.244.1.4不在同一个网段,这个时候我们就用到了以下信息kubectrl exec -n default busybox-pod -i -t -- route -n

Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         10.244.0.1      0.0.0.0         UG    0      0        0 eth0
10.244.0.0      0.0.0.0         255.255.255.0   U     0      0        0 eth0

它的网关是10.244.0.1,这个时候10.244.0.28就会发送ARP广播获取10.244.0.1的MAC地址,获取MAC地址后就会将消息发送给10.244.0.1让它来处理。
这个时候就涉及到一个问题,一个A网段中间隔着B网段,将数据发往C网段
在这里插入图片描述

  • 看到这张图,如果你给出解决方案,你的方案是什么呢?这个地方就可以玩很多的花样,下面我们引出跨节点通信。

跨节点通信

在这里我们稍作挺留,这块内容涉及到了一些知识点,不知道读者你是否已经掌握,包括但不限于ipables、BGP、UDP、IPVS、VXlan、dns等。下面我用几个小节对一些内容做一下预热。

热身
VXLAN(Virtual eXtensible Local Area Network虚拟可扩展的局域网)的使用实验
Linux 内核从Linux 3.7 版本开始支持VXLAN,到了内核3.12 版本对VXLAN 的支持已经完备,支持单播和组播,IPv4 和IPv6
  • HostA: 192.168.40.37 HostB: 192.168.40.38
  • HostA执行
ip link add vxlan1 type vxlan id 1 remote 192.168.40.38 dstport 4789 dev eth0
ip link set vxlan1 up
ip addr add 10.0.0.20/24 dev vxlan1

我们从add命令上可以看出,vxlan就是做了一个隧道将远端与本地连接起来,相当于灵魂链接

  • HostB执行
ip link add vxlan1 type vxlan id 1 remote 192.168.40.37 dstport 4789 dev eth0
ip link set vxlan1 up
ip addr add 10.0.0.21/24 dev vxlan1
  • 任意主机查看路由route -n
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         192.168.1.1     0.0.0.0         UG    100    0        0 eth0
10.0.0.0        0.0.0.0         255.255.255.0   U     0      0        0 vxlan1

去往10.0.0.0/24都去vxlan1口了

  • 任意主机查看端口 netstat -na | grep 4789
udp        0      0 0.0.0.0:4789            0.0.0.0:*  

自动启动了一个4789端口

  • HostB执行ping 10.0.0.20,在任意主机上执行tcpdump
  • 在vxlan1网口抓包,tcpdump -i vxlan1 -n -nn
00:03:03.141360 IP 10.0.0.21 > 10.0.0.20: ICMP echo request, id 3012, seq 34, length 64
00:03:03.141408 IP 10.0.0.20 > 10.0.0.21: ICMP echo reply, id 3012, seq 34, length 64
  • 在eth0网口抓包tcpdump -i eth0 -n -nn host 192.168.40.38
  00:03:31.141298 IP 192.168.40.38.38491 > 192.168.40.37.4789: VXLAN, flags [I] (0x08), vni 1
  IP 10.0.0.21 > 10.0.0.20: ICMP echo request, id 3012, seq 62, length 64
  00:03:31.141369 IP 192.168.40.37.40535 > 192.168.40.38.4789: VXLAN, flags [I] (0x08), vni 1
  IP 10.0.0.20 > 10.0.0.21: ICMP echo reply, id 3012, seq 62, length 64
  • 在eth0传输的vxlan的vni为1的数据,可以看到到vxlan1的数据已经是纯粹的ICMP的数据了。
  • 可见是,内核的vxlan模块会处理UDP的数据,去除UDP的包头,然后就查询路由表直接路由到vxlan1这个网口了
  • 这个协议名字看起来是vlan,其实是一种overlay,不过有一个vni id
IPVS(IP Virtual Server)
linux 内核2.4.x开始支持
  • 查看是否启用,如果显示如下,表示启用了,lsmod | grep ip_vs
ip_vs_rr               16384  98
ip_vs                 184320  100 ip_vs_rr
nf_conntrack          176128  5 xt_conntrack,nf_nat,nf_conntrack_netlink,xt_MASQUERADE,ip_vs
nf_defrag_ipv6         24576  2 nf_conntrack,ip_vs
libcrc32c              16384  4 nf_conntrack,nf_nat,nf_tables,ip_vs
  • from WIKI
IPVS (IP Virtual Server) implements transport-layer load balancing, usually called Layer 4 LAN switching, as part of the Linux kernel. It's configured via the user-space utility ipvsadm(8) tool.

可见ipvs实现的是传输层的负载均衡,通过用户空间的ipvsadm来管理内核态的映射关系。

  • 说白了,它可以将tcp/udp协议根据负载均衡的策略分发到不同的目标节点上,也就是内核中自带了一个负载均衡器。
iptables
  • iptables是基于linux的netfilter框架开发的
  • netfilter提供了5个hook点,其实就是在网络数据包处理的关键位置可以嵌入规则进行处理。
    • NF_IP_PRE_ROUTING: 进入路由之前
    • NF_IP_LOCAL_IN: 进入本地协议栈之前
    • NF_IP_FORWARD: 确认被转发但是还没有出去
    • NF_IP_LOCAL_OUT: 本地确认出去
    • NF_IP_POST_ROUTING: 本地出去与转发
      他们的关系如下,在这里插入图片描述
      ,不同的数据包走不通的路径。
  • iptables包含多张表,我们使用iptables -nvL默认查看的filter表,如果查看nat表,使用iptables -nvL -t nat
  • 每张表中都包含Chain,有多种类型的Chain
    • PREROUTING
    • INPUT
    • FORWARD
    • OUTPUT
    • POSTROUTING
    • 自定义Chain
      自定义的Chain将处理行为进行封装,从程序员的理解就是这就是定义了一个iptables方法
  • 所以hook、chain、table到底有啥关系呢?
    • 在数据包进入hook点之后,会按照如图所示的方式穿越不同的表的不同的chain,表的优先级: raw>mangle>nat>filter,在每个chain内,规则是顺次执行的。
      在这里插入图片描述
BGP(Border Gateway Protocol)协议
  • from wiki
Border Gateway Protocol (BGP) is a standardized exterior gateway protocol designed to exchange routing and reachability information among autonomous systems (AS) on the Internet.[2] BGP is classified as a path-vector routing protocol,[3] and it makes routing decisions based on paths, network policies, or rule-sets configured by a network administrator.
  • 是一个去中心化自治路由协议
  • 是一种路径矢量协议,非基于状态的
  • 每个路由器会配置一个AS(Autonomous system)编号,AS号是一个16bit的数字,全球共用这60000多个编号。1 – 64511 是全球唯一的,而 64512 – 65535 是可以自用的,类似于私网网段,比如联通的AS号是9800,AS编号查询
  • 路由器之间通信使用TCP协议
  • 有eBGP与iBGP两种模式
Tun(Tunnel)设备
  • TUN是一种虚拟网络设备,常用于实现网络层的隧道, 它是在操作系统内核中创建的虚拟设备,允许用户空间的程序读取或写入IP数据包,从而可以在不修改实际网络硬件或驱动的情况下实现自定义的网络行为。
    在这里插入图片描述

tunnel设备顾名思义,就是用来做隧道的,通过读文件的方式,实际上用来操作网络协议栈,操作系统太有趣了,这个设备一脚踩在文件层,一脚踩在网络中,头出溜在用户面前

  • 我们常说的VPN底层就是用这种设备和技术实现的。
小结

我们通过介绍几个技术项来做个简单的warmup,下面我们趁热打铁看看CNI如何利用这些技术完成通信的。

Flannel
  • Flannel is a simple and easy way to configure a layer 3 network fabric designed for Kubernetes. 它在k8s中部署一个daemonset,在每个节点运行一个flanneld进程来实现其功能。
VXLAN模式,默认模式
  • 我又部署了一个cni为flannel的k8s集群,我们查看一下相关信息。
  • 配置文件
{
  "Network": "10.244.0.0/16",
  "EnableNFTables": false,
  "Backend": {
    "Type": "vxlan"
  }
}

可以看到该实例基于vxlan模式。
具体例子拓扑如下
在这里插入图片描述

  • 在192.168.1.37执行 route -n
...
10.244.1.0      10.244.1.0      255.255.255.0   UG    0      0        0 flannel.1
...

将10.244.1.0/24的数据都路由到设备flannel.1

  • 在192.168.1.37使用ip -d link show flannel.1
6: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN mode DEFAULT group default 
    link/ether b6:dc:09:2b:21:ba brd ff:ff:ff:ff:ff:ff promiscuity 0 
    vxlan id 1 local 192.168.40.37 dev eth0 srcport 0 0 dstport 8472 nolearning ageing 300 noudpcsum noudp6zerocsumtx noudp6zerocsumrx addrgenmode eui64 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535 

该设备是一个vxlan设备

  • 在192.168.1.37使用bridge fdb show | grep flannel.1 查询fdb(Forwarding Database)转发表,flannel.1链接本地与192.168.40.38
  52:ab:e2:49:7c:c8 dev flannel.1 dst 192.168.40.38 self permanent
  • 在192.168.1.37查看启动的UDP端口etstat -anp | grep 8472
    udp 0  0  0.0.0.0:8472  0.0.0.0:*   -
    
  • 总之,去往10.244.1.0/24的流量被注入到flannel.1中,进入vxlan的处理逻辑,它根据vlan 1的信息得到源IP为192.168.40.37,目的IP为192.168.40.38,目的端口为8472
UDP模式,使用Tun设备
  • 下面我们把上面的flannel改为UDP模式
{
  "Network": "10.244.0.0/16",
  "EnableNFTables": false,
  "Backend": {
    "Type": "udp"
  }
}
  • 我们还是在192.168.40.37上执行netstat -anp |grep udp,发现启动了一个UDP端口

    udp        0      0 192.168.40.37:8285      0.0.0.0:*  2972/flanneld 
    
  • 多了一个flannel0,我们还是在192.168.40.37执行ip -d link show flannel0,我们发现是tun设备

    12: flannel0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1472 qdisc pfifo_fast state UNKNOWN mode DEFAULT group default qlen 500
      link/none  promiscuity 0 
      tun addrgenmode random numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535 
    
  • route -n,注意,这里是一个10.244.0.0/16的网段

    10.244.0.0      0.0.0.0         255.255.255.0   U     0      0        0 cni0
    10.244.0.0      0.0.0.0         255.255.0.0     U     0      0        0 flannel0
    
  • 具体的包的流程如下图
    在这里插入图片描述

  • 开发者在有了接收数据包和发送数据包的权利,那很多事情都可以干了。比如flannel中,容器的数据可以全部通过路由表发送到flanneld后台进程中,这个进程通过查看ip头的目的ip,然后通过etcd中的信息找到该ip所在的host,然后将收到的包封一个UDP包头向host发送出去。然后对端的flanneld收到该UDP包,然后查到目的ip就可以将数据从新发到目的host的tun设备上了。

host-gw模式
  • 顾名思义,就是用host做网关,直接寻址,因为网关的MAC地址查询是链路层完成的,所以这种模式适合于二层可达的节点网络。
  • 这种模式简单直接 在这里插入图片描述
Kube Router
BGP模式
  • kube router在每个Node上运行一个kube-router进程,他们之间可以进行BGP通信
  • 使用的iBGP,即使用相同的AS
  • 此处需要下载gobgp,在k0s的worker节点上运行一下命令,发现192.168.40.36的邻居为192.168.40.35192.168.40.35的邻居为192.168.40.36,都在AS 64512中
>  ./gobgp  neighbor -u 192.168.40.36
Peer             AS     Up/Down State       |#Received  Accepted
192.168.40.35 64512 1d 08:50:31 Establ      |        1         1

> ./gobgp  neighbor -u 192.168.40.35
Peer             AS     Up/Down State       |#Received  Accepted
192.168.40.36 64512 1d 08:52:03 Establ      |        1         1
  • 还可以使用./gobgp global rib, rib(route information base),查看详细的路由条目
   Network              Next Hop             AS_PATH              Age        Attrs
*> 10.244.0.0/24        192.168.40.35                             00:03:09   [{Origin: i}]
*> 10.244.1.0/24        192.168.40.36                             1d 09:03:40 [{Origin: i} {LocalPref: 100}]
  • BGP的Peer内部使用179作为TCP的连接端口,在192.168.40.36上执行netstat -anp | grep 179
tcp        0      0 192.168.40.36:179       0.0.0.0:*               LISTEN      1718/kube-router    
tcp        0      0 192.168.40.36:50547     192.168.40.35:179       ESTABLISHED 1718/kube-router 

在192.168.40.35上执行netstat -anp | grep 179

tcp        0      0 192.168.40.35:179       0.0.0.0:*               LISTEN      1764/kube-router    
tcp        0      0 192.168.40.35:179       192.168.40.36:50547     ESTABLISHED 1764/kube-router 

在有新的Pod新建的时候使用tcpdump -nn -n port 179 -i eth0有输出

18:27:40.907045 IP 192.168.40.36.50547 > 192.168.40.35.179: Flags [P.], seq 1414131710:1414131729, ack 4262141184, win 58, options [nop,nop,TS val 118456940 ecr 121403993], length 19: BGP
18:27:40.907109 IP 192.168.40.35.179 > 192.168.40.36.50547: Flags [.], ack 19, win 57, options [nop,nop,TS val 121418949 ecr 118456940], length 0
18:27:40.907255 IP 192.168.40.35.179 > 192.168.40.36.50547: Flags [P.], seq 1:20, ack 19, win 57, options [nop,nop,TS val 121418949 ecr 118456940], length 19: BGP
18:27:40.946787 IP 192.168.40.36.50547 > 192.168.40.35.179: Flags [.], ack 20, win 58, options [nop,nop,TS val 118456980 ecr 121418949], length 0
  • 我们在192.168.40.35使用route -n查看
...
10.244.1.0      192.168.40.36   255.255.255.0   UG    0      0        0 eth0
...

192.168.40.36使用route -n

...
10.244.0.0      192.168.40.35   255.255.255.0   UG    0      0        0 eth0
...

可以看到每个机器上都有一条路由将IP网段与所在的Host关联起来,这一点看起来是flannel的host-gw模式的结果是类似的

Calico
  • 有两种模式,一种是IPIP模式,一种是BGP模式,
  • IPIP的实现也是依赖于Tun设备,相对于flannel的UDP模式,不封装传输层的协议,直接封装IP
  • 理解了KubeRouter的BGP,这个基本可以脑补出来了

Service的实现

  • kube-proxy执行service的实现,kube-proxy也是deameonset,每个节点一个进程
  • 目前有两种实现iptables模式和ipvs模式
特别提及的LoadBalancer
  • 该模式就是为了对外暴露服务用的,从这个名字来看,我怀疑这个名字的初衷就是为了对接云厂商的负载均衡器。
  • LoadBalancer 的工作需要搭配第三方的负载均衡器来完成。
  • 此处采用的是Metallb来实现内网机器的ip分配
Metallb
  • 为没有负载均衡器的环境而设计
MetalLB is a load-balancer implementation for bare metal Kubernetes clusters, using standard routing protocols.
  • 有一个daemonset的speaker,有一个deployment的controller
  • Speaker 则会依据选择的协议进行相应的广播或应答,实现 IP 地址的通信响应
  • Controller 负责监听 Service 变化并分配或回收 IP,即实现IPAM
  • 有两种工作模式Lay2和BGP
Lay2模式
  • speaker实现了ARP协议
  • speaker会选举一个leader,所有的Loadbalancer的IP地址都在Leader下生成,不能有多个节点同时对外喊话,但是这样会有单节点瓶颈和故障转移慢的问题。
BGP模式
  • 不会对分配的IP地址进行ARP的回应
  • 是靠网络将数据带到宿主机上的
    在这里插入图片描述
iptables中实现service
  • 本小节所有的iptables的规则都来自于NAT表,与filter没有关联,使用的基础配置为
apiVersion: v1
kind: Service
metadata:
  name: my-service
  namespace: default
  labels:
    app: my-app
spec:
  type: ClusterIP
  selector:
    app: multi-container-example
  ports:
    - name: http
      protocol: TCP
      port: 9090
      targetPort: 3000
ClusterIP Service

在这里插入图片描述

该图是根据iptables -nvL -nat的结果根据调用链得到的

  • KUBE-SERVICES是k8s的service规则的入口,每个service占用KUBE-SERVICES的一条,target指向具体的规则,该规则由KUBE-SVC开头
  • 本例子中为Chain KUBE-SVC-I37Z43XJW6BD4TLV,有两条规则,每个规则是一个EndPoint
  • Endpoint使用KUBE-SEP开头的Chain表示,SEP是Kubernetes Service Endpoint,本例中为Chain KUBE-SEP-MBYDVCBRKBCVNA25Chain KUBE-SEP-ZZJ42GABN6SWXEM5,此处描述了真正的Endpoint的IP:Port,此处进行DNAT,将目的IP地址换为10.244.0.30:3000或者10.244.1.4:3000就可以走正常的正常的容器间的通信流程了。
NodePort Service
  • 本例中的NodePort为32732,使用netstat -an | grep 32732,可以看到并没有一个端口启动着,名为NodePort但是Node上并没有Port

  • 在这里插入图片描述

  • 从上图可以看出,它与ClusterIP的不同就是KUBE-SERVICES的尾部追加了一个Chain KUBE-NODEPORTS,里面添加了一个扩展的Chain KUBE-EXT-I37Z43XJW6BD4TLV,然后又重新调用了ClusterIp的Chain

LoadBalancer Service
  • 此处假定部署了一个内网负载均衡器,我使用L2模式安装了了一个MetalLB
用Lay2模式执行
  • 将上述里的服务类型改为LoadBalancer,然后执行 kubectrl get svc -n default,service赋予了可被外部访问的Ip 192.168.40.60
my-service   LoadBalancer   10.102.115.213   192.168.40.60   9090:32732/TCP   47m
  • 在这里插入图片描述

  • 从上图可以看出,它与ClusterIP的不同就是KUBE-SERVICES加了一条目的地为192.168.40.60(服务的对外地址)都执行Chain KUBE-EXT-I37Z43XJW6BD4TLV,这一条在NodePort模式下也是有的,然后就走后续流程了。

  • ifconfig | grep 192.168.40.60发现网卡上并没有该IP,所以这个IP地址也只在iptables中存在,那么问题来了,如果只在iptables存在,谁在对192.168.40.60做ARP的回应呢?

  • 测试一下,telnet 192.168.40.60 9090,然后arp -an

...
? (192.168.40.60) at ea:d1:d9:01:e8:3d [ether] on eth0
...

这个IP是有MAC地址的,奥秘就是metallb的Speaker在在对外喊话,192.168.40.60在我这,在我这,都到我这来

  • 这里有个巧妙的地方,metallb不仅玩"无中生有",还玩"移花接木",将虚拟出来的IP用speaker所在主机的MAC地址进行回应。 ifconfig eth0
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.40.36  netmask 255.255.0.0  broadcast 192.168.255.255
        ...
        ether ea:d1:d9:01:e8:3d  txqueuelen 1000  (Ethernet)

可以看到MAC地址192.168.40.36与192.168.40.60的相同,metabllb将宿主机的MAC地址当做它虚拟的IP地址来使用了

  • 确实只有一台机器的iptables做了9090端口的处理
切换为BGP模式执行
  • 我们重点关注网络部分的实现,service在iptables的实现与Lay2模式下一样
  • 这个时候就无法生成ARP记录了,ping 192.168.40.60之后arp -an
? (192.168.40.60) at <incomplete> on eth0

无法生成记录,因为这个模式下没有ARP回应

  • 这个时候如果要进行连通,需要添加一条路由规则
ip route add 192.168.40.60 via 192.168.40.36
  • 这个时候telnet 192.168.40.60 9090
Trying 192.168.40.60...
Connected to 192.168.40.60.

就连通了

  • 所以BGP模式其实用的是路由的方式将包丢给了宿主机。
IPVS实现service
  • IPVS目前已经
  • 直观点,先举个例子看一下
    ipvsadm -Ln
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port  Scheduler Flags  -> RemoteAddress:Port   Forward Weight ActiveConn InActConn
TCP  192.168.1.20:30183 rr               -> 10.101.0.76:9094     Masq    1      0          0         

kubectl get service -A | grep 30183

my-data          my-external           LoadBalancer   10.102.185.160   192.168.30.50   9094:30183/TCP                  51d

kubectl get endpoints my-external -n my-data -o jsonpath='{.subsets[0].addresses[*].ip}:{.subsets[0].ports[0].port}'

10.101.0.76:9094

可以看到192.168.1.20:30183对应的是my-data空间的my-external,endpoints为10.101.0.76:9094

  • 对比来看,不管是ipvs还是iptables都是将service的IP:Port转发到对应的Endpoint,不过是用的模块不一样,iptables和ipvs都是基于netfilter在对应的hook上做的规则,当service的数量很多的时候,比如到一万条,iptables就是一条一条的遍历,是O(N)的,而ipvs则使用哈希表效率更高。

Coredns

  • Service的命名规则如下
<service-name>.<namespace>.svc.cluster.local
  • 查看Pod的域名配置kubectrl exec -n default multi-container-pod -c busybox-container -i -t -- cat /etc/resolv.conf
search default.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.96.0.10
options ndots:5
  • 查看dns service kubectrl get svc -n kube-system | grep dns
kube-dns         ClusterIP   10.96.0.10     <none>        53/UDP,53/TCP,9153/TCP   2d12h
  • Pod就是在使用CoreDNS使用的域名解析服务
  • kubectrl exec -n default multi-container-pod -c busybox-container -i -t -- ping my-service.default.svc.cluster.localkubectrl exec -n default multi-container-pod -c busybox-container -i -t -- ping my-service.default都是
PING my-service.default (10.102.115.213): 56 data bytes

这样的结果。

总结

本文从网络的角度出发,总结和分析了容器化网络的的实现方式,有细节也有粗略,希望能够看官们提供一些帮助。如果遗漏或者可以提供更深的描述,欢迎大佬补充与评论。涉及到的内容比较多难免有所疏漏,如有错误,也望不吝指出,感谢。

微信公众号为“吹风的坚果”,欢迎关注,定期更新优质的计算机文章。

引用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值