「重启 kube-router RR 实例导致 k8s 集群 1/3 服务器断网」问题的分析与解决

问题发生的背景

在给k8s集群添加Node节点时发现新加的Node网络不通,经过排查发现需要同时重启新加Node节点和RouterReflector节点上的kube-router, 新加节点网络才能生效。以下为 BGP文档中的说明:


重启RR节点是一个非常危险的事情,在试图去解决这个问题的过程中,我们发现了一个更加严重的问题,在重启RR节点的期间,会导致约1/3服务器的网络断网。下图为重启RR3节点期间,BGP路由器上的10.1.4/5/7路由被删除,丢失时间大约10秒,此时间受kube-router的pod调度时间影响,可能更长。

而且还发现另外一个问题,每个RR节点代理的转发的路由不是全量路由,只转发了一部分,规模大约为k8s集群的1/3。


注: 新加节点需要重启RR节点上的kube-router网络才能生效的问题,有3个解决方案需要选择,会单独出一个文档讨论

分析原因以及可用的解决方案

在具体分析问题之前,我总结了一下,要解决「重启RR节点导致1/3节点会出现断网」,只要实现以下一种方案就能解决,为了更高的可用性,最少实现2种方案:

  • RR转发全量的路由到BGP路由器上
  • RR节点周期性地同步其代理的所有Node的路由到BGP路由器上
  • RR节点开启gracefulrestart功能,重启不删除其转发的路由

注:由于3个方案部分内容过多,可跳过直接看「总结与思考」部分

方案一:RR转发全量的路由到BGP路由器上

Kube-router主要是一个控制器,而BGP路由的通告主要是用gobgp库实现的,gobgp库是全球前5大电信公司NTT开源的,应该是一个被广发使用的库才对,而这个问题明显是一个功能缺陷才对,在kube-router和gobgp的issues里范例一圈,发现下面两个issues是和我们发生的问题一样:

  • https://github.com/osrg/gobgp/issues/2166
  • https://github.com/cloudnativelabs/kube-router/issues/773

发现有以下的解决方案:1)设置不同的clisterid,2)server和client设置为相同的id


经过在rz-op-k8s13-pm测试机上经过验证,发现2者都不能解决我们的问题,而且一个节点同时为Node和client形成逻辑环路,感觉这个问题想定位只能从代码中排查了,

由于是BGP问题,查看的主要是gobgp库的代码,发现BGP的FSM(有限状态机)的handler是handleFSMMesage,这就是BGP通告路由的入口了

func (server *BgpServer) handleFSMMessage(peer *Peer, e *FsmMsg) {
    switch e.MsgType {
     ...
    case FSM_MSG_BGP_MESSAGE:
        switch m := e.MsgData.(type) {
            ...
        case *bgp.BGPMessage:
            ...
            if len(pathList) > 0 {
                server.propagateUpdate(peer, pathList)
            }

通过日志发现了路由发送的出口函数

func (h *FSMHandler) sendMessageloop() error {
    conn := h.conn
    fsm := h.fsm
    ticker := keepaliveTicker(fsm)
    send := func(m *bgp.BGPMessage) error {
        ...
        switch m.Header.Type {
        ...
        case bgp.BGP_MSG_UPDATE:
            update := m.Body.(*bgp.BGPUpdate)
            log.WithFields(log.Fields{
                "Topic":       "Peer",
                "Key":         fsm.pConf.State.NeighborAddress,
                "State":       fsm.state.String(),
                "nlri":        update.NLRI,
                "withdrawals": update.WithdrawnRoutes,
                "attributes":  update.PathAttributes,
            }).Debug("sent update")

再结合日志发现,每次重启RR其能转发到BGP路由器路由条目都是不固定的,而能正常转发路由到BGP路由器的是下面这样

time="2020-08-12T22:20:45+08:00" level=debug msg="received update" Key=10.2.4.14 Topic=Peer attributes="[{Origin: i} {Nexthop: 10.2.4.14} {LocalPref: 100}]" nlri="[10.1.10.0/24]" withdrawals="[]"
time="2020-08-12T22:20:45+08:00" level=debug msg="sent update" Key=10.2.4.1 State=BGP_FSM_ESTABLISHED Topic=Peer attributes="[{Origin: i} {Nexthop: 10.2.4.14} {LocalPref: 100}]" nlri="[10.1.10.0/24]" withdrawals="[]"

注:10.2.4.1 为BGP路由器, msg="sent update" Key=10.2.04.1 表示成功发送到BGP路由器

而不能被转发是下面这样的

time="2020-08-12T22:20:45+08:00" level=debug msg="received update" Key=10.2.4.13 Topic=Peer attributes="[{Origin: i} {Nexthop: 10.2.4.13} {LocalPref: 100}]" nlri="[10.1.9.0/24]" withdrawals="[]"
time="2020-08-12T22:20:45+08:00" level=debug msg="received update" Key=10.2.4.4 Topic=Peer attributes="[{Origin: i} {Nexthop: 10.2.4.13} {LocalPref: 100}]" nlri="[10.1.9.0/24]" withdrawals="[]"
time="2020-08-12T22:20:45+08:00" level=debug msg="From same AS, ignore." Data="{ 10.1.9.0/24 | src: { 10.2.4.4 | as: 65003, id: 10.2.4.4 }, nh: 10.2.4.13 }" Key=10.2.4.1 Topic=Peer

经过分析发现一条路由会有两种路径被RR收到,1)通过RR的client直接收取到,2)通过另外一个RR反射过来的,而RR的特性是当收到另外一个RR反射过来的路由,会直接丢弃掉,因为同处于一个clusterid,且不是RR的client,最后通过代码定位到问题出现在优选路由的计算上了。当RR收到一个client发送来的路由,但是还没有转发到BGP路由器上,这是时候从另外一个RR也收到了一条同样的路由,在优先路由计算时,因为没有区分RR转发的路和client转发的路由,因为两条路由的路径一直,属性一致,这时候路由优先选用RouteID号更低的路由为最优路由,而RR的ID是最低的,因此被选中,但是随后被检查到这一条RR转发的路由,最终被抛弃,没有被转发到BGP路由器上。

func (dst *Destination) sort() {
    sort.SliceStable(dst.knownPathList, func(i, j int) bool {
        path1 := dst.knownPathList[i]
        path2 := dst.knownPathList[j]
 
        var better *Path
        reason := BPR_UNKNOWN
 
        // draft-uttaro-idr-bgp-persistence-02
        if better == nil {
            better = compareByLLGRStaleCommunity(path1, path2)
            reason = BPR_NON_LLGR_STALE
        }
        ...
        if better == nil {
            var e error = nil
            better, e = compareByRouterID(path1, path2)
            if e != nil {
                log.WithFields(log.Fields{
                    "Topic": "Table",
                    "Error": e,
                }).Error("Could not get best path by comparing router ID")
            }
            reason = BPR_ROUTER_ID
        }
        better.reason = reason
        return better == path1
    })
}

通过修改gobgp的代码,在做优选路由计算的时候区分RR转发过来的路由,经过部署测试,RR可以转发全量的路由到BGP路由器了。

翻看上下文的代码,发现这块根本就没有考虑RR反射的逻辑,感觉有点反常,不应该出现这样的情况才对啊, 继续翻看issues看看有什么线索,还没有发现,突然灵光一现,在上面列的Issues里提到需要在RR上需要同时配置server和client两个角色,但是在实验的时候,只配置了测试机,是不是需要所有的RR都要同样的配置呢?经过测试在4台RR(k8s13是测试机)上都配置server和client角色,测试RR可以全量转发路由到BGP路由器了。

kubectl annotate node rz-op-k8smaster1-pm "kube-router.io/rr.server=345"
kubectl annotate node rz-op-k8smaster1-pm "kube-router.io/rr.client=345"
kubectl annotate node rz-op-k8smaster2-pm "kube-router.io/rr.server=345"
kubectl annotate node rz-op-k8smaster2-pm "kube-router.io/rr.client=345"
kubectl annotate node rz-op-k8smaster3-pm "kube-router.io/rr.server=345"
kubectl annotate node rz-op-k8smaster3-pm "kube-router.io/rr.client=345"
kubectl annotate node rz-op-k8s13-pm "kube-router.io/rr.server=345"
kubectl annotate node rz-op-k8s13-pm "kube-router.io/rr.client=345"

但是此此配置实际会产生路由通告环路的问题,而且通告出去的路由无法删除

*> 10.1.1.0/26       10.2.4.4          60000                 46d 00:25:55 [{Origin: i} {LocalPref: 100} {Originator: 192.168.2.1} {ClusterList: [10.2.4.2]}]
*  10.1.1.0/26       10.2.4.4          60000                 46d 00:25:55 [{Origin: i} {LocalPref: 100} {Originator: 192.168.1.1} {ClusterList: [10.2.4.2]}]
*> 10.1.1.0/26       10.1.2.4          60000                 46d 19:46:03 [{Origin: ?} {Med: 0} {LocalPref: 100} {Originator: 192.168.1.1} {ClusterList: [10.2.4.2]}]

真正解决此问题一种是使用GR方案,一种就是改代码。

一个BGP路由器既是RR角色又是client觉得只会用在多层RR集群上,社区方案实际上会产生一个逻辑环,必须要通过originator_id和cluster_list来放环。gobgp是如何处理一个BGP角色既是RR又是RR的client的呢?,在gobgp里会优先判定为RR client。

方案二:RR节点周期性地同步其代理的所有Node的路由到BGP路由器上

kube-router在周期性同步路由默认是5分钟一次,但是为什么没有周期性同步给BGP路由器呢?在抓包上看也确实没有同步,而IGP路由协议都会有这样的机制的,问题出现在哪儿呢?翻看kube-router和gobgp没有看到任何的Issue,通过翻看gobgp的代码发现,在经历方案一个的优选路由后,在输出处理模块里,会判断路由是否发生变化,如果没有发生变化则过滤。

func dstsToPaths(id string, as uint32, dsts []*table.Update) ([]*table.Path, []*table.Path, [][]*table.Path) {
    bestList := make([]*table.Path, 0, len(dsts))
    oldList := make([]*table.Path, 0, len(dsts))
    mpathList := make([][]*table.Path, 0, len(dsts))
 
    for _, dst := range dsts {
        best, old, mpath := dst.GetChanges(id, as, false)
        bestList = append(bestList, best)
        oldList = append(oldList, old)
        if mpath != nil {
            mpathList = append(mpathList, mpath)
        }
    }
    return bestList, oldList, mpathList
}

没有发生变化就过滤,为什么会有这样的规则呢?在翻看BGP的RFC 4486/4456/4724/2796/1654/4271后在1771的第三节中发现了如下定义,明确BGP路由不会发送周期性的路由通告,只在路由或者状态机发生变化时才改变路由,具体看: RFC 1771 华为BGP


既然BGP不会周期性地通告BGP路由,那为什么gobgp会周期性通告路由呢?为了增加可靠性?软件BGP在使用上和硬件BGP看来还是有些一些不同

方案三:RR节点开启gracefulrestart功能,重启不删除其转发的路由

在kube-router添加"--bgp-graceful-restart=true"的参数即可开启GR功能,但是在测试过程中发现,在kube-router上启动GR后,无法向BGP路由器转发路由,gobgp把所有的路由全部丢失。

经过排查发现,BGP协议规定,当一个节点在down的时候会向对端的bgp路由器发送一个Notification状态包,状态包中标明自己已经down了,这时候路由器会删除其所有转发来的路由。而GracefulRestart机制,需要两边协商一致才能生效,单边配置是无效的。协商成功后,在接收到对端bgp路由器down了后,会将其所有转发的路由设置为stale,stale状态的路由依然有转发能力。并且还会启动一个GR 超时定时器,只有超过定时器后才会真正的删除这些路由。

而且社区也推荐使用GR方案 issue:https://github.com/cloudnativelabs/kube-router/issues/676
kube-router的社区contributor说杀RR不是一个危险的动作,没有任何影响,因为用了soft-restart,应该就是GR。

你可能感兴趣的:(「重启 kube-router RR 实例导致 k8s 集群 1/3 服务器断网」问题的分析与解决)