1. 程式人生 > >不要盲目增加ip_conntrack_max-理解Linux核心記憶體

不要盲目增加ip_conntrack_max-理解Linux核心記憶體

               

1.由ip_conntrack引出的Linux記憶體對映

有很多文章在討論關於ip_conntrack表爆滿之後丟棄資料包的問題,對此研究深入一些的知道Linux有個核心引數ip_conntrack_max,在擁有較大記憶體的機器中預設65536,於是瘋狂的增加這個引數,比如設定成10000…00,只要不報設定方面的錯誤,就一定要設定成最大值。這種方式實在是將軟體看成大神了,殊不知軟體的技術含量還不如鍋爐呢!       如果考慮的再全面一些,比如經驗豐富的程式設計師或者網管,可能會想到記憶體的問題,他們知道所有的連線跟蹤資訊都是保存於記憶體中的,因此會考慮單純放大這個ip_conntrack_max引數會佔據多少記憶體,會權衡記憶體的佔用,如果系統沒有太大的記憶體,就不會將此值設定的太高。       但是如果你的系統有很大的記憶體呢?比如有8G的記憶體,分個1G給連線跟蹤也不算什麼啊,這是合理的,然而在傳統的32位架構Linux中是做不到,為什麼?因為你可能根本不懂Linux核心的記憶體管理方式。       記憶體越來越便宜的今天,linux的記憶體對映方式確實有點過時了。然而事實就擺在那裡,ip_conntrack處於核心空間,它所需的記憶體必須對映到核心空間,而傳統的32位Linux記憶體對映方式只有1G屬於核心,這1G的地址空間中,前896M是和實體記憶體一一線性對映的,後面的若干空洞之後,有若干vmalloc的空間,這些vmalloc空間和一一對映空間相比,很小很小,算上4G封頂下面的很小的對映空間,一共可以讓核心使用的地址空間不超過1G。對於ip_conntrack來講,由於其使用slab分配器,因此它還必須使用一一對映的地址空間,這就是說,它最多隻能使用不到896M的記憶體!

       為何Linux使用如此“落後”的記憶體對映機制這麼多年還不改進?其實這種對核心空間記憶體十分苛刻的設計在64位架構下有了很大的改觀,然而問題依然存在,即使64位架構,核心也無法做到透明訪問所有的實體記憶體,它同樣需要把實體記憶體對映到核心地址空間後才能訪問,對於一一對映,這種對映是事先確定的,對於大小有限(實際上很小)非一一對映空間,需要動態建立頁表,頁目錄等。另外還有一個解釋,那就是“核心本來就不該做ip_conntrack這種事”,那是協議棧的事,而不巧的是,Liunx的協議棧完全在核心中實現,可能在skb接收軟中斷中處理的ip_conntrack不能睡眠,因此也就不能將此任務交給程序,也就不能利用程序地址空間(程序地址空間[使用者態+核心態]可以訪問所有的實體記憶體)。

       Linux之所以對核心記憶體要求如此苛刻,目的就是不想讓你隨意使用,因為它寶貴,你才更要珍惜它們。

2.在32位架構Linux系統上的實驗

以下是為了證明以上的事實所作的實驗,可能實驗中使用的一些手段仍然不符合常識,然而我覺得成一家之言即可,畢竟這種方案永遠不會也不可能出現在公司的標準文件上,那樣會讓人學會投機取巧或者稱偷懶,但是為了備忘,還得有個地方留著,那就寫成部落格吧。       還有一個引數會影響查詢連線跟蹤的時間複雜度和空間複雜度,那就是ip_conntrack_buckets。該值描述了雜湊桶的數量,理論上,這個值越大,雜湊碰撞就會越小,查詢時間就會越快,但是需要為每一個桶預分配一塊不是很大的記憶體,如果桶數量很大,就會佔用很大的記憶體,並且這些記憶體還都是寶貴的“僅有1G空間內的核心記憶體”,和ip_conntrack結構體的分配策略不同,這個雜湊桶可以分配在vmalloc空間而不一定非要在一一線性對映空間。

2.1.快速壓滿ip_conntrack的方法

使用loadrunner絕對是一種方式,然而術業有專攻,工作之餘我又很討厭windows上的一切,因此需要採用其它方式,下班在家,隻身一人,也不想使用netcat之類的“瑞士軍刀”,我怕端口占滿,又怕我的macbook狂熱,因此需要再想辦法。由於目的只是想測試ip_conntrack最多能佔用多少記憶體,其實這個我早就知道了,只是想證實一下子,那麼辦法也就有了,那就是增加ip_conntrack結構體的大小,而這很容易,只需要在結構體後面增加一個很大的欄位即可。下面的修改基於Red Hat Enterprise 5的2.6.18核心

2.2.測試前對ip_conntrack核心模組的修改

編輯$build/include/linux/netfilter_ipv4/ip_conntrack.h檔案,在結構體ip_conntrack的最後加上下面一句:
char aaa[102400]; //這個102400是通過二分法得到的,如果設定成2xxxxx則在載入的時候就會使核心crash,因為這個陣列是直接分配(類似棧上分配)的而不是動態分配的,它載入的時候很可能會沖掉核心的關鍵資料,因此還是選取一個可行的數值,然後慢慢加連線吧,畢竟擴大了起碼100000倍呢~~
進入$src/net/ipv4/netfilter,執行:
make –C /lib/modules/2.6.18-92.e15/build SUBDIRS=`pwd` modules
如此一來載入ip_conntrack.ko之後,核心日誌將打印出:ip_conntrack version 2.4 (8192 buckets, 65536 max) - 102628 bytes per conntrack由此看出ip_conntrack結構體已經增大了,這樣撐滿整個可用記憶體所需的網路連線壓力就大大減小了,也就不用什麼loadrunner之類的東西了。為了儘快撐滿可以使用的記憶體,還要將關於ip_conntrack的所有timeout設定的比較長,相當長:
sysctl -w net.ipv4.netfilter.ip_conntrack_generic_timeout=600000sysctl -w net.ipv4.netfilter.ip_conntrack_icmp_timeout=300000sysctl -w net.ipv4.netfilter.ip_conntrack_tcp_timeout_close=1000000sysctl -w net.ipv4.netfilter.ip_conntrack_tcp_timeout_time_wait=120000sysctl -w net.ipv4.netfilter.ip_conntrack_tcp_timeout_last_ack=300000sysctl -w net.ipv4.netfilter.ip_conntrack_tcp_timeout_close_wait=60000sysctl -w net.ipv4.netfilter.ip_conntrack_tcp_timeout_fin_wait=120000sysctl -w net.ipv4.netfilter.ip_conntrack_tcp_timeout_established=432000sysctl -w net.ipv4.netfilter.ip_conntrack_tcp_timeout_syn_recv=600000sysctl -w net.ipv4.netfilter.ip_conntrack_tcp_timeout_syn_sent=120000
這樣既有的一個流就會“永久保持”了,一直佔著ip_conntrack結構體不放,直到可用的記憶體溢位。       在載入了ip_conntrack模組之後,所有過往的資料包就會自動被追蹤,下面編寫以下指令碼:
for (( i=1; i<255; i++));do    for (( j=1; j<255; j++));    do        ping 192.168.$i.$j -c 1 -W 1        curl --connect-timeout 1 http://138.$i.$j.80/        curl --connect-timeout 1 http://38.$i.$j.80/        curl --connect-timeout 1 http://$i.1.$j.80/        curl --connect-timeout 1 http://$j.$i.9.8/    donedone

2.3.測試過程

本機配置:記憶體:3032M,free命令識別3003M執行上述指令碼,抽根菸,拉一脬(pao),得到下列的資料:封頂連線數:6149個使用記憶體:886M此時在本機ping 127.0.0.1也不通了,說明ip_conntrack已經達到了極限,同時由於在alloc ip_conntrack的地方插入了列印語句(肯定是一堆#號),核心列印了記憶體分配失敗的資訊。一共3G的記憶體,僅僅使用了886M(而且我不斷使用sysctl –w vm.drop_caches=3清楚cache),剩餘的都無法給ip_conntrack使用。為了使結果更有說服力,我在ip_conntrack模組的初始化函式中插入了下列程式碼:
for (j=0; I < 400; j++)    __get_free_pages(GFP_KERNEL, 8);
意思是我先佔去核心空間的400M記憶體,看看最終總的連線跟蹤數量是否也會減少相應的,得到資料如下:封頂連線數:3421個使用記憶體:879M可見,核心記憶體被額外佔據了,能分給ip_conntrack的就少了。更進一步,保持上述的__get_free_pages不變,再增加下列的程式碼:
for (j=0; I < 400; j++)    __get_free_pages(GFP_HIGHUSER, 8);
最終的結果如下:封頂連線數:3394個使用記憶體:1203M可見,HIGHUSER記憶體並不會怎麼影響核心記憶體,要知道使用者程序的記憶體幾乎都是使用這個HIGHUSER標識分配的。如果去掉GFP_KERNEL的分配,僅僅保留GFP_HIGHUSER的分配,得到下列結果:封頂連線數:6449個使用記憶體:1312M可見,HIGHUSER記憶體的分配盡力在高階進行,不會怎麼影響核心的一一對映空間。

2.4.測試結果

綜上所述,32位架構上Linux的ip_conntrack使用的記憶體只能在核心地址空間的一一對映區,換句話說,它只能使用實體記憶體的前896M,除掉ip_conntrack結構體的新增的char aaa[],也是這個結果,只不過要想壓滿所有的可用記憶體,不是很容易,需要動用幾臺機器以及loadrunner之類的壓力工具。

3.在64位架構Linux系統上做的實驗

MD!由於我大動了核心資料結構,載入模組沒有成功,至今仍在除錯,排錯,已幾個時辰有餘…

4.結論

最後,需要說明的是,ip_conntrack_max的初始值是核心根據你機器的記憶體計算出來的,包括ip_conntrack_buckets也是這樣算出來的,核心之所以設定這樣的初始值,那是經過精心測試的經驗值,因此除了非要改不可,不要去提高這個值。如果你的機器面臨大量連線,你提高了ip_conntrack_max的值,那麼代價就是佔用了大量可貴的核心記憶體,可能會引起其它的核心記憶體分配失敗,並且,一旦核心記憶體使用超過了核心記憶體空間對映的閥值,那麼系統會默默的丟棄你的資料包,而不會報出:table full, dropping packet error and solution之類的錯誤,這是可悲的一件事。

       作業系統核心影響協議棧行為帶來了一種錯覺:我有這麼多記憶體,為何不讓我使用?!事實上,不是你要使用,而是核心要使用,你可以控制的只是程序,對於核心,程式設計師是沒法控制的。當然你可以重新編譯,甚至修改核心,甚至修改ip_conntrack的記憶體分配方式,不再使用夥伴系統的slab記憶體,而是重定向一個userspace,然則,然則這個開發需要成本,一個人日幾百塊,沒有幾個公司願意花這筆錢。因此,最終的結論:不要盲目增加ip_conntrack_max。呼應了題目。

附:

1.關於GFP_XXX

GFP_ATOMIC:設定這個標識,在核心對映區域的緊急池分配,不成功則簡單返回NULL,不釋放其它記憶體,也就不kill程序,分配路徑不睡眠。ip_conntrack結構體就是使用這個標識分配的(它大多數處於軟中斷路徑)GFP_KERNEL:設定這個標識,在核心對映區域分配,不成功則嘗試釋放可以釋放的記憶體,嘗試呼叫oom_killer,可以睡眠GFP_HIGHUSER:設定這個標識,可以分配到所有的實體記憶體(排除很小一部分固定記憶體以及用於匯流排IO的記憶體,這個在x86上很明顯,具體參見/proc/iomem獲得詳細實體記憶體對映資訊)。一般而言,使用者態程序所需的記憶體都使用這個標識來分配,儘量使用核心“很難”使用的高階記憶體(1G以後的實體記憶體,核心還要動態對映,這要操作頁表),從而將前896M(32位架構)儘量留給核心使用    

2.關於ip_conntrack的雜湊

Linux的ip_conntrack使用了 jhash函式來進行雜湊計算,關於該函式的實現可以參考下面這個網址:http://burtleburtle.net/bob/hash/doobs.html作者的解釋很清晰