一、nginx之tcp_nopush、tcp_nodelay、sendfile
1、TCP_NODELAY
你怎么可以強制 socket 在它的緩沖區里發送數據?
一個解決方案是 TCP 堆棧的 TCP_NODELAY選項。這樣就可以使緩沖區中的數據立即發送出去。
Nginx的 TCP_NODELAY 選項使得在打開一個新的 socket 時增加了TCP_NODELAY選項。但這時會造成一種情況:
終端應用程序每產生一次操作就會發送一個包,而典型情況下一個包會擁有一個字節的數據以及40個字節長的包頭,於是產生4000%的過載,很輕易地就能令網絡發生擁塞。為了避免這種情況,TCP堆棧實現了等待數據 0.2秒鍾,因此操作后它不會發送一個數據包,而是將這段時間內的數據打成一個大的包。這一機制是由Nagle算法保證。
Nagle化后來成了一種標准並且立即在因特網上得以實現。它現在已經成為默認配置了,但有些場合下把這一選項關掉也是合乎需要的。現在假設某個應用程序發出了一個請求,希望發送小塊數據。我們可以選擇立即發送數據或者等待產生更多的數據然后再一次發送兩種策略。
如果我們馬上發送數據,那么交互性的以及客戶/服務器型的應用程序將極大地受益。如果請求立即發出那么響應時間也會快一些。以上操作可以通過設置套接字的 TCP_NODELAY = on 選項來完成,這樣就禁用了Nagle 算法。(不需要等待0.2s)
2、TCP_NOPUSH
在 nginx 中,tcp_nopush 配置和 tcp_nodelay “互斥”。它可以配置一次發送數據的包大小。也就是說,它不是按時間累計 0.2 秒后發送包,而是當包累計到一定大小后就發送。
注:在 nginx 中,tcp_nopush 必須和 sendfile 搭配使用。
3、sendfile
現在流行的web 服務器里面都提供 sendfile選項用來提高服務器性能,那到底 sendfile是什么,怎么影響性能的呢?
sendfile實際上是 Linux2.0+以后的推出的一個系統調用,web服務器可以通過調整自身的配置來決定是否利用 sendfile這個系統調用。先來看一下不用 sendfile的傳統網絡傳輸過程:
read(file,tmp_buf, len);
write(socket,tmp_buf, len);
硬盤 >> kernel buffer >> user buffer>> kernel socket buffer >>協議棧
1)一般來說一個網絡應用是通過讀硬盤數據,然后寫數據到socket 來完成網絡傳輸的。上面2行用代碼解釋了這一點,不過上面2行簡單的代碼掩蓋了底層的很多操作。來看看底層是怎么執行上面2行代碼的:
- 系統調用 read()產生一個上下文切換:從 user mode 切換到 kernel mode,然后 DMA 執行拷貝,把文件數據從硬盤讀到一個 kernel buffer 里。
- 數據從 kernel buffer拷貝到 user buffer,然后系統調用 read() 返回,這時又產生一個上下文切換:從kernel mode 切換到 user mode。
- 系統調用write()產生一個上下文切換:從 user mode切換到 kernel mode,然后把步驟2讀到 user buffer的數據拷貝到 kernel buffer(數據第2次拷貝到 kernel buffer),不過這次是個不同的 kernel buffer,這個 buffer和 socket相關聯。
- 系統調用 write()返回,產生一個上下文切換:從 kernel mode 切換到 user mode(第4次切換了),然后 DMA 從 kernel buffer拷貝數據到協議棧(第4次拷貝了)。
上面4個步驟有4次上下文切換,有4次拷貝,我們發現如果能減少切換次數和拷貝次數將會有效提升性能。在kernel2.0+ 版本中,系統調用 sendfile() 就是用來簡化上面步驟提升性能的。sendfile() 不但能減少切換次數而且還能減少拷貝次數。
2)再來看一下用 sendfile()來進行網絡傳輸的過程:
sendfile(socket,file, len);
硬盤 >> kernel buffer (快速拷貝到kernelsocket buffer) >>協議棧
- 系統調用sendfile()通過 DMA把硬盤數據拷貝到 kernel buffer,然后數據被 kernel直接拷貝到另外一個與 socket相關的 kernel buffer。這里沒有 user mode和 kernel mode之間的切換,在 kernel中直接完成了從一個 buffer到另一個 buffer的拷貝。
- DMA 把數據從 kernelbuffer 直接拷貝給協議棧,沒有切換,也不需要數據從 user mode 拷貝到 kernel mode,因為數據就在 kernel 里。
步驟減少了,切換減少了,拷貝減少了,自然性能就提升了。這就是為什么說在Nginx 配置文件里打開 sendfile on 選項能提高 web server性能的原因。
綜上,這三個參數都應該配置成on:sendfile on; tcp_nopush on; tcp_nodelay on;
二、nginx長連接——keepalive
當使用nginx作為反向代理時,為了支持長連接,需要做到兩點:
- 從client到nginx的連接是長連接
- 從nginx到server的連接是長連接
1、保持和client的長連接:
默認情況下,nginx已經自動開啟了對client連接的keep alive支持(同時client發送的HTTP請求要求keep alive)。一般場景可以直接使用,但是對於一些比較特殊的場景,還是有必要調整個別參數(keepalive_timeout和keepalive_requests)。
1 |
http { |
1)keepalive_timeout
語法:
keepalive_timeout timeout [header_timeout];
- 第一個參數:設置keep-alive客戶端連接在服務器端保持開啟的超時值(默認75s);值為0會禁用keep-alive客戶端連接;
- 第二個參數:可選、在響應的header域中設置一個值“Keep-Alive: timeout=time”;通常可以不用設置;
注:keepalive_timeout默認75s,一般情況下也夠用,對於一些請求比較大的內部服務器通訊的場景,適當加大為120s或者300s;
2)keepalive_requests:
keepalive_requests指令用於設置一個keep-alive連接上可以服務的請求的最大數量,當最大請求數量達到時,連接被關閉。默認是100。這個參數的真實含義,是指一個keep alive建立之后,nginx就會為這個連接設置一個計數器,記錄這個keep alive的長連接上已經接收並處理的客戶端請求的數量。如果達到這個參數設置的最大值時,則nginx會強行關閉這個長連接,逼迫客戶端不得不重新建立新的長連接。
大多數情況下當QPS(每秒請求數)不是很高時,默認值100湊合夠用。但是,對於一些QPS比較高(比如超過10000QPS,甚至達到30000,50000甚至更高) 的場景,默認的100就顯得太低。
簡單計算一下,QPS=10000時,客戶端每秒發送10000個請求(通常建立有多個長連接),每個連接只能最多跑100次請求,意味着平均每秒鍾就會有100個長連接因此被nginx關閉。同樣意味着為了保持QPS,客戶端不得不每秒中重新新建100個連接。因此,就會發現有大量的TIME_WAIT的socket連接(即使此時keep alive已經在client和nginx之間生效)。因此對於QPS較高的場景,非常有必要加大這個參數,以避免出現大量連接被生成再拋棄的情況,減少TIME_WAIT。
2、保持和server的長連接:
為了讓nginx和后端server(nginx稱為upstream)之間保持長連接,典型設置如下:(默認nginx訪問后端都是用的短連接(HTTP1.0),一個請求來了,Nginx 新開一個端口和后端建立連接,后端執行完畢后主動關閉該鏈接)
1 |
http { |
1)location中有兩個參數需要設置:
1 |
http { |
HTTP協議中對長連接的支持是從1.1版本之后才有的,因此最好通過proxy_http_version指令設置為”1.1”;
而”Connection” header應該被清理。清理的意思,我的理解,是清理從client過來的http header,因為即使是client和nginx之間是短連接,nginx和upstream之間也是可以開啟長連接的。這種情況下必須清理來自client請求中的”Connection” header。
2)upstream中的keepalive設置:
此處keepalive的含義不是開啟、關閉長連接的開關;也不是用來設置超時的timeout;更不是設置長連接池最大連接數。官方解釋:
- The connections parameter sets the maximum number of idle keepalive connections to upstream servers connections(設置到upstream服務器的空閑keepalive連接的最大數量)
- When this number is exceeded, the least recently used connections are closed. (當這個數量被突破時,最近使用最少的連接將被關閉)
- It should be particularly noted that the keepalive directive does not limit the total number of connections to upstream servers that an nginx worker process can open.(特別提醒:keepalive指令不會限制一個nginx worker進程到upstream服務器連接的總數量)
我們先假設一個場景: 有一個HTTP服務,作為upstream服務器接收請求,響應時間為100毫秒。如果要達到10000 QPS的性能,就需要在nginx和upstream服務器之間建立大約1000條HTTP連接。nginx為此建立連接池,然后請求過來時為每個請求分配一個連接,請求結束時回收連接放入連接池中,連接的狀態也就更改為idle。我們再假設這個upstream服務器的keepalive參數設置比較小,比如常見的10.
A、假設請求和響應是均勻而平穩的,那么這1000條連接應該都是一放回連接池就立即被后續請求申請使用,線程池中的idle線程會非常的少,趨進於零,不會造成連接數量反復震盪。
B、顯示中請求和響應不可能平穩,我們以10毫秒為一個單位,來看連接的情況(注意場景是1000個線程+100毫秒響應時間,每秒有10000個請求完成),我們假設應答始終都是平穩的,只是請求不平穩,第一個10毫秒只有50,第二個10毫秒有150:
- 下一個10毫秒,有100個連接結束請求回收連接到連接池,但是假設此時請求不均勻10毫秒內沒有預計的100個請求進來,而是只有50個請求。注意此時連接池回收了100個連接又分配出去50個連接,因此連接池內有50個空閑連接。
- 然后注意看keepalive=10的設置,這意味着連接池中最多容許保留有10個空閑連接。因此nginx不得不將這50個空閑連接中的40個關閉,只留下10個。
- 再下一個10個毫秒,有150個請求進來,有100個請求結束任務釋放連接。150 - 100 = 50,空缺了50個連接,減掉前面連接池保留的10個空閑連接,nginx不得不新建40個新連接來滿足要求。
C、同樣,如果假設相應不均衡也會出現上面的連接數波動情況。
造成連接數量反復震盪的一個推手,就是這個keepalive 這個最大空閑連接數。畢竟連接池中的1000個連接在頻繁利用時,出現短時間內多余10個空閑連接的概率實在太高。因此為了避免出現上面的連接震盪,必須考慮加大這個參數,比如上面的場景如果將keepalive設置為100或者200,就可以非常有效的緩沖請求和應答不均勻。
總結:
keepalive 這個參數一定要小心設置,尤其對於QPS比較高的場景,推薦先做一下估算,根據QPS和平均響應時間大體能計算出需要的長連接的數量。比如前面10000 QPS和100毫秒響應時間就可以推算出需要的長連接數量大概是1000. 然后將keepalive設置為這個長連接數量的10%到30%。比較懶的同學,可以直接設置為keepalive=1000之類的,一般都OK的了。
3、綜上,出現大量TIME_WAIT的情況
1)導致 nginx端出現大量TIME_WAIT的情況有兩種:
- keepalive_requests設置比較小,高並發下超過此值后nginx會強制關閉和客戶端保持的keepalive長連接;(主動關閉連接后導致nginx出現TIME_WAIT)
- keepalive設置的比較小(空閑數太小),導致高並發下nginx會頻繁出現連接數震盪(超過該值會關閉連接),不停的關閉、開啟和后端server保持的keepalive長連接;
2)導致后端server端出現大量TIME_WAIT的情況:
nginx沒有打開和后端的長連接,即:沒有設置proxy_http_version 1.1;和proxy_set_header Connection “”;從而導致后端server每次關閉連接,高並發下就會出現server端出現大量TIME_WAIT