kubernetes flannel代码解析
flannel是coreos贡献给社区的一个kubernetes网络插件。overlay。
1 VXLAN
1.1 VXLAN协议
二层数据中心网络的一个关键特征就是它们的使用虚拟局域网(VLAN)提供广播隔离,从而更好的为多租户提供隔离。但随着租户数量越来越多,VLAN由于上限容量4096,越来越捉襟见肘。
vxlan(Virtual eXtensible Local Area Network)是一种隧道协议,用来解决IEEE 802.1q VLAN ID只能最多4096的限制。VXLAN网络中的子网标识符扩展到了24位(成为VNI,VXLAN Network Identifier),其最大容量也达到了16777216。
vlan协议在IETF RFC 7348中定义,有多种实现,如linux kernel的vxlan模块,OpenVswitch等。vxlan协议跑在UDP上,UDP连接的端口固定,如linux kernel vxlan的端口号为8472。
跟其他隧道不同,VXLAN并不是一个点到点的网络,而是1 to N。VXLAN设备可以动态的学习IP地址,也可以从静态配置的转发表中学习。
VXLAN的配置管理使用iproute2包,这个工具是和VXLAN一起合入到内核的。
1.2 VXLAN配置
先来看看如何管理VXLAN接口。
1 创建VXLAN接口
ip link add vxlan0 type vxlan id 42 group 239.1.1.1 dev eth1 dstport 4789
这条命令会创建一个叫做vxlan0的新接口,它使用在eth1上的组播组239.1.1.1来通信。初始化时没有转发表。目的端口号是IANA规定的4789。在VXLAN中,一般将vxlan接口叫做VTEP(Vxlan tunnel endpoint),VXLAN子网的报文,都需要从VTEP出去。
多播组主要用来泛洪学习arp:vxlan子网内广播ARP请求,对应VM响应。但并不是必须的。
如果网络不复杂,可以认为某一Hypervisor上所有的子网IP的MAC,和Hypervisor上的VTEP的MAC一致,可以直接用VTEP MAC封装报文;而VTEP的MAC,可以用bridge命令手工配置。
2 配置错了可以删除vxlan
ip link delete vxlan0
3 查看vxlan的信息
ip -d link show vxlan0
可以用bridge命令查看,删除,查看VXLAN的转发表。
1 创建一条转发表项。MAC即对端VTEP的MAc,地址即对端VTEP的地址
bridge fdb add to 00:17:42:8a:b4:05 dst 192.19.0.2 dev vxlan0
2 删除一条转发表项
bridge fdb delete 00:17:42:8a:b4:05 dev vxlan0
3 查看VXLAN接口的转发表
bridge fdb show dev vxlan0
一个典型的数据中心vxlan网络:
一个典型的vxlan报文:
2 vxlan flannel
flannel的vxlan相对数据中心来说,是比较简单的,因为其在Layer 3上只有1个Vxlan网络,只有1个vxlan接口(flannel.[VNI],默认为flannel.1)。VTEP的MAC地址不是通过组播学习的,而是通过从apiserver的node接口watch到并静态下发的。
2.1 flannel.1接口是怎么创建的?
一、main.go查找ExtIface(Hypervisor L3出接口,所有vxlan报文都要封装后走ExtIface出去overlay)。flannel的出接口允许用户按以下方式来选择(LookupExtIface):
- 指定具体接口
- 指定接口的正则表达式来匹配查找
- 啥也不指定,由flannel根据默认网关找出接口
1.5.4版本默认kubeadm不会指定flannel的出接口,所以出接口选的是默认网关的接口。如果机器上网络比较复杂,可能需要手工指定出接口。
二、确定subnet
flannel网络中,以我们的集群为例,整个flannel网络是10.244.0.0/16,每个node都是一个子网(subnet),如10.244.0.0/24, 10.244.1.0/24。
subnet的网段、长度是怎么确定的呢?k8s创建时会生成net-conf.json,其设置了Network和Backend的信息(configmap kube-flannel-cfg,以volume形式挂到flanneld容器里去)。
# flanneld容器中
cat /etc/kube-flannel/net-conf.json
{
"Network": "10.244.0.0/16",
"Backend": {
"Type": "vxlan"
}
}
ParseConfig中会确定subnet的配置项:SubnetMin, SubnetMax, SubnetLen, BackendType。如果net-conf.json没有指定Backend,则默认使用udp。我这里使用了vxlan。注意,由于udp方式下,报文是通过tun从内核上送到用户态的flanneld进程,在用户态进程做udp封装、解封装,性能不佳,所以生产环境最好选用vxlan。
三、根据backend类型创建backend,然后调用be.RegisterNetwork函数去初始化flannel接口。步骤如下。
1 创建flannel接口。netlink下内核创建flannel.1接口,需要指定vni,出接口,源地址,目的端口,nolearning(netlink.Vxlan),并设置app_solicit为3。相当于如下命令:
ip link add $DEVNAME type vxlan id $VNI dev eth0 local $IP dstport $PORT nolearning
echo '3' > /proc/sys/net/ipv4/neigh/$DEVNAME/app_solicit
2 调用kube subnet manager获取租约。ksm会去调用apiserver的node api,查询node的PodCIDR,即该节点的pod overlay网络,如10.244.1.0/24(kubeSubnetManager.AcquireLease, subnet/kube/kube.go:213)
3 如果从apiserver查询node的backend Annotations跟实际node的信息不一致(例如VTEP MAC不同),则向node打patch,更新Anootations。此更新会被其他node watch到,从而更新其本地的fdb table(下面会提到)。
4 设置flannel.1接口地址,激活接口,并增加网段路由(vxlanDevice.Configure, backend/vxlan/device.go:125)
ip address add $VXSUBNET dev $DEVNAME
ip link set $DEVNAME up
ip route add $SUBNET dev $DEVNAME scope global
一个典型的flannel.1接口信息如下。
ip -d link show flannel.1
8: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN mode DEFAULT
link/ether 12:c0:e6:d3:0d:df brd ff:ff:ff:ff:ff:ff promiscuity 0
vxlan id 1 local 192.168.136.31 dev bond0 srcport 0 0 dstport 8472 nolearning ageing 300 addrgenmode eui64
至此,flannel.1接口配置完毕,接下来就是配置fdb静态表项了。
2.2 fdb 转发数据库
flannel接口配置完毕后,接下来就是在集群中配置fdb表了。要做的其实也就是完成下面这条命令的下发,只是由flannel自动完成,用户不需要手动操作。
bridge fdb add/append $mac-of-vtep-on-node-2 dev $DEVNAME dst $DESTIP
跟数据中心的vxlan不一样,flannel网络中的VTEP的MAC并不是通过组播学习的,而是通过apiserver去做的同步(或者是etcd)。前面在创建flannel.1接口时有提到,各个节点会将自己的VTEP信息上报给apiserver,而apiserver会再同步给各节点上正在watch node api的listener(flanneld),flanneld拿到了更新消息后,再通过netlink下发到内核,更新fdb表项,从而达到了整个集群的同步。
2.3 newKubeSubnetManager
flanneld会在启动的时候,创建一个针对apiserver node api的informer(newKubeSubnetManager)。
flannel的子网管理器SubnetManager有2种:基于etcd、基于k8s的apiserver。我这里使用的是基于k8s的kubeSubnetManager(subnet/kube/kube.go, 下面简称ksm),它会去listWatch k8s apiserver的node api,通过ksm.events与backend通信。ksm.events是一个长度为5000的subnet.Event chan,ksm作为生产者,会将从k8s那里watch到的信息封装(nodeToLease,节点信息转为租约信息)后写到ksm.events里去。
ksm.events的消费者是backend。backend/vxlan_network.go在run时,会拉起一个goroutine,调用subnet/watch/WatchLeases(),在这个函数里批量读取ksm.events chan,并将批量事件推入backend的接收chan;backend会在其goroutine中处理这些事件:根据事件中带的nodes信息,如IP,VtepMAC,下发到本机内核fdb table。
subnet/watch/WatchLeases()做了个优化:node比较活跃时,ksm.events里可能会有比较多的events;读取ksm.events的时候,可以一次性读出一组events,并且在处理这些events时,可以将同一SUBNET的lease合并,不过事件的总数不会减少。
第一批事件的处理函数是handleInitialSubnetEvents,进行初始化+事件处理。其获取内核的fdbtable,之后会根据获取的上面WatchLeases丢过来的event,对比fdbtable刷新内核fdb table:删除内核多余的表项,增加内核缺少的表项,保持跟flannel网络一致。这里有个疑问,initial的时候,怎么保证event都是Added的呢?如果是删除的,刷新内核arp table的流程是有问题的。
后续事件的处理函数是handleSubnetEvents,它比较纯粹,根据event.Type是Added还是Removed,修改内核fdb表现 和 flannel维护的routes(后面arp解析时会用到)
fdb更新下发内核相当于调用了如下命令:
bridge fdb add $mac-of-vtep-on-node dev $DEVNAME dst $DESTIP
一个典型的fdb表项如下。
bridge fdb show dev flannel.1
76:bc:c1:37:14:32 dst 192.168.136.35 self permanent
ca:07:82:ff:d1:6a dst 192.168.136.34 self permanent
0e:c4:9f:71:f3:56 dst 192.168.136.32 self permanent
66:73:1c:ea:76:58 dst 192.168.136.33 self permanent
至此,fdb静态表项配置完成,并且可以达到与集群同步更新。
2.4 arp table
fdb完成后,报文转发没问题了,但在L2封装时,还需要对端ip的arp表项。linux ARP解析是这样的:
When there is no forward progress, ARP tries to reprobe. It first
tries to ask a local arp daemon app_solicit times for an updated MAC
address. If that fails and an old MAC address is known, a unicast
probe is sent ucast_solicit times. If that fails too, it will
broadcast a new ARP request to the network. Requests are sent only
when there is data queued for sending.
即:
- 向用户态arp daemon请求app_solicit次;
- 如果还有旧的MAC地址记录,单播一下试试;
- 绝望了,怒而广播之
(说起来好像跟某些事件有点类似)
所以,明白为啥前面创建flannel.1接口时,需要设置app_solicit为3了吗?
在flannel网络中,flanneld会在启动时监听RTM_GETNEIGH(vxlanDevice.MonitorMisses),flanneld相当于arp daemon;arp请求会在内核中通过netlink发送请求到用户态的flanneld,由flanneld根据该arp请求ip所属网段(该node上所有ip的MAC也就是VTEP的MAC),查询其记录的routes信息,然后netlink下发内核。
func (nw *network) handleL3Miss(miss *netlink.Neigh) {
route := nw.routes.findByNetwork(ip.FromIP(miss.IP))
err := nw.dev.AddL3(neighbor{IP: ip.FromIP(miss.IP), MAC: route.vtepMAC})
}
所以如果你先arp -d x.x.x.x -i flannel.1
删掉arp缓存,然后再ping想触发一个arp请求,抓包是抓不到arp报文的。
arp table更新下发内核相当于调用了如下命令:
ip neighbor add/replace $ip-on-node-2 lladdr $mac-of-vtep-on-node-2 dev flannel.1
至此,vxlan的流程就走通了。
3 udp flannel
vxlan要求内核版本3.7+,最好3.9+,所以在一些旧版本的linux上就无法使用基于vxlan的flannel了,只能使用基于udp的flannel。
udp flannel原理与vxlan类似,都是package in udp,只是一个在内核做报文封装,一个在用户态做(通过tun, pkg/ip/tun.go)。可想而知,udp flannel的性能不会太好,所以除非是内核版本太低,一般还是用vxlan flannel性能会比较好。
ref:
- Virtual eXtensible Local Area Networking documentation
- vxlan在Flannel中的Overlay网络的实现
- Difference of VXLAN L3MISS between flannel and docker overlay implementation