問題發(fā)生的背景
在給k8s集群添加Node節(jié)點時發(fā)現(xiàn)新加的Node網(wǎng)絡(luò)不通,經(jīng)過排查發(fā)現(xiàn)需要同時重啟新加Node節(jié)點和RouterReflector節(jié)點上的kube-router, 新加節(jié)點網(wǎng)絡(luò)才能生效。以下為 BGP文檔中的說明:

重啟RR節(jié)點是一個非常危險的事情,在試圖去解決這個問題的過程中,我們發(fā)現(xiàn)了一個更加嚴重的問題,在重啟RR節(jié)點的期間,會導致約1/3服務器的網(wǎng)絡(luò)斷網(wǎng)。下圖為重啟RR3節(jié)點期間,BGP路由器上的10.1.4/5/7路由被刪除,丟失時間大約10秒,此時間受kube-router的pod調(diào)度時間影響,可能更長。
而且還發(fā)現(xiàn)另外一個問題,每個RR節(jié)點代理的轉(zhuǎn)發(fā)的路由不是全量路由,只轉(zhuǎn)發(fā)了一部分,規(guī)模大約為k8s集群的1/3。

注: 新加節(jié)點需要重啟RR節(jié)點上的kube-router網(wǎng)絡(luò)才能生效的問題,有3個解決方案需要選擇,會單獨出一個文檔討論
分析原因以及可用的解決方案
在具體分析問題之前,我總結(jié)了一下,要解決「重啟RR節(jié)點導致1/3節(jié)點會出現(xiàn)斷網(wǎng)」,只要實現(xiàn)以下一種方案就能解決,為了更高的可用性,最少實現(xiàn)2種方案:
- RR轉(zhuǎn)發(fā)全量的路由到BGP路由器上
- RR節(jié)點周期性地同步其代理的所有Node的路由到BGP路由器上
- RR節(jié)點開啟gracefulrestart功能,重啟不刪除其轉(zhuǎn)發(fā)的路由
注:由于3個方案部分內(nèi)容過多,可跳過直接看「總結(jié)與思考」部分
方案一:RR轉(zhuǎn)發(fā)全量的路由到BGP路由器上
Kube-router主要是一個控制器,而BGP路由的通告主要是用gobgp庫實現(xiàn)的,gobgp庫是全球前5大電信公司NTT開源的,應該是一個被廣發(fā)使用的庫才對,而這個問題明顯是一個功能缺陷才對,在kube-router和gobgp的issues里范例一圈,發(fā)現(xiàn)下面兩個issues是和我們發(fā)生的問題一樣:
發(fā)現(xiàn)有以下的解決方案:1)設(shè)置不同的clisterid,2)server和client設(shè)置為相同的id

經(jīng)過在rz-op-k8s13-pm測試機上經(jīng)過驗證,發(fā)現(xiàn)2者都不能解決我們的問題,而且一個節(jié)點同時為Node和client形成邏輯環(huán)路,感覺這個問題想定位只能從代碼中排查了,
由于是BGP問題,查看的主要是gobgp庫的代碼,發(fā)現(xiàn)BGP的FSM(有限狀態(tài)機)的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)
}
通過日志發(fā)現(xiàn)了路由發(fā)送的出口函數(shù)
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")
再結(jié)合日志發(fā)現(xiàn),每次重啟RR其能轉(zhuǎn)發(fā)到BGP路由器路由條目都是不固定的,而能正常轉(zhuǎn)發(fā)路由到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 表示成功發(fā)送到BGP路由器
而不能被轉(zhuǎn)發(fā)是下面這樣的
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
經(jīng)過分析發(fā)現(xiàn)一條路由會有兩種路徑被RR收到,1)通過RR的client直接收取到,2)通過另外一個RR反射過來的,而RR的特性是當收到另外一個RR反射過來的路由,會直接丟棄掉,因為同處于一個clusterid,且不是RR的client,最后通過代碼定位到問題出現(xiàn)在優(yōu)選路由的計算上了。當RR收到一個client發(fā)送來的路由,但是還沒有轉(zhuǎn)發(fā)到BGP路由器上,這是時候從另外一個RR也收到了一條同樣的路由,在優(yōu)先路由計算時,因為沒有區(qū)分RR轉(zhuǎn)發(fā)的路和client轉(zhuǎn)發(fā)的路由,因為兩條路由的路徑一直,屬性一致,這時候路由優(yōu)先選用RouteID號更低的路由為最優(yōu)路由,而RR的ID是最低的,因此被選中,但是隨后被檢查到這一條RR轉(zhuǎn)發(fā)的路由,最終被拋棄,沒有被轉(zhuǎn)發(fā)到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的代碼,在做優(yōu)選路由計算的時候區(qū)分RR轉(zhuǎn)發(fā)過來的路由,經(jīng)過部署測試,RR可以轉(zhuǎn)發(fā)全量的路由到BGP路由器了。
翻看上下文的代碼,發(fā)現(xiàn)這塊根本就沒有考慮RR反射的邏輯,感覺有點反常,不應該出現(xiàn)這樣的情況才對啊, 繼續(xù)翻看issues看看有什么線索,還沒有發(fā)現(xiàn),突然靈光一現(xiàn),在上面列的Issues里提到需要在RR上需要同時配置server和client兩個角色,但是在實驗的時候,只配置了測試機,是不是需要所有的RR都要同樣的配置呢?經(jīng)過測試在4臺RR(k8s13是測試機)上都配置server和client角色,測試RR可以全量轉(zhuǎn)發(fā)路由到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"
但是此此配置實際會產(chǎn)生路由通告環(huán)路的問題,而且通告出去的路由無法刪除
*> 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集群上,社區(qū)方案實際上會產(chǎn)生一個邏輯環(huán),必須要通過originator_id和cluster_list來放環(huán)。gobgp是如何處理一個BGP角色既是RR又是RR的client的呢?,在gobgp里會優(yōu)先判定為RR client。
方案二:RR節(jié)點周期性地同步其代理的所有Node的路由到BGP路由器上
kube-router在周期性同步路由默認是5分鐘一次,但是為什么沒有周期性同步給BGP路由器呢?在抓包上看也確實沒有同步,而IGP路由協(xié)議都會有這樣的機制的,問題出現(xiàn)在哪兒呢?翻看kube-router和gobgp沒有看到任何的Issue,通過翻看gobgp的代碼發(fā)現(xiàn),在經(jīng)歷方案一個的優(yōu)選路由后,在輸出處理模塊里,會判斷路由是否發(fā)生變化,如果沒有發(fā)生變化則過濾。
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
}
沒有發(fā)生變化就過濾,為什么會有這樣的規(guī)則呢?在翻看BGP的RFC 4486/4456/4724/2796/1654/4271后在1771的第三節(jié)中發(fā)現(xiàn)了如下定義,明確BGP路由不會發(fā)送周期性的路由通告,只在路由或者狀態(tài)機發(fā)生變化時才改變路由,具體看: RFC 1771 華為BGP

既然BGP不會周期性地通告BGP路由,那為什么gobgp會周期性通告路由呢?為了增加可靠性?軟件BGP在使用上和硬件BGP看來還是有些一些不同
方案三:RR節(jié)點開啟gracefulrestart功能,重啟不刪除其轉(zhuǎn)發(fā)的路由
在kube-router添加"--bgp-graceful-restart=true"的參數(shù)即可開啟GR功能,但是在測試過程中發(fā)現(xiàn),在kube-router上啟動GR后,無法向BGP路由器轉(zhuǎn)發(fā)路由,gobgp把所有的路由全部丟失。
經(jīng)過排查發(fā)現(xiàn),BGP協(xié)議規(guī)定,當一個節(jié)點在down的時候會向?qū)Χ说腷gp路由器發(fā)送一個Notification狀態(tài)包,狀態(tài)包中標明自己已經(jīng)down了,這時候路由器會刪除其所有轉(zhuǎn)發(fā)來的路由。而GracefulRestart機制,需要兩邊協(xié)商一致才能生效,單邊配置是無效的。協(xié)商成功后,在接收到對端bgp路由器down了后,會將其所有轉(zhuǎn)發(fā)的路由設(shè)置為stale,stale狀態(tài)的路由依然有轉(zhuǎn)發(fā)能力。并且還會啟動一個GR 超時定時器,只有超過定時器后才會真正的刪除這些路由。
而且社區(qū)也推薦使用GR方案 issue:https://github.com/cloudnativelabs/kube-router/issues/676
kube-router的社區(qū)contributor說殺RR不是一個危險的動作,沒有任何影響,因為用了soft-restart,應該就是GR。