简介
之前详细解析过 Flannel 的 vxlan 模式的网络通信原理,本篇将继续深入结合源码进行探索
前情提要
阅读本文需要知道 flannel vxlan 网络模式的网络请求路径,可以参考以前博主写的文章:K8S探索之Service+Flannel本机及跨主机网络访问原理详解
本篇的示例场景还是之前文章中的图,如下:
在主节点 121.4.190.84,直接是可以 ping 通容器 ip:10.244.1.8
➜ ~ ping 10.244.1.8PING 10.244.1.8 (10.244.1.8) 56(84) bytes of data.64 bytes from 10.244.1.8: icmp_seq=1 ttl=63 time=29.3 ms64 bytes from 10.244.1.8: icmp_seq=2 ttl=63 time=29.3 ms64 bytes from 10.244.1.8: icmp_seq=3 ttl=63 time=29.3 ms
复制代码
本篇就以 121.4.190.84 --> 10.244.1.8 的网络请求路径开始探索下 flannel 的相关源码
源码探索
初始启动配置 vxlan 网卡
在主节点 121.4.190.84,ping10.244.1.8 时,由于后者不是公网 ip,所以是不能直接 ping 的,在 vxlan 网络模式下,是将请求交给了 vxlan 网卡进行封包,封了一层后,包的请求目的地址换成了目标公网 ip 和 vxlan 的监听端口
在初次启动时,肯定得是 vxlan 网卡的添加了,手动配置的命令类似如下:
ip link add vxlan0 type vxlan \ id 42 \ dstport 4789 \ dev enp0s8 \ ......
复制代码
flannel 初次启动的时候肯定是要配置自己的 vxlan,在机器上我们可以看到:
➜ ~ ifconfigflannel.1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1450 inet 10.244.0.0 netmask 255.255.255.255 broadcast 10.244.0.0 inet6 fe80::f4f2:b0ff:fefa:7573 prefixlen 64 scopeid 0x20<link> ether f6:f2:b0:fa:75:73 txqueuelen 0 (Ethernet) RX packets 205910 bytes 22397966 (21.3 MiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 296287 bytes 41537899 (39.6 MiB) TX errors 0 dropped 8 overruns 0 carrier 0 collisions 0
➜ ~ ip -d link show dev flannel.139: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN mode DEFAULT group default link/ether f6:f2:b0:fa:75:73 brd ff:ff:ff:ff:ff:ff promiscuity 0 vxlan id 1 local 172.17.16.14 dev eth0 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
复制代码
在上面的查看中我们可以看 flannel 的 vxlan 网卡信息:vxlan id 1 local 172.17.16.14 dev eth0 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
使用的是宿主机的 ip 172.17.16.14 和网卡,监听在 8472 端口,所以需要我们的机器在配置的时候开放 8472 端口,其他的参数各位有兴趣的话可以去查一查
通过相关的关键字“flannel.”在源码中搜索,在源码的目录:backend/vxlan 下的 vxlan.go 看到一个关键函数:
func (be *VXLANBackend) RegisterNetwork(ctx context.Context, wg *sync.WaitGroup, config *subnet.Config) (backend.Network, error) { // Parse our configuration cfg := struct { VNI int Port int GBP bool Learning bool DirectRouting bool }{ VNI: defaultVNI, }
if len(config.Backend) > 0 { if err := json.Unmarshal(config.Backend, &cfg); err != nil { return nil, fmt.Errorf("error decoding VXLAN backend config: %v", err) } } log.Infof("VXLAN config: VNI=%d Port=%d GBP=%v Learning=%v DirectRouting=%v", cfg.VNI, cfg.Port, cfg.GBP, cfg.Learning, cfg.DirectRouting)
var dev, v6Dev *vxlanDevice var err error if config.EnableIPv4 { devAttrs := vxlanDeviceAttrs{ vni: uint32(cfg.VNI), name: fmt.Sprintf("flannel.%v", cfg.VNI), vtepIndex: be.extIface.Iface.Index, vtepAddr: be.extIface.IfaceAddr, vtepPort: cfg.Port, gbp: cfg.GBP, learning: cfg.Learning, }
dev, err = newVXLANDevice(&devAttrs) if err != nil { return nil, err } dev.directRouting = cfg.DirectRouting } if config.EnableIPv6 { v6DevAttrs := vxlanDeviceAttrs{ vni: uint32(cfg.VNI), name: fmt.Sprintf("flannel-v6.%v", cfg.VNI), vtepIndex: be.extIface.Iface.Index, vtepAddr: be.extIface.IfaceV6Addr, vtepPort: cfg.Port, gbp: cfg.GBP, learning: cfg.Learning, } v6Dev, err = newVXLANDevice(&v6DevAttrs) if err != nil { return nil, err } v6Dev.directRouting = cfg.DirectRouting }
subnetAttrs, err := newSubnetAttrs(be.extIface.ExtAddr, be.extIface.ExtV6Addr, uint16(cfg.VNI), dev, v6Dev) if err != nil { return nil, err }
lease, err := be.subnetMgr.AcquireLease(ctx, subnetAttrs) switch err { case nil: case context.Canceled, context.DeadlineExceeded: return nil, err default: return nil, fmt.Errorf("failed to acquire lease: %v", err) }
// Ensure that the device has a /32 address so that no broadcast routes are created. // This IP is just used as a source address for host to workload traffic (so // the return path for the traffic has an address on the flannel network to use as the destination) if config.EnableIPv4 { if err := dev.Configure(ip.IP4Net{IP: lease.Subnet.IP, PrefixLen: 32}, config.Network); err != nil { return nil, fmt.Errorf("failed to configure interface %s: %w", dev.link.Attrs().Name, err) } } if config.EnableIPv6 { if err := v6Dev.ConfigureIPv6(ip.IP6Net{IP: lease.IPv6Subnet.IP, PrefixLen: 128}, config.IPv6Network); err != nil { return nil, fmt.Errorf("failed to configure interface %s: %w", v6Dev.link.Attrs().Name, err) } } return newNetwork(be.subnetMgr, be.extIface, dev, v6Dev, ip.IP4Net{}, lease)}
复制代码
其中的 vni,port 之类令人熟悉,于是我们追踪其调用链路,发现其实在系统启动时的 main 函数中进行调用的,符合我们的猜测,是在 flannel 启动时进行配置
其中还发现了,linux 和 Windows 有不同的实现文件,而不同系统好像是通过下面的源码中的关键字生效的,又学到了一手
//go:build !windows// +build !windows
复制代码
当然其中还有很多的细节,留待后面探索,本篇先初步关键请求链路熟悉下源码,知道了核心路径后,出问题想查看的话,能提供思路上的帮助
路由和 fdb 的自动配置
在上面配置好 vxlan 网卡后,我们的请求经过大致如下:
请求 10.244.1.8,转到本机的 vxlan 网卡 flannel.1 进行处理
将目标公网 ip:106.55.227.160 作为封包后的目标 ip
上面两步都需要进行配置,首先 1 是要配置本机的路由,让 10.244.1.8 的请求转到网卡 flannel.1 进行处理,我们查看机器上的路由,确实是有,如下:
➜ ~ routeKernel IP routing tableDestination Gateway Genmask Flags Metric Ref Use Iface10.244.1.0 10.244.1.0 255.255.255.0 UG 0 0 0 flannel.110.244.4.0 10.244.4.0 255.255.255.0 UG 0 0 0 flannel.1
复制代码
其中就吧 10.244.1.0/24 这个网段都交给了网卡 flannel.1 进行处理
路由配置这一步,得是 flannel 进行处理,如新增了一台机器(目前试验下来,一台机器对应一个子网),需要添加相关的路由
第二步也很关键,请求的是 10.244.1.8,vxlan 怎么知道目标地址是公网:106.55.227.160,这个是通过 fdb 进行实现的(vxlan 这个功能都是操作系统已经有的,如果看到这迷糊,需要仔细再阅读下 vxlan 的原理介绍,可参考书籍:《Kubenetes 网络权威指南:基础、原理与实践》),我们参考 flannel 的 fdb 可以看到:
➜ ~ bridge fdb show dev flannel.1da:a9:48:cb:15:3b dst 106.55.227.160 self permanentc6:8a:3b:81:2f:d1 dst 121.37.246.218 self permanent
# 添加命令bridge fdb append da:a9:48:cb:15:3b dev flannel.0 dst 106.55.227.160
复制代码
上面的 fdb 就是目标机器人 Mac 地址和公网 ip 的关系,如 da:a9:48:cb:15:3b 对应的就是 106.55.227.160,而 106.55.227.160 是机器 106.55.227.160 的 vxlan 网卡:
➜ ~ ifconfigflannel.1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1450 inet 10.244.1.0 netmask 255.255.255.255 broadcast 10.244.1.0 inet6 fe80::d8a9:48ff:fecb:153b prefixlen 64 scopeid 0x20<link> ether da:a9:48:cb:15:3b txqueuelen 0 (Ethernet)
复制代码
那还有一个疑问,我请求的 10.244.1.8,你怎么知道请求发到 106.55.227.160 的 vxlan 网卡上,这个就是 arp 起的作用了,我们查看 arp 如下:
➜ ~ ip neigh10.244.1.0 dev flannel.1 lladdr da:a9:48:cb:15:3b PERMANENT
# 添加命令ip neigh add 10.244.1.0 lladdr da:a9:48:cb:15:3b flannel.1
复制代码
大致的意思就是告诉这个 ip 的 Mac 地址是多少
通过上面的描述,我们知道了更详情的请求处理路径如下:
ping 10.244.1.8
通过路由表配置,得到目标网络为 10.224.1.0 的都交给 vxlan 网卡处理
通过 arp 配置:10.224.1.0 在 arp 中得到其对应目标机器 Mac 地址
通过 fdb 配置:得到 Mac 地址对应的 ip 地址
这样,请求就成功的抵达对面了
下面我们看下 flannel 中的相关源码
首先我们在运行日志中看到一个监听类型启动成功的日志:watching for new subnet leases
➜ ~ kubectl logs kube-flannel-ds-vwn7x -n kube-systemI0707 04:00:26.313804 1 main.go:533] Using interface with name eth0 and address 172.17.16.14I0707 04:00:26.313867 1 main.go:546] Using 121.4.190.84 as external addressW0707 04:00:26.313888 1 client_config.go:608] Neither --kubeconfig nor --master was specified. Using the inClusterConfig. This might not work.I0707 04:00:26.419108 1 kube.go:116] Waiting 10m0s for node controller to syncI0707 04:00:26.419153 1 kube.go:299] Starting kube subnet managerI0707 04:00:27.419250 1 kube.go:123] Node controller sync successfulI0707 04:00:27.419274 1 main.go:254] Created subnet manager: Kubernetes Subnet Manager - crio-masterI0707 04:00:27.419279 1 main.go:257] Installing signal handlersI0707 04:00:27.419343 1 main.go:392] Found network config - Backend type: vxlanI0707 04:00:27.419401 1 vxlan.go:123] VXLAN config: VNI=1 Port=0 GBP=false Learning=false DirectRouting=falseI0707 04:00:27.439715 1 main.go:307] Setting up masking rulesI0707 04:00:27.520123 1 main.go:315] Changing default FORWARD chain policy to ACCEPTI0707 04:00:27.520207 1 main.go:323] Wrote subnet file to /run/flannel/subnet.envI0707 04:00:27.520223 1 main.go:327] Running backend.I0707 04:00:27.520233 1 main.go:345] Waiting for all goroutines to exitI0707 04:00:27.520258 1 vxlan_network.go:59] watching for new subnet leases
复制代码
直接在源码中找到:
# 注意是在vxlan_network.go文件中func (nw *network) Run(ctx context.Context) { wg := sync.WaitGroup{}
log.V(0).Info("watching for new subnet leases") events := make(chan []subnet.Event) wg.Add(1) go func() { subnet.WatchLeases(ctx, nw.subnetMgr, nw.SubnetLease, events) log.V(1).Info("WatchLeases exited") wg.Done() }()
defer wg.Wait()
for { evtBatch, ok := <-events if !ok { log.Infof("evts chan closed") return } nw.handleSubnetEvents(evtBatch) }}
复制代码
大意是启动了一个协程去获取容器添加(通过之前的分析,机器增加和容器增加应该要维护路由/arp/fdb 表)引起的事件,在当前线程中进行事件的相关处理
使用 channel 进行同步通信实现,一个写一个读
subnet.WatchLeases(ctx, n.SM, n.SubnetLease, evts),在查看的过程中还有两种不同的实现,分别是 local 和 kubenetes,猜测前者是不依赖与 kubenetes 的,单独部署的情况下启动的,这部分就不分析,kubenetes 的源码还没有开始搞,留待日后分析
看看事件处理的函数,下面是处理网络添加相关:
func (nw *network) handleSubnetEvents(batch []subnet.Event) { for _, event := range batch { sn := event.Lease.Subnet v6Sn := event.Lease.IPv6Subnet attrs := event.Lease.Attrs if attrs.BackendType != "vxlan" { log.Warningf("ignoring non-vxlan v4Subnet(%s) v6Subnet(%s): type=%v", sn, v6Sn, attrs.BackendType) continue }
var ( vxlanAttrs, v6VxlanAttrs vxlanLeaseAttrs directRoutingOK, v6DirectRoutingOK bool directRoute, v6DirectRoute netlink.Route vxlanRoute, v6VxlanRoute netlink.Route )
# 路由添加相关的 if event.Lease.EnableIPv4 && nw.dev != nil { if err := json.Unmarshal(attrs.BackendData, &vxlanAttrs); err != nil { log.Error("error decoding subnet lease JSON: ", err) continue }
// This route is used when traffic should be vxlan encapsulated vxlanRoute = netlink.Route{ LinkIndex: nw.dev.link.Attrs().Index, Scope: netlink.SCOPE_UNIVERSE, Dst: sn.ToIPNet(), Gw: sn.IP.ToIP(), } vxlanRoute.SetFlag(syscall.RTNH_F_ONLINK)
// directRouting is where the remote host is on the same subnet so vxlan isn't required. directRoute = netlink.Route{ Dst: sn.ToIPNet(), Gw: attrs.PublicIP.ToIP(), } if nw.dev.directRouting { if dr, err := ip.DirectRouting(attrs.PublicIP.ToIP()); err != nil { log.Error(err) } else { directRoutingOK = dr } } } ······ # arp和fdb添加相关的 switch event.Type { case subnet.EventAdded: if event.Lease.EnableIPv4 { if directRoutingOK { log.V(2).Infof("Adding direct route to subnet: %s PublicIP: %s", sn, attrs.PublicIP)
if err := 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 := nw.dev.AddARP(neighbor{IP: sn.IP, MAC: net.HardwareAddr(vxlanAttrs.VtepMAC)}); err != nil { log.Error("AddARP failed: ", err) continue }
if err := 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 := 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 := 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 subnet.EventRemoved: ······ default: log.Error("internal error: unknown event type: ", int(event.Type)) } }}
复制代码
如上所示,在源码中也找到了 route、arp、fdb 相关的配置,猜测得到了印证
总结
本篇文章中通过分析 ip 请求的路径,结合如何手动配置,然后去找对应的源码进行印证,大致找到了 flannel 源码的 vxlan 核心代码,但没有具体分析,这个感觉还是需要时间和精力的,所以本篇就初步探索下,细节日后分析
总结来说核心就是基于操作系统的 vxlan,使用程序代替手动配置 route、arp、fdb 表,实现了跨主机的容器通信:
路由表配置:将目标网络的请求都交给 vxlan 网卡处理
arp 配置:arp 中得到其对应目标机器 Mac 地址
fdb 配置:得到 Mac 地址对应的 ip 地址
评论