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