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 network

一个典型的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.

即:

  1. 向用户态arp daemon请求app_solicit次;
  2. 如果还有旧的MAC地址记录,单播一下试试;
  3. 绝望了,怒而广播之

(说起来好像跟某些事件有点类似)

所以,明白为啥前面创建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: