一個歷時三年的核心 Bug 引發大量的容器系統出現網路故障

dockone發表於2016-02-22

最近發現的一個 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 很相似。有趣!

一個歷時三年的核心 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,我們設計了最簡單的測試流程:

  1. 單客戶端開啟一個 socket,每秒傳送一個簡單且長的報文
  2. 單服務端(使用處於偵聽模式的 nc)在 socket 端偵聽,列印輸出收到的訊息。
  3. 網路工具,tc,可以用於傳送前,任意修改包內容。
  4. 一旦客戶端和服務端對接上,用網路工具失效所有發出包,傳送10秒鐘。

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

一個歷時三年的核心 bug 引發大量的容器系統出現網路故障
這樣是工作的:錯誤的資料不會接收,TCP轉發資料

Linux 和容器

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

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

為了構建一個擁有虛擬以太裝置的容器,必須:

  1. 建立一個虛機,
  2. 建立一個 veth,
  3. 將 veth 繫結與容器端,
  4. 給 veth 指定一個 IP 地址,
  5. 設定路由,用於 Linux 流量控制,這樣包就可以進出容器了。

為什麼是虛擬造成了問題

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

一個歷時三年的核心 bug 引發大量的容器系統出現網路故障
失效資料被轉遞給應用,參見左側視窗。

我們可以在雲平臺重建此測試環境。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;
}
- /* don’t change ip_summed == CHECKSUM_PARTIAL, as that
- * will cause bad checksum on forwarded packets
- */
- 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 引發多少應用崩潰和不可知的行為!如果確信由此引發問題請及時跟我聯絡。

相關文章