Linux高性能網(wǎng)絡(luò)編程十談 | 性能優(yōu)化(網(wǎng)絡(luò))
上一篇文章講了《性能優(yōu)化(CPU和內(nèi)存)》,這一節(jié)我們主要是聊聊網(wǎng)絡(luò)優(yōu)化。
第一部分:網(wǎng)絡(luò)性能度量
1、設(shè)備度量
設(shè)備主要是指塊設(shè)備,由于我們在開發(fā)過程中,需要磁盤操作,比如寫日志等,所以對(duì)于塊設(shè)備的I/O對(duì)于我們需要度量性能的一個(gè)重要指標(biāo)。
(1)I/O等待
CPU等待I/O操作發(fā)生的時(shí)間,較高和持續(xù)的值很多時(shí)候表明IO有瓶頸,一般通過iostat -x命令查看:
[root@VM-0-11-centos ~]# iostat -x
Linux 3.10.0-1127.19.1.el7.x86_64 (VM-0-11-centos) 2023年09月23日 _x86_64_ (2 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
0.40 0.00 0.41 0.25 0.00 98.93
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
vda 0.00 10.12 0.08 10.13 1.82 85.81 17.16 0.02 2.03 9.06 1.97 0.36 0.37
scd0 0.00 0.00 0.00 0.00 0.00 0.00 7.14 0.00 0.27 0.27 0.00 0.27 0.00
其中avgqu-sz,avgrq-sz,await,iowait,svctm等這些都需要關(guān)注,其含義如下:
- avgqu-sz是平均每次IO操作的數(shù)據(jù)量(扇區(qū)數(shù)為單位)
- avgrq-sz是平均等待處理的IO請(qǐng)求隊(duì)列長度
- await是平均每次IO請(qǐng)求等待時(shí)間(包括等待時(shí)間和處理時(shí)間,毫秒為單位)
- svctm平均每次IO請(qǐng)求的處理時(shí)間(毫秒為單位)
- iowait等待磁盤io所消耗的cpu比例
(2)平均隊(duì)列長度
未完成的I/O請(qǐng)求數(shù)量,一般情況下,小于3個(gè)是合理的,如果超過表示I/O存儲(chǔ)瓶頸,具體可以通過上面的iostat -x命令查看。
(3)平均等待時(shí)間
服務(wù)I/O請(qǐng)求所測量的平均時(shí)間,等待時(shí)間不能過長,如果平均等待過長,說明I/O繁忙,具體可以通過上面的iostat -x命令查看。
(4)每秒傳輸
描述每秒讀寫的性能,塊設(shè)備的讀寫性能隨著型號(hào)或者調(diào)度算法的不同存在比較大的差異,比如使用iostat -x可以看到rkB/s,wkB/s,rrqm/s,r/s和w/s,對(duì)于I/0滿載的情況下,這些值越大越好。
- rrqm/s是每秒對(duì)該設(shè)備的讀請(qǐng)求被合并次數(shù),文件系統(tǒng)會(huì)對(duì)讀取同塊(block)的請(qǐng)求進(jìn)行合并
- wrqm/s是每秒對(duì)該設(shè)備的寫請(qǐng)求被合并次數(shù)
- r/s是每秒完成的讀次數(shù)
- w/s是每秒完成的寫次數(shù)
- rkB/s是每秒讀數(shù)據(jù)量(kB為單位)
- wkB/s是每秒寫數(shù)據(jù)量(kB為單位)
2、網(wǎng)絡(luò)度量
高性能編程一般都離不開網(wǎng)絡(luò)收發(fā),對(duì)于RPC Server的開發(fā),我們希望的收發(fā)和處理越快越好,那具體指標(biāo)有哪些?
(1)接收和發(fā)送的數(shù)據(jù)包和字節(jié)
網(wǎng)絡(luò)接口性能可以按照數(shù)據(jù)包或者字節(jié)大小來決定, TODO:
(2)丟包
丟包是指被內(nèi)核丟棄的數(shù)據(jù)包,丟棄的原因如下:
- 連接隊(duì)列滿了
- 收發(fā)緩沖區(qū)滿了
- TCP底層重傳次數(shù)超過設(shè)置
- 開啟了sync cookie等配置,阻止了一些攻擊包
- 防火墻設(shè)置等
查看丟包是否增長可以通過netstat -s|grep drop命令查看:
[root@VM-0-11-centos ~]# netstat -s|grep drop
40 dropped because of missing route
10 SYNs to LISTEN sockets dropped
(3)連接隊(duì)列
這里連接隊(duì)列是指TCP的三次握手連接隊(duì)列(SYN半連接隊(duì)列和ACCEPT連接隊(duì)列),網(wǎng)絡(luò)接收隊(duì)列和網(wǎng)路發(fā)送隊(duì)列。
我們通過netstat -s | grep "SYNs to LISTEN"查看:
[root@VM-0-11-centos ~]# netstat -s | grep "SYNs to LISTEN"
11 SYNs to LISTEN sockets dropped
或者通過ss -lt查看:
[root@VM-0-11-centos ~]# ss -lt
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 *:ssh
- Recv-Q:表示收到的數(shù)據(jù)在接收隊(duì)列中,但是還有多少?zèng)]有被進(jìn)程取走(非LISTEN的句柄),如果接收隊(duì)列一直處于阻塞狀態(tài)(這個(gè)值很高),可能緩存區(qū)太小了或者發(fā)包太快;
- Send-Q:表示發(fā)送的數(shù)據(jù)在發(fā)送隊(duì)列中未確認(rèn)的字節(jié)數(shù)(非LISTEN的句柄),如果發(fā)送隊(duì)列Send-Q不能很快的清零,可能是有應(yīng)用向外發(fā)送數(shù)據(jù)包過快,或者是對(duì)方接收數(shù)據(jù)包不夠快;
(4)其他異常
除了上述度量還有其他一些異常度量,比如大量reset包,大量重傳包錯(cuò)誤,或者能發(fā)包,但是不能收包等,網(wǎng)絡(luò)相關(guān)的度量和排查其實(shí)相對(duì)復(fù)雜,如果大家有興趣可以讀讀《Wireshark網(wǎng)絡(luò)分析就這么簡單》,可以通過自己抓包具體分析。
第二部分:網(wǎng)絡(luò)層優(yōu)化
1、零拷貝
在《Linux高性能網(wǎng)絡(luò)編程十談|系統(tǒng)調(diào)用》一文中,當(dāng)時(shí)介紹了網(wǎng)絡(luò)收發(fā)包需要經(jīng)過多次系統(tǒng)調(diào)用和內(nèi)存拷貝:
sendfile
為了高性能,Linux底層提供了一些零拷貝的系統(tǒng)調(diào)用如sendfile,以減少用戶態(tài)和內(nèi)核態(tài)的切換,原理是什么呢?
如果內(nèi)核在讀取文件后,直接把PageCache中的內(nèi)容拷貝到socket緩沖區(qū),待到網(wǎng)卡發(fā)送完畢后,再通知進(jìn)程,這樣就只有2次上下文切換,和3次內(nèi)存拷貝;
如果網(wǎng)卡支持SG-DMA(The Scatter-Gather Direct Memory Access)技術(shù),還可以再去除socket緩沖區(qū)的拷貝,這樣一共只有2次內(nèi)存拷貝;
除了上述說的sendfile這種零拷貝可以減少用戶態(tài)到內(nèi)核態(tài)切換和拷貝(因?yàn)榫W(wǎng)絡(luò)調(diào)用),還有一種DirectIO,這種常用于大文件讀寫(比如FTP Server或者其他CDN下載服務(wù)器),原因是由于大文件拷貝難以命中PageCache,導(dǎo)致額外的內(nèi)存拷貝,如果用DirectIO就可以直接操作磁盤,開發(fā)者自己控制緩存。
2、解決C1000K
在早期的服務(wù)器開發(fā),從C10K(服務(wù)器同時(shí)處理1萬個(gè)TCP連接),C100K(服務(wù)器同時(shí)處理10萬個(gè)TCP連接)到C1000K(服務(wù)器同時(shí)處理100萬TCP連接),其實(shí)原理上還是使用前面說的方式:事件驅(qū)動(dòng),異步IO或者協(xié)程等,具體的原理在《IO復(fù)用和模式》《協(xié)程》這兩篇文章已經(jīng)介紹了,如果有興趣可以再回顧一下。而這里我們討論一下可能面臨的幾個(gè)場景如何解決:
遇到計(jì)算任務(wù),雖然內(nèi)存、CPU 的速度很快,然而循環(huán)執(zhí)行也可能耗時(shí)達(dá)到秒級(jí),所以,如果一定要引入需要密集計(jì)算才能完成的請(qǐng)求,為了不阻礙其他事件的處理,要么把這樣的請(qǐng)求放在獨(dú)立的線程中完成,要么把請(qǐng)求的處理過程拆分成多段,確保每段能夠快速執(zhí)行完,同時(shí)每段執(zhí)行完都要均等地處理其他事件,這樣通過放慢該請(qǐng)求的處理時(shí)間,就保障了其他請(qǐng)求的及時(shí)處理,比如像Nginx;
讀寫文件,充分利用PageCache,小文件通過mmap加載到內(nèi)存,而大文件拆分多個(gè)小文件處理;
所有socket操作全部都改為非阻塞,通過epoll或者kqueue來監(jiān)聽讀寫事件,并將事件拆分給對(duì)應(yīng)的線程處理;
3、提升TCP握手和揮手性能
我們在前面網(wǎng)絡(luò)篇中已經(jīng)清楚了講解了三次握手和四次揮手的流程,但是實(shí)際由于TCP的握手協(xié)議和揮手協(xié)議交互流程過多,會(huì)導(dǎo)致一些性能問題,該如何解決?
(1)優(yōu)化握手參數(shù)
正常情況下,握手環(huán)節(jié)客戶端發(fā)送SYN開啟握手,服務(wù)器會(huì)在幾毫秒內(nèi)返回ACK,但如果客戶端遲遲沒有收到ACK會(huì)怎么樣呢?客戶端會(huì)重發(fā)SYN,重試的次數(shù)由tcp_syn_retries參數(shù)控制,默認(rèn)是6次:
net.ipv4.tcp_syn_retries = 6
同時(shí)每次傳輸時(shí)間是按照倍數(shù)遞增(1,2,4,8,32,64 ... 秒),所以在網(wǎng)絡(luò)繁忙情況下或者在業(yè)務(wù)明確不能太多超時(shí)情況下,調(diào)整這個(gè)時(shí)間到net.ipv4.tcp_syn_retries = 3,這樣能減少服務(wù)端在網(wǎng)絡(luò)繁忙情況下的連鎖反應(yīng)。
上述是客戶端側(cè)調(diào)整,而服務(wù)端可能會(huì)出現(xiàn)半連接隊(duì)列滿了的場景,控制半連接隊(duì)列是net.ipv4.tcp_max_syn_backlog = 1024內(nèi)核參數(shù),可以適當(dāng)?shù)恼{(diào)大取值;同樣服務(wù)端在從半連接隊(duì)列轉(zhuǎn)換到ESTABLISHED,也需要確認(rèn)客戶端回復(fù)的確認(rèn)ACK,如果沒有收到也會(huì)重發(fā)SYN+ACK,所以這里L(fēng)inux也提供調(diào)整的參數(shù)net.ipv4.tcp_synack_retries = 5,減少重傳次數(shù),降低加劇的風(fēng)險(xiǎn);
除了以上的調(diào)整,減少握手的RTT也是一種優(yōu)化手段 —— TOF(TCP fast open),TFO到底怎樣達(dá)成這一目的呢?它把通訊分為兩個(gè)階段,第一階段為首次建立連接,這時(shí)走正常的三次握手,但在客戶端的SYN報(bào)文會(huì)明確地告訴服務(wù)器它想使用TFO功能,這樣服務(wù)器會(huì)把客戶端IP地址用只有自己知道的密鑰加密,作為Cookie攜帶在返回的SYN+ACK報(bào)文中,客戶端收到后會(huì)將Cookie緩存在本地;
第二階段就是每次TCP底層的報(bào)文都會(huì)帶上Cookie,只要帶上了Cookie的請(qǐng)求,服務(wù)端不需要收到客戶端的確認(rèn)包,就可以直接傳輸數(shù)據(jù)了,這樣就減少了RTT;設(shè)置參數(shù)可以通過net.ipv4.tcp_fastopen = 3;
(2)優(yōu)化揮手參數(shù)
握手優(yōu)化有一些相應(yīng)的方法,那揮手階段是否也可以優(yōu)化呢?我們應(yīng)該在開發(fā)中經(jīng)常遇到是TIME_WAIT狀態(tài),估計(jì)踩過坑的應(yīng)該不少,TIME_WAIT過多會(huì)消耗系統(tǒng)資源,端口耗盡導(dǎo)致想新建連接失敗,觸發(fā)Linux內(nèi)核查找可用端口導(dǎo)致循環(huán)問題等。如何解決?設(shè)置tcp_max_tw_buckets參數(shù),當(dāng)TIME_WAIT的連接數(shù)量超過該參數(shù)時(shí),新關(guān)閉的連接就不再經(jīng)歷TIME_WAIT而直接關(guān)閉;或者快速復(fù)用端口tcp_tw_reuse,在安全條件下使用TIME_WAIT狀態(tài)下的端口。
在回顧一下TCP揮手的圖,被調(diào)方會(huì)有CLOSE_WAIT,如果在我們服務(wù)中有大量的CLOSE_WAIT時(shí)候,我們就得注意:
- 是否在處理完調(diào)用方的時(shí)候后,忘記關(guān)閉連接
- 服務(wù)負(fù)載太高,導(dǎo)致close調(diào)用被延時(shí)
- 處理進(jìn)程處于Pending狀態(tài),導(dǎo)致不能及時(shí)關(guān)閉連接
4、一些網(wǎng)絡(luò)內(nèi)核參數(shù)
"提升TCP握手和揮手性能"已經(jīng)提到了一些優(yōu)化參數(shù),除了這些參數(shù)外還有一些對(duì)性能有幫助的參數(shù):
- net.ipv4.tcp_syncookies = 1減少SYN泛洪攻擊
- net.ipv4.tcp_abort_on_overflow = 1快速回復(fù)RST包,減緩accept隊(duì)列滿的情況
- net.ipv4.tcp_orphan_retries = 5如果FIN_WAIT1狀態(tài)連接有很多,考慮調(diào)小該值
- net.ipv4.tcp_fin_timeout = 60調(diào)整該值可以減少FIN的確認(rèn)時(shí)間
- net.ipv4.tcp_window_scaling = 1調(diào)整滑動(dòng)窗口的指數(shù)
- net.ipv4.tcp_wmem = 4096 16384 4194304調(diào)整寫緩沖區(qū)大小
- net.ipv4.tcp_rmem = 4096 87380 6291456調(diào)整讀緩沖區(qū)大小
- net.ipv4.tcp_congestion_control = cubic調(diào)整擁塞控制的算法,可以分析具體網(wǎng)絡(luò)場景,通過設(shè)置擁塞控制算法提升發(fā)包的效率
5、DPDK
在C1000K問題中,各種軟件、硬件的優(yōu)化很可能都已經(jīng)做到頭了,無論怎么調(diào)試參數(shù),提升性能能力已經(jīng)有限,根本的問題是,LINUX網(wǎng)絡(luò)協(xié)議棧做了太多太繁重的工作。
于是英特爾公司的網(wǎng)絡(luò)通信部門2008年提出DPDK,提供豐富、完整的框架,讓CPU快速實(shí)現(xiàn)數(shù)據(jù)平面應(yīng)用的數(shù)據(jù)包處理,高效完成網(wǎng)絡(luò)轉(zhuǎn)發(fā)等工作,具體細(xì)節(jié)大家可以查閱資料,這里我整理了DPDK高性能的大概原理:
跳過內(nèi)核協(xié)議棧,直接由用戶態(tài)進(jìn)程通過輪詢的方式,來處理網(wǎng)絡(luò)接收,在PPS非常高的場景中,查詢時(shí)間比實(shí)際工作時(shí)間少了很多,絕大部分時(shí)間都在處理網(wǎng)絡(luò)包,而跳過內(nèi)核協(xié)議棧后,就省去了繁雜的硬中斷、軟中斷再到 Linux 網(wǎng)絡(luò)協(xié)議棧逐層處理的過程,應(yīng)用程序可以針對(duì)應(yīng)用的實(shí)際場景,有針對(duì)性地優(yōu)化網(wǎng)絡(luò)包的處理邏輯,而不需要關(guān)注所有的細(xì)節(jié);
通過大頁、CPU 綁定、內(nèi)存對(duì)齊、流水線并發(fā)等多種機(jī)制,優(yōu)化網(wǎng)絡(luò)包的處理效率;
DPDK網(wǎng)絡(luò)圖
第三部分:應(yīng)用層協(xié)議優(yōu)化
網(wǎng)絡(luò)編程中除了設(shè)計(jì)一個(gè)好的底層server和調(diào)整內(nèi)核參數(shù)外,其實(shí)應(yīng)用層的協(xié)議選型和設(shè)計(jì)也很重要,那本小節(jié)討論一下當(dāng)前的優(yōu)化方案。
1、HTTP/1.1
從互聯(lián)網(wǎng)發(fā)展到現(xiàn)在,HTTP/1.1一直是最廣泛使用的應(yīng)用層協(xié)議,主要是使用簡單,方便,但是缺點(diǎn)也很明顯:HTTP頭部使用 ASCII編碼,信息冗余,協(xié)議濫用等,導(dǎo)致對(duì)其的優(yōu)化都集中在業(yè)務(wù)層。那有哪些優(yōu)化,我這里總結(jié)一些:
1.充分利用緩存
- 客戶端緩存:利用Cache-control,Expires,etags等HTTP/1.1特性,減少重復(fù)請(qǐng)求的次數(shù);
- CDN緩存:靜態(tài)資源優(yōu)先考慮使用CDN服務(wù),本身CDN是邊緣節(jié)點(diǎn),同時(shí)已經(jīng)充分考慮HTTP/1.1一些性能加速策略;
- 3XX狀態(tài)碼:返回3XX狀態(tài)碼,讓客戶端根據(jù)狀態(tài)碼使用緩存策略
2.合并請(qǐng)求
- 當(dāng)多個(gè)訪問小文件的請(qǐng)求被合并為一個(gè)訪問大文件的請(qǐng)求時(shí),這樣雖然傳輸?shù)目傎Y源體積未變,但減少請(qǐng)求就意味著減少了重復(fù)發(fā)送的HTTP頭部,同時(shí)也減少了TCP連接的數(shù)量,因而省去了TCP握手和慢啟動(dòng)過程消耗的時(shí)間
- 由于瀏覽器限制同一個(gè)域名下并發(fā)請(qǐng)求數(shù)(如Chrome是6個(gè)),所以我們優(yōu)先加載客戶端急需的數(shù)據(jù),其他數(shù)據(jù)可以懶加載
3.使用壓縮算法
- 利用HTTP的Accept-Encoding頭部字段,讓服務(wù)端知道客戶端的壓縮算法然后返回壓縮后的數(shù)據(jù)給客戶端
- 對(duì)于圖片請(qǐng)求,可以考慮使用webp,svg等格式,這些圖片壓縮后的數(shù)據(jù)較小
4.優(yōu)化HTTPS
- 優(yōu)先使用TLS1.3,減少RTT次數(shù)
- 明確某些靜態(tài)資源不需要加密,或者可以自己通過預(yù)埋加密協(xié)議的,改為HTTP請(qǐng)求,減少握手次數(shù)
- 使用長連接,緩解每次請(qǐng)求都需要走TLS的握手協(xié)議
2、HTTP/2
現(xiàn)在使用HTTP/2的服務(wù)越來越多了,包括gRPC框架的默認(rèn)協(xié)議就是HTTP/2,對(duì)比HTTP/1,HTTP/2性能非常大的提升,可以從上圖看出:
- 圖1 中HTTP/2使用的流式傳輸,在一條連接上可以有多個(gè)數(shù)據(jù)幀,這樣就不需要再像HTTP/1一個(gè)請(qǐng)求需要新建一個(gè)連接了;
- 圖2 中HTTP/2使用HTTP Header靜態(tài)和動(dòng)態(tài)編碼表的方式,這樣每次請(qǐng)求不需要傳遞重復(fù)或者通用的一些Header信息,減少了包體的大小;
HTTP/1.1 不支持服務(wù)器主動(dòng)推送消息,因此當(dāng)客戶端需要獲取通知時(shí),只能通過定時(shí)器不斷地拉取消息,而HTTP/2的消息可以主動(dòng)推送,可以節(jié)省大量帶寬和服務(wù)器資源;
3、HTTP/3
HTTP3圖3
上面介紹的HTTP/2雖然已經(jīng)提升很多性能,減少了網(wǎng)絡(luò)請(qǐng)求,但是底層使用TCP,避免不了握手,慢啟動(dòng)和擁塞控制等問題,于是HTTP/3通過使用UDP繞過這些限制來優(yōu)化性能。
- HTTP/3可以實(shí)現(xiàn)0 RTT建立連接,HTTP/2的連接建立需要3 RTT,如果考慮會(huì)話復(fù)用,即把第一次握手計(jì)算出來的對(duì)稱密鑰緩存起來,那也需要2 RTT,更進(jìn)一步的,如果TLS升級(jí)到1.3,那么HTTP/2連接需要2 RTT,考慮會(huì)話復(fù)用需要1 RTT。而HTTP/3首次連接只需要1 RTT, 后面的鏈接只需要0 RTT,其原理和cookie類似,維持conntion id,實(shí)現(xiàn)連接遷移。
- 解決隊(duì)頭阻塞,在HTTP/2中雖然是TCP多路復(fù)用,但是TCP的包確是順序的,所以如果一個(gè)連接上的包在TCP層沒有被確認(rèn),這個(gè)連接上HTTP/2請(qǐng)求都會(huì)被卡住,但是HTTP/3基于UDP就可以不存在這個(gè)問題,Packet可以發(fā)送給服務(wù)端,服務(wù)端根據(jù)需要自己組裝包順序,即使Packet丟了,可以重傳當(dāng)前Packet即可;
上述都是HTTP/3對(duì)比HTTP/2改進(jìn)的地方,但是從目前看全面使用還是有一些局限,比如:防火墻對(duì)UDP包限制,連接遷移特性使情況變得更加復(fù)雜等問題,有興趣的可以在客戶端嘗試,但是估計(jì)會(huì)要踩比較多的坑。
4、RPC協(xié)議
RPC協(xié)議包括很多(如HTTP JSON,XML,ProtoBuf等),框架也比較多(gRPC,Thrift,Brpc,Spring Cloud),隨著微服務(wù)的架構(gòu)被大家熟知,內(nèi)網(wǎng)的RPC協(xié)議設(shè)計(jì)往往是網(wǎng)絡(luò)框架的重要部分。當(dāng)然RPC框架底層的架構(gòu)還是前面介紹的異步,多路復(fù)用,多線程等設(shè)計(jì),但是上層我們要考慮高性能,更多要解決如下問題:
- 根據(jù)業(yè)務(wù)場景設(shè)計(jì)不同的協(xié)議,比如采用ProtoBuf能壓縮編碼,采用HTTP JSON協(xié)議方便支持各個(gè)客戶端對(duì)接;
- 報(bào)文的格式設(shè)計(jì),好的報(bào)文格式能提升編碼和解碼效率;
- 降低開發(fā)成本,考慮注冊中心和負(fù)載均衡,讓框架和RPC緊密結(jié)合,減少業(yè)務(wù)層的開發(fā)負(fù)擔(dān);
- 充分考慮分布式場景,比如事務(wù)超時(shí),消息冪等等等問題;
之前在業(yè)務(wù)中也開發(fā)了一些RPC框架,以上便是我對(duì)RPC協(xié)議的簡單總結(jié),不過RPC框架對(duì)于性能的考慮可能不是那么重要,更多的是考慮便利性,我們只需要把底層網(wǎng)絡(luò)框架設(shè)計(jì)的足夠高性能,并且選擇與業(yè)務(wù)匹配的網(wǎng)絡(luò)協(xié)議,這樣基本能滿足大部分業(yè)務(wù)需求。