一次openelb-can_reach配置导致的故障排查
背景
openelb配置阶段需要配置一个EIP Custom Resource对象,作为LB ip的IP池。
在EIP对象的spec里,有一个字段叫做interface,需要配置openelb相应arp请求的网卡。通常配置和eip对应的网卡名称即可。
但是在kubernetes集群中,有时会出现多个node同一网段对应的网卡名称不一样的情况。因此openelb也相应地支持在该字段中用can_reach: $IP
的格式来表示网卡。
interface
: NIC on which OpenELB listens for ARP or NDP requests. This field is valid only whenprotocol
is set tolayer2
.If the NIC names of the Kubernetes cluster nodes are different, you can set the value to
can_reach:IP address
(for example,can_reach:192.168.0.5
) so that OpenELB automatically obtains the name of the NIC that can reach the IP address. In this case, you must ensure that the IP address is not used by Kubernetes cluster nodes but can be reached by the cluster nodes.
根据官方文档,使用interface: can_reach:$IP
格式时,ip应该使用未被k8s node使用过且node可达的ip。比如node ip为172.16.0.1/24-172.16.0.3/24,那么这个配置里可以使用172.16.0.4等任意一个同网段的ip。
问题
根据文档的使用说明,我们在部署阶段,为EIP对象配置了interface: can_reach:$IP
,其中ip用的是EIP IP段的第一个IP。根据文档,这么做是符合要求的。但是实际应用中出现了问题。
根据代码,openelb维护了一个map缓存,key为interface字段,value为对应的speaker实例。第一次启动时,由于没有缓存,所以会调用layer2.NewSpeaker,构造一个speaker实例。
// pkg/controllers/ipam/ipam.go:209
sp = speaker.GetSpeaker(e.GetSpeakerName())
if sp == nil {
sp, err = layer2.NewSpeaker(e.Spec.Interface, e.Status.V4)
if err == nil {
err = speaker.RegisterSpeaker(e.GetSpeakerName(), sp)
}
}
NewSpeaker方法中,针对配置了can_reach
的interface,会调用ip route get
这一系统api,获取can_reach
ip对应的网卡名称。
case "can_reach":
ip := net.ParseIP(strs[1])
if ip == nil {
return nil, fmt.Errorf("invalid can_reach address %s", strs[1])
}
routers, err := netlink.RouteGet(ip)
if err != nil {
return nil, err
}
iface, err = net.InterfaceByIndex(routers[0].LinkIndex)
if err != nil {
return nil, err
}
if iface.Name == "lo" {
return nil, fmt.Errorf("invalid interface lo")
}
当interface: can_reach:$IP
中的ip配置为EIP池的第一个ip时,如果eip池的第一个ip始终未被分配,那么openelb不管何时重启,都可以正常通过该ip获取到对应的interface名称。
当can_reach配置的ip被lb分配使用后,此时如果不重启openelb,那么之前speaker在内存缓存中,依然正常。但是此时如果openelb被重启,NewSpeaker方法会被运行,此时依然通过ip route get
api尝试获取interface时,会因为ip被lb使用,拿到的interface会变成lo。因此就引发了无法获取正确网卡的问题。
发现这个问题后,也在github issue里发起了讨论:Eip object with can_reach leads to lo interface, not sure if this is a bug, some help would be appreciated · Issue #331 · openelb/openelb (github.com)
解决
interface: can_reach:$IP
配置IP时使用的ip不能是集群node ip,也不能是集群内部可能用到的lb ip:包括kube-vip里配置的vip、EIP池里的vip等各类vip。因为这些ip在某个node上用ip route get
获取网卡时都可能得到lo网卡。- 那么可以配置成哪些ip?当前尝试配置成网关的ip,或者选择其他任意一个不会被集群使用但在同网段的ip
dive deeper
为什么被openelb分配使用后的ip,通过ip route get
命令获取的网卡名称会变成lo?
首先了解ip route get命令。根据man page的描述,特别强调了,ip route get和ip route show中的路由表并不等价。ip route get展示的是模拟发包到某个ip时真正路径的情况,等价于ping该ip,数据包在内核中真实流动的情况。就像用traceroute可以看到数据包真实跳跃情况一样,用ip route get可以看到某个机器上发往指定的ip的数据包是通过哪个网卡走的。
# man ip route
ip route get
get a single route
this command gets a single route to a destination and prints its contents exactly as the kernel sees it.Note that this operation is not equivalent to ip route show. show shows existing routes. get resolves them and creates new clones if necessary. Essentially, get is equiv‐
alent to sending a packet along this path. If the iif argument is not given, the kernel creates a route to output packets towards the requested destination. This is
equivalent to pinging the destination with a subsequent ip route ls cache, however, no packets are actually sent. With the iif argument, the kernel pretends that a packet
arrived from this interface and searches for a path to forward the packet.
知道了命令的逻辑后,我们直观地在某个node上用ip route get对一些ip做一下验证:比如我在一台ip为172.16.236.152/24的ubuntu node上执行下面的命令
# 自己的ip
root@master1:~# ip route get 172.16.236.152
local 172.16.236.152 dev lo src 172.16.236.152 uid 0
cache <local>
# 集群里另一个node的ip
root@master1:~# ip route get 172.16.236.153
172.16.236.153 dev enp175s0f0 src 172.16.236.152 uid 0
cache
# eip里一个已经被分配使用了的ip
root@master1:~# ip route get 172.16.236.40
local 172.16.236.40 dev lo src 172.16.236.40 uid 0
cache <local>
# eip里尚未被使用的ip
root@master2:~# ip route get 172.16.236.41
172.16.236.41 dev enp175s0f0 src 172.16.236.153 uid 0
cache
可以看到,node探测自己的ip,返回的是lo网卡;node探测openelb使用的lb ip,同样会返回lo网卡,这也正是openelb layer2模式的原理,通过arp协议拦截了数据包到node上。
但是我们通过route -n
或者ip route show
命令直接看到的路由表里并不能看到lb ip的路由信息,那么这些路由信息存在了哪里呢?
此时想起来之前在openwrt中配置wireguard时看到过的,路由表是有多个的(main/default/local等)。
常用命令ip route show
显示的只是main表中的路由。我们看local表中的路由,就可以看到lb ip的路由信息了。
root@master1:~# ip route show table local|grep 172.16.236.40
local 172.16.236.40 dev kube-ipvs0 proto kernel scope host src 172.16.236.40
参考
负载均衡器 OpenELB ARP 欺骗技术解析 (kubesphere.io)
Configure IP Address Pools Using Eip | OpenELB
ARP协议具体解释之Gratuitous ARP(免费ARP) - wzzkaifa - 博客园 (cnblogs.com)
本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。