openelb-layer2模式service中断问题
问题表现
openelb部署配置了layer2模式,在开发环境一切正常。
来到生产环境后,测试反馈lb service有时能ping通,有时又ping不通
排查:切入
-
首先服务不是完全不通,且k8s内的service lb能正常获取到ip,可以基本确定集群内配置无误。
-
由于之前在openstack的vm里部署k8s+openelb,遇到过layer2模式的arp包被openstack安全规则拦截的情况,因此首先还是怀疑是不是生产环境的交换机有些arp相关的高级设置,影响了openelb arp包的功能。
-
浏览了新华三交换机的文档中和arp相关的功能介绍,由于内容过多,很难快速一眼定位到是哪个配置影响了openelb功能。
-
既然如此,就先直接定位一下lb service不通时发现了什么。
排查:为什么能通
-
生产环境下,三台集群节点+业务交换机组成了一个广播域,由于在这个子网内没有额外的机器,流量是从外部交换机,经过路由来到业务交换机,再通过openelb的arp引导,来到三台k8s node中的其中之一的。
-
因此首先确定业务交换机是否能正常访问lb service。
-
在交换机上ping之,发现在交换机上就出现了测试描述的问题。测试是在其他外部交换机连接的机器测试的。
-
查看交换机上的arp表,可以看到类似下图中的情况
172.31.11.X是LB IP池的网段,11.2和11.4被openelb分配了不同的node,因此可以看到arp表中的mac地址是不同的,分别对应了两台node,但是却都被交换机通过BAGG21这个口进行了发送,而不是按照mac地址发往各自正确的口。
如果数据包被发往了错误的口,那对应机器的网卡发现mac地址不是自己的,必然就进行了丢弃动作,也就出现了不通的情况。
-
那么为什么交换机的arp表会出现上图中的情况呢?这里就需要结合openelb的代码和一些实验验证去进行推测了。这里我们以我们使用的openelb v0.4.4版本为例进行分析。
-
首先我们需要确认lb service在创建完成后,openelb做了哪些事,尤其是和arp相关的事:
// controller的最外层的,即协调函数 // pkg/controllers/lb/controller.go:269 func (r *ServiceReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { ... // 分配ip args := r.constructIPAMArgs(svc) result, err = ipam.IPAMAllocator.UnAssignIP(args, true) ... // 跳转下一步操作 err = r.callSetLoadBalancer(result, svc) } // pkg/controllers/lb/controller.go:199 func (r *ServiceReconciler) callSetLoadBalancer(result ipam.IPAMResult, svc *corev1.Service) error { // 选择可用的node nodes, err := r.getServiceNodes(svc) ... // 继续跳转下一步操作 return result.Sp.SetBalancer(svcIP, announceNodes) } // pkg/speaker/layer2/arp.go:179 func (a *arpSpeaker) SetBalancer(ip string, nodes []corev1.Node) error { // 多网卡打了指定注解的情况下,会根据注解找到node上对应的网卡ip if nodes[0].Annotations != nil { nexthop := nodes[0].Annotations[constant.OpenELBLayer2Annotation] if net.ParseIP(nexthop) != nil { // 确定了lb ip -> node网卡ip的映射,准备下一步开始arp相关操作 return a.setBalancer(ip, []string{nexthop}) } } ... } // pkg/speaker/layer2/arp.go:200 func (a *arpSpeaker) setBalancer(ip string, nexthops []string) error { // 简单包装了一层,将string类型的ip解析成net包的IP结构体 return a.gratuitous(net.ParseIP(ip), net.ParseIP(nexthops[0])) } // 最终,我们终于来到了实际进行arp操作的地方 func (a *arpSpeaker) gratuitous(ip, nodeIP net.IP) error { // 缓存里如果有,就fast return,不需要重复后续操作 if a.getMac(ip.String()) != nil { return nil } // 解析目标网卡的ip对应的mac地址 hwAddr, err := a.resolveIP(nodeIP) ... // 设置缓存 a.setMac(ip.String(), hwAddr) // openelb-manager部署多副本时,使用leader election选主,非leader不会进行后续arp发送 if !leader.Leader { return nil } for _, op := range []arp.Operation{arp.OperationRequest, arp.OperationReply} { a.logger.Info("send gratuitous arp packet", "eip", ip, "nodeIP", nodeIP, "hwAddr", hwAddr) // 传入参数,生成arp数据帧 fb, err := generateArp(a.intf.HardwareAddr, op, hwAddr, ip, ethernet.Broadcast, ip) if err != nil { a.logger.Error(err, "generate gratuitous arp packet") return err } // 发送 if _, err = a.p.WriteTo(fb, &raw.Addr{HardwareAddr: ethernet.Broadcast}); err != nil { a.logger.Error(err, "send gratuitous arp packet") return err } } return nil }
综上代码所述,每当建立一个lb service时,openelb-manager最终会广播发送一个免费arp数据帧。免费arp数据帧的意义,一来确定ip在广播域中没有重复,而来向局域网中的设备广播该ip和mac的映射关系。
在新建完某lb service,openelb-manager正常处理完成后,我们登录到交换机上,即可以看到上图arp表中新增了表项:
ip mac vlan port ttl type 172.31.11.2 xx-xx-xx-xx-xx-xx y BAGG21 1100 D 表项各字段的含义很重要,我没有做十分细致深入的研究,仅针对排查问题做了观察和了解:
- ip:很直接,就是service的lb ip
- mac:这个mac是经过openelb代码逻辑,选择node,再从node上根据配置的网卡获取的该网卡的mac地址。客户端发起对lb ip的访问流量,在二层数据帧中,就会使用该mac地址作为目的mac地址
- vlan:就是vlan,跟本问题无关
- port:指交换机的端口,每个端口连接了集群的物理机节点。二层数据帧最终发送要选择端口发往不同的机器网卡,就是通过该字段确定的
- ttl:指该表项的过期时间。我们使用的新华三交换机动态arp的过期老化时间是1200s
- type:arp有不同的类型,D代表dynamic,大意就是动态学习到的;相对应的有静态的,即绑死的。我们在k8s中使用的lb控制器肯定是动态的。而交换机通过接收到的免费arp,动态学习ip和mac的映射关系。
-
搞明白了上一点里lb service的建立过程,我们实测发现:上述流程完成后,交换机里的arp表项没问题,从交换机上ping该lb service也正常。这样我们就验证了lb service有一个阶段是能通的。
排查:为什么不通
排查定位问题时,比较棘手的就是偶尔出现、有一定概率出现的问题。这类问题从表象上看是随机、偶现的,但是真的符合数学意义上随机事件的情况,我至今还未遇到。本质上都是由表象难以看到本质,很难从表象中找到规律,从而难以获取对问题本质的推理,进而也就难以进行后续的验证了。
由于本次排查问题中,不通的情况从表象上看没有什么规律,所以只能等到某个service出现了该问题时,我们立即对其进行深入排查。
在收到测试同事反馈的某个service出现不通的情况后,我们对其进行了后续的排查:
- 查看交换机的arp表
- 在交换机ping时,在三台node均使用tcpdump抓包
通过上述两件事,我们发现了不通的原因:交换机arp表项中,第四列port的值,和第二列mac的值是不匹配的。
以下面的表项为例,openelb为lb service分配的node上的网卡mac是xx-xx-xx-xx-xx-xx
,该机器是接在BAGG21口的,但是表项里的port却是BAGG22。抓包后也可以发现,数据包里写着目的mac地址是xx-xx-xx-xx-xx-xx
,却被发送到了另外的node
p | mac | vlan | port | ttl | type |
---|---|---|---|---|---|
172.31.11.2 | xx-xx-xx-xx-xx-xx | y | BAGG22 | 1100 | D |
排查:arp表项中端口错误
为什么交换机上一开始学习的表项均正确,后面会突然出现端口错误的情况呢?
我们观察port错误的arp表项,发现所有错误的端口值都是同一个。那么这个端口有什么特殊之处呢?观察后发现,那个网卡对应的node上,跑了我们的openelb-leader。因此我们又要返回openelb的代码,看看它又做了什么,是不是它的什么操作导致arp表项发生了错误的变化?
刚才分析的lb service建立时广播发送的免费arp,其中有打印日志。如果我们观察openelb leader的日志,可以发现除了广播免费arp,它还会有单播的arp reply发送日志。因此我们需要看看这些单播的arp reply是什么逻辑
// arp speaker启动
// pkg/speaker/layer2/arp.go:213
func (a *arpSpeaker) Start(stopCh <-chan struct{}) error {
go a.run(stopCh)
...
}
func (a *arpSpeaker) run(stopCh <-chan struct{}) {
for {
err := a.processRequest()
...
}
}
// 处理arp request
func (a *arpSpeaker) processRequest() dropReason {
...
// 同样只有leader会reply
if !leader.Leader {
return dropReasonLeader
}
// 不需要处理reply
if pkt.Operation != arp.OperationRequest {
return dropReasonARPReply
}
// 从缓存中取出目标mac地址,这个在上面分析service建立时已经加入到了缓存
hwAddr := a.getMac(pkt.TargetIP.String())
// 生成arp reply帧
fb, err := generateArp(a.intf.HardwareAddr, arp.OperationReply, *hwAddr, pkt.TargetIP, pkt.SenderHardwareAddr, pkt.SenderIP)
...
// 发送
...
}
综上,可以看到openelb会针对缓存里存在的lb ip -> mac的映射,如果有对该lb ip的arp request,那么就会构造相应的arp reply并发送。这里需要注意一个关键点:reply里的目标mac,不一定是openelb leader所在机器的网卡mac,也就是说,arp reply的sender mac和reply里的目标mac是可能不同的。
到这里,就可以初步解释通与不通的奥妙所在了:
- 对于openelb在建立service时发送的广播的免费arp,交换机除了正确学习到lb ip对应的mac地址以外,也能正确学习到端口,所以此时网络正常
- 而当该arp表项过期后,交换机再次试图ping lb ip时,发现未知的ip,就会发起arp request,然后收到openelb发来的response,在学到了mac地址的同时,也顺便学到了端口,但是这个端口可能是错误的:如果openelb leader所在的node对应的交换机端口和reply 里的mac地址网卡没有对应,那么帧就会转发到错误的节点,网络就会不通
关于交换机
经过和新华三交换机技术支持团队的工作人员沟通该问题后,工作人员表示:
- 交换机在以上过程中的学习是没问题的,符合交换机的正常工作逻辑
- 发给我们一份交换机arp相关配置指南
和组里同事研究了这份指南后,确实有找到几个可疑的和arp学习行为相关的配置,比如
- arp表接口一致性检查功能(
arp mac-interface-consistency check enable
):遗憾的是,该功能开启后,虽然能检测到错误的arp表项,但是检测到后的行为是继续发送arp request,在本文这个问题下,获得的reply依然是错的,无法解决问题。 - 动态arp表项检查功能(
arp check enable
):看描述和免费arp有关,实测并不影响普通arp的学习。
因此,最终我们认为交换机的行为本质上没有什么问题,且交换机在这个方面能给我们灵活调整的余地不多,想解决问题还是得从openelb那边入手。
进一步思考
问题排查到这里,看起来似乎好像已经比较明朗了,但是还有两个点令人疑惑:
- 为什么之前在开发环境这个问题从来没有出现过?
- 交换机为什么能从免费arp中学到正确的端口?
- openelb的layer2是否可以进一步完善,以解决这个问题?
问题1
这个问题是令我非常不解的,通过上面研究openelb发送arp的机制,我们发现openelb这边的行为是比较易于理解的。但是交换机那边的行为整体来说还是比较不透明的。带着对交换机和二层网络的工作机制和细节的疑惑,我在地铁上展开了和gpt的如下对话:
来自你的消息:
请给我介绍一下二层网络交换的流程,重点展开arp相关的内容,包括客户端和交换机的
来自 ChatGPT 的消息:
二层网络交换主要通过 MAC 地址(Media Access Control)来进行数据包的转发。ARP(Address Resolution Protocol)是一种用于解析 IP 地址到 MAC 地址的协议,它在这个过程中起到了关键的作用。下面是一个简单的介绍:
ARP 请求:当一个设备(比如说,客户端 A)想要发送数据到另一个设备(客户端 B),但是只知道 B 的 IP 地址,不知道 B 的 MAC 地址时,A 会发送一个 ARP 请求包。这个 ARP 请求包包含了 A 的 IP 地址和 MAC 地址,以及 B 的 IP 地址。这个包会被广播到同一网络段的所有设备。
ARP 响应:当 B 收到这个 ARP 请求包时,它会检查包中的目标 IP 地址,发现这个地址就是自己的 IP 地址。然后,B 会发送一个 ARP 响应包给 A。这个响应包包含了 B 的 IP 地址和 MAC 地址。
ARP 表:当 A 收到这个 ARP 响应包后,它会更新自己的 ARP 表。ARP 表是一个映射表,它将 IP 地址映射到对应的 MAC 地址。这样,下次 A 再需要发送数据到 B 时,就可以直接从 ARP 表中查找到 B 的 MAC 地址,无需再发送 ARP 请求。
数据传输:现在,A 知道了 B 的 MAC 地址,就可以开始发送数据了。数据会被封装在一个以太网帧中,这个帧的目标 MAC 地址就是 B 的 MAC 地址。当这个帧到达交换机时,交换机会检查它的目标 MAC 地址,然后根据自己的转发表(也就是 MAC 地址表)将这个帧转发到正确的端口。
交换机的 MAC 地址表:交换机会维护一个 MAC 地址表,这个表记录了每个端口上连接的设备的 MAC 地址。当交换机收到一个帧时,它会查看这个帧的源 MAC 地址和接收端口,并更新自己的 MAC 地址表。这样,交换机就可以知道下一次应该如何转发到这个 MAC 地址的帧。
这就是二层网络交换和 ARP 协议的基本流程。在实际的网络环境中,可能还会涉及到一些其他的因素,比如 ARP 缓存、ARP 欺骗等问题。
来自你的消息:
交换机的mac地址表和arp表是同一张表吗?
来自 ChatGPT 的消息:
不,交换机的 MAC 地址表和 ARP 表是两种不同的表,它们有不同的作用和存储位置。
交换机的 MAC 地址表:这是交换机用来记录每个端口上连接的设备的 MAC 地址的表。当交换机收到一个帧时,它会查看这个帧的源 MAC 地址和接收端口,并更新自己的 MAC 地址表。这样,交换机就可以知道下一次应该如何转发到这个 MAC 地址的帧。这个表存储在交换机内部。
ARP 表:这是主机(例如,计算机、服务器等)用来记录 IP 地址和 MAC 地址之间映射关系的表。当主机需要向某个 IP 地址发送数据,但不知道对应的 MAC 地址时,它会查看自己的 ARP 表。如果 ARP 表中有这个映射关系,主机就可以直接使用;如果没有,主机就需要发送 ARP 请求来获取。这个表存储在主机内部。
所以,虽然这两种表都涉及到 MAC 地址,但它们的作用和存储位置是不同的。
来自你的消息:
交换机第一次需要转发一个没有见过的mac地址的帧时,转发表没有信息,这时怎么办
来自 ChatGPT 的消息:
当交换机接收到一个目标 MAC 地址在其地址表中不存在的帧时,它会执行一种操作叫做 "泛洪"(flooding)。在这种情况下,交换机会将接收到的帧发送到除了源端口外的所有其他端口。这样,接收到的帧会被发送到与交换机连接的所有设备。
这个过程的目的是尝试找到帧的目标设备。如果目标设备在网络上,它会接收到这个帧,并可能会回应,这样交换机就可以学习到这个设备的 MAC 地址和对应的端口。
然后,交换机会更新其 MAC 地址表,将新学习到的 MAC 地址和对应的端口添加进去。这样,下次交换机接收到目标为这个 MAC 地址的帧时,就可以直接转发到正确的端口,不需要再进行泛洪。
需要注意的是,这种泛洪过程只在交换机第一次接收到未知 MAC 地址的帧时发生。一旦交换机学习到了新的 MAC 地址,就会更新其地址表,以后就可以直接转发了。
结束这次对话后,我一下子明白了为什么开发环境没有出现这个问题:
- 开发环境机器多,我们测试时不是在交换机上测试,而是在第四台机器上,将其作为客户端对lb ip进行访问。
- 首先,arp request在第四台机器上发出,由openelb reply,然后被这台机器收到
- reply里的mac地址被用于填充进帧里,然后帧就发到了交换机
- 交换机发现一个未知的帧,此时并不是像其他客户端一样发arp,而是用了泛洪的方式,将帧从所有端口发出(来源口除外)
- 匹配目标mac地址的机器后续响应了该帧,完成了后续通信,交换机这时在转发表里记录下了mac地址和端口的正确映射
- 而本次出现问题的场景下,我们没有额外的机器去做客户端测试lb ip,不得已在交换机上对其进行测试
- 交换机作为客户端发起ping时,发现未知的ip,也会像客户端一样发起arp request,并接收到reply
- 交换机不仅通过arp交互,完成了数据帧的目标mac地址的填充,还顺带学习了自己的arp表:将arp reply的来源口作为arp表项中的port,和arp reply中的ip、mac一起作为一个表项加入了arp表。就是这一步学习导致了潜在的错误。
- 由于已经通过arp交互学到了转发需要的信息,就不再需要泛洪,直接按照arp表项中的port转发到指定的端口,孰不知这个端口有可能是错误的,网络不通就出现了。
问题2[TBC]
由于我自己的实验环境没有企业级的交换机,无法确定交换机的端口问题。需要等环境空余,再做实验探究一下免费arp阶段交换机的arp表细节
问题3
向社区提了issue:后续关注该问题
问题解决
- 如果应用场景中,不需要从网关发起访问,那么这个问题其实可以忽略不管
- 由于我们的产品形态、交付模式和生产环境网络规划,最终流量必须通过交换机从外部进入集群,所以这个问题是致命的
- 最终把openelb替换为metallb,同样使用layer2模式,即可规避该问题。因为metallb的layer2模式设计架构上,用了daemonset做arp speaker,每个speaker仅发送当前节点mac的arp reply,不会出现sender和reply mac不匹配的情况,故而不会出现该问题
- 关于metallb的细节,参见后面的metallb代码分析
参考
本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。