Categories
程式開發

eBay流量管理之Kubernetes網絡硬核排查案例


一、引子

在eBay新一代基於Kubernetes的雲平台Tess環境中,流量管理的實現逐步從傳統的硬件Load Balancer向軟件過渡。在Tess的設計中,選用了目前比較流行的開源Serivce Mesh框架Istio。

Istio的組件中,控制平面的Pilot和數據平面的Envoy無疑最為重要。 Pilot 提供了服務發現功能,並且把高層的規則轉化為數據面組件識別的配置推送給數據面Envoy,Envoy根據這些規則去管理整個雲平台環境的網絡流量。

在eBay對於Istio的落地實踐中,隨著規模的不斷擴大,Tess系統迎來了諸多挑戰,各種奇奇怪怪的問題也應運而生。最近,我們遇到Pilot在推規則給Envoy的時候,耗費時間很長甚至因此失敗的問題

在問題出現的時候,通過查看Pilot Pod的日誌文件,發現了Reset Connection這樣的日誌。這些日誌說明Pilot在推送規則的過程中,和Envoy之間的網絡連接不時的被Reset。正常傳輸過程中是不會出現連接被Reset的情況,那究竟為何會導致這不正常的Reset呢?因此,本文就這個典型案例做了比較詳細深入的分析。

二、Kubernetes網絡基本原理

既然從日誌看這是一個和網絡相關的問題,我們首先得了解一下Tess(eBay內部容器雲平台的代號)也就是Kubernetes的網絡基本原理。

在Kubernetes裡,每個Pod都有一個自己的IP地址,Pod之間通過這些IP地址可以互相訪問。為了保持網絡的簡潔性,Kubernetes要求網絡的實現中,Pod之間的通信不需要做網絡地址轉化(NAT),不管Pod在同一個節點還是在不同的節點,都可以直接通信,從Pod A 到Pod B 的網絡報文必須保持源IP地址和目的IP地址不變。

當Pod需要和Kubernetes集群外部通信的時候,情況就變得大不一樣。 Pod在集群中獲取的IP地址非常不穩定,隨時都有可能因為種種原因不能正常工作。

為了保證Pod能提供穩定的服務,Kubernetes引入了Service的概念。Service允許長時間對外暴露穩定的IP地址和端口,因此後面一系列的Pod對外提供服務由Service替代進行,原先對於Pod的訪問就變成對這個Service的訪問,Service再根據它所監管的Pod的健康狀況或者負載均衡要求,把請求發送給這些Pod。Kubernetes的Service實際上就是一個架設在Pod前面的4層網絡負載均衡器

Service有多種類型,其中ClusteIP是最基本的類型,這種類型Service的特點是它的唯一VIP是一個集群內部可見的地址,並且這個ClusteIP在集群內部不同的節點之間不可見,必須通過每個節點上Kube-proxy這個組件來保證節點之間的可達。通常情況下,Kube-proxy通過Iptables的規則或者IPVS的規則來完成節點之間的可達。

有了以上的基礎知識,我們再來分析這次問題就比較容易理解了。在我們這次出現問題的環境中,拓撲結構如下圖所示:

eBay流量管理之Kubernetes網絡硬核排查案例 1

圖1

報文的轉發流程如下:

去程

1 src(10.148.74.37:48640)    dst(192.168.107.214:15010)
2 src(10.148.74.37:48640)    dst(10.148.74.34:15010) # DNAT

回程

3 src(10.148.74.34:15010)    dst(10.148.74.37:48640)
4 src(192.168.107.214:15010) dst(10.148.74.37.48640) # SNAT

三、問題的排查過程

我們都知道,一切的網絡問題,在你經過無數次的邏輯推理仍然毫無頭緒的時候,只能回歸到最原始的抓包大法,於是我們決定去案發現場全面撒網捕獲兇手。對於我們遇到的問題,通信的雙方來自Pilot Pod和Envoy Pod,於是我們在這2個Pod上面同時開啟Tcpdump。規則推送是基於TCP端口15010,所以只抓TCP端口15010的包(不限定IP地址),同時打開Pilot的Log,看什麼時候Reset的Log出現了,就停止抓包。由

於這個問題出現的概率很高,5分鐘之後就再現了。但是抓包文件多達幾百兆,在經過了對網絡拓撲結構的分析後,在Envoy Pod上的抓包過濾出10.148.74.37:48640 192.168.107.214:15010的一條TCP流,在Pilot Pod上的抓包過濾出10.148.74.37:48640 10.148.74.34:15010的一條TCP流,這樣每個抓包文件大約30M,保存成2個文件。最後我們同時打開2個Wireshark,對照分析。劇透一下,事實上這裡我們先通過TCP 五元組過濾出我們認為正確的TCP流來分析,反倒是誤導了我們分析的方向,讓我們走入了死胡同。點開這2個抓包文件以後,我大致掃了一眼也開始凌亂,這5000多個報文來來回回,重傳看上去很嚴重,TCP重傳的報文本來也算正常,只要不是一直丟包,最後總能成功,這不就是TCP存在的意義麼?因此我第一猜想是網絡環境導致丟包嚴重,應用程序一直等不到數據,然後超時,所以應用主動結束了連接。於是去查證Envoy的抓包文件,是不是收到了所有Pilot發過來的數據?

通過對比發送報文的序列號和接受到的ACK信息,發現最終結果Pilot發送的所有數據都收到了來自Envoy的Ack,儘管中間出現了亂序重傳的現象,但也算是完整的收到了所有數據,在2個抓包文件的最後都發現了Reset報文。圖2是Pilot Pod的部分截圖,可以看到1992號報文是Pilot收到的第一個Reset報文,後面Pilot持續收到Reset報文,從這些Reset的報文源IP(10.148.74.37)來看應該來自於Envoy Pod。同時從1994號報文開始,Pilot Pod也持續的發Reset報文給Envoy。

eBay流量管理之Kubernetes網絡硬核排查案例 2

圖2

再去Envoy Pod的抓包文件(圖3)看看是怎麼回事,奇怪的是我們過濾出來的TCP流只有一個Reset報文,這個Reset報文源IP來自於Pilot Service 的ClusterIP, 可以認為是來自Pilot Pod。Envoy Pod並沒有發出任何Reset的報文。那麼問題來了,Pilot 收到的這些Reset報文都是誰發出來的呢?從源地址看的確是來自於Envoy Pod的IP,但是Envoy上我們過濾出來的TCP流也的確沒有這些Reset報文

eBay流量管理之Kubernetes網絡硬核排查案例 3

圖3

憑藉多年防火牆開發經驗,我立馬想到防火牆經常乾的事情不就是給Client/Server 兩端同時發TCP Reset,讓Client和Server都以為是對方發出來的嘛?難道是Pilot和Envoy中間的什麼組件構造了TCP Reset 發給了Pilot和Envoy?於是和同事討論了這個假設存在的可能,如果真是這樣,那麼就得知道Pilot和Envoy中間到底經歷了哪些設備或者組件。

從軟件角度看,中間也就是經過了Kube-proxy,然後我們Tess的底層網絡實現OVN。但是OVN作為底層實現,不太可能涉及到傳輸層的東西,OVN去主動發報文的可能性應該沒有。後來和OVN大牛周涵確認,OVN的確不會幹這種事情,所以只能懷疑是Kube-proxy。

Kube-proxy是通過Iptables或者IPVS來實現,我們Tess環境裡目前用的是IPVS。理論上說,Kernel裡Connection Track 模塊是基於4層網絡來實現,如果出現異常是有可能主動發Reset到Client和Server。於是去IPVS官網查詢是否有相關機制去主動向兩端發Reset,結果並沒有找到相關證據。

在Google尋找相關問題的時候,發現有人遇到過IPVS TCP Connection Reset的問題,他們遇到的問題是TCP Connection在長時間Idle以後被Reset, 這是因為IPVS裡設置的Connection Idle超時時間比應用設置的超時時間短,當這個發生在應用是長連接的協議的時候,IPVS Idle 超時時間默認是900秒,如果該連接空閒時間超過900秒,那麼IPVS就會刪除該連接,如果Client/Server再發TCP報文,IPVS就會回應Reset。

這個和我們的情況有相似性,因為Pilot和Envoy之間是gRPC協議,gRPC是長連接協議。但是從抓包和實際情況看,我們的環境並沒有出現連接空閒這麼長時間的情況,於是這個可能性也被否定了。

哪裡來的Reset?這個問題始終找不到答案,難道是我們漏掉了抓包?突然想起最初我們分析的抓包文件是過濾了TCP流之後的(即限定了五元組)。為了確保不漏掉任何報文,我們重新去看了原始的抓包文件,在Envoy的原始抓包文件裡意外發現了另外一條TCP流 (見下圖):

eBay流量管理之Kubernetes網絡硬核排查案例 4

圖4

這條TCP流應該出現在Pilot上,Envoy是只知道Polit Service的IP地址,而不知道Pilot Pod的地址,那怎麼會在Envoy上抓到Pilot Pod的IP的報文呢?從這個抓包文件上看,之前找不到源頭的TCP Reset全部都在這裡了。之前的一番困惑,迎刃而解。

Pilot收到的和發出的Reset 都在這裡,但並不是出現在本應該出現的10.148.74.37:48640 192.168.107.214:15010 這條TCP 流上。於是大膽猜測:從Pilot發給Envoy的報文沒有經過Kube-proxy的SNAT直接到了Envoy上面。 Envoy收到源地址是自己不知道的TCP報文,於是就發Reset, 同時這個Reset報文的五元組和Pilot上五元組是一致的,而且Kubernetes裡Pod之間又是可達的,那麼Pilot Pod就會收到這個它認為合法的Reset報文,然後把自己的TCP連接刪除,於是我們在Pilot Pod的Log裡看到了Connection Reset!之後因為網絡環境的問題,還有一些重傳的報文在Pilot Pod 刪除了連接之後再收到,所以Pilot也會發Reset。這樣就解釋了抓包文件裡所有的Reset報文的產生原因了。描述起來可能比較麻煩,看下圖會比較直觀:

eBay流量管理之Kubernetes網絡硬核排查案例 5

圖5

“兇手”算是找到了,可是“動機”呢?接下來的問題就是Envoy為什麼會收到沒有經過Kube-proxy SNAT的報文?從Pilot發給Envoy的報文,Kube-proxy(IPVS) 肯定會收到,除非就是IPVS沒有處理這樣的報文直接交還給協議棧,然後送回給了Envoy Pod。帶著這樣的猜想,繼續Google相關內容,終於找到Linux Kernel裡有一個選項nf_conntrack_tcp_be_liberal,從官方網站上看到這個選項的說明:

nf_conntrack_tcp_be_liberal - BOOLEAN
   0 - disabled (default)
   not 0 - enabled

   Be conservative in what you do, be liberal in what you accept from others.
   If it's non-zero, we mark only out of window RST segments as INVALID.

就是說如果Kernel在做Connection Track的時候發現報文的序列號在TCP的窗口之外, 那麼就認為這個報文是不合法的,不在Connection Track要處理的範圍,但是並不會丟棄這樣的報文,所以還是會交給協議棧繼續處理。

有了這樣的說明,我們重新仔細去審查所有的抓包文件,發現那個觸發RST的包之前所有的TCP通信都已經完成,沒有任何未被確認的數據,但異常包出現了,它的序列號比上一個包小了5,794,996,也就是說它是最近收到的5M字節之前的數據,遠遠超出了當時的接收窗口。

四、內核選項的介紹

下面我們來看看這個內核選項到底乾了啥事情,當然是直接上內核代碼了。

找到nf_conntrack_proto_tcp.c 文件,查看函數:

static bool tcp_in_window()
{
     
      …
        if (before(seq, sender->td_maxend + 1) &&
       in_recv_win &&
       before(sack, receiver->td_end + 1) &&
       after(sack, receiver->td_end - MAXACKWINDOW(sender) - 1))#检查收到的包的序列号是否在TCP接收窗口之内
        {
      …
        }
        else {#这里就是不正常情况下,报文不在窗口里的处理,如果没有enable nf_conntrack_tcp_be_liberal, 那么res为false, 反之为true
         res = false; 
             if (sender->flags & IP_CT_TCP_FLAG_BE_LIBERAL ||
                 tn->tcp_be_liberal)
                 res = true;
             if (!res) {
              ...
     }
         }
         return res;
        
        
     }
}
接着看tcp_in_window返回值的处理:
static int tcp_packet() {
if (!tcp_in_window(ct, &ct->proto.tcp, dir, index,
           skb, dataoff, th)) {
       spin_unlock_bh(&ct->lock);
       return -NF_ACCEPT;
   }
}
#define NF_DROP 0
#define NF_ACCEPT 1

如果報文不在窗口裡,tcp_packet() 會返回 -NF_ACCEPT, 也就是-1。事實上返回0的時候會丟棄這個報文,這個時候也不會有未經NAT的報文到達Envoy Pod,我們的問題也不會出現;如果返回1,報文會正常命中NAT規則,Envoy Pod也不會發Reset。現在返回了-1,報文沒有被丟棄,但同時也沒有去查詢NAT規則,最終導致了我們遇到的問題。

五、解決方案

從前面的分析,我們當然可以打開nf_conntrack_tcp_be_liberal,但為什麼報文會出現在窗口之外仍然沒有明確的答案。考慮到Tess環境下出現問題的集群CPU分配比較滿,同時從抓包來看亂序也比較嚴重,我們懷疑這和Hyperviser對網絡報文的處理能力有比較大的關係,Hyperviser導致某些報文處理不及時,被延遲發送到了Kube-proxy,這個時候重傳的報文已經處理過了,這些延遲到達的報文就成了窗口之外的報文。後來負責網絡硬件性能的同事打開了RPS (Receive Packet Steering),使性能得到提升,之前遇到的問題就沒有再次發生。

因為更改內核參數還是具有一定未知風險,目前問題在性能提升的情況下沒有再次出現,所以我們暫時沒有去更改內核參數的計劃。

六、總結

對於這次出現的問題的整個追踪過程比較曲折,從這個過程中我們也學習到了不少知識,累積了一些經驗:

  1. 抓包盡可能的完整,在按照正常邏輯分析問題過濾的同時,也要關注非正常情況下的特殊報文,因為問題的出現​​往往就是非正常邏輯的報文引起的。
  2. 內核網絡處理部分有很多細節的選項,在遇到百思不得其解的網絡問題時,我們可以從這些選項上入手分析問題。
  3. 性能問題也會帶來一些意想不到的功能問題。在我們系統設計開發的前期需要盡可能地關注和測試性能問題。
  4. Ipvs本身是有連接空閒超時時間的,在沒有TCP Keepalive的情況下,中間組件也是有可能刪除連接導致Client/Server 被Reset。雖然我們目前遇到的不是這種情況,但也是以後值得關注的問題。

作者介紹

張博,eBay 軟件工程師,目前致力於eBay網絡流量管理以及eBay Kubernetes平台網絡相關組件開發維護。

本文轉載自公眾號eBay技術薈(ID:eBayTechRecruiting)。

原文鏈接

https://mp.weixin.qq.com/s/phcaowQWFQf9dzFCqxSCJA