2021-05-12 14:32:11
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運算,我放寬了一些約束,目的是使得計算更加迅速,無狀態的東西根本不需要維護!
第二,我發現我逐漸看不懂我以前寫的程式碼分析了,同時也很難看明白大批批的程式碼分析的書,我覺得很難找到對應的版本和修補程式,但是基本思想卻是完全一樣的。因此我比較傾向於整理出事件被處理的流程,而不是單純的去分析程式碼。
宣告:本文是針對底端通用裝置的最後補償,如果有硬體結合的方案,自然要忽略本文的做法。
本文永久更新連結地址:http://www.linuxidc.com/Linux/2015-07/119424.htm
相關文章