Kubernetes 虚拟网络模型

在 K8s 的网络模型中,Pod 就像是一个个 Host,拥有独立的 IP,它们之间可以不需要经过 NAT 直接使用对方的 IP 进行通信。一般来说,在同一个机器上的 Pod,我们可以使用 Linux 网桥将不同 Network Namespace 的网卡桥接起来,从而使得它们好像处在同一个二层网络一样。但是到了多机的容器互联互通场景,网桥就行不通了。而其实,要使得网络互联互通,只有两种办法:路由隧道

其实,K8s 的虚拟网络,也可以算作一种 SDN(软件定义网络)。

Underlay 模型

Underlay 指的是直接使用底层的物理设施,通过标准路由协议如 BGP、OSPF 等设置路由表等转发规则改变网络拓扑,使得 Pod 网络之间可以像正常主机一样直接发送数据包而不用经过虚拟化封装。

优点:

  • 减少了额外的封装开销,使得数据包利用率增加
  • 和传统的网络配置没有区别
  • 易于调试和排错。

缺点:

  • 往往需要网络设备的支持,在公有云上,需要厂商配合设置 VPC 等虚拟网络。

可以说,Underlay 通过直接设置路由的办法使得虚拟网络互联互通。

Overlay 模型

Overlay 指的是不修改原有的网络拓扑,而是通过隧道协议封装数据包,例如 IPSec、VxLAN、ipip、GRE 等各种封包格式,然后再借助 Underlay 网络将封装后的数据包送达指定的主机,就像是在原来的网络拓扑上覆盖了一层新的网络拓扑一样。

优点:

  • 兼容性好,只需要运行容器的主机能互相连通即可,不需要底层网络设备支持
  • 配置灵活,支持灵活的网络策略

缺点:

  • 经过了隧道封装,带来额外开销,数据包利用率下降
  • 虚拟化的网络包给调试和排错带来一定的困难

Flannel VxLAN 模式例子

VxLAN 是一种 L2 Overlay 的封装格式,将原来的二层以太网帧使用 UDP 格式封装再发送出去:

VXLAN - Huawei DCN Design Guide - Huawei

首先,Flannel 会在每个主机上创建一个 flannel.<VLANID> 的 VxLAN 网卡用于封装和解封 VxLAN 数据包,通过下面的命令可以查看其详情:

$ ip -d link show flannel.1
13: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN mode DEFAULT group default
    link/ether da:42:52:a6:68:99 brd ff:ff:ff:ff:ff:ff promiscuity 0
    vxlan id 1 local 192.168.1.110 dev eno1 srcport 0 0 dstport 8472 nolearning ageing 300 addrgenmode none
$ ip -d addr show flannel.1
13: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN group default
    link/ether da:42:52:a6:68:99 brd ff:ff:ff:ff:ff:ff promiscuity 0
    vxlan id 1 local 192.168.1.110 dev eno1 srcport 0 0 dstport 8472 nolearning ageing 300
    inet 10.244.1.0/32 scope global flannel.1
       valid_lft forever preferred_lft forever

可以看到,本机的 VTEP 分配了 10.244.1.0 这个 IP 地址。而 MTU 被设置成了 1450 字节,这是因为 VxLAN 需要额外的 MAC + IP + UDP + VxLAN Header = 14 + 20 + 8 + 8 = 50 字节的封装。

cni0 网桥则相当于一个本机交换机,带有一个 VLAN:

$ ip -d link show cni0
14: cni0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 1a:9b:9d:6d:4f:f2 brd ff:ff:ff:ff:ff:ff promiscuity 0
    bridge forward_delay 1500 hello_time 200 max_age 2000 ageing_time 30000 stp_state 0 priority 32768 vlan_filtering 0 vlan_protocol 802.1Q addrgenmode eui64
$ ip -d addr show cni0
14: cni0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default qlen 1000
    link/ether 1a:9b:9d:6d:4f:f2 brd ff:ff:ff:ff:ff:ff promiscuity 0
    bridge forward_delay 1500 hello_time 200 max_age 2000 ageing_time 30000 stp_state 0 priority 32768 vlan_filtering 0 vlan_protocol 802.1Q
    inet 10.244.1.1/24 scope global cni0
       valid_lft forever preferred_lft forever
    inet6 fe80::189b:9dff:fe6d:4ff2/64 scope link
       valid_lft forever preferred_lft forever

查看路由表可以发现,同一个主机上的 Pod 直接使用 cni0 网桥通信,而如果要跨机通信,则需要通过 Flannel 网卡进行隧道转发到对应主机上的 Flannel VxLAN 网卡:

$ ip route show
...
10.244.0.0/24 via 10.244.0.0 dev flannel.1 onlink
10.244.1.0/24 dev cni0  proto kernel  scope link  src 10.244.1.1
10.244.2.0/24 via 10.244.2.0 dev flannel.1 onlink
10.244.3.0/24 via 10.244.3.0 dev flannel.1 onlink
10.244.4.0/24 via 10.244.4.0 dev flannel.1 onlink
...

那么问题来了,例如本机需要发送一个到 10.244.2.233 的数据包,经过路由表发现需要发送给网关 10.244.2.0。毫无疑问,这时候主机需要知道 10.244.2.0 的 MAC 地址,此时相当于发生了一次 l3miss,主机会尝试发送 ARP 来获取 10.244.2.0 的 MAC 地址。当然,为了避免广播风暴,Flannel 会提前在 ARP 表中写好对端 VTEP 的 MAC 地址:

$ ip neigh
...
10.244.1.57 dev cni0 lladdr 1e:fd:14:d0:08:51 REACHABLE
10.244.3.0 dev flannel.1 lladdr 56:34:8f:23:99:30 PERMANENT
10.244.7.0 dev flannel.1 lladdr ea:60:1e:c4:3d:6f PERMANENT
10.244.5.0 dev flannel.1 lladdr d6:ea:dd:2b:cc:86 PERMANENT
10.244.1.47 dev cni0 lladdr b6:02:9a:fd:b9:a9 REACHABLE
10.244.1.60 dev cni0 lladdr 4a:25:01:b7:a0:b2 REACHABLE
10.244.2.0 dev flannel.1 lladdr aa:63:8e:fa:63:4e PERMANENT
...

知道了 MAC 地址之后,还有一个问题:这个 MAC 地址应该发送到哪个主机上呢,我要从哪个端口发送数据包,用哪个 IP 地址封装?此时,相当于发生了一次 l2miss,如果是交换机,就会尝试泛洪(Flooding),同样为了避免广播,Flannel 会提前在 FDB (相当于交换机的 MAC 地址表)写好对应的主机 IP:

$ bridge fdb show dev flannel.1
...
fa:80:e7:b1:b9:95 dst 192.168.1.117 self permanent
52:3a:7c:fc:04:ce dst 192.168.1.113 self permanent
ea:3a:36:4a:d0:f5 dst 192.168.1.114 self permanent
d6:ea:dd:2b:cc:86 dst 192.168.1.119 self permanent
ea:60:1e:c4:3d:6f dst 192.168.1.115 self permanent
aa:63:8e:fa:63:4e dst 192.168.1.111 self permanent
...

于是就用 192.168.1.111 作为对端地址封装好 VxLAN 数据包,主机再通过物理网卡,按照正常的流程将 UDP 发送到对端主机。而对端主机收到 VxLAN 数据包后,首先解封装,得到原始二层数据包之后,按照路由表,转发到自己的 cni0 网桥中,网桥转发给对应的 Pod,也就完成了一次通信。