一、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 2 3 4 |
http { keepalive_timeout 120s 120s; keepalive_requests 10000; } |
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
http { upstream BACKEND { server 192.168.0.1:8080 weight=1 max_fails=2 fail_timeout=30s; server 192.168.0.2:8080 weight=1 max_fails=2 fail_timeout=30s; keepalive 300; // 這個很重要! } server { listen 8080 default_server; server_name ""; location / { proxy_pass http://BACKEND; proxy_set_header Host $Host; proxy_set_header x-forwarded-for $remote_addr; proxy_set_header X-Real-IP $remote_addr; add_header Cache-Control no-store; add_header Pragma no-cache; proxy_http_version 1.1; // 這兩個最好也設定 proxy_set_header Connection ""; } } } |
1)location中有兩個引數需要設定:
1 2 3 4 5 6 7 8 |
http { server { location / { proxy_http_version 1.1; // 這兩個最好也設定 proxy_set_header Connection ""; } } } |
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
三、nginx配置https
1、配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
server { listen 80 default_server; listen 443 ssl; server_name toutiao.iqiyi.com toutiao.qiyi.domain m.toutiao.iqiyi.com; root /data/none; index index.php index.html index.htm; ###ssl settings start ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_certificate /usr/local/nginx/conf/server.pem; ssl_certificate_key /usr/local/nginx/conf/server.key; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; ssl_ciphers ALL:!kEDH!ADH:RC4+RSA:+HIGH:+EXP; ssl_prefer_server_ciphers on; ###ssl settings end … |
2、效能比較:
通過https訪問Nginx一般會比http訪問慢30%(https方式訪問主要是耗Nginx伺服器的cpu)通過下面實驗驗證:
- nginx後端掛了5臺java伺服器,java伺服器中有個簡單的java程式,從redis快取隨機讀取一個value值輸出到前端;(掛的java伺服器越多,對nginx壓力越大)
- 壓測nginx,3000併發,一共請求30000次,返回結果都是200的情況下進行對比;
實驗結果:
A、伺服器負載對比:
https訪問,伺服器cpu最高可以達到20%,而http的訪問,伺服器cpu基本在1%左右;無論那種訪問,nginx伺服器負載、記憶體都不高;
B、nginx吞吐量對比(qps):
• https訪問,30000次請求花了28s;(是http的3倍)
• http訪問,30000次請求花了9s;
統計qps時,每次清空nginx日誌,然後加壓,執行完畢後使用如下命令檢視qps:
1 2 |
# cat log.2.3000https | grep '/api/news/v1/info?newsId=' | awk '{print$3}'| uniq | wc -l 37 |
注:不能持續加壓,否則無限加大壓力後往往是後端java服務出現瓶頸,導致返回給nginx的響應變慢,從而使得nginx壓力變小。
3、優化:
Nginx預設使用DHE演算法來產生密匙,該加密演算法效率很低。可以通過如下命令,刪掉了kEDH演算法。
ssl_ciphers ALL:!kEDH!ADH:RC4+RSA:+HIGH:+EXP;