一次openelb多网卡配置问题导致的奇怪现象排查
问题
3节点集群,每个节点有多块网卡。部署了openelb,并配置了和node同网段的IP池供LB使用。
接到测试同事反映,集群里的lb service分到的ip有的可以ping通并访问业务,但是有的却不能ping通。
排查
一开始怀疑ip冲突,在交换机上确认后发现不能访问的ip并没有被占用。
于是在同网段的其他机器上安装了arp-scan,扫描下arp看看结果。
172.16.236.40 xx:a6:b7:41:c5:68 Intel Corporate
172.16.236.41 xx:a6:b7:41:c6:e3 (40:a6:b7:41:c5:68) Intel Corporate
40是正常能访问的lb ip,其mac地址是k8s 其中一台node符合配置预期的网卡mac
41是不能访问的lb ip,多出来的一个mac经过查询,是某一台node上另一个网段的网卡的mac
这样就从结果上解释了为什么有些ip无法访问
TODO: xx:a6:b7:41:c6:e3 (40:a6:b7:41:c5:68) 括号是什么意思?
代码分析
接下来通过分析代码,分析openelb是如何决定一个lb service用哪个node的哪个网卡的mac来伪造arp请求的
首先,当发现有对lb ip的arp请求时,openelb里的arp speaker会代为回答,回答时最重要的就是要确定用哪个mac地址来伪造目标ip的arp response,可以看到代码里会调用getMac()方法查询。而getMac()方法逻辑很简单,就是从一个map里查找。
// pkg/speaker/layer2/arp.go:240
func (a *arpSpeaker) processRequest() dropReason {
pkt, _, err := a.conn.Read()
...
hwAddr := a.getMac(pkt.TargetIP.String())
if hwAddr == nil {
return dropReasonUnknowTargetIP
}
...
}
// pkg/speaker/layer2/arp.go:38
func (a *arpSpeaker) getMac(ip string) *net.HardwareAddr {
a.lock.Lock()
defer a.lock.Unlock()
result, ok := a.ip2mac[ip]
if !ok {
return nil
}
return &result
}
所以接下来,需要找到ip2mac这个map是在哪里被写入的。下面的gratuitous方法就是为lb ip确定伪造mac地址的地方。
// pkg/speaker/layer2/arp.go:144
func (a *arpSpeaker) gratuitous(ip, nodeIP net.IP) error {
if a.getMac(ip.String()) != nil {
return nil
}
hwAddr, err := a.resolveIP(nodeIP)
if err != nil {
return fmt.Errorf("failed to resolve ip %s, err=%v", nodeIP, err)
}
a.setMac(ip.String(), hwAddr)
a.logger.Info("map ingress ip", "ingress", ip.String(), "nodeIP", nodeIP.String(), "nodeMac", hwAddr.String())
...
}
进一步,看一下resolveIP方法的实现,就可以明白mac地址是怎么决定的了
// pkg/speaker/layer2/arp.go:107
func (a *arpSpeaker) resolveIP(nodeIP net.IP) (hwAddr net.HardwareAddr, err error) {
routers, err := netlink.RouteGet(nodeIP)
if err != nil {
return nil, err
}
iface, err := net.InterfaceByIndex(routers[0].LinkIndex)
if err != nil {
return nil, err
}
if iface.Name == "lo" {
hwAddr = a.intf.HardwareAddr
} else {
//Resolve mac
...
}
这里还是调用了ip route get
的系统api,去获取数据包发往某个ip需要经过的interface,然后返回该interface的mac地址。特殊情况是,当入参中的ip恰好是node上网卡的ip时,ip route get
会返回lo网卡,而代码里也专门针对这种情况做了特殊处理,这种情况下就会使用EIP对象里指定的interface作为目标interface。
整明白mac地址是基于ip route get
+ip得来的以后,那么问题来了,这个ip是怎么确定的呢?继续追查代码可以发现,这个ip优先使用node注解中指定的ip,如果没有,则默认使用nodeStatus中的ip
// pkg/speaker/layer2/arp.go:179
func (a *arpSpeaker) SetBalancer(ip string, nodes []corev1.Node) error {
if nodes[0].Annotations != nil {
nexthop := nodes[0].Annotations[constant.OpenELBLayer2Annotation]
if net.ParseIP(nexthop) != nil {
return a.setBalancer(ip, []string{nexthop})
}
}
...
}
SetBalancer函数入参中的node,可以在lb service的注解中查到。
查看环境中node配置的注解,发现注解里配置的ip并不是lb ip那个网段的,这样就能解释为什么无法访问了。
至于为什么有的lb ip还能访问,这其实是一种特例:当openelb pod所在的node和lb service分配的node是同一个时,ip route get
会得到lo网卡,而代码里又针对lo网卡做了特殊处理,会使用EIP中配置的interface,而EIP中的interface是正确的,因此就可以正常访问了。
解决
把node上的注解改对即可……
启示
-
当openelb部署的集群node有多网卡时,需要按照文档配置注解;切换EIP网段时也需要注意一并修改node注解。
-
注解中的ip如果配置错误,得到的结果并不是lb ip全部不可用,而是部分可用部分不可用。这一表现对问题排查并不友好,如果能加一些配置校验,并利用k8s的event将问题抛到上层可能会更好
参考
openelb v0.4.4
本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。