flannel网络
前言
知识补充
arp表
在局域网内,通常是先执行ARP(地址解析协议)然后才进行路由。ARP用于将IP地址解析为MAC地址,以便在局域网上直接传输数据帧。具体流程如下:
- 源设备要发送数据包到目标设备,首先它会检查目标设备是否在同一子网(局域网)上。如果目标设备与源设备在同一子网上,那么源设备将首先查看本地的ARP缓存表,查找目标设备的MAC地址。
- 如果ARP缓存中没有目标设备的MAC地址,源设备将广播一个ARP请求,询问整个局域网中是否有设备知道目标IP地址对应的MAC地址。
- 目标设备接收到ARP请求后,会回应一个ARP响应,其中包含了目标设备的MAC地址。
- 源设备接收到ARP响应后,会将目标设备的MAC地址存储在ARP缓存中,以便后续通信。
一旦源设备知道了目标设备的MAC地址,它就可以构建以太网数据帧并将数据包发送到目标设备,而无需进行路由。只有当目标设备不在同一子网上,需要通过路由器进行跨子网通信时,才会进行路由操作。
所以,先执行ARP以解析MAC地址,然后进行路由以确保数据包最终到达目标设备。
通常情况下,设备在同一子网内通信时会发送 ARP 请求以获取目标设备的 MAC 地址,然后将数据包发送到目标设备的 MAC 地址。这适用于局域网内的直接通信。设备不会自动将默认网关用作目标,除非要访问不在同一子网内的目标设备或外部网络。在这种情况下,设备将使用默认网关来路由数据包到其他子网或外部网络。
原理
在k8s网络中,pod内部通讯直接通过lo网卡就可以实现,因为他们是同一个命名空间,而同节点,不同pod之间的通讯是通过物理机的网桥进行实现的。
其原理是在物理机上创建一个虚拟网桥,然后为每个POD都创建一对veth,一个绑定到POD的网络命名空间中,一个绑定到创建的虚拟网桥上(veth虚拟以太网)。POD与POD之间的访问只需要通过网桥就可以进行通讯。POD发起请求后,首先通过arp获取到服务端mac地址,请求会到达cni0网桥上(如果是k8s并且使用的cni,那么k8s会创建一个cni0网桥,然后把容器绑定到该网桥上,与docker0网桥区分开来。),网桥根据mac地址找到服务端绑定在物理机上的网卡然后转发。docker会创建一个docker0的网桥,docker run运行的容器都会绑定到这个网桥上。如果是k8s创建的容器,那么会根据策略,创建对应的网桥。
对于跨节点的访问,因为网桥只绑定了节点内部的网卡,所以如果将请求还转发给网桥肯定是无法找到目的地的,那么就只有将请求发送给物理机的默认网卡(这里需要将物理网卡添加到网关中),但是由于目的地址是pod的ip,在没有路由规则或者nat的情况下,是无法路由到目的地的。那么这时候我们只需要通过某种方式将两个局域网(跨节点POD)之间的路由打通,就能实现跨节点的通讯了
常用的做法是使用flannel或者calico进行将路由打通。
flannel: vxlan,将要扩节点访问的请求,先通过本地路由到flanneld里,flanneld再对请求包进行封装(使用vxlan技术),然后发送给跨节点的flanneld程序中,对方flanneld程序对请求包进行解析,然后再转发数据到指定的pod中。
实战
下面只说使用vxlan 的时候的流量走向
-
进入容器内部使用arp -n 以及route -n 分别查看mac映射表以及路由表,由此我们可以知道,当跨节点通讯的时候,因为目标地址不是同网段所以mac地址会直接使用默认网关的mac地址,然后通过eth0网卡发出,因为eth0网卡另一头连着物理机的网桥,所以会发送给网桥。
/ # arp -n (10.233.64.1) at 6a:64:e3:ef:02:1c [ether] on eth0 (10.233.64.47) at 26:28:6e:f9:f0:fb [ether] on eth0 / # route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 10.233.64.1 0.0.0.0 UG 0 0 0 eth0 10.233.64.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0 10.233.64.0 10.233.64.1 255.255.192.0 UG 0 0 0 eth0
-
在物理机上,网关继续转发请求,运行 arp -n 以及route -n,查看mac映射表以及路由表,通过路由表我们可知目标地址网关为
10.233.65.0
,那么对应的mac地址就是82:51:c7:9c:d1:d7 通过flannel.1转发。路由表 10.233.65.0 10.233.65.0 255.255.255.0 UG 0 0 0 flannel.1 arp表 10.233.65.0 ether 82:51:c7:9c:d1:d7 CM flannel.1 10.233.64.55 ether be:e7:18:88:ff:39 C cni0
-
通过 bridge fdb show |grep 82:51:c7:9c:d1:d7 我们可以获取到目标地址的物理节点ip地址
82:51:c7:9c:d1:d7 dev flannel.1 dst 10.20.31.107 self permanent
-
通过ip -d link show flannel.1 查看flannel.1网桥的详细信息
vxlan id 1 local 10.20.31.106 dev ens192 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策略,源ip为10.20.31.106 目标地址为10.20.31.107 端口为8472,通过ens192网卡转发出去。
命令
-
bridge fdb show 查看 Linux 桥接设备上的桥接表 (Forwarding Database) 中的信息。该命令显示了在网络交换机或网桥上学到的 MAC 地址的记录,以及与这些地址关联的端口信息。
-
ip -d link show flannel.1 查看flannel.1的vxlan配置 输出
vxlan id 1 local 10.20.31.106 dev ens192 srcport 0 0 dstport 8472
的意思是-
vxlan id 1
: 这是 VXLAN 的虚拟网络标识符,通常用于标识不同的 VXLAN 网络。在这种情况下,VXLAN 的 ID 被设置为 1。 -
local 10.20.31.106
: 这指定了 VXLAN 接口的本地 IP 地址,即用于通信的本地 IP 地址。 -
dev ens192
: 这指定了 VXLAN 接口的底层物理网络设备,即 VXLAN 封装的数据将通过ens192
网卡发送和接收。 -
srcport 0 0
: 这定义了 VXLAN 包的源端口范围。在此示例中,范围被设置为 0 到 0,这意味着 VXLAN 包的源端口将由系统动态分配。 -
dstport 8472
: 这指定了 VXLAN 包的目标端口,即用于 VXLAN 数据包的传输的目标端口号,通常为 8472。 -
nolearning
: 这表明 VXLAN 接口不会学习 MAC 地址,而是将数据包发送到指定的目标地址。
-
-
arp -n 获取ip-> mac的映射表
-
route -n 获取路由表
源码分析
flannel是一个daemonsets 类型的应用,它会在每台机器上都启动一个pod,使用hostNetwork: true 物理节点的网络,这样以便于管理arp表、route表、fdb表(vxlan)信息。
注册网络策略
获取配置
flanneld在启动的时候,会读取配置文件(配置文件有两种获取方式,如果开启了kube-subnet-mgr
默认为false 便使用net-conf.json,否则直接使用etcdclient访问 /coreos.com/network/config 获取配置信息) ,里面存放了当前k8s集群POD的IP范围,以及要使用的策略(vxlan、udp…)。配置文件读取完成后,就开始根据配置文件里面的type进行定向注册网络策略工作。
具体结构体如下
type Config struct {
EnableIPv4 bool
EnableIPv6 bool
Network ip.IP4Net
IPv6Network ip.IP6Net
Networks []ip.IP4Net
IPv6Networks []ip.IP6Net
SubnetMin ip.IP4
SubnetMax ip.IP4
IPv6SubnetMin *ip.IP6
IPv6SubnetMax *ip.IP6
SubnetLen uint
IPv6SubnetLen uint
BackendType string `json:"-"`
Backend json.RawMessage `json:",omitempty"`
}
获取subset
因为使用的kube-subnet-mgr
策略,所以下面以kube-subnet-mgr
策略的角度解析flannel是如何利用subset的。
kube-subnet-mgr
策略是使用k8s中node资源里面PodCIDR
属性作为subset(该属性由kube controller manager设置)。
func (ksm *kubeSubnetManager) AcquireLease(ctx context.Context, attrs *lease.LeaseAttrs) (*lease.Lease, error) {
var cachedNode *v1.Node
var err error
if ksm.disableNodeInformer {
cachedNode, err = ksm.client.CoreV1().Nodes().Get(ctx, ksm.nodeName, metav1.GetOptions{ResourceVersion: "0"})
} else {
cachedNode, err = ksm.nodeStore.Get(ksm.nodeName)
}
n := cachedNode.DeepCopy()
var bd, v6Bd []byte
bd, err = attrs.BackendData.MarshalJSON()
if err != nil {
return nil, err
}
var cidr, ipv6Cidr *net.IPNet
switch {
case len(n.Spec.PodCIDRs) == 0:
_, parseCidr, err := net.ParseCIDR(n.Spec.PodCIDR)
if err != nil {
return nil, err
}
if len(parseCidr.IP) == net.IPv4len {
cidr = parseCidr
}
case len(n.Spec.PodCIDRs) < 3:
for _, podCidr := range n.Spec.PodCIDRs {
_, parseCidr, err := net.ParseCIDR(podCidr)
if err != nil {
return nil, err
}
if len(parseCidr.IP) == net.IPv4len {
cidr = parseCidr
} else if len(parseCidr.IP) == net.IPv6len {
ipv6Cidr = parseCidr
}
}
}
...
return lease, nil
}
获取到subset后会将该信息写入到**/run/flannel/subnet.env**
中,该文件会被flannel-cni插件使用,当pod中的sandbox被创建时,容器运行时会循环调用cni,去配置该pod的网络,并分配ip地址(先创建sandbox然后再创建正常的容器)
运行
flannel会通过infomer监听node资源的变化,当node资源变化时,根据node的PodCIDR 重新生成对应的route、arp、fdb。
func (nw *network) handleSubnetEvents(batch []lease.Event) {
for _, event := range batch {
sn := event.Lease.Subnet
v6Sn := event.Lease.IPv6Subnet
attrs := event.Lease.Attrs
switch event.Type {
case lease.EventAdded:
if event.Lease.EnableIPv4 {
if directRoutingOK {
log.V(2).Infof("Adding direct route to subnet: %s PublicIP: %s", sn, attrs.PublicIP)
if err := retry.Do(func() error {
return netlink.RouteReplace(&directRoute)
}); err != nil {
log.Errorf("Error adding route to %v via %v: %v", sn, attrs.PublicIP, err)
continue
}
} else {
log.V(2).Infof("adding subnet: %s PublicIP: %s VtepMAC: %s", sn, attrs.PublicIP, net.HardwareAddr(vxlanAttrs.VtepMAC))
if err := retry.Do(func() error {
return nw.dev.AddARP(neighbor{IP: sn.IP, MAC: net.HardwareAddr(vxlanAttrs.VtepMAC)})
}); err != nil {
log.Error("AddARP failed: ", err)
continue
}
if err := retry.Do(func() error {
return nw.dev.AddFDB(neighbor{IP: attrs.PublicIP, MAC: net.HardwareAddr(vxlanAttrs.VtepMAC)})
}); err != nil {
log.Error("AddFDB failed: ", err)
// Try to clean up the ARP entry then continue
if err := retry.Do(func() error {
return nw.dev.DelARP(neighbor{IP: event.Lease.Subnet.IP, MAC: net.HardwareAddr(vxlanAttrs.VtepMAC)})
}); err != nil {
log.Error("DelARP failed: ", err)
}
continue
}
// Set the route - the kernel would ARP for the Gw IP address if it hadn't already been set above so make sure
// this is done last.
if err := retry.Do(func() error {
return netlink.RouteReplace(&vxlanRoute)
}); err != nil {
log.Errorf("failed to add vxlanRoute (%s -> %s): %v", vxlanRoute.Dst, vxlanRoute.Gw, err)
// Try to clean up both the ARP and FDB entries then continue
if err := nw.dev.DelARP(neighbor{IP: event.Lease.Subnet.IP, MAC: net.HardwareAddr(vxlanAttrs.VtepMAC)}); err != nil {
log.Error("DelARP failed: ", err)
}
if err := nw.dev.DelFDB(neighbor{IP: event.Lease.Attrs.PublicIP, MAC: net.HardwareAddr(vxlanAttrs.VtepMAC)}); err != nil {
log.Error("DelFDB failed: ", err)
}
continue
}
}
}
case lease.EventRemoved:
if event.Lease.EnableIPv4 {
if directRoutingOK {
log.V(2).Infof("Removing direct route to subnet: %s PublicIP: %s", sn, attrs.PublicIP)
if err := retry.Do(func() error {
return netlink.RouteDel(&directRoute)
}); err != nil {
log.Errorf("Error deleting route to %v via %v: %v", sn, attrs.PublicIP, err)
}
} else {
log.V(2).Infof("removing subnet: %s PublicIP: %s VtepMAC: %s", sn, attrs.PublicIP, net.HardwareAddr(vxlanAttrs.VtepMAC))
// Try to remove all entries - don't bail out if one of them fails.
if err := retry.Do(func() error {
return nw.dev.DelARP(neighbor{IP: sn.IP, MAC: net.HardwareAddr(vxlanAttrs.VtepMAC)})
}); err != nil {
log.Error("DelARP failed: ", err)
}
if err := retry.Do(func() error {
return nw.dev.DelFDB(neighbor{IP: attrs.PublicIP, MAC: net.HardwareAddr(vxlanAttrs.VtepMAC)})
}); err != nil {
log.Error("DelFDB failed: ", err)
}
if err := retry.Do(func() error {
return netlink.RouteDel(&vxlanRoute)
}); err != nil {
log.Errorf("failed to delete vxlanRoute (%s -> %s): %v", vxlanRoute.Dst, vxlanRoute.Gw, err)
}
}
}
}
}