大家好,分享即關愛,我們很樂意和你分享一些新的知識,我們準備了一個 Nginx 的教程,分為三個系列,如果你對 Nginx 有所耳聞,或者想增進 Nginx 方面的經驗和理解,那麼恭喜你來對地方了。
我們會告訴你 Nginx 如何工作及其背後的理念,還有如何優化以加快應用的效能,如何安裝啟動和保持執行。
這個教程有三個部分:
- 基本概念 —— 這部分需要去了解 Nginx 的一些指令和使用場景,繼承模型,以及 Nginx 如何選擇 server 塊,location 的順序。
- 效能 —— 介紹改善 Nginx 速度的方法和技巧,我們會在這裡談及 gzip 壓縮,快取,buffer 和超時。
- SSL 安裝 —— 如何配置伺服器使用 HTTPS
建立這個系列,我們希望,一是作為參考書,可以通過快速查詢到相關問題(比如 gzip 壓縮,SSL 等)的解決方式,也可以直接通讀全文。為了獲得更好的學習效果,我們建議你在本機安裝 Nginx 並且嘗試進行實踐。
tcp_nodelay
, tcp_nopush
和 sendfile
tcp_nodelay
在 TCP 發展早期,工程師需要面對流量衝突和堵塞的問題,其中湧現了大批的解決方案,其中之一是由 John Nagle 提出的演算法。
Nagle 的演算法旨在防止通訊被大量的小包淹沒。該理論不涉及全尺寸 tcp 包(最大報文長度,簡稱 MSS)的處理。只針對比 MSS 小的包,只有當接收方成功地將以前的包(ACK)的所有確認傳送回來時,這些包才會被髮送。在等待期間,傳送方可以緩衝更多的資料之後再傳送。
1 2 3 4 5 6 7 |
if package.size >= MSS.size send(package) elsif acks.all_received? send(package) else # acumulate data end |
與此同時,誕生了另一個理論,延時 ACK
在 TCP 通訊中,在傳送資料後,需要接收回應包(ACK)來確認資料被成功傳達。
延時 ACK 旨在解決線路被大量的 ACK 包擁堵的狀況。為了減少 ACK 包的數量,接收者等待需要回傳的資料加上 ACK 包回傳給傳送方,如果沒有資料需要回傳,必須在至少每 2 個 MSS,或每 200 至 500 毫秒內傳送 ACK(以防我們不再收到包)。
1 2 3 4 5 6 7 |
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 標誌來安全地關閉它。
1 |
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 中開啟它
1 |
sendfile on; |
tcp_nopush
tcp_nopush 與 tcp_nodelay 相反。不是為了儘可能快地推送資料包,它的目標是一次性優化資料的傳送量。
在傳送給客戶端之前,它將強制等待包達到最大長度(MSS)。而且這個指令只有在 sendfile 開啟時才起作用。
1 2 |
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 密集型的任務)。
1 |
worker_process auto; |
worker 連線
與 worker_process 直接繫結的指令是 worker_connections。它指定一個工作程式可以一次開啟多少個連線。這個數目包括所有連線(例如與代理伺服器的連線),而不僅僅是與客戶端的連線。此外,值得記住的是,一個客戶端可以開啟多個連線,同時獲取其他資源。
1 |
worker_connections 1024; |
開啟檔案數目限制
在基於 Unix 系統中的“一切都是檔案”。這意味著文件、目錄、管道甚至套接字都是檔案。系統對一個程式可以開啟多少檔案有一個限制。要檢視該限制:
1 2 |
ulimit -Sn # soft limit ulimit -Hn # hard limit |
這個系統限制必須根據 worker_connections 進行調整。任何傳入的連線都會開啟至少一個檔案(通常是兩個連線套接字以及後端連線套接字或磁碟上的靜態檔案)。所以這個值等於 worker_connections*2 是安全的。幸運的是,Nginx 提供了一個配置選項來增加這個系統的值。要使用這個配置,請新增具有適當數目的 worker_rlimit_nofile 指令並重新載入 nginx。
1 |
worker_rlimit_nofile 2048; |
配置
1 2 3 |
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. |
最大連線數
如上所述,我們可以計算一次可以處理多少個併發連線:
1 2 3 4 5 |
最大連線數 = worker_processes * worker_connections ---------------------------------------------- (keep_alive_timeout + avg_response_time) * 2 |
keep_alive_timeout (後續有更多介紹) + avg_response_time 告訴我們:單個連線持續了多久。我們也除以 2,通常情況下,你將有一個客戶端開啟 2 個連線的情況:一個在 nginx 和客戶端之間,另一個在 nginx 和上游伺服器之間。
Gzip
啟用 gzip 可以顯著降低響應的(報文)大小,因此,客戶端(網頁)會顯得更快些。
壓縮級別
Gzip 有不同的壓縮級別,1 到 9 級。遞增這個級別將會減少檔案的大小,但也會增加資源消耗。作為標準我們將這個數字(級別)保持在 3 – 5 級,就像上面說的那樣,它將會得到較小的節省,同時也會得到更大的 CPU 使用率。
這有個通過 gzip 的不同的壓縮級別壓縮檔案的例子,0 代表未壓縮檔案。
1 |
curl -I -H 'Accept-Encoding: gzip,deflate' https://netguru.co/ |
1 2 3 4 5 6 7 8 9 10 11 |
❯ 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 |
1 2 3 4 5 6 7 8 9 10 11 |
❯ ls -al -rw-r--r-- 1 matDobek staff 61711 3 Nov 08:46 0_gzip -rw-r--r-- 1 matDobek staff 12331 3 Nov 08:48 1_gzip -rw-r--r-- 1 matDobek staff 12123 3 Nov 08:48 2_gzip -rw-r--r-- 1 matDobek staff 12003 3 Nov 08:48 3_gzip -rw-r--r-- 1 matDobek staff 11264 3 Nov 08:49 4_gzip -rw-r--r-- 1 matDobek staff 11111 3 Nov 08:50 5_gzip -rw-r--r-- 1 matDobek staff 11097 3 Nov 08:50 6_gzip -rw-r--r-- 1 matDobek staff 11080 3 Nov 08:50 7_gzip -rw-r--r-- 1 matDobek staff 11071 3 Nov 08:51 8_gzip -rw-r--r-- 1 matDobek staff 11005 3 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。
配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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 客戶端的向後相容性
快取本身可以分為兩類:公共快取和私有快取。公共快取是被多個使用者共同使用的。專用快取專用於單個使用者。我們可以很容易地區分,應該使用哪種快取:
1 2 |
add_header Cache-Control public; add_header Pragma public; |
對於標準資源,我們想儲存1個月:
1 2 3 4 5 |
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 在雜湊裡儲存檔案(為以後使用):
1 2 3 4 5 |
{ ... netguru.co/styles.css => FILE("/sites/netguru/style.css") ... } |
然後將其返回給客戶端。
現在,新的瀏覽器傳送相同的請求到 CDN,請求 netguru.co/style.css,獲取 gzip 打包的資源。由於 CDN 僅通過 URI 標識資源,它將為新瀏覽器返回一樣的未壓縮資源。新的瀏覽器將嘗試提取未打包的檔案,但是將獲得無用的東西。
如果我們能夠告訴公共快取是怎樣進行 URI 和編碼的資源識別,我們就可以避免這個問題。
1 2 3 4 5 6 7 |
{ ... (netguru.co/styles.css, gzip) => FILE("/sites/netguru/style.css.gzip") (netguru.co/styles.css, text/css) => FILE("/sites/netguru/style.css") ... } `` |
這正是 Vary Accept-Encoding: 完成的。它告訴公共快取,可以通過 URI 和 Accept-Encoding header 區分資源。
所以我們的最終配置如下:
1 2 3 4 5 6 |
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 設定向客戶端傳送響應的超時時間。超時僅在兩次連續的寫入操作之間被設定,而不是用於整個響應的傳輸過程。如果客戶端在給定時間內沒有收到任何內容,則連線將被關閉。
設定這些值時要小心,因為等待時間過長會使你容易受到攻擊者的攻擊,並且等待時間太短的話會切斷與速度較慢的客戶端的連線。
1 2 3 4 |
# 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
設定客戶端請求主體的最大允許範圍,在請求頭欄位中指定“內容長度”。如果您希望允許使用者上傳檔案,調整此配置以滿足您的需要。
配置
1 2 3 4 |
client_body_buffer_size 16K; client_header_buffer_size 1k; large_client_header_buffers 2 1k; client_max_body_size 8m; |
Keep-Alive
HTTP 所依賴的 TCP 協議需要執行三次握手來啟動連線。這意味著在伺服器可傳送資料(例如影像)之前,需要在客戶機和伺服器之間進行三次完整的往返。
假設你從 Warsaw 請求的 /image.jpg,並連線到在柏林最近的伺服器:
1 2 3 4 5 6 7 8 9 10 11 12 |
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
1 2 3 4 5 6 7 |
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
1 2 3 4 5 6 7 8 9 10 11 12 |
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 ""; } } |
就這些了。
總結
感謝您的閱讀。如果沒有大量的資源,這個系列是不可能完成的。在這一系列的寫作中,我們發現了一些特別有用的網站:
- nginx 文件
- nginx 部落格
- udemy(線上教育網站 )的 nginx 原理
- Ilya Grigorik 的部落格,和他的令人驚奇的書:《高效能瀏覽器網路》
- Martin Fjordvald 的部落格
我們會很感激你的反饋和評價,請隨意討論。你喜歡這系列嗎?你有什麼關於下一步應該解決什麼問題的建議嗎?或你發現了一個錯誤?告訴我們,下期再見。