TCP_NODELAY 和 TCP_NOPUSH的解釋

出處:http://www.cnblogs.com/wajika/p/6573014.html

TCP_NODELAY 和 TCP_NOPUSH的解釋

一、問題的來源

今天看到 huoding 大哥分享的 lamp 面試題,其中一點提到了:

Nginx 有兩個配置項: TCP_NODELAY 和 TCP_NOPUSH ,請說明它們的用途及注意事項。

 

初看到這個題目時,感覺有點印象:

1、在nginx.conf 中確實有這兩項,記得就是配置on或者off,跟性能有關,但具體如何影響性能不太清楚

2、在之前看過的huoding另一篇將memcache的文章中,有提到過tcp DELAY算法,記得說是當tcp傳輸小於mss的包時不會立即發生,會緩衝一段時間,當之前發生的包被ack後才繼續發生緩衝中的小包。

 

二、問題的研究

1、從nginx模塊中來查看:

語法: tcp_nodelay on | off;

默認值:  tcp_nodelay on;

上下文: http, server, location

開啓或關閉nginx使用TCP_NODELAY選項的功能。 這個選項僅在將連接轉變爲長連接的時候才被啓用。(譯者注,在upstream發送響應到客戶端時也會啓用)。

 

語法: tcp_nopush on | off;

默認值: tcp_nopush off;

上下文: http, server, location

開啓或者關閉nginx在FreeBSD上使用TCP_NOPUSH套接字選項, 在Linux上使用TCP_CORK套接字選項。 選項僅在使用sendfile的時候纔開啓。 開啓此選項允許

在Linux和FreeBSD 4.*上將響應頭和正文的開始部分一起發送;

一次性發送整個文件。

 

從模塊指令的解釋中帶出來幾個問題:

(1)tcp_nodelay的功能是什麼?爲什麼只有在長連接的時候才啓用?Only included in keep-alive connections.

(2)tcp_nopush在unix上影響TCP_NOPUSH,在linux上影響TCP_CORK,但估計這只是不同系統上的命名區別,但作用是什麼?爲什麼只在sendfile中才啓用?This option is only available when using sendfile.

這些問題我們需要逐一解決...

 

 

2、tcp_nodelay的功能是什麼

Nagle和DelayedAcknowledgment的延遲問題

 

老實說,這個問題和Memcached沒有半毛錢關係,任何網絡應用都有可能會碰到這個問題,但是鑑於很多人在寫Memcached程序的時候會遇到這個問題,所以還是拿出來聊一聊,

在這之前我們先來看看Nagle和DelayedAcknowledgment的含義:

 

在網絡擁塞控制領域,我們知道有一個非常有名的算法叫做Nagle算法(Nagle algorithm),這是使用它的發明人John Nagle的名字來命名的,John Nagle在1984年首次用這個算法來嘗試解決福特汽車公司的網絡擁塞問題(RFC 896),該問題的具體描述是:如果我們的應用程序一次產生1個字節的數據,而這個1個字節數據又以網絡數據包的形式發送到遠端服務器,那麼就很容易導致網絡由於太多的數據包而過載。比如,當用戶使用Telnet連接到遠程服務器時,每一次擊鍵操作就會產生1個字節數據,進而發送出去一個數據包,所以,在典型情況下,傳送一個只擁有1個字節有效數據的數據包,卻要發費40個字節長包頭(即ip頭20字節+tcp頭20字節)的額外開銷,這種有效載荷(payload)利用率極其低下的情況被統稱之爲愚蠢窗口症候羣(Silly Window Syndrome)。可以看到,這種情況對於輕負載的網絡來說,可能還可以接受,但是對於重負載的網絡而言,就極有可能承載不了而輕易的發生擁塞癱瘓。

通俗來說

Nagle:

假如需要頻繁的發送一些小包數據,比如說1個字節,以IPv4爲例的話,則每個包都要附帶40字節的頭,也就是說,總計41個字節的數據裏,其中只有1個字節是我們需要的數據

爲了解決這個問題,出現了Nagle算法。它規定:如果包的大小滿足MSS,那麼可以立即發送,否則數據會被放到緩衝區,等到已經發送的包被確認了之後才能繼續發送。

通過這樣的規定,可以降低網絡裏小包的數量,從而提升網絡性能

 

再看看DelayedAcknowledgment:

假如需要單獨確認每一個包的話,那麼網絡中將會充斥着無數的ACK,從而降低了網絡性能。

爲了解決這個問題,DelayedAcknowledgment規定:不再針對單個包發送ACK,而是一次確認兩個包,或者在發送響應數據的同時捎帶着發送ACK,又或者觸發超時時間後再發送ACK。

通過這樣的規定,可以降低網絡裏ACK的數量,從而提升網絡性能

 

 

3、Nagle和DelayedAcknowledgment是如何影響性能的

Nagle和DelayedAcknowledgment雖然都是好心,但是它們在一起的時候卻會辦壞事。

如果一個 TCP 連接的一端啓用了 Nagle‘s Algorithm,而另一端啓用了 TCP Delayed Ack,而發送的數據包又比較小,則可能會出現這樣的情況:

發送端在等待接收端對上一個packet 的 Ack 才發送當前的 packet,而接收端則正好延遲了此 Ack 的發送,那麼這個正要被髮送的 packet 就會同樣被延遲

當然 Delayed Ack 是有個超時機制的,而默認的超時正好就是 40ms。 

 

現代的 TCP/IP 協議棧實現,默認幾乎都啓用了這兩個功能,你可能會想,按我上面的說法,當協議報文很小的時候,豈不每次都會觸發這個延遲問題?

事實不是那樣的。僅當協議的交互是發送端連續發送兩個 packet,然後立刻 read 的 時候纔會出現問題

 

現在讓我們假設某個應用程序發出了一個請求,希望發送小塊數據。我們可以選擇立即發送數據或者等待產生更多的數據然後再一次發送兩種策略

如果我們馬上發送數據,那麼交互性的以及客戶/服務器型的應用程序將極大地受益

例如,當我們正在發送一個較短的請求並且等候較大的響應時,相關過載與傳輸的數據總量相比就會比較低,而且,如果請求立即發出那麼響應時間也會快一些

以上操作可以通過設置套接字的TCP_NODELAY選項來完成,這樣就禁用了Nagle 算法。 

 

另外一種情況則需要我們等到數據量達到最大時才通過網絡一次發送全部數據這種數據傳輸方式有益於大量數據的通信性能,典型的應用就是文件服務器。

應用Nagle算法在這種情況下就會產生問題。但是,如果你正在發送大量數據,你可以設置TCP_CORK選項禁用Nagle化,其方式正好同 TCP_NODELAY相反(TCP_CORK 和 TCP_NODELAY 是互相排斥的)。

 

假設客戶端的請求發生需要等待服務端的應答後才能繼續發生下一包,即串行執行,

好比在用ab性能測試時只有一個併發做10k的壓力測試,測試地址返回的內容只有Hello world;ab發出的request需要等待服務器返回response時,才能發生下一個request;

此時ab只會發生一個get請求,請求的相關內容包含在header中;而服務器需要返回兩個數據,一個是response頭,另一個是html body;

 

服務器發送端發送的第一個 write 是不會被緩衝起來,而是立刻發送的(response header),

這時ab接收端收到對應的數據,但它還期待更多數據(html)才進行處理,所以不會往回發送數據,因此也沒機會把 Ack 給帶回去,根據Delayed Ack 機制, 這個 Ack 會被 Hold 住。

這時服務器發送端發送第二個包,而隊列裏還有未確認的數據包(response header),這個 packet(html) 會被緩衝起來。

此時,服務器發送端在等待ab接收端的 Ack;ab接收端則在 Delay 這個 Ack,所以都在等待,

直到ab接收端 Deplayed Ack 超時(40ms),此 Ack 被髮送回去,發送端緩衝的這個 packet(html) 纔會被真正送到接收端,

此時ab才接受到完整的數據,進行對應的應用層處理,處理完成後才繼續發生下一個request,因此服務器端纔會在read時出現40ms的阻塞。

 

 

4、tcp_nodelay爲什麼只在keep-alive才啓作用

TCP中的Nagle算法默認是啓用的,但是它並不是適合任何情況,對於telnet或rlogin這樣的遠程登錄應用的確比較適合(原本就是爲此而設計),但是在某些應用場景下我們卻又需要關閉它。

在Apache對HTTP持久連接(Keep-Alive,Prsistent-Connection)處理時凸現的奇數包&結束小包問題(The Odd/Short-Final-Segment Problem),

這是一個並的關係,即問題是由於已有奇數個包發出,並且還有一個結束小包(在這裏,結束小包並不是指帶FIN旗標的包,而是指一個HTTP請求或響應的結束包)等待發出而導致的。

 

我們來看看具體的問題詳情,以3個包+1個結束小包爲例,可能發生的發包情況:

服務器向客戶端發出兩個大包;客戶端在接受到兩個大包時,必須回覆ack;

接着服務器向客戶端發送一箇中包或小包,但服務器由於Delayed Acknowledgment並沒有馬上ack;

由於發生隊列中有未被ack的包,因此最後一個結束的小包被阻塞等待。

 

最後一個小包包含了整個響應數據的最後一些數據,所以它是結束小包,如果當前HTTP是非持久連接,那麼在連接關閉時,最後這個小包會立即發送出去,這不會出現問題;

但是,如果當前HTTP是持久連接(非pipelining處理,pipelining僅HTTP 1.1支持,nginx目前對pipelining的支持很弱,它必須是前一個請求完全處理完後才能處理後一個請求),

即進行連續的Request/Response、Request/Response、…,處理,那麼由於最後這個小包受到Nagle算法影響無法及時的發送出去

(具體是由於客戶端在未結束上一個請求前不會發出新的request數據,導致無法攜帶ACK而延遲確認,進而導致服務器沒收到客戶端對上一個小包的的確認導致最後一個小包無法發送出來),

導致第n次請求/響應未能結束,從而客戶端第n+1次的Request請求數據無法發出。

 

在http長連接中,服務器的發生類似於:Write-Write-Read,即返回response header、返回html、讀取下一個request

而在http短連接中,服務器的發生類似於:write-read-write-read,即返回處理結果後,就主動關閉連接,短連接中的close之前的小包會立即發生,不會阻塞 

 

我的理解是這樣的:因爲第一個 write 不會被緩衝,會立刻到達接收端,如果是 write-read-write-read 模式,此時接收端應該已經得到所有需要的數據以進行下一步處理。

接收端此時處理完後發送結果,同時也就可以把上一個packet 的 Ack 可以和數據一起發送回去,不需要 delay,從而不會導致任何問題。 

 

我做了一個簡單的試驗,註釋掉了 HTTP Body 的發送,僅僅發送 Headers, Content-Length 指定爲 0。

這樣就不會有第二個 write,變成了 write-read-write-read 模式。此時再用 ab 測試,果然沒有 40ms 的延遲了。

 

因此在短連接中並不存在小包阻塞的問題,而在長連接中需要做tcp_nodelay開啓。

 

 

5、那tcp_nopush又是什麼?

TCP_CORK選項的功能類似於在發送數據管道出口處插入一個“塞子”,使得發送數據全部被阻塞,直到取消TCP_CORK選項(即拔去塞子)或被阻塞數據長度已超過MSS纔將其發送出去。

 

選項TCP_NODELAY是禁用Nagle算法,即數據包立即發送出去,而選項TCP_CORK與此相反,可以認爲它是Nagle算法的進一步增強,即阻塞數據包發送,

具體點說就是:TCP_CORK選項的功能類似於在發送數據管道出口處插入一個“塞子”,使得發送數據全部被阻塞,

直到取消TCP_CORK選項(即拔去塞子)或被阻塞數據長度已超過MSS纔將其發送出去。

舉個對比示例,比如收到接收端的ACK確認後,Nagle算法可以讓當前待發送數據包發送出去,即便它的當前長度仍然不夠一個MSS,

但選項TCP_CORK則會要求繼續等待,這在前面的tcp_nagle_check()函數分析時已提到這一點,即如果包數據長度小於當前MSS &&((加塞 || …)|| …),那麼緩存數據而不立即發送:

 

在TCP_NODELAY模式下,假設有3個小包要發送,第一個小包發出後,接下來的小包需要等待之前的小包被ack,在這期間小包會合並,直到接收到之前包的ack後纔會發生;

而在TCP_CORK模式下,第一個小包都不會發生成功,因爲包太小,發生管道被阻塞,同一目的地的小包彼此合併後組成一個大於mss的包後,纔會被髮生

 

TCP_CORK選項“堵塞”特性的最終目的無法是爲了提高網絡利用率,既然反正是要發一個數據包(零窗口探測包),

如果有實際數據等待發送,那麼幹脆就直接發送一個負載等待發送數據的數據包豈不是更好?

 

我們已經知道,TCP_CORK選項的作用主要是阻塞小數據發送,所以在nginx內的用處就在對響應頭的發送處理上。

一般而言,處理一個客戶端請求之後的響應數據包括有響應頭和響應體兩部分,那麼利用TCP_CORK選項就能讓這兩部分數據一起發送:

 

假設我們需要等到數據量達到最大時才通過網絡一次發送全部數據,這種數據傳輸方式有益於大量數據的通信性能,典型的應用就是文件服務器。

應用Nagle算法在這種情況下就會產生問題。因爲TCP_NODELAY在發生小包時不再等待之前的包有沒有ack,網絡中會存在較多的小包,但這會影響網絡的傳輸能力;

但是,如果你正在發送大量數據,你可以設置TCP_CORK選項禁用Nagle化,其方式正好同 TCP_NODELAY相反(TCP_CORK 和 TCP_NODELAY 是互相排斥的)。

 

下面就讓我們仔細分析下其工作原理。 

假設應用程序使用sendfile()函數來轉移大量數據。應用協議通常要求發送某些信息來預先解釋數據,這些信息其實就是報頭內容。

典型情況下報頭很小,而且套接字上設置了TCP_NODELAY。有報頭的包將被立即傳輸,在某些情況下(取決於內部的包計數器),因爲這個包成功地被對方收到後需要請求對方確認。

這樣,大量數據的傳輸就會被推遲而且產生了不必要的網絡流量交換。

 

但是,如果我們在套接字上設置了TCP_CORK(可以比喻爲在管道上插入“塞子”)選項,具有報頭的包就會填補大量的數據,所有的數據都根據大小自動地通過包傳輸出去。

當數據傳輸完成時,最好取消TCP_CORK 選項設置給連接“拔去塞子”以便任一部分的幀都能發送出去。這同“塞住”網絡連接同等重要。 

 

總而言之,如果你肯定能一起發送多個數據集合(例如HTTP響應的頭和正文),那麼我們建議你設置TCP_CORK選項,這樣在這些數據之間不存在延遲。

能極大地有益於WWW、FTP以及文件服務器的性能,同時也簡化了你的工作。

 

 

6、sendfile

從技術角度來看,sendfile()是磁盤和傳輸控制協議(TCP)之間的一種系統呼叫,但是sendfile()還能夠用來在兩個文件夾之間移動數據。

在各種不同的操作系統上實現sendfile()都會有所不同,當然這種不同只是極爲細微的差別。通常來說,我們會假定所使用的操作系統是Linux核心2.4版本。

系統呼叫的原型有如下幾種:

ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count)

in_fd 是一種用來讀文件的文件描述符。

out_fd 是一種用來寫文件的描述符。

Offset 是一種指向被輸入文件變量位置的指針,sendfile()將會從它所指向的位置開始數據的讀取。

Count 表示的是兩個文件描述符之間數據拷貝的字節數。

 

sendfile()的威力在於,它爲大家提供了一種訪問當前不斷膨脹的Linux網絡堆棧的機制。

這種機制叫做“零拷貝(zero-copy)”,這種機制可以把“傳輸控制協議(TCP)”框架直接的從主機存儲器中傳送到網卡的緩存塊(network card buffers)中去。

 

爲了更好的理解“零拷貝(zero-copy)”以及sendfile(),讓我們回憶一下以前我們在傳送文件時所需要執行的那些步驟。

首先,一塊在用戶機器存儲器內用於數據緩衝的位置先被確定了下來。

然後,我們必須使用read()這條系統呼叫來把數據從文件中拷貝到前邊已經準備好的那個緩衝區中去。

(在通常的情況下,這個操做會把數據從磁盤上拷貝到操作系統的高速緩衝存儲器中去,然後纔會把數據從高速緩衝存儲器中拷貝至用戶空間中去,這種過程就是所謂的“上下文切換”。)

在完成了上述的那些步驟之後,我們得使用write()系統呼叫來將緩衝區中的內容發送到網絡上去,程序段如下所示:

intout_fd, intin_fd;

char buffer[BUFLEN];

/* unsubstantial code skipped for clarity */

read(in_fd, buffer, BUFLEN); /* syscall, make context switch */

write(out_fd, buffer, BUFLEN); /* syscall, make context switch */

 

操作系統核心不得不把所有的數據至少都拷貝兩次:先是從核心空間到用戶空間的拷貝,然後還得再從用戶空間拷貝回核心空間。

每一次操做都需要上下文切換(context-switch)的這個步驟,其中包含了許多複雜的高度佔用CPU的操作。

系統自帶的工具vmstat能夠用來在絕大多數UNIX以及與其類似的操作系統上顯示當前的“上下文切換(context-switch)”速率。

請看叫做“CS”的那一欄,有相當一部分的上下文切換是發生在取樣期間的。用不同類型的方式進行裝載可以讓使用者清楚的看到使用這些參數進行裝載時的不同效果。

 

在有了sendfile()零拷貝(zero-copy)之後,如果可能的話,通過使用直接存儲器訪問(Direct Memory Access)的硬件設備,數據從磁盤讀取到操作系統高速緩衝存儲器中會變得非常之迅速。

而TLB高速緩衝存儲器則被完整無缺的放在那裏,沒有充斥任何有關數據傳輸的文件。

應用軟件在使用sendfile() primitive的時候會有很高的性能表現,這是因爲系統呼叫沒有直接的指向存儲器,因此,就提高了傳輸數據的性能。

通常來說,要被傳輸的數據都是從系統緩衝存儲器中直接讀取的,其間並沒有進行上下文切換的操作,也沒有垃圾數據佔據高速緩衝存儲器。

因此,在服務器應用程序中使用sendfile()能夠顯著的減少對CPU的佔用。

 

 

 

TCP/IP網絡的數據傳輸通常建立在數據塊的基礎之上。從程序員的觀點來看,發送數據意味着發出(或者提交)一系列“發送數據塊”的請求。

在系統級,發送單個數據塊可以通過調用系統函數write() 或者sendfile() 來完成。

因爲在網絡連接中是由程序員來選擇最適當的應用協議,所以網絡包的長度和順序都在程序員的控制之下。同樣的,程序員還必須選擇這個協議在軟件中得以實現的方式。

TCP/IP協議自身已經有了多種可互操作的實現,所以在雙方通信時,每一方都有它自身的低級行爲,這也是程序員所應該知道的情況。

 

儘管有許多TCP選項可供程序員操作,而我們卻最關注如何處置其中的兩個選項,它們是TCP_NODELAY 和 TCP_CORK,這兩個選項都對網絡連接的行爲具有重要的作用。

許多UNIX系統都實現了TCP_NODELAY選項,但是,TCP_CORK則是Linux系統所獨有的而且相對較新;它首先在內核版本2.4上得以實現。

此外,其他UNIX系統版本也有功能類似的選項,值得注意的是,在某種由BSD派生的系統上的TCP_NOPUSH選項其實就是TCP_CORK的一部分具體實現。

 

 

三、總結

你的數據傳輸並不需要總是準確地遵守某一選項或者其它選擇。在那種情況下,你可能想要採取更爲靈活的措施來控制網絡連接:

在發送一系列當作單一消息的數據之前設置TCP_CORK,而且在發送應立即發出的短消息之前設置TCP_NODELAY。

 

如果需要提供網絡的傳輸效率,應該減少小包的傳輸,使用TCP_CORK來做彙總傳輸,在利用sendfile來提高效率;

但如果是交互性的業務,那應該讓任意小包可以快速傳輸,關閉Nagle算法,提高包的傳輸效率。

 

TCP_CORK優化了傳輸的bits效率,tcp_nodelay優化了傳輸的packet效率。

 

語法: tcp_nodelay on | off;

默認值:

tcp_nodelay on;

上下文: http, server, location

開啓或關閉nginx使用TCP_NODELAY選項的功能。 這個選項僅在將連接轉變爲長連接的時候才被啓用。(譯者注,在upstream發送響應到客戶端時也會啓用)。

 

語法: tcp_nopush on | off;

默認值:

tcp_nopush off;

上下文: http, server, location

開啓或者關閉nginx在FreeBSD上使用TCP_NOPUSH套接字選項, 在Linux上使用TCP_CORK套接字選項。 選項僅在使用sendfile的時候纔開啓。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章