歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux基礎 >> Linux教程 >> 從Linux 2.6.8內核的一個TSO/NAT bug引出的網絡問題排查觀點(附一個skb的優化點)

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

日期:2017/2/28 14:00:28   编辑:Linux教程

四年多前的一個往事

大約在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%重現。

Copyright © Linux教程網 All Rights Reserved