獨悲須要忍受,快樂須要分享
對Linux協議棧屢次perf的結果,我沒法忍受conntrack的性能,然而它的功能是如此強大,以致於我沒法 對其割捨,我想本身實現一個快速流表,可是我不得不拋棄依賴於conntrack的諸多功能,好比state match,Linux NAT等,誠然,我雖然對NAT也是抱怨太多,但無論怎樣,不是還有不少人在用它嗎。
曾經,我針對conntrack查找作過一個基於離線統計的優化,其思路很簡單,就使用動態的計算模式代替統一的hash算法。我事先會對通過該BOX的 全部五元組進行採樣記錄,而後離線分析這些數據,好比將五元組拼接成一個32源IP地址+32位目標IP地址+8位協議號+16位源端口+16位目標端口 的104位的長串(在個人實現中,我忽略了源端口,由於它是一個易變量,值得被我任性地忽略),而後根據hash桶的大小,好比說是N,以logN位爲一 個窗口大小在104位的串上滑動,找出相異數量最大的區間,以此區間爲模區間,這樣就能夠將數據流均勻分佈在各個hash桶中,若是數據流過多致使衝突鏈 表過長,能夠創建多維嵌套hash,把這個hash表倒過來看,它是多麼像一棵平衡N叉樹啊,N叉Trie樹不就是這回事嗎?這裏的hash函數就是「取 某些位」,這又一次展現了Trie與hash的統一。
以上的優化雖然優美,可是卻仍是複雜了,這個優化思路是我從硬件cache的設計思路中借鑑的。可是和硬件cache好比CPU cache相比,軟件的相似方式效果大打折扣,緣由在於軟件處理hash衝突的時候只能遍歷或者查找,而硬件卻能夠同時進行。請學校裏面的不要認爲是算法 不夠優越,這是物理本質決定的。硬件使用的是門電路,流動的是電流,而電流是像水流同樣並行連通的,軟件使用的邏輯,流動的是步驟,這就是算法,算法就是 一系列的邏輯步驟的組合,固然,也有不少複雜的所謂並行算法,可是據我所知,不少效果並很差,複雜帶來了更多的複雜,最終經不起繼續複雜,只好做罷,另 外,這麼簡單個事兒,搞複雜算法有點大炮打蒼蠅了。
面試
若是什麼東西和後續的處理速率不匹配,成爲了瓶頸,那麼就增長一個cache來平滑這種差別。CPU cache就是利用了這個思路。對於nf_conntrack的效率問題,咱們也應該使用相同的思路。可是具體怎麼作,仍是須要起碼的一些哪怕是定性的分析。
若是你用tcpdump抓包,就會發現,結果幾乎老是一連串連續被抓取的數據包屬於同一個五元組數據流,可是也不絕對,有時會有一個數據包插入到一個流中,一個很合理的抓包結果多是下面這個樣子:
數據流a 正方向
數據流a 正方向
數據流a 反方向
數據流a 正方向
數據流c 正方向
數據流a 反方向
數據流a 發方向
數據流b 反方向
數據流b 正方向
數據流 正方向
....
看 出規律了嗎?數據包到達BOX遵循非嚴格意義上的時間局部性,也就是屬於一個流的數據包會持續到達。至於空間局部性,不少人都說不明顯,可是若是你仔細分 析數據流a,b,c,d...的源/目標IP元組,你會發現它們的空間局部性,這是TCAM硬件轉發表設計的根本原則。TCAM中「取某些位」中「某些 位」說明這些位是空間上最分散的局部,這是一種對空間局部性的逆向運用,好比核心傳輸網上,你會發現大量的IP都是去往北美或者北歐的。
我本但願在本文中用數學和統計學來闡述這一規律,可是這個行爲實在不適合在一篇大衆博客中進行,當有人面試個人時候問到我這個問題,我也只能匆匆幾句話帶 過,而後若是須要,我會用電郵的方式來深刻解析,可是對於一篇博客,這種方式顯得賣弄了,並且會失去不少讀者,天然也就沒有人爲我提意見了。博客中最重要 的就是快速給出結果,也就是該怎麼作。言歸正傳。
若是說上述基於「空間局部性逆向利用」的「取某些位hash」的優化是原自「效率來自規則」這個定律的話,那麼規則的代價就是複雜化,這個複雜化讓我沒法 繼續。還有一個比這個定律更加普適的原則就是「效率來自簡單」,我喜歡簡單的東西和簡單的人,此次,我再次證實了個人正確。在繼續以前,我會先簡單描述一 下nf_conntrack的瓶頸到底在哪。
1.nf_conntrack的正反向tuple使用一個hash表,插入,刪除,修改操做須要全局的lock進行保護,這會同意大量的串行化操做。
2.nf_conntrack的hash表使用了jhash算法,這種算法操做步驟太多,若是conntrack數量少,hash操做將會消耗巨大的性能。
[Tips: 若是你瞭解密碼學中的DES/AES等對稱加密算法,就會明白,替換,倒置,異或操做可數據完成最佳混淆,使得輸出與輸出無關,從而達到最佳散列,然而這 效果的代價就是操做複雜化了,加解密效率問題多在此,這種操做是如此規則(各類盒)以致於徹底能夠用硬件電路實現,但是若是沒有這種硬件使用CPU的話, 這種操做是極其消耗CPU的,jhash也是如此,雖然不很。]
3.nf_conntrack表在多個CPU間是全局的,這會涉及到數據同步的問題,雖然能夠經過RCU最大限度緩解,但萬一有人寫它們呢。
算法
鑑於以上,逐步擊破,解決方案就有了。網絡
1.cache的構建基於每CPU一個,這徹底符合cache的本地化設計原則,CPU cache不也如此嗎。
2.cache儘量小,保存最有可能命中的數據流項,同時保證cache缺失的代價不至於過大。
tcp
3.創建一個合理的cache替換自適應原則,保證在位者謀其職,不思進取者自退位的原則ide
我 的設計思路就是以上這些,在逐步落實的過程當中,我起初只保留了一個cache項,也就是最後一次在conntrack hash表中被找到的那個項,這徹底符合時間局部性,然而在我測試的時候發現,若是網絡中有諸如ICMP這類慢速流的話,cache抖動會很是厲害,和 TCP流比起來,ICMP太慢,可是按照排隊原則,它終究會插隊到一個TCP流中間,形成cache替換,爲了不這種使人悲哀的狀況,我爲 conntrack項,即conn結構體加入了時間戳字段,每次hash查找到的時候,用當時的jiffers減去該時間戳字段,同時更新這個字段爲當前 jiffers,只有當這個差值小於一個預約值的時候,纔會執行cache替換,這個值能夠經過網絡帶寬加權得到。
可是這樣就完美了嗎?遠不!考慮到CPU cache的設計,我發現conntrack cache徹底不一樣,對於CPU,因爲虛擬內存機制,cache裏面保存的確定來自同一個進程的地址空間(不考慮更復雜的CPU cache原理...),所以除非發生分支跳轉或者函數調用,時間局部性是必定的。可是對於網絡數據包,徹底是排隊論統計決定的,全部的數據包的命名空間 就是全世界的IP地址集合,指不定哪一下子就會有任意流的數據包插入進來。最多見的一種狀況就是數據流切換,好比數據流a和數據流b的發送速率,通過的網 絡帶寬實力至關,它們頗有可能交替到達,或者間隔兩三個數據包交替到達,這種狀況下,你要照顧誰呢?這就是第三個原則:效率來自公平。
函數
所以,個人最終設計是如下的樣子:性能
1.cache是一個鏈表,該鏈表的長度是一個值得微調的參數
cache鏈表過短:流項頻繁在conntrack hash表和cache中跳動被替換。
cache鏈表太長:對待沒法命中cache的流項,cache缺失代價過高。
勝者原則:勝者通吃。凡是有的,還要加給他叫他多餘。沒有的,連他全部的也要奪過來。(《馬太福音》)均衡原則1-針對勝者:遍歷cache鏈表的時間不能比標準hash計算+遍歷衝突鏈表的時間更長(平均狀況)。
均衡原則2-針對敗者:若是遍歷了鏈表沒有命中,雖然損失了些不應損失的時間,可是把這種損失維持在一個能夠接受的範圍內。
測試
效果:數據流到達速率越快就越容易以極低的代價命中cache,數據流達到速率越慢越不容易命中cache,然而也不用付出高昂的代價。優化
2.基於時間戳的cache替換原則
只有連續的數據包到達時間間隔小於某個動態計算好的值的時候,纔會執行cache替換。
this
個人中間步驟測試代碼以下:
//修改net/netfilter/nf_conntrack_core.c //Email:marywangran@126.com //1.定義 #define A #ifdef A /* * MAX_CACHE動態計算原則: * cache鏈表長度 = 平均衝突鏈表長度/3, 其中: * 平均衝突鏈表長度 = net.nf_conntrack_max/net.netfilter.nf_conntrack_buckets * 3 = 經驗值 * */ #define MAX_CACHE 4 struct conntrack_cache { struct nf_conntrack_tuple_hash *caches[MAX_CACHE]; }; DEFINE_PER_CPU(struct conntrack_cache, conntrack_cache); #endif //2.修改resolve_normal_ct static inline struct nf_conn * resolve_normal_ct(struct net *net, struct sk_buff *skb, unsigned int dataoff, u_int16_t l3num, u_int8_t protonum, struct nf_conntrack_l3proto *l3proto, struct nf_conntrack_l4proto *l4proto, int *set_reply, enum ip_conntrack_info *ctinfo) { struct nf_conntrack_tuple tuple; struct nf_conntrack_tuple_hash *h; struct nf_conn *ct; #ifdef A int i; struct conntrack_cache *cache; #endif if (!nf_ct_get_tuple(skb, skb_network_offset(skb), dataoff, l3num, protonum, &tuple, l3proto, l4proto)) { pr_debug("resolve_normal_ct: Can't get tuple\n"); return NULL; } #ifdef A cache = &__get_cpu_var(conntrack_cache); rcu_read_lock(); if (0 /* 優化3 */) { goto slowpath; } for (i = 0; i < MAX_CACHE; i++) { struct nf_conntrack_tuple_hash *ch = cache->caches[i]; struct nf_conntrack_tuple_hash *ch0 = cache->caches[0]; if (ch && nf_ct_tuple_equal(&tuple, &ch->tuple)) { ct = nf_ct_tuplehash_to_ctrack(ch); if (unlikely(nf_ct_is_dying(ct) || !atomic_inc_not_zero(&ct->ct_general.use))) { h = NULL; goto slowpath; } else { if (unlikely(!nf_ct_tuple_equal(&tuple, &ch->tuple))) { nf_ct_put(ct); h = NULL; goto slowpath; } } /*************************************** 優化1簡介 *****************************************/ /* 並不是直接提高到第一個,而是根據兩次cache命中的間隔酌情提高,提高的步數與時間間隔成反比 */ /* 這就避免了cache隊列自己的劇烈抖動。事實上,命中的時間間隔若是能加權歷史間隔值,效果更好 */ /*******************************************************************************************/ /* * 基於時間局部性提高命中項的優先級 */ if (i > 0 /* && 優化1 */) { cache->caches[0] = ch; cache->caches[i] = ch0; } h = ch; } } ct = NULL; slowpath: rcu_read_unlock(); if (!h) #endif /* look for tuple match */ h = nf_conntrack_find_get(net, &tuple); if (!h) { h = init_conntrack(net, &tuple, l3proto, l4proto, skb, dataoff); if (!h) return NULL; if (IS_ERR(h)) return (void *)h; } #ifdef A else { int j; struct nf_conn *ctp; struct nf_conntrack_tuple_hash *chp; /*********************** 優化2簡介 **************************/ /* 只有連續兩個數據包到達的時間間隔小於n時纔會執行cache替換 */ /* 這是爲了不諸如ICMP之類的慢速流致使的cache抖動 */ /************************************************************/ if (0 /* 優化2 */) { goto skip; } /************************** 優化3簡介 *****************************/ /* 只有在總的conntrack數量大於hash bucket數量的4倍時才啓用cache */ /* 由於conntrack數量小的話,通過一次hash運算就能夠一次定位, */ /* 或者通過遍歷很短的衝突鏈表便可定位,使用cache反而下降了性能 */ /******************************************************************/ if (0 /* 優化3 */) { goto skip; } ct = nf_ct_tuplehash_to_ctrack(h); nf_conntrack_get(&ct->ct_general); chp = cache->caches[MAX_CACHE-1]; for (j = MAX_CACHE-1; j > 0; j--) { cache->caches[j] = cache->caches[j-1]; } cache->caches[0] = h; if (chp) { ctp = nf_ct_tuplehash_to_ctrack(chp); nf_conntrack_put(&ctp->ct_general); } } skip: if (!ct) { ct = nf_ct_tuplehash_to_ctrack(h); } #else ct = nf_ct_tuplehash_to_ctrack(h); #endif /* It exists; we have (non-exclusive) reference. */ if (NF_CT_DIRECTION(h) == IP_CT_DIR_REPLY) { *ctinfo = IP_CT_ESTABLISHED + IP_CT_IS_REPLY; /* Please set reply bit if this packet OK */ *set_reply = 1; } else { /* Once we've had two way comms, always ESTABLISHED. */ if (test_bit(IPS_SEEN_REPLY_BIT, &ct->status)) { pr_debug("nf_conntrack_in: normal packet for %p\n", ct); *ctinfo = IP_CT_ESTABLISHED; } else if (test_bit(IPS_EXPECTED_BIT, &ct->status)) { pr_debug("nf_conntrack_in: related packet for %p\n", ct); *ctinfo = IP_CT_RELATED; } else { pr_debug("nf_conntrack_in: new packet for %p\n", ct); *ctinfo = IP_CT_NEW; } *set_reply = 0; } skb->nfct = &ct->ct_general; skb->nfctinfo = *ctinfo; return ct; } //2.修改nf_conntrack_init int nf_conntrack_init(struct net *net) { int ret; #ifdef A int i; #endif if (net_eq(net, &init_net)) { ret = nf_conntrack_init_init_net(); if (ret < 0) goto out_init_net; } ret = nf_conntrack_init_net(net); if (ret < 0) goto out_net; if (net_eq(net, &init_net)) { /* For use by REJECT target */ rcu_assign_pointer(ip_ct_attach, nf_conntrack_attach); rcu_assign_pointer(nf_ct_destroy, destroy_conntrack); /* Howto get NAT offsets */ rcu_assign_pointer(nf_ct_nat_offset, NULL); } #ifdef A /* 初始化每CPU的conntrack cache隊列 */ for_each_possible_cpu(i) { int j; struct conntrack_cache *cache; cache = &per_cpu(conntrack_cache, i); for (j = 0; j < MAX_CACHE; j++) { cache->caches[j] = NULL; } } #endif return 0; out_net: if (net_eq(net, &init_net)) nf_conntrack_cleanup_init_net(); out_init_net: return ret; }
但願看到的人有機會測試一下。效果和疑問能夠直接發送到代碼註釋中所示的郵箱。