歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux綜合 >> Linux內核

Linux內核軟RPS實現網絡接收軟中斷的負載均衡分發

例行的Linux軟中斷分發機制與問題Linux的中斷分為上下兩半部,一般而言(事實確實也是如此),被中斷的CPU執行中斷處理函數,並在在本CPU上觸發軟中斷(下半部),等硬中斷處理返回後,軟中斷隨即開中斷在本CPU運行,或者wake up本CPU上的軟中斷內核線程來處理在硬中斷中pending的軟中斷。

換句話說,Linux和同一個中斷向量相關的中斷上半部和軟中斷都是在同一個CPU上執行的,這個可以通過raise_softirq這個接口看出來。這種設計的邏輯是正確的,但是在某些不甚智能的硬件前提下,它工作得並不好。內核沒有辦法去控制軟中斷的分發,因此也就只能對硬中斷的發射聽之任之。這個分為兩類情況:

1.硬件只能中斷一個CPU按照上述邏輯,如果系統存在多個CPU核心,那麼只能由一個CPU處理軟中斷了,這顯然會造成系統負載在各個CPU間不均衡。

2.硬件盲目隨機中斷多個CPU注意”盲目“一詞。這個是和主板以及總線相關的,和中斷源關系並不大。因此具體中斷哪個CPU和中斷源的業務邏輯也無關聯,比如主板和中斷控制器並不是理會網卡的數據包內容,更不會根據數據包的元信息中斷不同的CPU...即,中斷源對中斷哪個CPU這件事可以控制的東西幾乎沒有。為什麼必須是中斷源呢?因此只有它知道自己的業務邏輯,這又是一個端到端的設計方案問題。

因此,Linux關於軟中斷的調度,缺乏了一點可以控制的邏輯,少了一點靈活性,完全就是靠著硬件中斷源中斷的CPU來,而這方面,硬件中斷源由於被中斷控制器和總線與CPU隔離了,它們之間的配合並不好。因此,需要加一個軟中斷調度層來解決這個問題。

本文描述的並不是針對以上問題的一個通用的方案,因為它只是針對為網絡數據包處理的,並且RPS在被google的人設計之初,其設計是高度定制化的,目的很單一,就是提高Linux服務器的性能。而我,將這個思路移植到了提高Linux路由器的性能上。

基於RPS的軟中斷分發優化在Linux轉發優化那篇文章《Linux轉發性能評估與優化(轉發瓶頸分析與解決方案)》中,我嘗試了網卡接收軟中斷的負載均衡分發,當時嘗試了將該軟中斷再次分為上下半部:

上半部:用於skb在不同的CPU間分發。

下半部:用戶skb的實際協議棧接收處理。

事實上,利用Linux 2.6.35以後加入的RPS的思想可能會有更好的做法,根本不用重新分割網絡接收軟中斷。它基於以下的事實:

事實1:網卡很高端的情況如果網卡很高端,那麼它一定支持硬件多隊列特性以及多中斷vector,這樣的話,就可以直接綁定一個隊列的中斷到一個CPU核心,無需軟中斷重分發skb。

事實2:網卡很低檔的情況如果網卡很低檔,比如它不支持多隊列,也不支持多個中斷vector,且無法對中斷進行負載均衡,那麼也無需讓軟中斷來分發,直接要驅動裡面分發豈不更好(其實這樣做真的不好)?事實上,即便支持單一中斷vector的CPU間負載均衡,最好也要禁掉它,因為它會破壞CPU cache的親和力。

為什麼以上的兩點事實不能利用中斷中不能進行復雜耗時操作,不能由復雜計算。中斷處理函數是設備相關的,一般不由框架來負責,而是由驅動程序自己負責。協議棧主框架只維護一個接口集,而驅動程序可以調用接口集內的API。你能保證驅動的編寫人員可以正確利用RPS而不是誤用它嗎?

正確的做法就是將這一切機制隱藏起來,外部僅僅提供一套配置,你(驅動編寫人員)可以開啟它,關閉它,至於它怎麼工作的,你不用關心。

因此,最終的方案還是跟我最初的一樣,看來RPS也是這麼個思路。修改軟中斷路徑中NAPI poll回調!然而poll回調也是驅動維護的,因此就在數據包數據的公共路徑上掛接一個HOOK,來負責RPS的處理。

為什麼要禁掉低端網卡的CPU中斷負載均衡答案似乎很簡單,答案是:因為我們自己用軟件可以做得更好!而基於簡單硬件的單純且愚蠢的盲目中斷負載均衡可能會(幾乎一定會)弄巧成拙!

這是為什麼?因為簡單低端網卡硬件不識別網絡流,即它只能識別到這是一個數據包,而不能識別到數據包的元組信息。如果一個數據流的第一個數據包被分發到了CPU1,而第二個數據包分發到了CPU2,那麼對於流的公共數據,比如nf_conntrack中記錄的東西,CPU cache的利用率就會比較低,cache抖動會比較厲害。對於TCP流而言,可能還會因為TCP串行包並行處理的延遲不確定性導致數據包亂序。因此最直接的想法就是將屬於一個流的所有數據包分發了一個CPU上。

我對原生RPS代碼的修改要知道,Linux的RPS特性是google人員引入的,他們的目標在於提升服務器的處理效率。因此他們著重考慮了以下的信息:

哪個CPU在為這個數據流提供服務;

哪個CPU被接收了該流數據包的網卡所中斷;

哪個CPU運行處理該流數據包的軟中斷。

理想情況,為了達到CPU cache的高效利用,上面的三個CPU應該是同一個CPU。而原生RPS實現就是這個目的。當然,為了這個目的,內核中不得不維護一個”流表“,裡面記錄了上面三類CPU信息。這個流表並不是真正的基於元組的流表,而是僅僅記錄上述CPU信息的表。

而我的需求則不同,我側重數據轉發而不是本地處理。因此我的著重看的是:

哪個CPU被接收了該流數據包的網卡所中斷;

哪個CPU運行處理該流數據包的軟中斷。

其實我並不看中哪個CPU調度發送數據包,發送線程只是從VOQ中調度一個skb,然後發送,它並不處理數據包,甚至都不會去訪問數據包的內容(包括協議頭),因此cache的利用率方面並不是發送線程首要考慮的。

因此相對於Linux作為服務器時關注哪個CPU為數據包所在的流提供服務,Linux作為路由器時哪個CPU數據發送邏輯可以忽略(雖然它也可以通過設置二級緩存接力[最後講]來優化一點)。Linux作為路由器,所有的數據一定要快,一定盡可能簡單,因為它沒有Linux作為服務器運行時服務器處理的固有延遲-查詢數據庫,業務邏輯處理等,而這個服務處理的固有延遲相對網絡處理處理延遲而言,要大得多,因此作為服務器而言,網絡協議棧處理並不是瓶頸。服務器是什麼?服務器是數據包的終點,在此,協議棧只是一個入口,一個基礎設施。

在作為路由器運行時,網絡協議棧處理延遲是唯一的延遲,因此要優化它!路由器是什麼?路由器不是數據包的終點,路由器是數據包不得不經過,但是要盡可能快速離開的地方!

所以我並沒有直接采用RPS的原生做法,而是將hash計算簡化了,並且不再維護任何狀態信息,只是計算一個hash:

[plain] view plaincopyprint?

target_cpu = my_hash(source_ip, destination_ip, l4proto, sport, dport) % NR_CPU;

target_cpu = my_hash(source_ip, destination_ip, l4proto, sport, dport) % NR_CPU;[my_hash只要將信息足夠平均地進行散列即可!]

僅此而已。於是get_rps_cpu中就可以僅有上面的一句即可。

這裡有一個復雜性要考慮,如果收到一個IP分片,且不是第一個,那麼就取不到四層信息,因為可能會將它們和片頭分發到不同的CPU處理,在IP層需要重組的時候,就會涉及到CPU之間的數據互訪和同步問題,這個問題目前暫不考慮。

NET RX軟中斷負載均衡總體框架本節給出一個總體的框架,網卡很低端,假設如下:

不支持多隊列;

不支持中斷負載均衡;

只會中斷CPU0。

它的框架如下圖所示:

CPU親和接力優化本節稍微提一點關於輸出處理線程的事,由於輸出處理線程邏輯比較簡單,就是執行調度策略然後有網卡發送skb,它並不會頻繁touch數據包(請注意,由於采用了VOQ,數據包在放入VOQ的時候,它的二層信息就已經封裝好了,部分可以采用分散/聚集IO的方式,如果不支持,只能memcpy了...),因此CPU cache對它的意義沒有對接收已經協議棧處理線程的大。然而不管怎樣,它還是要touch這個skb一次的,為了發送它,並且它還要touch輸入網卡或者自己的VOQ,因此CPU cache如果與之親和,勢必會更好。

為了不讓流水線單獨的處理過長,造成延遲增加,我傾向於將輸出邏輯放在一個單獨的線程中,如果CPU核心夠用,我還是傾向於將其綁在一個核心上,最好不要綁在和輸入處理的核心同一個上。那麼綁在哪個或者哪些上好呢?

我傾向於共享二級cache或者三級cache的CPU兩個核心分別負責網絡接收處理和網絡發送調度處理。這就形成了一種輸入輸出的本地接力。按照主板構造和一般的CPU核心封裝,可以用下圖所示的建議:

為什麼我不分析代碼實現第一,基於這樣的事實,我並沒有完全使用RPS的原生實現,而是對它進行了一些修正,我並沒有進行復雜的hash運算,我放寬了一些約束,目的是使得計算更加迅速,無狀態的東西根本不需要維護!

第二,我發現我逐漸看不懂我以前寫的代碼分析了,同時也很難看明白大批批的代碼分析的書,我覺得很難找到對應的版本和補丁,但是基本思想卻是完全一樣的。因此我比較傾向於整理出事件被處理的流程,而不是單純的去分析代碼。

聲明:本文是針對底端通用設備的最後補償,如果有硬件結合的方案,自然要忽略本文的做法。

Copyright © Linux教程網 All Rights Reserved