问题

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

文章目录