服務(wù)器性能優(yōu)化之網(wǎng)絡(luò)性能優(yōu)化
hi ,大家好,今天分享一篇后臺服務(wù)器性能優(yōu)化之網(wǎng)絡(luò)性能優(yōu)化,希望大家對Linux網(wǎng)絡(luò)有更深的理解。
曾幾何時,一切都是那么簡單。網(wǎng)卡很慢,只有一個隊列。當數(shù)據(jù)包到達時,網(wǎng)卡通過DMA復(fù)制數(shù)據(jù)包并發(fā)送中斷,Linux內(nèi)核收集這些數(shù)據(jù)包并完成中斷處理。隨著網(wǎng)卡越來越快,基于中斷的模型可能會因大量傳入數(shù)據(jù)包而導(dǎo)致 IRQ 風(fēng)暴。這將消耗大部分 CPU 功率并凍結(jié)系統(tǒng)。
為了解決這個問題,NAPI(中斷+輪詢)被提議。當內(nèi)核收到來自網(wǎng)卡的中斷時,它開始輪詢設(shè)備并盡快收集隊列中的數(shù)據(jù)包。NAPI 可以很好地與現(xiàn)在常見的 1 Gbps 網(wǎng)卡配合使用。但是,對于10Gbps、20Gbps甚至40Gbps的網(wǎng)卡,NAPI可能還不夠。如果我們?nèi)匀皇褂靡粋€ CPU 和一個隊列來接收數(shù)據(jù)包,這些卡將需要更快的 CPU。
幸運的是,現(xiàn)在多核 CPU 很流行,那么為什么不并行處理數(shù)據(jù)包呢?
RSS:接收端縮放
Receive Side Scaling(RSS)是所述機構(gòu)具有多個RX / TX隊列過程的數(shù)據(jù)包。當帶有RSS 的網(wǎng)卡接收到數(shù)據(jù)包時,它會對數(shù)據(jù)包應(yīng)用過濾器并將數(shù)據(jù)包分發(fā)到RX 隊列。過濾器通常是一個哈希函數(shù),可以通過“ethtool -X”進行配置。如果你想在前 3 個隊列中均勻分布流量:
# ethtool -X eth0 equal 3
或者,如果你發(fā)現(xiàn)一個特別有用的魔法哈希鍵:
# ethtool -X eth0 hkey <magic hash key>
對于低延遲網(wǎng)絡(luò),除了過濾器之外,CPU 親和性也很重要。最佳設(shè)置是分配一個 CPU 專用于一個隊列。首先通過檢查/proc/interrupt找出IRQ號,然后將CPU位掩碼設(shè)置為/proc/irq/<IRQ_NUMBER>/smp_affinity來分配專用CPU。為避免設(shè)置被覆蓋,必須禁用守護進程irqbalance。請注意,根據(jù)內(nèi)核文檔,超線程對中斷處理沒有任何好處,因此最好將隊列數(shù)與物理 CPU 內(nèi)核數(shù)相匹配。
RPS:接收數(shù)據(jù)包控制
RSS提供硬件隊列,一個稱為軟件隊列機制Receive Packet Steering (RPS)在Linux內(nèi)核實現(xiàn)。
當驅(qū)動程序接收到數(shù)據(jù)包時,它會將數(shù)據(jù)包包裝在套接字緩沖區(qū) ( sk_buff ) 中,其中包含數(shù)據(jù)包的u32哈希值。散列是所謂的第 4 層散列(l4 散列),它基于源 IP、源端口、目的 IP 和目的端口,由網(wǎng)卡或__skb_set_sw_hash() 計算。由于相同 TCP/UDP 連接(流)的每個數(shù)據(jù)包共享相同的哈希值,因此使用相同的 CPU 處理它們是合理的。
RPS 的基本思想是根據(jù)每個隊列的 rps_map 將同一流的數(shù)據(jù)包發(fā)送到特定的 CPU。這是 rps_map 的結(jié)構(gòu):映射根據(jù) CPU 位掩碼動態(tài)更改為/sys/class/net/<dev>/queues/rx-<n>/rps_cpus。比如我們要讓隊列使用前3個CPU,在8個CPU的系統(tǒng)中,我們先構(gòu)造位掩碼,0 0 0 0 0 1 1 1,到0x7,然后
#echo 7 > /sys/class/net /eth0/queues/rx-0/rps_cpus
這將保證從 eth0 中隊列 0 接收的數(shù)據(jù)包進入 CPU 1~3。驅(qū)動程序在 sk_buff 中包裝一個數(shù)據(jù)包后,它將到達netif_rx_internal()或netif_receive_skb_internal(),然后到達 get_rps_cpu()
struct rps_map {
unsigned int len;
struct rcu_head rcu;
u16 cpus[0];
};
將被調(diào)用以將哈希映射到 rps_map 中的條目,即 CPU id。得到CPU id后,enqueue_to_backlog()將sk_buff放到特定的CPU隊列中進行進一步處理。每個 CPU 的隊列在 per-cpu 變量softnet_data 中分配。
使用RPS的好處是可以在 CPU 之間分擔(dān)數(shù)據(jù)包處理的負載。但是,如果RSS 可用,則可能沒有必要,因為網(wǎng)卡已經(jīng)對每個隊列/CPU 的數(shù)據(jù)包進行了排序。但是,如果隊列中的CPU數(shù)更多,RPS 仍然可以發(fā)揮作用。在這種情況下,每個隊列可以與多個 CPU相關(guān)聯(lián)并在它們之間分發(fā)數(shù)據(jù)包。
RFS: Receive Flow Steering
盡管 RPS 基于流分發(fā)數(shù)據(jù)包,但它沒有考慮用戶空間應(yīng)用程序。應(yīng)用程序可能在 CPU A 上運行,而內(nèi)核將數(shù)據(jù)包放入 CPU B 的隊列中。由于 CPU A 只能使用自己的緩存,因此 CPU B 中緩存的數(shù)據(jù)包變得無用。Receive Flow Steering(RFS)進一步延伸為RPS的應(yīng)用程序。
代替每個隊列的哈希至CPU地圖,RFS維護全局flow-to-CPU的表,rps_sock_flow_table:該掩模用于將散列值映射成所述表的索引。由于表大小將四舍五入到 2 的冪,因此掩碼設(shè)置為table_size - 1。
struct rps_sock_flow_table {
u32 mask;
u32 ents[0];
};
并且很容易找到索引:a sk_buff與hash & scok_table->mask。
該條目由 rps_cpu_mask劃分為流 id 和 CPU id。低位用于CPU id,而高位用于流id。當應(yīng)用程序?qū)μ捉幼诌M行操作時(inet_recvmsg()、inet_sendmsg()、inet_sendpage()、tcp_splice_read()),將調(diào)用sock_rps_record_flow()來更新sock 流表。
當數(shù)據(jù)包到來時,將調(diào)用get_rps_cpu()來決定使用哪個 CPU 隊列。下面是get_rps_cpu()如何決定數(shù)據(jù)包的 CPU
ident = sock_flow_table->ents[hash & sock_flow_table->mask];
if ((ident ^ hash) & ~rps_cpu_mask)
goto try_rps;
next_cpu = ident & rps_cpu_mask;
使用流表掩碼找到條目的索引,并檢查散列的高位是否與條目匹配。如果是,它會從條目中檢索 CPU id 并為數(shù)據(jù)包分配該 CPU。如果散列不匹配任何條目,它會回退到使用 RPS 映射。
可以通過rps_sock_flow_entries調(diào)整 sock 流表的大小。例如,如果我們要將表大小設(shè)置為 32768:
#echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
sock流表雖然提高了應(yīng)用的局部性,但也帶來了一個問題。當調(diào)度器將應(yīng)用程序遷移到新 CPU 時,舊 CPU 隊列中剩余的數(shù)據(jù)包變得未完成,應(yīng)用程序可能會得到亂序的數(shù)據(jù)包。為了解決這個問題,RFS 使用每個隊列的rps_dev_flow_table來跟蹤未完成的數(shù)據(jù)包。
下面是該結(jié)構(gòu)rps_dev_flow_table:到襪子流表中,類似的rps_dev_flow_table也使用table_size - 1作為掩模而表的大小也必須被向上舍入到2的冪當流量分組被入隊,last_qtail被更新
struct rps_dev_flow {
u16 cpu;
u16 filter; /* For aRFS */
unsigned int last_qtail;
};
struct rps_dev_flow_table {
unsigned int mask;
struct rcu_head rcu;
struct rps_dev_flow flows[0];
};
到 CPU 隊列的尾部。如果應(yīng)用程序遷移到新 CPU,則 sock 流表將反映更改,并且get_rps_cpu()將為流設(shè)置新 CPU。在設(shè)置新 CPU 之前,get_rps_cpu() 會檢查當前隊列的頭部是否已經(jīng)通過 last_qtail。如果是這樣,這意味著隊列中沒有更多未完成的數(shù)據(jù)包,并且可以安全地更改 CPU。否則,get_rps_cpu()仍將使用rps_dev_flow->cpu 中記錄的舊 CPU 。
每個隊列的流表(rps_dev_flow_table)的大小可以通過 sysfs 接口進行配置:
/sys/class/net/<dev>/queues/rx-<n>/rps_flow_cnt
建議將rps_flow_cnt設(shè)置為 ( rps_sock_flow_entries / N) 而 N 是 RX 隊列的數(shù)量(假設(shè)流在隊列中均勻分布)。
ARFS:加速接收流量轉(zhuǎn)向
Accelerated Receive Flow Steering(aRFS)進一步延伸RFS為RX隊列硬件過濾。要啟用 aRFS,它需要具有可編程元組過濾器和驅(qū)動程序支持的網(wǎng)卡。要啟用ntuple 過濾器。
# ethtool -K eth0 ntuple on
要使驅(qū)動程序支持aRFS,它必須實現(xiàn)ndo_rx_flow_steer以幫助set_rps_cpu()配置硬件過濾器。當get_rps_cpu()決定為流分配一個新 CPU 時,它會調(diào)用set_rps_cpu()。set_rps_cpu()首先檢查網(wǎng)卡是否支持 ntuple 過濾器。如果是,它將查詢rx_cpu_rmap為流找到合適的 RX 隊列。
rx_cpu_rmap是驅(qū)動維護的特殊映射。該映射用于查找哪個 RX 隊列適合 CPU。它可以是與給定 CPU 直接關(guān)聯(lián)的隊列,也可以是處理 CPU 在緩存位置最接近的隊列。獲取 RX 隊列索引后,set_rps_cpu()調(diào)用ndo_rx_flow_steer()以通知驅(qū)動程序為給定的流創(chuàng)建新過濾器。ndo_rx_flow_steer()將返回過濾器 id,過濾器 id 將存儲在每個隊列的流表中。
除了實現(xiàn)ndo_rx_flow_steer() 外,驅(qū)動程序還必須調(diào)用rps_may_expire_flow() 定期檢查過濾器是否仍然有效并刪除過期的過濾器。
SO_REUSEPORT
linux man文檔中一段文字描述其作用:
The new socket option allows multiple sockets on the same host to bind to the same port, and is intended to improve the performance of multithreaded network server applications running on top of multicore systems.
簡單說,SO_REUSEPORT支持多個進程或者線程綁定到同一端口,用以提高服務(wù)器程序的性能。我們想了解為什么這個特性這么火(經(jīng)常被大廠面試官問到),到底是解決什么問題。
Linux系統(tǒng)上后臺應(yīng)用程序,為了利用多核的優(yōu)勢,一般使用以下比較典型的多進程/多線程服務(wù)器模型:
- 單線程listen/accept,多個工作線程接收任務(wù)分發(fā),雖CPU的工作負載不再是問題,但會存在:
1. 單線程listener,在處理高速率海量連接時,一樣會成為瓶頸;
2. CPU緩存行失效(丟失套接字結(jié)構(gòu)socket structure)現(xiàn)象嚴重;
- 所有工作線程都accept()在同一個服務(wù)器套接字上呢,一樣存在問題:
1. 多線程訪問server socket鎖競爭嚴重;
2. 高負載下,線程之間處理不均衡,有時高達3:1不均衡比例;
3. 導(dǎo)致CPU緩存行跳躍(cache line bouncing);
4. 在繁忙CPU上存在較大延遲;
上面模型雖然可以做到線程和CPU核綁定,但都會存在以下問題:
- 單一listener工作線程在高速的連接接入處理時會成為瓶頸
- 緩存行跳躍
- 很難做到CPU之間的負載均衡
- 隨著核數(shù)的擴展,性能并沒有隨著提升
SO_REUSEPORT支持多個進程或者線程綁定到同一端口:
- 允許多個套接字 bind()/listen() 同一個TCP/UDP端口
1.每一個線程擁有自己的服務(wù)器套接字。
2.在服務(wù)器套接字上沒有了鎖的競爭。
- 內(nèi)核層面實現(xiàn)負載均衡。
- 安全層面,監(jiān)聽同一個端口的套接字只能位于同一個用戶下面。
其核心的實現(xiàn)主要有三點:
- 擴展socket option,增加
- SO_REUSEPORT選項,用來設(shè)置 reuseport。
- 修改 bind 系統(tǒng)調(diào)用實現(xiàn),以便支持可以綁定到相同的 IP 和端口。
- 修改處理新建連接的實現(xiàn),查找 listener 的時候,能夠支持在監(jiān)聽相同 IP 和端口的多個 sock 之間均衡選擇
帶來意義
- CPU之間平衡處理,水平擴展,模型簡單,維護方便了,進程的管理和應(yīng)用邏輯解耦,進程的管理水平擴展權(quán)限下放給程序員/管理員,可以根據(jù)實際進行控制進程啟動/關(guān)閉,增加了靈活性。這帶來了一個較為微觀的水平擴展思路,線程多少是否合適,狀態(tài)是否存在共享,降低單個進程的資源依賴,針對無狀態(tài)的服務(wù)器架構(gòu)最為適合。
- 針對對客戶端而言,表面上感受不到其變動,因為這些工作完全在服務(wù)器端進行。
- 服務(wù)器無縫重啟/切換,熱更新,提供新的可能性。我們迭代了一版本,需要部署到線上,為之啟動一個新的進程后,稍后關(guān)閉舊版本進程程序,服務(wù)一直在運行中不間斷,需要平衡過度。這就像Erlang語言層面所提供的熱更新一樣。
SO_REUSEPORT已知問題
- SO_REUSEPORT分為兩種模式,即熱備份模式和負載均衡模式,在早期的內(nèi)核版本中,即便是加入對reuseport選項的支持,也僅僅為熱備份模式,而在3.9內(nèi)核之后,則全部改為了負載均衡模式,兩種模式?jīng)]有共存,雖然我一直都希望它們可以共存。
- SO_REUSEPORT根據(jù)數(shù)據(jù)包的四元組{src ip, src port, dst ip, dst port}和當前綁定同一個端口的服務(wù)器套接字數(shù)量進行數(shù)據(jù)包分發(fā)。若服務(wù)器套接字數(shù)量產(chǎn)生變化,內(nèi)核會把本該上一個服務(wù)器套接字所處理的客戶端連接所發(fā)送的數(shù)據(jù)包(比如三次握手期間的半連接,以及已經(jīng)完成握手但在隊列中排隊的連接)分發(fā)到其它的服務(wù)器套接字上面,可能會導(dǎo)致客戶端請求失敗。
如何預(yù)防以上已知問題,一般解決思路:
1.使用固定的服務(wù)器套接字數(shù)量,不要在負載繁忙期間輕易變化。
2.允許多個服務(wù)器套接字共享TCP請求表(Tcp request table)。
3.不使用四元組作為Hash值進行選擇本地套接字處理,比如選擇 會話ID或者進程ID,挑選隸屬于同一個CPU的套接字。
4. 使用一致性hash算法。
與其他特性關(guān)系
1. SO_REUSEADDR:主要是地址復(fù)用
1.1 讓處于time_wait狀態(tài)的socket可以快速復(fù)用原ip+port
1.2 使得0.0.0.0(ipv4通配符地址)與其他地址(127.0.0.1和10.0.0.x)不沖突
1.3 SO_REUSEADDR 的缺點在于,沒有安全限制,而且無法保證所有連接均勻分配。
2.與RFS/RPS/XPS-mq協(xié)作,可以獲得進一步的性能
2.1.服務(wù)器線程綁定到CPUs
2.2.RPS分發(fā)TCP SYN包到對應(yīng)CPU核上
2.3.TCP連接被已綁定到CPU上的線程accept()
2.4. XPS-mq(Transmit Packet Steering for multiqueue),傳輸隊列和CPU綁定,發(fā)送 數(shù)據(jù)
2.5. RFS/RPS保證同一個連接后續(xù)數(shù)據(jù)包都會被分發(fā)到同一個CPU上,網(wǎng)卡接收隊列 已經(jīng)綁定到CPU,則RFS/RPS則無須設(shè)置,需要注意硬件支持與否,目的是數(shù)據(jù)包的軟硬中斷、接收、處理等在一個CPU核上,并行化處理,盡可能做到資源利用最大化。
SO_REUSEPORT的演進
- 3.9之前內(nèi)核,能夠讓多個socket同時綁定完全相同的ip+port,但不能實現(xiàn)負載均衡,實現(xiàn)是熱備。
- Linux 3.9之后,能夠讓多個socket同時綁定完全相同的ip+port,可以實現(xiàn)負載均衡。
- Linux4.5版本后,內(nèi)核引入了reuseport groups,它將綁定到同一個IP和Port,并且設(shè)置了SO_REUSEPORT選項的socket組織到一個group內(nèi)部。目的是加快socket查詢。
總結(jié)
Linux網(wǎng)絡(luò)堆棧所存在問題
- TCP處理&多核
- 一個完整的TCP連接,中斷發(fā)生在一個CPU核上,但應(yīng)用數(shù)據(jù)處理可能會在另外一個核上
- 不同CPU核心處理,帶來了鎖競爭和CPU Cache Miss(波動不平衡)
- 多個進程監(jiān)聽一個TCP套接字,共享一個listen queue隊列
- 用于連接管理全局哈希表格,存在資源競爭
- epoll IO模型多進程的驚群現(xiàn)象
- Linux VFS的同步損耗嚴重
- Socket被VFS管理
- VFS對文件節(jié)點Inode和目錄Dentry有同步需求
- SOCKET只需要在內(nèi)存中存在即可,非嚴格意義上文件系統(tǒng),不需要Inode和Dentry
- 代碼層面略過不必須的常規(guī)鎖,但又保持了足夠的兼容性
RSS、RPS、RFS 和 aRFS,這些機制是在 Linux 3.0 之前引入的,SO_REUSEPORT選項在Linux 3.9被引入內(nèi)核,因此大多數(shù)發(fā)行版已經(jīng)包含并啟用了它們。深入了解它們,以便為我們的服務(wù)器系統(tǒng)找到最佳性能配置。
性能優(yōu)化無極限,我們下期再繼續(xù)分享!