滴普技术荟 - 云原生基座 OpenKube 开放容器实践(八):flannel-vxlan 模式原理解析
前言
上一章介绍完 flannel 的 udp 模式后,接着来介绍一下 vxlan 模式,因为很多生产的 K8S 都正在使用这个模式,所以将会介绍得详细些。
在前面的章节中,我们介绍过配置 linux vxlan 完成跨主机的容器通信,flannel 的 vxlan 模式大概完成的事情就是把那个章节中手工完成的事情自动化了而已,想了解的童鞋可以看回那一章
vxlan 模式下的 flannel 比 udp 模式少了一个组件,就是用 c 语言写的打开 tun 设备的守护进程 flanneld,因为 vxlan 模式下通信的全程都由 linux 内核完成,所以只剩下二进制的 flannel 文件和以 k8s 的 daemonset 的方式运行的 kube-flannel 这两个组件了,下面将详细介绍这两个组件以及他们如何与 kubelet 配合完成一个 POD 的网络编织。
二进制 flannel 文件
二进制 flannel 文件存放在每个节点的/opt/cni/bin 目录下,这个目录下还有 cni 官方默认提供的其它插件,这些 cni 插件分为三类:
1. ipam,负责地址分配,主要有:host-local、dhcp、static
2. main,负责主机和容器网络的编织,主要有:bridge、ptp、ipvlan、macvlan、host-device、
3. meta,其它,主要有:flannel、bandwidth、firewall、portmap、tuning、sbr
这些文件是我们在安装 kubeadm 和 kubelet 时自动安装的,如果发现这个目录为空,也可以用下面的命令手动安装:
yum install kubernetes-cni -y
这个 flannel 文件不做具体的网络编织的工作,而是生成其它 cni 插件需要的配置文件,然后调用其它的 cni 插件(通常是 bridge 和 host-local),完成主机内容器到主机的网络互通,事实上这个 flannel 文件的源码已经不在 flannel 项目上了,因为这个 flannel 文件已经是 cni 的默认组件之一了,所以它的源码在 cni 的 plugins 中,地址如下:
https://github.com/containernetworking/plugins/tree/master/plugins/meta/flannel
kubelet 创建一个 POD 时,先会创建一个 pause 容器,然后用 pause 容器的网络命名空间文件路径为入参(类似:/var/run/docker/netns/xxxx,前面的文章讲过这个路径如何获取及使用),加上其它一些参数,调用/etc/cni/net.d/目录下的配置文件指定的 cni 插件,这个目录的配置文件是 kube-flannel 启动时复制进去的,内容如下:
cat /etc/cni/net.d/10-flannel.conflist
{
"name": "cbr0",
"cniVersion": "0.3.1",
"plugins": [
{
"type": "flannel",
"delegate": {
"hairpinMode": true,
"isDefaultGateway": true
}
},
{
"type": "portmap",
"capabilities": {
"portMappings": true
}
}
]
}
这个文件中指定的 cni 插件叫 flannel,于是就调用了/opt/cni/bin/flannel 文件,这个文件先会读取/run/flannel/subnet.env 文件,这个文件也是 kube-flannel 启动时写入的,里面主要包含当前节点的子网信息,内容如下:
cat /run/flannel/subnet.env
FLANNEL_NETWORK=10.244.0.0/16
FLANNEL_SUBNET=10.244.1.1/24
FLANNEL_MTU=1450
FLANNEL_IPMASQ=true
flannel 读取该文件内容后,紧接着会生成一个符合 cni 标准的配置文件,内容如下:
{
"cniVersion": "0.3.0",
"name": "networks",
"type": "bridge",
"bridge": "cni0",
"isDefaultGateway": true,
"ipam": {
"type": "host-local",
"subnet": "10.244.1.0/24",
"dataDir": "/var/lib/cni/",
"routes": [{ "dst": "0.0.0.0/0" }]
}
}
然后像 kubelet 调用 flannel 的方式一样调用另一个 cni 插件 bridge,并把上面的配置文件的内容用标准输入的方式传递过去,调用方式如下:
echo '{ "cniVersion": "0.3.0", "name": "network", "type":"bridge","bridge":"cni0", "ipam":{"type":"host-local","subnet": "10.244.1.0/24","dataDir": "/var/lib/cni/","routes": [{ "dst": "0.0.0.0/0" }]}}' | CNI_COMMAND=ADD
CNI_CONTAINERID=xxx
CNI_NETNS=/var/run/docker/netns/xxxx
CNI_IFNAME=xxx
CNI_ARGS='IgnoreUnknown=1;K8S_POD_NAMESPACE=applife;K8S_POD_NAME=redis-59b4c86fd9-wrmr9'
CNI_PATH=/opt/cni/bin/
./bridge
剩余的工作就会由/opt/cni/bin/bridge 插件完成,它会在主机上创建一个 cni0 的 linux bridge,然后创建一条主机路由,并创建一对 Veth 网卡,把一端插到新创建的 POD,另一端插到网桥上,这些内容我们在前面的文章中介绍过,感兴趣的童鞋可以看回:
host-local 是以写本地文件的方式来标识哪些 IP 已经被占用,它会在/var/lib/cni/network/host-local/(这个目录其实是上面的 dataDir 参数指定的)目录下生成一些文件,文件名为已分配的 IP,文件内容为使用该 IP 的容器 ID,有一个指示当前已分配最新的 IP 的文件。
kube-flannel
kube-flannel 以 k8s 的 daemonset 方式运行,启动后会完成以下几件事情:
1. 启动容器会把/etc/kube-flannel/cni-conf.json 文件复制到/etc/cni/net.d/10-flannel.conflist:这个文件是容器启动时从配置项挂载到容器上的,可以通过修改 flannel 部署的 yaml 文件来修改配置,选择使用其它的 cni 插件。
2. 运行容器会从 api-server 中获取属于本节点的 pod-cidr,然后写一个配置文件/run/flannel/subnet.env 给二进制的 flannel 用
3. 创建一个名为 flannel.1 的 vxlan 设备,把这个设备的 MAC 地址和 IP 以及本节点的 IP 记录到 ETCD。
4. 启动一个进程,不断地检查本机的路由信息是否被删除,如果检查到缺失,则重新创建,防止误删导致网络不通的情况。
5. 最后会通过 api-server 或 etcd 订阅关于节点信息变化的事件
接下来介绍一下当 kube-flannel 收到节点新增事件时会完成的事情,假设现在有一个 k8s 集群拥有 master、node1 和 node2 三个节点,这时候新增了一个节点 node3,node3 的 IP 为:192.168.3.10,node3 上的 kube-flannel 为 node3 创建的 vxlan 设备 IP 地址为 10.244.3.0,mac 地址为:02:3f:39:67:7d:f9 ,相关的信息保存在节点的注解上,用 kubectl 查看 node3 的节点信息如下:
[root@node1]# kubectl describe node node3
Name: node3
...
Annotations: flannel.alpha.coreos.com/backend-data: {"VtepMAC":"02:3f:39:67:7d:f9"}
flannel.alpha.coreos.com/backend-type: vxlan
flannel.alpha.coreos.com/kube-subnet-manager: true
flannel.alpha.coreos.com/public-ip: 192.168.3.10
...
PodCIDR: 10.244.3.0/24
node1 上的 kube-flannel 收到 node3 的新增事件,会完成以下几件事:
1. 新增一条到 10.244.3.0/24 的主机路由,并指明通过 flannel.1 设备走,下一跳为 node3 上的 vxlan 设备地址:
ip route add 10.244.3.0/24 via 10.244.3.0 dev flannel.1 onlink
2. 新增一条邻居表信息,指明 node3 的 vxlan 设备 10.244.3.0 的 mac 地址为:02:3f:39:67:7d:f9,并用 nud permanent 指明该 arp 记录不会过期,不用做存活检查:
ip neigh add 10.244.3.0 lladdr 02:3f:39:67:7d:f9 dev flannel.1 nud permanent
3. 新增一条 fdb(forwarding database)记录,指明到 node3 的 vxlan 设备的 mac 地址的下一跳主机为 node3 的 ip:
bridge fdb append 02:3f:39:67:7d:f9 dev vxlan0 dst 192.168.3.10 self permanent
如果在配置中启用了 Directrouting,那么在这里会判断新增节点与当前节点是否在同一子网,如果是,则直接新建一条主机路由,前面三步都不会发生,取而代之的是:
ip route add 10.244.3.0/24 via 192.168.3.10 dev eth0 onlink
这就是 host-gw 下增加的主机路由,也是我们前面介绍过的。
下面我们通过一个例子来介绍一下上面新增的这些记录的实际用途,假设 node1 上有个 pod1,IP 为 10.244.1.3;node3 上有个 pod2,pod2 的 IP 为 10.244.3.3,来看一下在 vxlan 模式下从 pod1 到 pod2 的数据包发送与接收的过程。
发送
1.数据包从 pod1 出来,到达 node1 的协议栈,经过路由判决,走 node1 的 forward 链。
2.forward 时,要去的 pod2 的 IP 为 10.244.3.3,主机路由匹配到应该走 flannel.1,下一跳为 10.244.3.0(节点新增时,添加的主机路由)
3.数据包到达 flannel.1 设备,它发现下一跳的地址为 10.244.3.0,于是它先会查找 10.244.3.0 的 mac 地址,在 arp 表中找到了匹配的记录为 02:3f:39:67:7d:f9(上面节点新增时,步骤二添加的 ARP 记录在这里就用上了),然后完成 mac 头封装,准备发送。
4.因为是 vxlan 设备,发送方法与普通的网卡有些区别(详见下面的代码 vxlan_xmit),数据包也没有被提交到网卡的发送队列,而是由 vxlan 设备进一步封装成一个 udp 数据包,它会根据目标 mac 地址来反查下一跳的主机地址以决定把这个 udp 数据包发给哪个主机,这时候就会用到上面提到的 fdb 表了,它查到去往 02:3f:39:67:7d:f9 的下一跳主机地址为 192.168.3.10(节点新增时,步骤三添加的 FDB 记录),于是封装 udp 包,走 ip_local_out,发往 node3 。
// linux-4.18\drivers\net\vxlan.c
static netdev_tx_t vxlan_xmit(struct sk_buff *skb, struct net_device *dev)
{
...
//取链路层头部
eth = eth_hdr(skb);
// 根据目标 mac 地址查找 fdb 表项
f = vxlan_find_mac(vxlan, eth->h_dest, vni);
...
vxlan_xmit_one(skb, dev, vni, fdst, did_rsc);
}
static void vxlan_xmit_one(struct sk_buff *skb, struct net_device *dev,
__be32 default_vni, struct vxlan_rdst *rdst,
bool did_rsc)
{
...
// 封装 vxlan 头
err = vxlan_build_skb(skb, ndst, sizeof(struct iphdr),
vni, md, flags, udp_sum);
if (err < 0)
goto tx_error;
// 封装 UDP 头、外部 IP 头,最后走 ip_local_out
udp_tunnel_xmit_skb(rt, sock4->sock->sk, skb, local_ip.sin.sin_addr.s_addr,
dst->sin.sin_addr.s_addr, tos, ttl, df,
src_port, dst_port, xnet, !udp_sum);
...
}
接收
1. node3 接收后,走主机协议栈,判断这是发往本机的 udp 包,于是走 INPUT 方向,最终发到 UDP 层处理。
2. 当我们创建 vxlan UDP 套接字的时候,会为其 encap_rcv 覆值 vxlan_rcv,所以在收到 vxlan 的 UDP 报文后,会调用 vxlan_rcv 处理,vxlan_rcv 做的事情就是剥去 vxlan 头,将内部的一个完整的二层包重新送入主机协议栈。
3. 剥去 vxlan 头部后的包重新来到主机协议栈,此时包的目标地址是 10.244.3.3,经过路由判决时,发现不是本机地址,走 FORWARD 方向,找到合适的路由,最终发往 pod2。
// linux-4.18\drivers\net\vxlan.c
//创建 vxlan 设备时,会调用 vxlan_open -> vxlan_sock_add -> __vxlan_sock_add -> vxlan_socket_create
static struct vxlan_sock *vxlan_socket_create(struct net *net, bool ipv6,
__be16 port, u32 flags)
{
...
tunnel_cfg.encap_rcv = vxlan_rcv; //这是最关键的点,收包的时候,会把 vxlan 的包给 vxlan_rcv 处理
...
}
//udp 包接收方法,如果是 vxlan 相关的包,会给 vxlan_rcv 处理
static int udp_queue_rcv_skb(struct sock *sk, struct sk_buff *skb)
{
...
//这里就会把包给到 vxlan_rcv 处理
ret = encap_rcv(sk, skb);
...
}
/* Callback from net/ipv4/udp.c to receive packets */
static int vxlan_rcv(struct sock *sk, struct sk_buff *skb)
{
//剥 vxlan 头
if (__iptunnel_pull_header(skb, VXLAN_HLEN, protocol, raw_proto,
!net_eq(vxlan->net, dev_net(vxlan->dev))))
goto drop;
...
gro_cells_receive(&vxlan->gro_cells, skb);
...
}
int gro_cells_receive(struct gro_cells *gcells, struct sk_buff *skb)
{
...
if (!gcells->cells || skb_cloned(skb) || netif_elide_gro(dev))
//非 NAPI 收包处理,linux 虚拟网络设备接收如果需要软中断触发通常会走这里
return netif_rx(skb);
...
}
0.9.0 之前的版本
特别介绍一下 flannel 在 0.9.0 版本之前,用的策略完全不一样,kube-flannel 不会在新增节点的时候就增加 arp 表和 fdb 表,而是在数据包传递的过程中,需要用到某个 ip 的 arp 地址但没有找到时会发送一个 l3miss 的消息(RTM_GETNEIGH)给用户态的进程,让用户进程补齐 arp 记录; 在封装 udp 包时,在 fdb 表找不到 mac 的下一跳主机记录时,发送一个 l2miss 消息给用户态进程,让用户态的进程补齐 fdb 记录,让流程接着往下走。
它启动时会打开下面的标志位:
echo 3 > /proc/sys/net/ipv4/neigh/flannel.1/app_solicit
这样 vxlan 在封包过程中如果缺少 arp 记录和 fdb 记录就会往用户进程发送消息
从 0.9.0 版本开始,flannel 取消了监听 netlink 消息:
https://github.com/coreos/flannel/releases/tag/v0.9.0
总结
可以看出,从 0.9.0 版本后的 flannel 在 vxlan 模式下,容器的通信完全由 linux 内核完成,已经不用 kube-flannel 参与了,这就意味着,哪怕在运行的过程中,kube-flannel 挂掉了,也不会影响现有容器的通信,只会影响新加入的节点和新创建的容器。
了解更多信息请登录:https://www.deepexi.com/bbs/developer-bbs
版权声明: 本文为 InfoQ 作者【滴普科技2048实验室】的原创文章。
原文链接:【http://xie.infoq.cn/article/89768a070743f05881a3296ea】。文章转载请联系作者。
评论