內容簡介
本文是大廠著名大神Dog250在調試一些網(wǎng)絡問題時候的實戰(zhàn),希望讀者通過閱讀本文,領悟大神們是如何“不擇手段,利用手頭一切的便利,最快的速度精準打擊問題要害”,從而實現(xiàn)快速調試和解決問題的。
我們在工作中總是遇到一些需要快速解決的棘手問題,解決這類問題往往有一套可供遵循的常規(guī)思路,但是實際做起來往往非常耗時且依賴外部環(huán)境,更加棘手的是,為了按部就班地完成工作,你需要學習很多很多前置知識,比方說相關工具的使用。
我傾向于用最少的工作量來完成POC。
不會用crash/ebpf就不能debug內核了嗎?不懂編程就不能優(yōu)化系統(tǒng)了嗎?并不是。
讓我來展示一下縣城擺攤修傘的二胡師傅和瑞士宮廷制表匠的區(qū)別吧。
本文我會舉三個實際的例子。用的都是low到爆的過時玩意兒。
### 示例1:排查TCP連接僵死
netstat顯示一條TCP連接的Send-Q堆積了很多數(shù)據(jù),對端相應的Recv-Q卻是0,tcpdump顯示該連接持續(xù)無任何交互。
此時應該怎么辦?
經(jīng)過ss -it確認tcp_info信息,結論是該連接的RWND/CWND,RTT,RTO,MSS等數(shù)據(jù)均正常,網(wǎng)卡也無相關錯誤統(tǒng)計,但在事實上它就是僵住了,這是一個異?,F(xiàn)象,既然Send-Q中有數(shù)據(jù),它是無論如何都要 ***嘗試*** 發(fā)送出去的。
幾乎可以肯定,原因無外乎兩點:
- 應用程序進入系統(tǒng)調用時lock住了socket并且阻塞了。
- 內核存在問題。
如何來確認?大多數(shù)人的思路傾向于使用crash工具去分析內核數(shù)據(jù)結構,但是這是一個龐大的靜態(tài)分析工程。
我傾向于開著飛機修引擎,我不擅長分析死因,但我擅長做復蘇。我的方法是嘗試給該TCP做復蘇手術。
TCP的發(fā)送一直是靠ACK時鐘驅動的,事實上直到BBR開啟的基于pacing的新TCP時代,也依然沒有放棄ACK時鐘,雖然ACK在原教旨意義上不再需要,但BBR依然使用它來計算pacing rate,假如沒有ACK到來了,那么pacing rate便會逐漸跌到0,TCP也就僵住了。
***因此,TCP的復蘇手術,主要是構造一個ACK去擊打它!***
如果你對TCP足夠了解,那么你一定會大贊我的做法。
在TCP連接顯示Send-Q堆積的數(shù)據(jù)發(fā)送端構造這個ACK,需要從本機的網(wǎng)卡注入,為了避開路由子系統(tǒng)的Martian報文校驗,需要另起一個net namespace來做這事。
接下來我們來構造這個ACK:
為了最快速定位問題,我往往不會遵守什么編碼規(guī)范,所以我會寫死地址和端口,哪怕需要改的時候再編輯一下代碼。
然后我們來注入:
注意,代碼中的seq,ack字段我們并不知道,如何將這個ACK來精確注入這個僵死的TCP連接?
精確注入需要兩步,用一個bpftrace腳本配合上述python代碼獲取TCP的snd_una,rcv_nxt等字段:
注意,我hook的是tcp_rcv_established,當我實施第一步注入的時候,沒有進入這個trace,那幾乎可以肯定是應用程序lock住了該連接,進而將該ACK排入了backlog以延后處理,這種情況就需要應用程序開發(fā)人員來接鍋了。
如果順利進入了該trace,那么我們便獲取了TCP連接的info信息,接下來我們可以用打印出來的snd_una,rcv_nxt信息來填充python代碼中的seq和ack了:
ACK構造配合bpftrace腳本,如此便可以一路跟蹤到數(shù)據(jù)的發(fā)送邏輯,進而定位發(fā)送僵死的原因。核心的思路我已經(jīng)給出了,本文不是case by case分析,也就沒有繼續(xù)的必要了。
順便說一句,我不喜歡使用bpftrace,太麻煩且限制太多,還是systemtap順手,特別是-g選項。bpftrace無需編譯執(zhí)行快并非不可或缺的優(yōu)點,大家都用bpftrace更多是因為它新潮。
### 示例2:實現(xiàn)tun網(wǎng)卡的readv
最近我雖然將golang實現(xiàn)的tun UDP隧道的總吞吐逼近了物理網(wǎng)卡極限25Gbps,但是對于單流吞吐而言,卻一直無法突破2~3Gbps,因此我想看看瓶頸到底在哪。
事實上,允許IP分片的情況下,我把tun的MTU設置成8000,單流吞吐可達8Gbps。然而在長傳有丟包的線路,IP分片(分片丟失會造成TCP時鐘卡頓)可能會使TCP的性能劣化,打亂BBR所依賴的pacing rate保真。
之前測量的結果,直連環(huán)境,通過tun UDP隧道的ping時延是物理網(wǎng)卡ping時延的10倍起步,那么tun和UDP socket處理的系統(tǒng)消耗大概要損耗10倍起步的吞吐,25Gbps下降到2~3Gbps是合理的。
因此我需要減少tun的read/write開銷。
批量讀寫是一個合理的思路,比方說io_uring,readv/writev等??墒莟un并不支持這些,怎么辦?
io_uring直接拋棄,太復雜了。
如果要實現(xiàn)一個完備的讀寫數(shù)據(jù)包的readv/writev,我需要在內核和用戶態(tài)均實現(xiàn)數(shù)據(jù)包邊界的拆包組包問題,我不得不處理各種協(xié)議,以在一塊整個的內存中獲取數(shù)據(jù)包的長度并把它切下來,我不得不時刻當心內存的邊界,把不連續(xù)的內存想辦法組合成一個看上去連續(xù)的內存,以便后面的加密解密goroutine可以處理它們。
這看上去很復雜,需要對整個程序進行修改(當然了,這對于標準程序員根本不是事,但對于我,這很要命),至少也要花費一整天的時間,可作為業(yè)余的事情,每天回家都很晚了,我哪有時間折騰這些。
下面是我一個小時完成事情全部的做法。我改變了readv的語義:
- 每一個iovec僅存放一個skb的數(shù)據(jù),下一個skb放在下一個iovec。
- 返回copy成功的skb的數(shù)量,而不是copy數(shù)據(jù)的總字節(jié)數(shù)。
下面是我對tun_do_read的改造:
就這么幾行代碼。是不是很簡單。
下面是對應的golang代碼:
下面是golang中的Readv:
...
### 示例3:實現(xiàn)松散TCP語義
來,最后一個例子,我簡單說。
我想為直播業(yè)務提供一個松散TCP傳輸協(xié)議,如何?
什么是松散TCP?很容易理解:
- 網(wǎng)絡狀態(tài)很好或者輕微丟包時,執(zhí)行完備TCP邏輯。
- 嚴重擁塞時不再重傳,直接發(fā)后面的數(shù)據(jù),能不能到達,聽天由命。
- 接收端可以發(fā)送NAK指示發(fā)送端是否重傳。
- ...
這對于直播是有意義的,體現(xiàn)在三個方面:
- 直播防卡頓體驗要比清晰度體驗更核心,嚴重擁塞時用戶可以接受模糊但不能接受卡頓,因此可以丟幀,但不能卡住。
- 直播流量在嚴重擁塞時的松散非重傳處理可以降低帶寬成本。
- 大家都不拼命重傳了,或許網(wǎng)絡擁塞就過去了,可期待一種良性全局同步。
既能優(yōu)化體驗,又能降低成本,何樂而不為?那么怎么落地呢?
開會立項,確定deadline,然后大改TCP協(xié)議的實現(xiàn)代碼嗎?Linux內核中TCP的那一大脬代碼能把人看瘋。誰人改得動?然后可以期許的就是開會,延期,加班,哪來的快樂?
因此我用Netfilter:
- 發(fā)送端在IP層用Netfilter截獲出方向的TCP段,在嚴重擁塞時偽造ACK回復。
- 接收端在IP層用Netfilter截獲入方向的TCP段,在嚴重擁塞時用0填充丟包亂序造成的sequence空洞。
是不是不依賴TCP本身的實現(xiàn)了呢?而且實現(xiàn)起來很快,可以唱著歌寫。先把0.1版本推上去了,業(yè)務點了贊,然后慢慢再改那脬TCP代碼。
## 最后再寫點兒
其實還有很多類似的例子,但是時間有限,所以只能先寫幾個。將上面例子抽象一下,聊點形而上的。
不管做什么事情,什么最重要?是過程?是結果?還是別的什么?
過程和結果是經(jīng)理和用戶最關注的,工人則需要先把活干好。
工人在沒有一個理性,可行且快速,簡單的方案之前,過程和結果都是奢談。
很少有工人能給自己一個明確的定位,關注手頭最要緊的事情。所以很多工人在解決問題的時候顯得吃力,效率低。工人的信條就是, ***不擇手段,利用手頭一切的便利,最快的速度精準打擊問題要害。*** 這至少是我的信條。
我擅長用火柴修雨傘,用筆帽修自行車鏈子,手工纏耳機實現(xiàn)重低音,用釘子和銅線做電容,自己纏電機,升壓器,自制穩(wěn)壓器,用牛仔褲做提包,用椅子做桌子,自制分頻器,... 所有這些玩意兒,都不需要去店里采購材料,完全利用家里廢舊物什完成。
所有這些事,我關注的兩點核心是:
- ***快速-快速成功要么就失敗***
- ***簡單-不依賴其它工具***
這思路從小時候一直持續(xù)到后來做了工人,樸素,但管用啊...
***
浙江溫州皮鞋濕,下雨進水不會胖。