首頁 > 軟體

從Linux 2.6.8核心的一個TSO/NAT bug引出的網路問題排查觀點(附一個skb的優化點)

2020-06-16 17:57:52

四年多前的一個往事

大約在2010年的時候,我排查了一個問題。問題描述如下:

伺服器端:Linux Kernel 2.6.8/192.168.188.100

用戶端:Windows XP/192.168.40.34

業務流程(簡化版):

1.用戶端向伺服器端發起SSL連線

2.傳輸資料

現象:SSL握手的時候,伺服器端傳送Certificate特別慢。

分析:

具體思路,也就是當時怎麼想到的,我已經忘了,但是記住一個結論,那就是糾出了Linux 2.6.8的NAT模組的一個bug。

在抓取了好多封包後,我發現本機總是發給自己一個ICMP need frag的報錯資訊,發現伺服器端的Certificate太大,超過了本機出網絡卡的MTU,以下的一步步的思路,最終糾出了bug:

1.證實伺服器端程式設定了DF標誌。這是顯然的,因為只有DF標誌的封包才會觸發ICMP need frag資訊。

2.疑問:在TCP往IP傳送資料的時候,會檢測MTU,進而確定MSS,明知道MSS的值,怎麼還會傳送超限的包呢?計算錯誤可能性不大,畢竟Linux也是準工業級的了。

3.疑問解答:幸虧我當時還真知道一些名詞,於是想到了TCP Segment Offload這個技術。

TCP Segment Offload簡稱TSO,它是針對TCP的硬體分段技術,並不是針對IP分片的,這二者區別應該明白,所以這與IP頭的DF標誌無關。對於IP分片,只有第一個分片才會有完整的高層資訊(如  果頭長可以包括在一個IP分片中的話),而對於TSO導致的IP封包,每一個IP封包都會有標準的TCP頭,網絡卡硬體自行計算每一個分段頭部的校驗值,序列號等頭部欄位且自動封裝IP頭。它旨在提高TCP的效能。

4.印證:果然伺服器啟用了TSO

5.疑問:一個大於MTU的IP報文傳送到了IP層,且它是的資料一個TCP段,這說明TCP已經知道自己所在的機器有TSO的功能,否則對於本機始發的封包,TCP會嚴格按照MSS封裝,它不會封裝一個大包,然後讓IP去分片的,這是由於對於本機始發而言,TCP MSS對MTU是可以感知到的。對於轉發而言,就不是這樣了,然而,對於這裡的情況,明顯是本機始發,TCP是知道TSO的存在的。

6.猜測:既然TCP擁有對TSO的存在感知,然而在IP傳送的時候,卻又丟失了這種記憶,從TCP發往IP的入口,到IP分片決定的終點,中間一定發生了什麼嚴重的事,迫使TCP丟失了TSO的記憶。

7.質疑:這種故障情況是我在公司模擬的,通過報告人員的資訊,我了解到並不是所有的情況都會這樣。事實上,我一直不太承認是Linux協定棧本身的問題,不然早就被Fix了,我一直懷疑是外部模組或者一些外部行為比如抓包導致的。

8.可用的資訊:到此為止,我還有一個資訊,那就是只要載入NAT模組(事實上這是分析出來的,報告人員是不知道所謂的NAT模組的,只知道NAT規則)就會有這個現象,於是目標很明確,死盯NAT模組。

9.開始debug:由於Linux Netfilter NAT模組比較簡單,根本不需要高階的可以touch到記憶體級的工具,只需要printk即可,但是在哪裡print是個問題。

10.出錯點:在呼叫ip_fragment(就是該函數裡面傳送了ICMP need frag)之前,有一個判斷(省略了不相關的):

if (skb->len > dst_pmtu(skb->dst) && !skb_shinfo(skb)->tso_size) {

return ip_fragment(skb, ip_finish_output);

}

前一個判斷顯然為真,如果要想呼叫ip_fragment的話,後一個判斷一定要是假,實際上,如果開啟了TSO,就不該呼叫ip_fragment的。

11.查詢tso_size欄位:事情很明顯了,一定是哪個地方將tso_size設定成了0!而且一定在NAT模組中(98%以上的可能性吧...),於是在NAT模組中查詢設定tso_size的地方。

12.跟蹤ip_nat_fn:這是NAT的入口,進入這個入口的時候,tso_size不是0,可是呼叫了skb_checksum_help之後tso_size就是0了,問題一定在這個函數中,注意,呼叫這個help有一個前提,那就是硬體已經計算了校驗和。在這個help函數中,有一個skb_copy的操作,正是在這個copy之後,tso_size變成了0,於是進一步看skb_copy,最終定位到,copy_skb_header的最後,並沒有將原始skb的tso_size複製到新的skb中,這就是問題所在!

13.觸發條件:什麼時候會呼叫skb_copy呢?很簡單,如果skb不完全屬於當前的執行流的情況下,按照寫時拷貝的原則,需要複製一份。故障現象就是慢,而資料為本機始發,且為TCP。我們知道,TCP在沒有ACK之前,skb是不能被刪除的,因此當前的skb肯定只是一個副本,因此就需要拷貝一份了。

14.影響:如此底層的一個函數。搜尋程式碼,影響巨大,各種慢!對於那次的慢,其慢的流程為:socket傳送DF資料--感知TSO--丟失TSO--ICMP need frag--TCP裁成小段繼續傳送...如果禁止了lo的ICMP,那麼更慢,因為TCP會觸發超時重傳,而不是ICMP的建議裁減,並且重傳是不會成功的,直到使用者程式感知,自行減小傳送長度。

為什麼舊事重提

提起那件事有兩個原因,其一是當時沒有記錄下來整個過程,可是後續的patch卻一直在用,最終我自己都快不知其所以然了,其二,是通過那次的分析,按照現在的理解,就可以發現Linux協定棧的一個優化點,即TCP情況下,由於保留了資料skb佇列直到ack,那麼後續向下的所有skb處理流程都至少要經過一次skb_copy,這種複製操作難道就不能避開嗎?如果載入了某些Netfilter勾點,需要對skb進行寫操作,這種序列化行為會嚴重影響Linux網路協定棧的處理效率,這是Netfilter的通病之一。

附:skb操作的優化點

1.如果把資料和後設資料徹底分開是不是更好呢?

2.進一步將寫操作的粒度細分

有些寫操作是針對每一個封包的,這些不得不複製,但是能否區域性複製,然後採取分散聚集IO進行拼接呢?盡量採用指標操作而不是複製資料本身,這正是借鑒了UNIX fork模型以及虛擬地址空間的COW。如果把skb的空間進行細粒度劃分,那麼就可以做到,需要COW哪部分就只有那部分,不會導致全域性複製。

前幾天的一個TCP問題排查過程

現象與過程

早就習慣了那種驚心動魄的三規制度(規定的時間,規定的地點,和規定的人一起解決問題),反而不習慣了按部就班了。事情是這樣的。

週末的時候,中午,正在跟朋友一起聊天吃飯,收到了公司的簡訊,說是有一個可能與TCP/IP有關的故障,需要定位,我沒有隨即回復,因為這種事情往往需要大量的資訊,而這些資訊一般簡訊傳來的時候早就經過了N手,所以為了不做無用功,等有關人員打電話給我再說吧。

...

(以下描述有所簡化)

我方伺服器端:Linux/IP不確定(處在內網,不知道NAT策略以及是否有代理以及其它七層處理情況)

測試用戶端:Windows/192.168.2.100/GW 192.168.2.1

中間鏈路:公共Internet

可用接入方式:3G/有線撥號

伺服器端裝置:第三方負載均衡裝置。防火器等

業務流程:用戶端與伺服器端建立SSL連線

故障:

用戶端連線3G網絡卡使用無線鏈路,業務正常;用戶端使用有線鏈路,SSL握手不成功,SSL握手過程的Client Certificate傳輸失敗。

分析:

1.通過抓包分析,在有線鏈路上,傳送用戶端證書(長度超過1500)後,會收到一條ICMP need frag訊息,說是長度超限,鏈路MTU為1480,而實際傳送的是1500。通過無線鏈路,同樣收到了這個ICMP need frag,只是報告的MTU不同,無線鏈路對應的是1400。

2.有線鏈路,用戶端接受ICMP need frag,重新傳送,只是截掉了20位元組的長度,然而抓包發現用戶端會不斷重傳這個包,始終收不到伺服器端的ACK,其間,由於用戶端久久不能傳送成功資料到伺服器端,伺服器端會回復Dup ACK,以示催促。

3.猜想:起初,我以為是時間戳的原因,由於兩端沒有開啟TCP時間戳,所以在RTT以及重傳間隔估算方面會有誤差,但是這不能解釋100%失敗的情形,如果是由於時間戳計算的原因,那不會100%失敗,因為計算結果受波動權值影響會比較大。

4.對比無線鏈路,和有線鏈路的唯一區別就是ICMP報告的MTU不同。

5.中途總結:

5.1.此時,我並沒有把思路往運營商鏈路上引導,因為我始終認為那不會有問題,同樣,我也不認為是SSL的問題,因為錯誤總是在傳送大包後呈現,事實上,接受了ICMP need frag後,之前發的那個超限包已經被丟棄,重新傳送的是一個小一點的包,對於TCP另一端來講,這是完全正常的。

5.2.根本無需檢視服務紀錄檔,因為還沒有到達那個層次。抓包結果很明確,就是大包傳不過去,其實已經按照MTU發現的值傳輸了,還是過不去,而無線鏈路能過去。因此應該不是MTU的問題。

5.3.除了運營商鏈路,MTU,伺服器端處理之外,還會是哪的問題呢?事實上,程式的bug也不是不可能的,或者說是一些不為人知的動作,不管怎樣,需要隔離問題。

6.猜測是中間某台裝置沒法處理大包,這個和MTU沒有關係,可能就是它處理不了或者根本上不想處理大包,多大呢?反正1480的包處理不了,減去IP頭,TCP頭,剩餘的是1440的純資料。於是寫一個簡單的TCP client程式,在TCP握手完成後馬上傳送(為了防止由於不是Client Hello而主動斷開,因此必須馬上發,只是為了觀察針對大包的TCP ACK情況,此時與服務無關)長度1440的資料,驗證!

7.果然沒有ACK迅速返回,用戶端不斷重試傳送1440的包(之後10秒到20秒,會有ACK到來,但不是每次都會到來,這明顯是不正常的)。為了證明這種方式的合理性,傳送無線鏈路上MTU限制的資料大小,即1400-20-20=1360的資料,ACK秒回。因此猜測中間裝置的封包處理的長度臨界點在1360和1440之間。

8.經過不斷的測試,二分法查詢臨界點,找到了1380是可處理長度臨界點。傳送1380的純資料是正常的,傳送1381的純資料就不正常了。抓包的目標地址是12.23.45.67,簡稱MA,現在不確定的是MA是什麼,是我方的裝置,還是它方的裝置,如果是我方的裝置,排錯繼續,如果不是,排錯終止。總之,1380這個臨界點是一個疑點,常規來講是不正常的,但也不能排除有這麼限制的正常理由。無線鏈路沒有問題是因為無線鏈路的MTU比較小,最大純資料長度1360小與臨界值1380。

9.補充測試,模擬問題機器,將其本機的MTU改為1380+20+20=1420,傳輸也是正常的,然而改為1421,就不行了。(注意,只有本機的MTU修改才有效,因為只有TCP資料始發裝置,MSS才與MTU關聯)

.....

1x.第9步後面的排查我沒有參與,但是最終,我方裝置確實沒有收到用戶端SSL握手過程傳出的證書,說明確實是中間裝置阻止了這個”大包“的傳輸,至於它到底是誰,到底怎麼回事,與我們無關了,但對於我個人而言,對其還是比較感興趣的。

對於該次排錯的總結

這是一個典型的網路問題,涉及到IP和TCP,細節不多,但足夠典型。其實這個問題與最終的業務邏輯沒有關係,但是事實往往是,只有在業務邏輯無法正常時,這類底層的問題才會暴露,這是TCP/IP協定棧的性質所致。此類問題的排查要點在於,你要用最快的速度把它與高層協定隔離開來,並且不能陷入任何細節。

TCP細節:為何不必考慮TCP細節?這類場景既不特殊,又不複雜,如果陷入TCP細節的話,會掩蓋或者忽略大量橫向的問題,比如你會死盯著TCP的重傳機制做細緻研究,或者細緻地研究RTT計算方法,最終也不一定能得到什麼結論。換句話說,你一定要相信TCP是正常的。

服務程式細節:這個也是要隔離的。因為伺服器並沒有真的開始服務,且故障是100%重現的,因此可以確定這不是什麼複雜的問題所導致,真正複雜的問題往往不是100%重現,即便是你挖掘出其重現規律,也夠你喝一壺的。

TCP問題和IP問題的相異:它們雖然都是網路協定棧的一員,但是使用方式卻大不相同。實際上TCP提高了使用者的門檻,一般而言,TCP是讓程式去使用的,因此你要想TCP跑起來,起碼要理解其大致原理,或者說懂socket機制,如果你上網瀏覽網頁,雖然也是用的TCP,它確實跑起來了,但是使用者不是你,而是你的瀏覽器。IP就不同,IP的設定者可以是小白,並且隨意設定都不會報錯。再往下,佈線問題,拓撲問題,幾乎沒有什麼門檻,但是卻更加容易出錯。因此首先要排除的就是這類問題。

防火牆策略或者程式BUG:實際上,第一步就需要詢問管理員,是不是防火牆上特殊的策略所致,然而對於無法得到這個訊息的時候,你就不能從這兒開始了。接下來,與之平等的是懷疑程式的處理BUG,此時,隔離出原有的業務邏輯細節是重要的,現象是大包無法收到ACK,此時就要忽略掉這個大包的內容以及其上下文,直接傳送一個任意大包進行測試。

因此,這類問題的排查是一個逐步隔離的過程,相對四年前的那次NAT bug的排查,這個故障在技術上要更容易些,所有的複雜性和時間的耽擱全部在人員協調交流上,人員之間資訊的誤傳或者漏傳也是一個難點,四年前的那個NAT bug,是一個技術上更加深入的問題,涉及到了核心協定棧程式碼級別,同時在此之前,我還要找到這個點,然而它的容易點在於,這個問題只涉及到我一個人,而且也是100%重現。

本文永久更新連結地址http://www.linuxidc.com/Linux/2015-07/119593.htm


IT145.com E-mail:sddin#qq.com