一、問題的來源
今天看到 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的時候才開啟。