本文題目有點大,但其實我只想描述一些我個人一直比較關注的特性,並且不會太詳細,跟往常一樣,主要是幫忙理清思路的,不會分析源碼。這主要是為了哪一天突然忘了的時候,一目十行掃一眼就能記憶當時的理解,不然寫的太細節了,自己都看不懂了。
Lockless TCP listener先 從TCP的syncookie說起,如果都能使用syncookie機制該有多好,但是不能,因為它會丟失很多選項協商信息,這些信息對TCP的性能至關 重要。TCP的syncookie主要是為了防止半連接的syn flood攻擊,超級多的節點發送大量的syn包,然後就不管了,而被攻擊的協議棧收到一個syn就會建立一個request,綁定在syn針對的 Listener的request隊列上。這會消耗很大的內存。 但是仔細想想,拋開選項協商不說,僅僅針對TCP的syn,synack而言,事實上TCP在3次握手過程,只需要查找一下Listener即可,只要它 存在,就可以直接根據syn包構造synack包了,根本就不用Listener了,要記住2次握手包的信息,有兩個辦法,第一個辦法就是 syncookie機制給encode並echo回去,等第3次握手ack來了之後,TCP會decode這個ack的序列號信息,構造子socket, 插入Listener的accept隊列,還有一種辦法就是在本地分配內存,記錄這個連接客戶端的信息,等第3次握手包ack到來之後,找到這個 request,構造子socket,插入Listener的accept隊列。 在4.4之前,一個request是屬於一個Listener的,也就是說一個Listener有一個request隊列,每構造一個request,都 要操作這個Listner本身,但是4.4內核給出了突破性的方法,就是基於這個request構造一個新的socket!插入到全局的socket哈希 表中,這個socket僅僅記錄一個它的Listener的輕引用即可。等到第3個握手包ack到來後,查詢socket哈希表,找到的將不再是 Listnener本身,而是syn包到來時構造的那個新socket了,這樣傳統的下面的邏輯就可以將Listener解放出了:傳統的TCP協議棧接收
sk=lookup(skb); lock_sk(sk); if(skisListener);then process_handshake(sk,skb); else process_data(skb); endif unlock_sk(sk);
可以看出,sk的lock期間,將是一個瓶頸,所有的握手邏輯將全部在lock期間處理。4.4內核改變了這一切,下面是新的邏輯:
sk=lookup_form_global(skb); if(skisListener);then rv=process_syn(skb); new_sk=build_synack_sk(skb,rv); new_sk.listener=sk; new_sk.state=SYNRECV; insert_sk_into_global(sk); send_synack(skb); gotodone; elseif(sk.state==SYNRECV);then listener=sk.lister; child_sk=build_child_sk(skb,sk); remove_sk_from_global(sk); add_sk_into_acceptq(listener,child_sk); fi lock_sk(sk); process_data(skb); unlock_sk(sk); done:
這個邏輯中,只需要細粒度lock具體的隊列就可以了,不需要lock整個socket了。對於syncookie邏輯更簡單,根本連SYNRECV socket都不用構造,只要保證有Listener即可! 這是周四早上蹲廁所的時候猛然看到的4.4新特性,當時就震驚了,這正是我在2014年偶然想到的,但是後來由於沒有環境就沒有跟進,如今已經並在 mainline了,不得不說這是一件好事。當時我的想法是依照一個syn包完全可以無視Listner而構造synack,需要協商的信息可以保存在別 的地方而不必非要和Listner綁定,這樣可以解放Listener的職責。但是我沒有想到再構造一個socket,與所有socket平行插入到同一 個socket哈希表中。 我覺得,4.4之前的邏輯是簡單明了的,不管是握手包和數據包,處理邏輯完全一致,但是4.4將代碼復雜化了,分離了那麼多的if-else...但是這 是不可避免的。事實上,syn構造的request本身就應該與Listener進行綁定,只是如果想到優化,代碼會變得復雜,但是如果在代碼本身下一番 功夫,代碼也會很好看,只是,我沒有那個能力,我代碼寫的不好。 這個Lockless的思想跟nf_conntrack的思想類似,但是我覺得conntrack對於related conn邏輯也可以這麼玩。
TCP listener的CPU親和力與REUSEPORT緊隨著Lockless TCP Listener而來的accept隊列的優化!眾所周知,一個Listener只有一個accept隊列,在多核環境下這個單一的隊列絕對是個瓶頸,一個高性能服務器怎麼可以忍受這樣! 其實這個問題早就被REUSEPORT解決了。REUSEPORT允許多個獨立的socket同時偵聽同一個IP/Port對,這對於當今的多隊列網卡, 多CPU環境絕對是個福音。但是,雖然路寬了,車道多了,沒有規則的話,性能反而下降,擁擠程度反而降級! 4.4內核為socket引入了一個SO_INCOMING_CPU選項,如果一個socket的該選項設置為n,意味著只有在n號cpu上處理協議棧邏 輯的執行流才可以將數據包插入這個socket。體現在代碼上,就是在compute_score上給與加分,也就是說,除了目標IP,目標端口,源 IP,源端口之外,cpu也成了一個匹配項目。 正如patch說明說的,此特性與REUSEPORT,多隊列網卡相結合,一定是一道美味佳肴!
新的基於流的多路徑路由選路以 前的時候,有路由cache,一個路由cache項就是一個帶有源信息的n元組信息,每一個數據包在匹配到FIB條目後都會建立一條cache項,後續的 查找首先去查找cache,因此都是基於流的。然而在路由cache下課後,多路徑選路變成了基於包的,這對於TCP這種協議而言肯定會造成亂序問題。為 此4.4內核在多路徑選路的時候,hash計算中引入了源信息,避免了這個問題。只要計算方法不變,永遠一個流的數據hash到一個dst。
攜帶version number的socket路由緩存這 個不是4.4內核攜帶的特性,是我自己的一些想法。early_demux已經被引入了內核,旨在消除本機入流量的路由查找,畢竟路由查找後還要再 socket查找,為何不直接socket查找呢?查找到的結果緩存路由信息。對於本機提供服務的設備而言,開啟這個選項吧。 但是對於出流量,還是會有很大的開銷浪費在路由查找上。雖然IP是無連接的,但是TCP socket或者一個connected UDP socket卻是可以明確標示一個5元組的,如果把路由信息存儲在socket中,是不是更好的。好吧!很多人會問,怎麼解決同步問題,路由表改了怎麼 辦,要notify socket嗎?如果你被此引導而去設計一個“高效的同步協議”,你就輸了!辦法很簡單,就是引入兩個計數器-緩存計數器和全局計數器,socket的路 由緩存如下:
sk_rt_cache{ atomic_tversion; dst_entry*dst; };
全局計數器如下:
atomic_tgversion;
每當socket設置路由緩存的時候,讀取全局gversion的值,設置進緩存version,每當路由發生任何改變的時候,全局gversion計數器遞增。如果cache計數器的值與全局計數器值一致,就可用,否則不可用,當然,dst本身也要由引用計數保護。