1. 程式人生 > >Linux核心bug引起Mesos、Kubernetes、Docker的TCP/IP資料包失效

Linux核心bug引起Mesos、Kubernetes、Docker的TCP/IP資料包失效

最近發現Linux核心bug,會造成使用veth裝置進行路由的容器(例如Docker on IPv6、Kubernetes、Google Container Engine和Mesos)不檢查TCP校驗碼(checksum),這會造成應用在某些場合下,例如壞的網路裝置,接收錯誤資料。這個bug可以在三年前任何一個測試過的核心版本中發現。補丁已經被整合進核心程式碼,正在回遷入3.14之前的多個發行版中(例如SuSE,Canonical)。如果在自己環境中使用容器,強烈建議打上此補丁,或者等釋出後,部署已經打上補丁的核心版本。

注:Docker預設NAT網路並不受影響,而實際上,Google Container Engine也通過自己的虛擬網路防止硬體錯誤。

編者:Jake Bower指出這個bug跟一段時間前發現的另外一個bug很相似。有趣!

起因
十一月的某個週末,一組Twitter負責服務的工程師收到值班通知,每個受影響的應用都報出“impossible”錯誤,看起來像奇怪的字元出現在字串中,或者丟失了必須的欄位。這些錯誤之間的聯絡並不很明確指向Twitter分散式架構。問題加劇表現為:任何分散式系統,資料,一旦出問題,將會引起更長的欄位出問題(他們都存在快取中,寫入日誌磁碟中,等等…)。經過一整天應用層的排錯,團隊可以將問題定位到某幾個機櫃內的裝置。團隊繼續深入調查,發現在第一次影響開始前,扇入的TCP糾錯碼錯誤大幅度升高;這個調查結果將應用從問題中摘了出來,因為應用只可能引起網路擁塞而不會引起底層包問題。

編者:用“團隊”這個詞可能費解,是不是很多人來解決這個問題。公司內部很多工程師參與排錯,很難列出每個人名字,但是主要貢獻者包括:Brian Martin、David Robinson、Ken Kawamoto、Mahak Patidar、Manuel Cabalquinto、Sandy Strong、Zach Kiehl、Will Campbell、Ramin Khatibi、Yao Yue、Berk Demir、David Barr、Gopal Rajpurohit、Joseph Smith、Rohith Menon、Alex Lambert and Ian Downes、Cong Wang。

一旦機櫃被移走,應用失效問題就解決了。當然,很多因素可以造成網路層失效,例如各種奇怪的硬體故障,線纜問題,電源問題,等….;TCP/IP糾錯碼就是為保護這些錯誤而設計的,而且實際上,從這些裝置的統計證據表明錯誤都可以檢測到—那麼為什麼這些應用也開始失效呢?

隔離特定交換機後,嘗試減少這些錯誤(大部分複雜的工作是由SRE Brain Martin完成的)。通過傳送大量資料到這些機櫃可以很容易復現失效資料被接收。在某些交換機,大約~10%的包失效。然而,失效總是由於核心的TCP糾錯碼造成的(通過netstat -a返回的TcpInCsumError引數報告),而並不傳送給應用。(在Linux中,IPv4 UDP包可以通過設定隱含引數SO_NO_CHECK,以禁止糾錯碼方式傳送;用這種方式,我們可以發現數據失效)。

Evan Jones(@epcjones)有一個理論,說的是假如兩個bit剛好各自翻轉(例如0->1和1->0)失效資料有可能有有效的糾錯碼,對於16位序位元組,TCP糾錯碼會剛好抵消各自的錯誤(TCP糾錯碼是逐位求和)。當失效資料一直在訊息體固定的位置(對32位求模),事實是附著碼(0->1)消除了這種可能性。因為糾錯碼在儲存之前就無效了,一個也糾錯碼bit翻轉外加一個數據bit翻轉,也會抵消各自的錯誤。然而,我們發現出問題的bit並不在TCP糾錯碼內,因此這種解釋不可能發生。

很快,團隊意識到測試是在正常linux系統上進行的,許多Twitter服務是執行在Mesos上,使用Linux容器隔離不同應用。特別的,Twitter的配置建立了veth(virtual ethernet)裝置,然後將應用的包轉發到裝置中。可以很確定,當把測試應用跑在Mesos容器內後,立即發現不管TCP糾錯碼是否有效(通過TcpInCsumErrors增多來確認),TCP連結都會有很多失效資料。有人建議啟用veth以太裝置上的“checksum offloading” 配置,通過這種方法解決了問題,失效資料被正確的丟棄了。

到這兒,有了一個臨時解決辦法,Twitter Mesos團隊很快就將解決辦法作為fix推給了Mesos專案組,將新配置部署到所有Twiter的生產容器中。

排錯
當Evan和我討論這個bug時,我們決定由於TCP/IP是在OS層出現問題,不可能是Mesos不正確配置造成的,一定應該是核心內部網路棧未被發現bug的問題。

為了繼續查詢bug,我們設計了最簡單的測試流程:

單客戶端開啟一個socket,每秒傳送一個簡單且長的報文。

單服務端(使用處於偵聽模式的nc)在socket端偵聽,列印輸出收到的訊息。

網路工具,tc,可以用於傳送前,任意修改包內容。

一旦客戶端和服務端對接上,用網路工具失效所有發出包,傳送10秒鐘。

可以在一個桌上型電腦上執行客戶端,伺服器在另外一個桌上型電腦上。通過以太交換機連線兩臺裝置,如果不用容器執行,結果和我們預想一致,並沒有失效資料被接收到,也就是10秒內沒有失效包被接收到。客戶單停止修改包後,所有10條報文會立刻發出;這確認Linux端TCP棧,如果沒有容器,工作是正常的,失效包會被丟棄並重新發送直到被正確接收為止。

0 (2).gif
The way it should work: corrupt data are not delivered; TCP retransmits data

Linux和容器
現在讓我們快速回顧一下Linux網路棧如何在容器環境下工作會很有幫助。容器技術使得使用者空間(user-space)應用可以在機器內共存,因此帶來了虛擬環境下的很多益處(減少或者消除應用之間的干擾,允許多應用執行在不同環境,或者不同庫)而不需要虛擬化環境下的消耗。理想地,任何資源之間競爭都應該被隔離,場景包括磁碟請求佇列,快取和網路。

Linux下,veth裝置用於隔離同一裝置中執行的容器。Linux網路棧很複雜,但是一個veth裝置本質上應該是使用者角度看來的一個標準以太裝置。

為了構建一個擁有虛擬以太裝置的容器,必須(1)建立一個虛機,(2)建立一個veth,(3)將veth繫結與容器端,(4)給veth指定一個IP地址,(5)設定路由,用於Linux流量控制,這樣包就可以扇入扇出容器了。

為什麼是虛擬造成了問題
我們重建瞭如上測試場景,除了服務端運行於容器中。然後,當開始執行時,我們發現了很多不同:失效資料並未被丟棄,而是被轉遞給應用!通過一個簡單測試(兩個桌上型電腦,和非常簡單的程式)就重現了錯誤。

0 (3).gif
失效資料被轉遞給應用,參見左側視窗。

我們可以在雲平臺重建此測試環境。k8s的預設配置觸發了此問題(也就是說,跟Google Container Engine中使用的一樣),Docker的預設配置(NAT)是安全的,但是Docker的IPv6配置不是。

修復問題
重新檢查Linux核心網路程式碼,很明顯bug是在veth核心模組中。在核心中,從硬體裝置中接收的包有ip_summed欄位,如果硬體檢測糾錯碼,就會被設定為CHECKSUM_UNNECESSARY,如果包失效或者不能驗證,者會被設定為CHECKSUM_NONE。

veth.c中的程式碼用CHECKSUM_UNNECESSARY代替了CHECKSUM_NONE,這造成了應該由軟體驗證或者拒絕的糾錯碼被預設忽略了。移除此程式碼後,包從一個棧轉發到另外一個(如預期,tcpdump在兩端都顯示無效糾錯碼),然後被正確傳遞(或者丟棄)給應用層。我們不想測試每個不同的網路配置,但是可以嘗試不少通用項,例如橋接容器,在容器和主機之間使用NAT,從硬體裝置到容器見路由。我們在Twitter生產系統中部署了這些配置(通過在每個veth裝置上禁止RX checksum offloading)。

還不確定為什麼程式碼會這樣設計,但是我們相信這是優化設計的一個嘗試。很多時候,veth裝置用於連線統一物理機器中的不同容器。

邏輯上,包在同一物理主機不同容器間傳遞(或者在虛機之間)不需要計算或者驗證糾錯碼:唯一可能失效的是主機的RAM,因為包並未經過線纜傳遞。不幸的是,這項優化並不像預想的那樣工作:本地產生的包,ip_summed欄位會被預設為CHECKSUM_PARTIAL,而不是CHECKSUM_NONE。

這段程式碼可以回溯到驅動程式第一次提交(commit e314dbdc1c0dc6a548ecf [NET]: Virtual ethernet device driver)。 Commit 0b7967503dc97864f283a net/veth: Fix packet checksumming (in December 2010)修復了本地產生,然後發往硬體裝置的包,預設不改變CHECKSUM_PARTIAL的問題。然而,此問題仍然對進入硬體裝置的包生效。

核心修復補丁如下:

diff — git a/drivers/net/veth.c b/drivers/net/veth.c
index 0ef4a5a…ba21d07 100644
— — a/drivers/net/veth.c
+++ b/drivers/net/veth.c
@@ -117,12 +117,6 @@ static netdev_tx_t veth_xmit(struct sk_buff *skb, struct net_device *dev)
kfree_skb(skb);
goto drop;
}

  • if (skb->ip_summed == CHECKSUM_NONE &&
  • rcv->features & NETIF_F_RXCSUM)
  • skb->ip_summed = CHECKSUM_UNNECESSARY;

if (likely(dev_forward_skb(rcv, skb) == NET_RX_SUCCESS)) {
struct pcpu_vstats *stats = this_cpu_ptr(dev->vstats);
結論
我對Linux netdev和核心維護團隊的工作很欽佩;程式碼確認非常迅速,不到幾個星期補丁就被整合,不到一個月就被回溯到以前的(3.14+)穩定發行版本中(Canonical,SuSE)。在容器化環境佔優勢的今天,很驚訝這樣的bug居然存在了很久而沒被發現。很難想象因為這個bug引發多少應用崩潰和不可知的行為!