當伺服器的並發TCP串連數以十萬計時,我們就會對一個TCP串連在作業系統核心上消耗的記憶體多少感興趣。socket編程方法提供了SO_SNDBUF、SO_RCVBUF這樣的介面來設定串連的讀寫緩衝,linux上還提供了以下系統級的配置來整體設定伺服器上的TCP記憶體使用量,但這些配置看名字卻有些互相衝突、概念模糊的感覺,如下(sysctl -a命令可以查看這些配置):
net.ipv4.tcp_rmem = 8192 87380 16777216net.ipv4.tcp_wmem = 8192 65536 16777216net.ipv4.tcp_mem = 8388608 12582912 16777216net.core.rmem_default = 262144net.core.wmem_default = 262144net.core.rmem_max = 16777216net.core.wmem_max = 16777216
還有一些較少被提及的、也跟TCP記憶體相關的配置:
net.ipv4.tcp_moderate_rcvbuf = 1net.ipv4.tcp_adv_win_scale = 2
(註:為方便下文講述,介紹以上系統配置時首碼省略掉,配置值以空格分隔的多個數字以數組來稱呼,例如tcp_rmem[2]表示上面第一行最後一列16777216。)
網上可以找到很多這些系統配置項的說明,然而往往還是讓人費解,例如,tcp_rmem[2]和rmem_max似乎都跟接收緩衝最大值有關,但它們卻可以不一致,究竟有什麼區別。或者tcp_wmem[1]和wmem_default似乎都表示發送緩衝的預設值,衝突了怎麼辦。在用抓包軟體抓到的syn握手包裡,為什麼TCP接收視窗大小似乎與這些配置完全沒關係。
TCP串連在進程中使用的記憶體大小千變萬化,通常程式較複雜時可能不是直接基於socket編程,這時平台級的組件可能就封裝了TCP串連使用到的使用者態記憶體。不同的平台、組件、中介軟體、網路程式庫都大不相同。而核心態為TCP串連分配記憶體的演算法則是基本不變的,這篇文章將試圖說明TCP串連在核心態中會使用多少記憶體,作業系統使用怎樣的策略來平衡宏觀的輸送量與微觀的某個串連傳輸速度。這篇文章也將一如既往的面嚮應用程式開發人員,而不是系統級的核心開發人員,所以,不會詳細的介紹為了一個TCP串連、一個TCP報文作業系統分配了多少位元組的記憶體,核心級的資料結構也不是本文的關注點,這些也不是應用級程式員的關注點。這篇文章主要描述linux核心為了TCP串連上傳輸的資料是怎樣管理讀寫緩衝的。
一、緩衝上限是什麼。
(1)先從應用程式編程時可以設定的SO_SNDBUF、SO_RCVBUF說起。
無論何種語言,都對TCP串連提供基於setsockopt方法實現的SO_SNDBUF、SO_RCVBUF,怎麼理解這兩個屬性的意義呢。 SO_SNDBUF、SO_RCVBUF都是個體化的設定,即,只會影響到設定過的串連,而不會對其他串連生效。SO_SNDBUF表示這個串連上的核心寫緩衝上限。實際上,進程設定的SO_SNDBUF也並不是真的上限,在核心中會把這個值翻一倍再作為寫緩衝上限使用,我們不需要糾結這種細節,只需要知道,當設定了SO_SNDBUF時,就相當於劃定了所操作的TCP串連上的寫緩衝能夠使用的最大記憶體。然而,這個值也不是可以由著進程隨意設定的,它會受制於系統級的上下限,當它大於上面的系統配置wmem_max(net.core.wmem_max)時,將會被wmem_max替代(同樣翻一倍);而當它特別小時,例如在2.6.18核心中設計的寫緩衝最小值為2K位元組,此時也會被直接替代為2K。
SO_RCVBUF表示串連上的讀緩衝上限,與SO_SNDBUF類似,它也受制於rmem_max配置項,實際在核心中也是2倍大小作為讀緩衝的使用上限。SO_RCVBUF設定時也有下限,同樣在2.6.18核心中若這個值小於256位元組就會被256所替代。
(2)那麼,可以設定的SO_SNDBUF、SO_RCVBUF緩衝使用上限與實際記憶體到底有怎樣的關係呢。
TCP串連所用記憶體主要由讀寫緩衝決定,而讀寫緩衝的大小隻與實際使用情境有關,在實際使用未達到上限時,SO_SNDBUF、SO_RCVBUF是不起任何作用的。對讀緩衝來說,接收到一個來自串連對端的TCP報文時,會導致讀緩衝增加,當然,如果加上報文大小後讀緩衝已經超過了讀緩衝上限,那麼這個報文會被丟棄從而讀緩衝大小維持不變。什麼時候讀緩衝使用的記憶體會減少呢。當進程調用read、recv這樣的方法讀取TCP流時,讀緩衝就會減少。因此,讀緩衝是一個動態變化的、實際用到多少才分配多少的緩衝記憶體,當這個串連非常空閑時,且使用者進程已經把串連上接收到的資料都消費了,那麼讀緩衝使用記憶體就是0。
寫緩衝也是同樣道理。當使用者進程調用send或者write這樣的方法發送TCP流時,就會造成寫緩衝增大。當然,如果寫緩衝已經到達上限,那麼寫緩衝維持不變,向使用者進程返回失敗。而每當接收到TCP串連對端發來的ACK確認了報文的成功發送時,寫緩衝就會減少,這是因為TCP的可靠性決定的,發出去報文後由於擔心報文丟失而不會銷毀它,可能會由重發定時器來重發報文。因此,寫緩衝也是動態變化的,閒置正常串連上,寫緩衝所用記憶體通常也為0。
因此,只有當接收網路報文的速度大於應用程式讀取報文的速度時,可能使讀緩衝達到了上限,這時這個緩衝使用上限才會起作用。所起作用為:丟棄掉新收到的報文,防止這個TCP串連消耗太多的伺服器資源。同樣,當應用程式發送報文的速度大於接收對方確認ACK報文的速度時,寫緩衝可能達到上限,從而使send這樣的方法失敗,核心不為其分配記憶體。
二、緩衝的大小與TCP的滑動視窗到底有什麼關係。
(1)滑動視窗的大小與緩衝大小肯定是有關的,但卻不是一一對應的關係,更不會與緩衝上限具有一一對應的關係。因此,網上很多資料介紹rmem_max等配置設定了滑動視窗的最大值,與我們tcpdump抓包時看到的win視窗值完全不一致,是講得通的。下面我們來細探其分別在哪裡。
讀緩衝的作用有2個:1、將無序的、落在接收滑動視窗內的TCP報文緩衝起來;2、當有序的、可以供應用程式讀取的報文出現時,由於應用程式的讀取是延時的,所以會把待應用程式讀取的報文也儲存在讀緩衝中。所以,讀緩衝一分為二,一部分緩衝無序報文,一部分緩衝待延時讀取的有序報文。這兩部分緩衝大小之和由於受制於同一個上限值,所以它們是會互相影響的,當應用程式讀取速率過慢時,這塊過大的應用緩衝將會影響到通訊端緩衝,使接收滑動視窗縮小,從而通知串連的對端降低發送速度,避免無謂的網路傳輸。當應用程式長時間不讀取資料,造成應用緩衝將通訊端緩衝擠壓到沒空間,那麼串連對端會收到接收視窗為0的通知,告訴對方:我現在消化不了更多的報文了。
反之,接收滑動視窗也是一直在變化的,我們用tcpdump抓三向交握的報文:
14:49:52.421674 IP houyi-vm02.dev.sd.aliyun.com.6400 > r14a02001.dg.tbsite.net.54073: S 2736789705:2736789705(0) ack 1609024383 win 5792 <mss 1460,sackOK,timestamp 2925954240 2940689794,nop,wscale 9>
可以看到初始的接收視窗是5792,當然也遠小於最大接收緩衝(稍後介紹的tcp_rmem[1])。 這當然是有原因的,TCP協議需要考慮複雜的網路環境,所以使用了慢啟動、擁塞視窗(參見 高效能網路編程2----TCP訊息的發送 ),建立串連時的初始視窗並不會按照接收緩衝的最大值來初始化。這是因為,過大的初始視窗從宏觀角度,對整個網路可能造成過載引發惡性迴圈,也就是考慮到鏈路上各環節的諸多路由器、交換器可能扛不住壓力不斷的丟包(特別是廣域網路),而微觀的TCP串連的雙方卻只按照自己的讀緩衝上限作為接收視窗,這樣雙方的發送視窗(對方的接收視窗)越大就對網路產生越壞的影響。慢啟動就是使初始視窗盡量的小,隨著接收到對方的有效報文,確認了網路的有效傳輸能力後,才開始增大接收視窗。
不同的linux核心有著不同的初始視窗,我們以廣為使用的linux2.6.18核心為例,在乙太網路裡,MSS大小為1460,此時初始視窗大小為4倍的MSS,簡單列下代碼(*rcv_wnd即初始接收視窗):
int init_cwnd = 4; if (mss > 1460*3) init_cwnd = 2; else if (mss > 1460) init_cwnd = 3; if (*rcv_wnd > init_cwnd*mss) *rcv_wnd = init_cwnd*mss;
大家可能要問,為何上面的抓包上顯示視窗其實是5792,並不是1460*4為5840呢。這是因為1460想表達的意義是:將1500位元組的MTU去除了20位元組的IP頭、20位元組的TCP頭以後,一個最大報文能夠承載的有效資料長度。但有些網路中,會在TCP的可選頭部裡,使用12位元組作為時間戳記使用,這樣,有效資料就是MSS再減去12,初始視窗就是(1460-12)*4=5792,這與視窗想表達的含義是一致的,即:我能夠處理的有效資料長度。
在linux3以後的版本中,初始視窗調整到了10個MSS大小,這主要來自於GOOGLE的建議。原因是這樣的,接收視窗雖然常以指數方式來快速增加視窗大小(擁塞閥值以下是指數增長的,閥值以上進入擁塞避免階段則為線性增長,而且,擁塞閥值自身在收到128以上資料報文時也有機會快速增加),若是傳輸視頻這樣的大資料,那麼隨著視窗增加到(接近)最大讀緩衝後,就會“開足馬力”傳輸資料,但若是通常都是幾十KB的網頁,那麼過小的初始視窗還沒有增加到合適的視窗時,串連就結束了。這樣相比較大的初始視窗,就使得使用者需要更多的時間(RTT)才能傳輸完資料,體驗不好。
那麼這時大家可能有疑問,當視窗從初始視窗一路擴張到最大接收視窗時,最大接收視窗就是最大讀緩衝嗎。 不是,因為必須分一部分緩衝用於應用程式的延時報文讀取。到底會分多少出來呢。這是可配的系統選項,如下:
net.ipv4.tcp_adv_win_scale = 2
這裡的 tcp_adv_win_scale意味著,將要拿出1/(2^ tcp_adv_win_scale )緩衝出來做應用緩衝。即,預設 tcp_adv_win_scale配置為2時,就是拿出至少1/4的記憶體用於應用讀緩衝,那麼,最大的接收滑動視窗的大小隻能到達讀緩衝的3/4。
(2)最大讀緩衝到底應該設定到多少為合適呢。
當應用緩衝所佔的份額通過tcp_adv_win_scale配置確定後,讀緩衝的上限應當由最大的TCP接收視窗決定。初始視窗可能只有4個或者10個MSS,但在無丟包情形下隨著報文的互動視窗就會增大,當視窗過大時,“過大”是什麼意思呢。即,對於通訊的兩台機器的記憶體而言不算大,但是對於整個網路負載來說過大了,就會對網路裝置引發惡性迴圈,不斷的因為繁忙的網路裝置造成丟包。而視窗過小時,就無法充分的利用網路資源。所以,一般會以BDP來設定最大接收視窗(可計算出最大讀緩衝)。BDP叫做頻寬時延積,也就是頻寬與網路時延的乘積,例如若我們的頻寬為2Gbps,時延為10ms,那麼頻寬時延積BDP則為2G/8*0.01=2.5MB,所以這樣的網路中可以設最大接收視窗為2.5MB,這樣最大讀緩衝可以設為4/3*2.5MB=3.3MB。
為什麼呢。因為BDP就表示了網路承載能力,最大接收視窗就表示了網路承載能力內可以不經確認發出的報文。如下圖所示:
經常提及的所謂長肥網路,“長”就是是時延長,“肥”就是頻寬大,這兩者任何一個大時,BDP就大,都應導致最大視窗增大,進而導致讀緩衝上限增大。所以在長肥網路中的伺服器,緩衝上限都是比較大的。(當然,TCP原始的16位長度的數字表示視窗雖然有上限,但在RFC1323中定義的彈性滑動視窗使得滑動視窗可以擴充到足夠大。)
發送視窗實際上就是TCP串連對方的接收視窗,所以大家可以按接收視窗來推斷,這裡不再囉嗦。
三、linux的TCP緩衝上限自動調整策略 那麼,設定好最大緩衝限制後就高枕無憂了嗎。對於一個TCP串連來說,可能已經充分利用網路資源,使用大視窗、大緩衝來保持高速傳輸了。比如在長肥網路中,緩衝上限可能會被設定為幾十MB,但系統的總記憶體卻是有限的,當每一個串連都全速飛奔使用到最大視窗時,1萬個串連就會佔用記憶體到幾百G了,這就限制了高並發情境的使用,公平性也得不到保證。我們希望的情境是,在並發串連比較少時,把緩衝限制放大一些,讓每一個TCP串連開足馬力工作;當並發串連很多時,此時系統記憶體資源不足,那麼就把緩衝限制縮小一些,使每一個TCP串連的緩衝盡量的小一些,以容納更多的串連。
linux為了實現這種情境,引入了自動調整記憶體配置的功能,由tcp_moderate_rcvbuf配置決定,如下: net.ipv4.tcp_moderate_rcvbuf = 1 預設tcp_moderate_rcvbuf配置為1,表示開啟了TCP記憶體自動調整功能。若配置為0,這個功能將不會生效(慎用)。
另外請注意:當我們在編程中對串連設定了SO_SNDBUF、SO_RCVBUF,將會使linux核心不再對這樣的串連執行自動調整功能。
那麼,這個功能到底是怎樣起作用的呢。看以下配置:
net.ipv4.tcp_rmem = 8192 87380 16777216net.ipv4.tcp_wmem = 8192 65536 16777216net.ipv4.tcp_mem = 8388608 12582912 16777216
tcp_rmem[3]數組表示任何一個TCP串連上的讀緩衝上限,其中 tcp_rmem[0]表示最小上限, tcp_rmem[1]表示初始上限(注意,它會覆蓋適用於所有協議的rmem_default配置), tcp_rmem[2]表示最大上限。 tcp_wmem[3]數組表示寫緩衝,與tcp_rmem[3]類似,不再贅述。
tcp_mem[3]數組就用來設定TCP記憶體的整體使用狀況,所以它的值很大(它的單位也不是位元組,而是頁--4K或者8K等這樣的單位。)。這3個值定義了TCP整體記憶體的無壓力值、壓力模式開啟閥值、最大使用值。以這3個值為標記點則記憶體共有4種情況:
1、當TCP整體記憶體小於tcp_mem[0]時,表示系統記憶體總體無壓力。若之前記憶體曾經超過了tcp_mem[1]使系統進入記憶體壓力模式,那麼此時也會把壓力模式關閉。 這種情況下,只要TCP串連使用的緩衝沒有達到上限(注意,雖然初始上限是tcp_rmem[1],但這個值是可變的,下文會詳述),那麼新記憶體的分配一定是成功的。
2、當TCP記憶體在tcp_mem[0]與tcp_mem[1]之間時,系統可能處於記憶體壓力模式,例如總記憶體剛從tcp_mem[1]之上下來;也可能是在非壓力模式下,例如總記憶體剛從tcp_mem[0]以下上來。 此時,無論是否在壓力模式下,只要TCP串連所用緩衝未超過tcp_rmem[0]或者tcp_wmem[0],那麼都一定都能成功分配新記憶體。否則,基本上就會面臨分配失敗的狀況。(注意:還有一些例外情境允許分配記憶體成功,由於對於我們理解這幾個配置項意義不大,故略過。)
3、當TCP記憶體在tcp_mem[1]與tcp_mem[2]之間時,系統一定處於系統壓力模式下。其他行為與上同。
4、當TCP記憶體在tcp_mem[2]之上時,毫無疑問,系統一定在壓力模式下,而且此時所有的新TCP緩衝分配都會失敗。
下圖為需要新緩衝時核心的簡化邏輯:
當系統在非壓力模式下,上面我所說的每個串連的讀寫緩衝上限,才有可能增加,當然最大也不會超過tcp_rmem[2]或者tcp_wmem[2]。相反,在壓力模式下,讀寫緩衝上限則有可能減少,雖然上限可能會小於tcp_rmem[0]或者tcp_wmem[0]。
所以,粗略的總結下,對這3個數組可以這麼看: 1、只要系統TCP的總體記憶體超了 tcp_mem[2] ,新記憶體配置都會失敗。 2、tcp_rmem[0]或者tcp_wmem[0]優先順序也很高,只要條件1不超限,那麼只要串連記憶體小於這兩個值,就保證新記憶體配置一定成功。 3、只要總體記憶體不超過tcp_mem[0],那麼新記憶體在不超過串連緩衝的上限時也能保證分配成功。 4、tcp_mem[1]與tcp_mem[0]構成了開啟、關閉記憶體壓力模式的開關。在壓力模式下,串連緩衝上限可能會減少。在非壓力模式下,串連緩衝上限可能會增加,最多增加到tcp_rmem[2]或者tcp_wmem[2]。