Nginx 教程(2):性能

tcp_nodelay, tcp_nopush 和 sendfile

 

tcp_nodelay

 

在 TCP 發展早期,工程師須要面對流量衝突和堵塞的問題,其中涌現了大批的解決方案,其中之一是由 John Nagle 提出的算法。javascript

 

Nagle 的算法旨在防止通信被大量的小包淹沒。該理論不涉及全尺寸 tcp 包(最大報文長度,簡稱 MSS)的處理。只針對比 MSS 小的包,只有當接收方成功地將之前的包(ACK)的全部確認發送回來時,這些包纔會被髮送。在等待期間,發送方能夠緩衝更多的數據以後再發送。css

 

if package.size >= MSS.sizejava

  send(package)node

elsif acks.all_received?nginx

  send(package)算法

elsejson

  # acumulate data後端

end瀏覽器

 

與此同時,誕生了另外一個理論,延時 ACK緩存

 

在 TCP 通信中,在發送數據後,須要接收回應包(ACK)來確認數據被成功傳達。

 

延時 ACK 旨在解決線路被大量的 ACK 包擁堵的情況。爲了減小 ACK 包的數量,接收者等待須要回傳的數據加上 ACK 包回傳給發送方,若是沒有數據須要回傳,必須在至少每 2 個 MSS,或每 200 至 500 毫秒內發送 ACK(以防咱們再也不收到包)。

 

if packages.any?

  send

elsif last_ack_send_more_than_2MSS_ago|| 200_ms_timer.finished?

  send

else

  # wait

end

 

正如你可能在一開始就注意到的那樣 —— 這可能會致使在持久鏈接上的一些暫時的死鎖。讓咱們重現它!

 

假設:

 

  • 初始擁塞窗口等於 2。擁塞窗口是另外一個 TCP 機制的一部分,稱爲慢啓動。細節如今並不重要,只要記住它限制了一次能夠發送多少個包。在第一次往返中,咱們能夠發送 2 個 MSS 包。在第二次發送中:4 個 MSS 包,第三次發送中:8 個MSS,依此類推。

  • 4 個已緩存的等待發送的數據包:A, B, C, D

  • A, B, C是 MSS 包

  • D 是一個小包

 

場景:

 

  • 因爲是初始的擁塞窗口,發送端被容許傳送兩個包:A 和 B

  • 接收端在成功得到這兩個包以後,發送一個 ACK

  • 發件端發送 C 包。然而,Nagle 卻阻止它發送 D 包(包長度過小,等待 C 的ACK)

  • 在接收端,延遲 ACK 使他沒法發送 ACK(每隔 2 個包或每隔 200 毫秒發送一次)

  • 在 200ms 以後,接收器發送 C 包的 ACK

  • 發送端收到 ACK 併發送 D 包

 

在這個數據交換過程當中,因爲 Nagel 和延遲 ACK 之間的死鎖,引入了 200ms 的延遲。

 

Nagle 算法是當時真正的救世主,並且目前仍然具備極大的價值。但在大多數狀況下,咱們不會在咱們的網站上使用它,所以能夠經過添加 TCP_NODELAY 標誌來安全地關閉它。

 

tcp_nodelay on;     # sets TCP_NODELAY flag, used on keep-alive connections

 

享受這200ms提速吧!

 

sendfile

 

正常來講,當要發送一個文件時須要下面的步驟:

 

  • malloc(3) – 分配一個本地緩衝區,儲存對象數據。

  • read(2) – 檢索和複製對象到本地緩衝區。

  • write(2) – 從本地緩衝區複製對象到 socket 緩衝區。

 

這涉及到兩個上下文切換(讀,寫),並使相同對象的第二個副本成爲沒必要要的。正如你所看到的,這不是最佳的方式。值得慶幸的是還有另外一個系統調用,提高了發送文件(的效率),它被稱爲:sendfile(2)(想不到吧!竟然是這名字)。這個調用在文件 cache 中檢索一個對象,並傳遞指針(不須要複製整個對象),直接傳遞到 socket 描述符,Netflix 表示,使用 sendfile(2) 將網絡吞吐量從 6Gbps 提升到了 30Gbps。

 

然而,sendfile(2) 有一些注意事項:

 

  • 不可用於 UNIX sockets(例如:當經過你的上游服務器發送靜態文件時)

  • 可否執行不一樣的操做,取決於操做系統

 

在 nginx 中打開它

 

sendfile on;

 

tcp_nopush

 

tcp_nopush 與 tcp_nodelay 相反。不是爲了儘量快地推送數據包,它的目標是一次性優化數據的發送量。

 

在發送給客戶端以前,它將強制等待包達到最大長度(MSS)。並且這個指令只有在 sendfile 開啓時才起做用。

 

sendfile on;

tcp_nopush on;

 

看起來 tcp_nopush 和 tcp_nodelay 是互斥的。可是,若是全部 3 個指令都開啓了,nginx 會:

 

  • 確保數據包在發送給客戶以前是已滿的

  • 對於最後一個數據包,tcp_nopush 將被刪除 —— 容許 TCP 當即發送,沒有 200ms 的延遲

 

我應該使用多少進程?

 

工做進程

 

worker_process 指令會指定:應該運行多少個 worker。默認狀況下,此值設置爲 1。最安全的設置是經過傳遞 auto 選項來使用核心數量。

 

但因爲 Nginx 的架構,其處理請求的速度很是快 – 咱們可能一次不會使用超過 2-4 個進程(除非你正在託管 Facebook 或在 nginx 內部執行一些 CPU 密集型的任務)。

 

worker_process auto;

 

worker 鏈接

 

與 worker_process 直接綁定的指令是 worker_connections。它指定一個工做進程能夠一次打開多少個鏈接。這個數目包括全部鏈接(例如與代理服務器的鏈接),而不只僅是與客戶端的鏈接。此外,值得記住的是,一個客戶端能夠打開多個鏈接,同時獲取其餘資源。

 

worker_connections 1024;

 

打開文件數目限制

 

在基於 Unix 系統中的「一切都是文件」。這意味着文檔、目錄、管道甚至套接字都是文件。系統對一個進程能夠打開多少文件有一個限制。要查看該限制:

 

ulimit -Sn      # soft limit

ulimit -Hn      # hard limit

 

這個系統限制必須根據 worker_connections 進行調整。任何傳入的鏈接都會打開至少一個文件(一般是兩個鏈接套接字以及後端鏈接套接字或磁盤上的靜態文件)。因此這個值等於 worker_connections*2 是安全的。幸運的是,Nginx 提供了一個配置選項來增長這個系統的值。要使用這個配置,請添加具備適當數目的 worker_rlimit_nofile 指令並從新加載 nginx。

 

worker_rlimit_nofile 2048;

 

配置

 

worker_process auto;

worker_rlimit_nofile 2048# Changes the limit on the maximum number of open files (RLIMIT_NOFILE) for worker processes.

worker_connections 1024;   # Sets the maximum number of simultaneous connections that can be opened by a worker process.

 

最大鏈接數

 

如上所述,咱們能夠計算一次能夠處理多少個併發鏈接:

 

最大鏈接數 

 

    worker_processes worker_connections

----------------------------------------------

 (keep_alive_timeout avg_response_time2

 

keep_alive_timeout (後續有更多介紹) + avg_response_time 告訴咱們:單個鏈接持續了多久。咱們也除以 2,一般狀況下,你將有一個客戶端打開 2 個鏈接的狀況:一個在 nginx 和客戶端之間,另外一個在 nginx 和上游服務器之間。

 

Gzip

 

啓用 gzip 能夠顯著下降響應的(報文)大小,所以,客戶端(網頁)會顯得更快些。

 

壓縮級別

 

Gzip 有不一樣的壓縮級別,1 到 9 級。遞增這個級別將會減小文件的大小,但也會增長資源消耗。做爲標準咱們將這個數字(級別)保持在 3 – 5 級,就像上面說的那樣,它將會獲得較小的節省,同時也會獲得更大的 CPU 使用率。

 

這有個經過 gzip 的不一樣的壓縮級別壓縮文件的例子,0 表明未壓縮文件。

 

curl -I -H 'Accept-Encoding: gzip,deflate' https://netguru.co/

 

❯ du -sh ./*

 64K    ./0_gzip

 16K    ./1_gzip

 12K    ./2_gzip

 12K    ./3_gzip

 12K    ./4_gzip

 12K    ./5_gzip

 12K    ./6_gzip

 12K    ./7_gzip

 12K    ./8_gzip

 12K    ./9_gzip

 

❯ ls -al

-rw-r--r--   matDobek  staff  61711  Nov 08:46 0_gzip

-rw-r--r--   matDobek  staff  12331  Nov 08:48 1_gzip

-rw-r--r--   matDobek  staff  12123  Nov 08:48 2_gzip

-rw-r--r--   matDobek  staff  12003  Nov 08:48 3_gzip

-rw-r--r--   matDobek  staff  11264  Nov 08:49 4_gzip

-rw-r--r--   matDobek  staff  11111  Nov 08:50 5_gzip

-rw-r--r--   matDobek  staff  11097  Nov 08:50 6_gzip

-rw-r--r--   matDobek  staff  11080  Nov 08:50 7_gzip

-rw-r--r--   matDobek  staff  11071  Nov 08:51 8_gzip

-rw-r--r--   matDobek  staff  11005  Nov 08:51 9_gzip

 

gzip_http_version 1.1;

 

這條指令告訴 nginx 僅在 HTTP 1.1 以上的版本才能使用 gzip。咱們在這裏不涉及 HTTP 1.0,至於 HTTP 1.0 版本,它是不可能既使用 keep-alive 和 gzip 的。所以你必須作出決定:使用 HTTP 1.0 的客戶端要麼錯過 gzip,要麼錯過 keep-alive。

 

配置

 

gzip on;               # enable gzip

gzip_http_version 1.1# turn on gzip for http 1.1 and above

gzip_disable "msie6";  # IE 6 had issues with gzip

gzip_comp_level 5;     # inc compresion level, and CPU usage

gzip_min_length 100;   # minimal weight to gzip file

gzip_proxied any;      # enable gzip for proxied requests (e.g. CDN)

gzip_buffers 16 8k;    # compression buffers (if we exceed this value, disk will be used instead of RAM)

gzip_vary on;          # add header Vary Accept-Encoding (more on that in Caching section)

 

# define files which should be compressed

gzip_types text/plain;

gzip_types text/css;

gzip_types application/javascript;

gzip_types application/json;

gzip_types application/vnd.ms-fontobject;

gzip_types application/x-font-ttf;

gzip_types font/opentype;

gzip_types image/svg+xml;

gzip_types image/x-icon;

 

緩存

 

緩存是另外一回事,它能提高用戶的請求速度。

 

管理緩存能夠僅由 2 個 header 控制:

 

  • 在 HTTP/1.1 中用 Cache-Control 管理緩存

  • Pragma 對於 HTTP/1.0 客戶端的向後兼容性

 

緩存自己能夠分爲兩類:公共緩存和私有緩存。公共緩存是被多個用戶共同使用的。專用緩存專用於單個用戶。咱們能夠很容易地區分,應該使用哪一種緩存:

 

add_header Cache-Control public;

add_header Pragma public;

 

對於標準資源,咱們想保存1個月:

 

location ~* .(jpg|jpeg|png|gif|ico|css|js)$ {

  expires 1M;

  add_header Cache-Control public;

  add_header Pragma public;

}

 

上面的配置彷佛足夠了。然而,使用公共緩存時有一個注意事項。

 

讓咱們看看若是將咱們的資源存儲在公共緩存(如 CDN)中,URI 將是惟一的標識符。在這種狀況下,咱們認爲 gzip 是開啓的。

 

有2個瀏覽器:

 

  • 舊的,不支持 gzip

  • 新的,支持 gzip

 

舊的瀏覽器給 CDN 發送了一個 netguru.co/style 請求。可是 CDN 也沒有這個資源,它將會給咱們的服務器發送請求,而且返回未經壓縮的響應。CDN 在哈希裏存儲文件(爲之後使用):

 

{

  ...

  netguru.co/styles.css => FILE("/sites/netguru/style.css")

  ...

}

 

而後將其返回給客戶端。

 

如今,新的瀏覽器發送相同的請求到 CDN,請求 netguru.co/style.css,獲取 gzip 打包的資源。因爲 CDN 僅經過 URI 標識資源,它將爲新瀏覽器返回同樣的未壓縮資源。新的瀏覽器將嘗試提取未打包的文件,可是將得到無用的東西。

 

若是咱們可以告訴公共緩存是怎樣進行 URI 和編碼的資源識別,咱們就能夠避免這個問題。

 

{

  ...

  (netguru.co/styles.cssgzip=> FILE("/sites/netguru/style.css.gzip")

  (netguru.co/styles.csstext/css=> FILE("/sites/netguru/style.css")

  ...

}

 

這正是 Vary Accept-Encoding: 完成的。它告訴公共緩存,能夠經過 URI 和 Accept-Encoding header 區分資源。

 

因此咱們的最終配置以下:

 

location ~* .(jpg|jpeg|png|gif|ico|css|js)$ {

  expires 1M;

  add_header Cache-Control public;

  add_header Pragma public;

  add_header Vary Accept-Encoding;

}

 

超時

 

client_body_timeout 和 client_header_timeout 定義了 nginx 在拋出 408(請求超時)錯誤以前應該等待客戶端傳輸主體或頭信息的時間。

 

send_timeout 設置向客戶端發送響應的超時時間。超時僅在兩次連續的寫入操做之間被設置,而不是用於整個響應的傳輸過程。若是客戶端在給定時間內沒有收到任何內容,則鏈接將被關閉。

 

設置這些值時要當心,由於等待時間過長會使你容易受到攻擊者的攻擊,而且等待時間過短的話會切斷與速度較慢的客戶端的鏈接。

 

# Configure timeouts

client_body_timeout   12;

client_header_timeout 12;

send_timeout          10;

 

Buffers

 

client_body_buffer_size

 

設置讀取客戶端請求正文的緩衝區大小。若是請求主體大於緩衝區,則整個主體或僅其部分被寫入臨時文件。對 client_body_buffer_size 而言,設置 16k 大小在大多數狀況下是足夠的。

 

這是又一個能夠產生巨大影響的設置,必須謹慎使用。過小了,則 nginx 會不斷地使用 I/O 把剩餘的部分寫入文件。太大了,則當攻擊者能夠打開全部鏈接但你沒法在系統上分配足夠緩衝來處理這些鏈接時,你可能容易受到 DOS 攻擊。

 

client_header_buffer_size 和 large_client_header_buffers

 

若是 header 不能跟 client_header_buffer_size 匹配上,就會使用 large_client_header_buffers。若是請求也不適合 large_client_header_buffers,將給客戶端返回一個錯誤提示。對於大多數的請求來講,1KB 的緩存是足夠的。可是,若是一個包含大量記錄的請求,1KB 是不夠的。

 

若是請求行的長度超限,將給客戶端返回一個 414(請求的 URI 太長)錯誤提示。若是請求的 header 長度超限,將拋出一個 400(錯誤請求)的錯誤代碼

 

client_max_body_size

 

設置客戶端請求主體的最大容許範圍,在請求頭字段中指定「內容長度」。若是您但願容許用戶上傳文件,調整此配置以知足您的須要。

 

配置

 

client_body_buffer_size       16K;

client_header_buffer_size     1k;

large_client_header_buffers   1k;

client_max_body_size          8m;

 

Keep-Alive

 

HTTP 所依賴的 TCP 協議須要執行三次握手來啓動鏈接。這意味着在服務器可發送數據(例如圖像)以前,須要在客戶機和服務器之間進行三次完整的往返。

 

假設你從 Warsaw 請求的 /image.jpg,並鏈接到在柏林最近的服務器:

 

Open connection

 

TCP Handshake:

Warsaw  ->------------------ synchronize packet (SYN) ----------------->- Berlin

Warsaw  -<--------- synchronise-acknowledgement packet (SYN-ACK) ------<- Berlin

Warsaw  ->------------------- acknowledgement (ACK) ------------------->- Berlin

 

Data transfer:

Warsaw  ->---------------------- /image.jpg --------------------------->- Berlin

Warsaw  -<--------------------- (image data) --------------------------<- Berlin

 

Close connection

 

對於另外一次請求,你將不得再也不次執行整個初始化。若是你在短期內發送屢次請求,這可能會快速累積起來。這樣的話 keep-alive 使用起來就方便了。在成功響應以後,它保持鏈接空閒給定的時間段(例如 10 秒)。若是在這段時間內有另外一個請求,現有的鏈接將被重用,空閒時間將被刷新。

 

Nginx 提供了幾個指令來調整 keepalive 設置。這些能夠分爲兩類:

 

在客戶端和 nginx 之間 keep-alive

 

keepalive_disable msie6;        # disable selected browsers.

 

# The number of requests a client can make over a single keepalive connection. The default is 100, but a much higher value can be especially useful for testing with a load‑generation tool, which generally sends a large number of requests from a single client.

keepalive_requests 100000;

 

# How long an idle keepalive connection remains open.

keepalive_timeout 60;

 

在 nginx 和上游服務器之間 keep-alive

 

upstream backend {

    # The number of idle keepalive connections to an upstream server that remain open for each worker process

    keepalive 16;

}

 

server {

  location /http{

    proxy_pass http://http_backend;

    proxy_http_version 1.1;

    proxy_set_header Connection "";

  }

}

相關文章
相關標籤/搜索