本文題目有點大,但其實我只想描述一些我我的一直比較關注的特性,而且不會太詳細,跟往常同樣,主要是幫忙理清思路的,不會分析源碼。這主要是爲了哪一天忽然忘了的時候,一目十行掃一眼就能記憶當時的理解,否則寫的太細節了,本身都看不懂了。
緩存
先 從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 (sk is Listener); then process_handshake(sk, skb); else process_data(skb); endif unlock_sk(sk);
能夠看出,sk的lock期間,將是一個瓶頸,全部的握手邏輯將所有在lock期間處理。4.4內核改變了這一切,下面是新的邏輯:
cookie
sk = lookup_form_global(skb); if (sk is Listener); 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); goto done; else if (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邏輯也能夠這麼玩。
less
緊隨着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,多隊列網卡相結合,必定是一道美味佳餚!
socket
以 前的時候,有路由cache,一個路由cache項就是一個帶有源信息的n元組信息,每個數據包在匹配到FIB條目後都會創建一條cache項,後續的 查找首先去查找cache,所以都是基於流的。然而在路由cache下課後,多路徑選路變成了基於包的,這對於TCP這種協議而言確定會形成亂序問題。爲 此4.4內核在多路徑選路的時候,hash計算中引入了源信息,避免了這個問題。只要計算方法不變,永遠一個流的數據hash到一個dst。
ide
這 個不是4.4內核攜帶的特性,是我本身的一些想法。early_demux已經被引入了內核,旨在消除本機入流量的路由查找,畢竟路由查找後還要再 socket查找,爲什麼不直接socket查找呢?查找到的結果緩存路由信息。對於本機提供服務的設備而言,開啓這個選項吧。
可是對於出流量,仍是會有很大的開銷浪費在路由查找上。雖然IP是無鏈接的,可是TCP socket或者一個connected UDP socket倒是能夠明確標示一個5元組的,若是把路由信息存儲在socket中,是否是更好的。好吧!不少人會問,怎麼解決同步問題,路由表改了怎麼 辦,要notify socket嗎?若是你被此引導而去設計一個「高效的同步協議」,你就輸了!辦法很簡單,就是引入兩個計數器-緩存計數器和全局計數器,socket的路 由緩存以下:
性能
sk_rt_cache { atomic_t version; dst_entry *dst; };
全局計數器以下:
優化
atomic_t gversion;
每當socket設置路由緩存的時候,讀取全局gversion的值,設置進緩存version,每當路由發生任何改變的時候,全局gversion計數器遞增。若是cache計數器的值與全局計數器值一致,就可用,不然不可用,固然,dst自己也要由引用計數保護。
ui