flannel网络

flannel网络

前言

知识补充

arp表

在局域网内,通常是先执行ARP(地址解析协议)然后才进行路由。ARP用于将IP地址解析为MAC地址,以便在局域网上直接传输数据帧。具体流程如下:

  1. 源设备要发送数据包到目标设备,首先它会检查目标设备是否在同一子网(局域网)上。如果目标设备与源设备在同一子网上,那么源设备将首先查看本地的ARP缓存表,查找目标设备的MAC地址。
  2. 如果ARP缓存中没有目标设备的MAC地址,源设备将广播一个ARP请求,询问整个局域网中是否有设备知道目标IP地址对应的MAC地址。
  3. 目标设备接收到ARP请求后,会回应一个ARP响应,其中包含了目标设备的MAC地址。
  4. 源设备接收到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 的时候的流量走向

  1. 进入容器内部使用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
    
  2. 在物理机上,网关继续转发请求,运行 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
    
  3. 通过 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
    
  4. 通过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
    
  5. 由此可以明白吗,请求会使用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)
					}
				}
			}
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值