前言
大家好!分享即關懷,我們很樂意與你分享其他的一些知識。我們準備了一個 Nginx 指南,分為三個系列。如果你已經知道一些 Nginx 知識或者想擴展你的經驗和認知,這個再合適不過了。
我們將告訴你 Nginx 的運作模式、蘊含的概念,怎樣通過調優 Nginx 來提高應用性能,或是如何設置它的啟動和運行。
這個教程有三個部分:
- 基本概念 —— 這部分需要去了解 Nginx 的一些指令和使用場景,繼承模型,以及 Nginx 如何選擇 server 塊,location 的順序。
- 性能 —— 介紹改善 Nginx 速度的方法和技巧,我們會在這裡談及 gzip 壓縮,緩存,buffer 和超時。
- SSL 安裝 —— 如何配置服務器使用 HTTPS
創建這個系列,我們希望,一是作為參考書,可以通過快速查找到相關問題(比如 gzip 壓縮,SSL 等)的解決方式,也可以直接通讀全文。為了獲得更好的學習效果,我們建議你在本機安裝 Nginx 並且嘗試進行實踐。
Nginx基本概念
什麼是 Nginx?
Nginx 最初是作為一個 Web 服務器創建的,用於解決 C10k 的問題。作為一個 Web 服務器,它可以以驚人的速度為您的數據服務。但 Nginx 不僅僅是一個 Web 服務器,你還可以將其用作反向代理,與較慢的上游服務器(如:Unicorn 或 Puma)輕鬆集成。你可以適當地分配流量(負載均衡器)、流媒體、動態調整圖像大小、緩存內容等等。
基本的 nginx 體系結構由 master 進程和其 worker 進程組成。master 讀取配置文件,並維護 worker 進程,而 worker 則會對請求進行實際處理。
基本命令
要啟動 nginx,只需輸入:
[sudo] nginx
當你的 nginx 實例運行時,你可以通過發送相應的信號來管理它:
[sudo] nginx -s signal
可用的信號:
- stop – 快速關閉
- quit – 優雅關閉 (等待 worker 線程完成處理)
- reload – 重載配置文件
- reopen – 重新打開日誌文件
指令和上下文
nginx 的配置文件,默認的位置包括:
- /etc/nginx/nginx.conf,
- /usr/local/etc/nginx/nginx.conf,或
- /usr/local/nginx/conf/nginx.conf
配置文件的由下面的部分構成:
指令 – 可選項,包含名稱和參數,以分號結尾
gzip on;
上下文 – 分塊,你可以聲明指令 – 類似於編程語言中的作用域
worker_processes 2; # 全局上下文指令
http { # http 上下文
gzip on; # http 上下文中的指令
server { # server 上下文
listen 80; # server 上下文中的指令
}
}
指令類型
在多個上下文中使用相同的指令時,必須要小心,因為繼承模型不同時有著不同的指令。有三種類型的指令,每種都有自己的繼承模型。
普通指令
在每個上下文僅有唯一值。而且,它只能在當前上下文中定義一次。子級上下文可以覆蓋父級中的值,並且這個覆蓋值只在當前的子級上下文中有效。
gzip on;
gzip off; # 非法,不能在同一個上下文中指定同一普通指令2次
server {
location /downloads {
gzip off;
}
location /assets {
# gzip is on here
}
}
數組指令
在同一上下文中添加多條指令,將添加多個值,而不是完全覆蓋。在子級上下文中定義指令將覆蓋給父級上下文中的值。
error_log /var/log/nginx/error.log;
error_log /var/log/nginx/error_notive.log notice;
error_log /var/log/nginx/error_debug.log debug;
server {
location /downloads {
# 下面的配置會覆蓋父級上下文中的指令
error_log /var/log/nginx/error_downloads.log;
}
}
行動指令
行動是改變事情的指令。根據模塊的需要,它繼承的行為可能會有所不同。
例如 rewrite 指令,只要是匹配的都會執行:
server {
rewrite ^ /foobar;
location /foobar {
rewrite ^ /foo;
rewrite ^ /bar;
}
}
如果用戶想嘗試獲取 /sample:
- server的rewrite將會執行,從 /sample rewrite 到 /foobar
- location /foobar 會被匹配
- location的第一個rewrite執行,從/foobar rewrite到/foo
- location的第二個rewrite執行,從/foo rewrite到/bar
return 指令提供的是不同的行為:
server {
location / {
return 200;
return 404;
}
}
在上述的情況下,立即返回200。
處理請求
在 Nginx 內部,你可以指定多個虛擬服務器,每個虛擬服務器用 server{} 上下文描述。
server {
listen *:80 default_server;
server_name netguru.co;
return 200 "Hello from netguru.co";
}
server {
listen *:80;
server_name foo.co;
return 200 "Hello from foo.co";
}
server {
listen *:81;
server_name bar.co;
return 200 "Hello from bar.co";
}
這將告訴 Nginx 如何處理到來的請求。Nginx 將會首先通過檢查 listen 指令來測試哪一個虛擬主機在監聽給定的 IP 端口組合。
然後,server_name 指令的值將檢測 Host 頭(存儲著主機域名)。
Nginx 將會按照下列順序選擇虛擬主機:
- 匹配sever_name指令的IP-端口主機
- 擁有default_server標記的IP-端口主機
- 首先定義的IP-端口主機
- 如果沒有匹配,拒絕連接。
例如下面的例子:
Request to foo.co:80 => "Hello from foo.co"
Request to www.foo.co:80 => "Hello from netguru.co"
Request to bar.co:80 => "Hello from netguru.co"
Request to bar.co:81 => "Hello from bar.co"
Request to foo.co:81 => "Hello from bar.co"
server_name 指令
server_name指令接受多個值。它還處理通配符匹配和正則表達式。
server_name netguru.co www.netguru.co; # exact match
server_name *.netguru.co; # wildcard matching
server_name netguru.*; # wildcard matching
server_name ~^[0-9]*\\.netguru\\.co$; # regexp matching
當有歧義時,nginx 將使用下面的命令:
- 確切的名字
- 最長的通配符名稱以星號開始,例如“* .example.org”。
- 最長的通配符名稱以星號結尾,例如“mail.**”
- 首先匹配正則表達式(按照配置文件中的順序)
Nginx 會存儲 3 個哈希表:確切的名字,以星號開始的通配符,和以星號結尾的通配符。如果結果不在任何表中,則將按順序進行正則表達式測試。
值得謹記的是
server_name .netguru.co;
是一個來自下面的縮寫
server_name netguru.co www.netguru.co *.netguru.co;
有一點不同,.netguru.co 存儲在第二張表,這意味著它比顯式聲明的慢一點。
listen 指令
在很多情況下,能夠找到 listen 指令,接受IP:端口值
listen 127.0.0.1:80;
listen 127.0.0.1; # by default port :80 is used
listen *:81;
listen 81; # by default all ips are used
listen [::]:80; # IPv6 addresses
listen [::1]; # IPv6 addresses
然而,還可以指定 UNIX-domain 套接字。
listen unix:/var/run/nginx.sock;
你甚至可以使用主機名
listen localhost:80;
listen netguru.co:80;
但請慎用,由於主機可能無法啟動 nginx,導致無法綁定在特定的 TCP Socket。
最後,如果指令不存在,則使用 *:80。
最小化配置
有了這些知識 – 我們應該能夠創建並理解運行 nginx 所需的最低配置。
# /etc/nginx/nginx.conf
events {} # events context needs to be defined to consider config valid
http {
server {
listen 80;
server_name netguru.co www.netguru.co *.netguru.co;
return 200 "Hello";
}
}
root, location, 和 try_files 指令
root 指令
root 指令設置請求的根目錄,允許 nginx 將傳入請求映射到文件系統。
server {
listen 80;
server_name netguru.co;
root /var/www/netguru.co;
}
根據給定的請求,指定 nginx 服務器允許的內容
netguru.co:80/index.html # returns /var/www/netguru.co/index.html
netguru.co:80/foo/index.html # returns /var/www/netguru.co/foo/index.html
location 指令
location指令根據請求的 URI 來設置配置。
location [modifier] path
location /foo/ {
# ...
}
如果沒有指定修飾符,則路徑被視為前綴,其後可以跟隨任何東西。
以上例子將匹配
/foo
/fooo
/foo123
/foo/bar/index.html
...
此外,在給定的上下文中可以使用多個 location 指令。
server {
listen 80;
server_name netguru.co;
root /var/www/netguru.co;
location / {
return 200 "root";
}
location /foo/ {
return 200 "foo";
}
}
netguru.co:80 / # => "root"
netguru.co:80 /foo # => "foo"
netguru.co:80 /foo123 # => "foo"
netguru.co:80 /bar # => "root"
Nginx 也提供了一些修飾符,可用於連接 location。這些修飾符將影響 location 模塊使用的地方,因為每個修飾符都分配了優先級。
= - Exact match
^~ - Preferential match
~ && ~* - Regex match
no modifier - Prefix match
Nginx 會先檢查精確匹配。如果找不到,我們會找優先級最高的。如果這個匹配依然失敗,正則表達式匹配將按照出現的順序進行測試。至少,最後一個前綴匹配將被使用。
location /match {
return 200 'Prefix match: matches everything that starting with /match';
}
location ~* /match[0-9] {
return 200 'Case insensitive regex match';
}
location ~ /MATCH[0-9] {
return 200 'Case sensitive regex match';
}
location ^~ /match0 {
return 200 'Preferential match';
}
location = /match {
return 200 'Exact match';
}
/match/ # => 'Exact match'
/match0 # => 'Preferential match'
/match1 # => 'Case insensitive regex match'
/MATCH1 # => 'Case sensitive regex match'
/match-abc # => 'Prefix match: matches everything that starting with /match'
try_files 指令
嘗試不同的路徑,找到一個路徑就返回。
try_files $uri index.html =404;
所以對於 /foo.html 請求,它將嘗試按以下順序返回文件:
- $uri ( /foo.html )
- index.html
- 如果什麼都沒找到則返回 404
有趣的是,如果我們在服務器上下文中定義 try_files,然後定義匹配的所有請求的 location —— try_files 將不會執行。
這是因為在服務器上下文中定義的 try_files 是它的 pseudo-location,這是最不可能的位置。因此,定義 location/ 將比 pseudo-location 更具體。
server {
try_files $uri /index.html =404;
location / {
}
}
因此,你應該避免在 server 上下文中出現 try_files:
server {
location / {
try_files $uri /index.html =404;
}
}
tcp_nodelay, tcp_nopush 和 sendfile
tcp_nodelay
在 TCP 發展早期,工程師需要面對流量衝突和堵塞的問題,其中湧現了大批的解決方案,其中之一是由 John Nagle 提出的算法。
Nagle 的算法旨在防止通訊被大量的小包淹沒。該理論不涉及全尺寸 tcp 包(最大報文長度,簡稱 MSS)的處理。只針對比 MSS 小的包,只有當接收方成功地將以前的包(ACK)的所有確認發送回來時,這些包才會被髮送。在等待期間,發送方可以緩衝更多的數據之後再發送。
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(以防我們不再收到包)。
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_time) * 2
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-- 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。
配置
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.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 區分資源。
所以我們的最終配置如下:
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 2 1k;
client_max_body_size 8m;
Keep-Alive
HTTP 所依賴的 TCP 協議需要執行三次握手來啟動連接。這意味著在服務器可發送數據(例如圖像)之前,需要在客戶機和服務器之間進行三次完整的往返。
假設你從 Warsaw 請求的 /image.jpg,並連接到在柏林最近的服務器:
Open connection
TCP Handshake:
Warsaw ->------------------ synchronize packet (SYN) ----------------->- Berlin
Warsaw -
Warsaw ->------------------- acknowledgement (ACK) ------------------->- Berlin
Data transfer:
Warsaw ->---------------------- /image.jpg --------------------------->- Berlin
Warsaw -
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 "";
}
}
SSL 和 TLS
SSL(Socket Secure Layer 縮寫)是一種通過 HTTP 提供安全連接的協議。
SSL 1.0 由 Netscape 開發,但由於嚴重的安全漏洞從未公開發布過。SSL 2.0 於 1995 年發佈,它存在一些問題,導致了最終的 SSL 3.0 在 1996 年發佈。
TLS(Transport Layer Security 縮寫)的第一個版本是作為 SSL 3.0 的升級版而編寫的。之後 TLS 1.1 和 1.2 出來了。現在,就在不久之後,TLS 1.3 即將推出(這確實值得期待),並且已經被一些瀏覽器所支持。
從技術上講,SSL 和 TLS 是不同的(因為每個協議都描述了協議的不同版本),但其中使用的許多名稱是可以互換的。
基本 SSL/TLS 配置
為了處理 HTTPS 流量,你需要具有 SSL/TLS 證書。你可以通過使用 Let’s encrypt 以生成免費的證書。
當你擁有證書之後,你可以通過以下的方式輕易切換至 HTTPS:
- 開始監聽端口 443(當你輸入 https://sample.co 時瀏覽器將使用的默認端口)
- 提供證書及其密鑰
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
ssl_certificate /etc/nginx/ssl/netguru.crt;
ssl_certificate_key /etc/nginx/ssl/netguru.key;
}
我們也想通過調整配置實現:
- 僅使用 TLS 協議。由於眾所周知的漏洞,所有的 SSL 版本都將不再使用
- 使用預定義的安全的服務器密碼(類似於協議的情況 – 那些日子只有少數密碼被認為是安全的)
請牢記,上述設置總是在變化的。時不時重新更新是個好主意。
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:!MD5;
ssl_prefer_server_ciphers on;
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
ssl_certificate /etc/nginx/ssl/netguru.crt;
ssl_certificate_key /etc/nginx/ssl/netguru.key;
}
TLS 會話恢復
使用 HTTPS,在 TCP 之上需要增加 TLS 握手。這大大增加了此前實際數據傳輸的時間。假設你從華沙請求 /image.jpg,並接入到柏林最近的服務器:
為了在 TLS 握手期間節省一個 roundtrip 時間,以及生成新密鑰的計算開銷,我們可以重用在第一個請求期間生成的會話參數。客戶端和服務器可以將會話參數存儲在會話 ID 密鑰的後面。在接下來的 TLS 握手過程中,客戶端可以發送會話 ID,如果服務器在緩存中仍然有正確的條目,那麼會重用前一個會話所生成的參數。
server {
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1h;
}
OCSP Stapling
SSL 證書可以隨時撤銷。瀏覽器為了知道給定的證書是否不再有效,需要通過在線證書狀態協議 (Online Certificate Status Protocol ,OCSP) 執行額外的查詢。無需用戶執行指定的 OCSP 查詢,我們可以在服務器上執行此操作,緩存其結果,並在 TLS 握手期間為客戶端提供 OCSP 響應。它被稱為OCSP stapling。
server {
ssl_stapling on;
ssl_stapling_verify on; # verify OCSP response
ssl_trusted_certificate /etc/nginx/ssl/lemonfrog.pem; # tell nginx location of all intermediate certificates
resolver 8.8.8.8 8.8.4.4 valid=86400s; # resolution of the OCSP responder hostname
resolver_timeout 5s;
}
Security headers
有一些標頭確實值得調整以提供更高的安全性。有關更多關於標頭及其詳細信息,你絕對應該查看OWASP 項目之安全標頭。
HTTP Strict-Transport-Security
或簡稱 HSTS,強制用戶代理在向源發送請求時使用 HTTPS。
add_header Strict-Transport-Security "max-age=31536000; includeSubdomains; preload";
X-Frame-Options
表示瀏覽器是否需要在一幀、一個 iframe 或一個對象標籤中渲染頁面。
add_header X-Frame-Options DENY;
X-Content-Type-Options
此選項將阻止瀏覽器在判斷文件類型時嗅探文件。文件將會按照 Content-Type 頭中聲明的格式轉譯。
add_header X-Content-Type-Options nosniff;
Server tokens
另一個很好的做法是在 HTTP 響應頭字段中隱藏有關 Web 服務器的信息:
Server : nginx/1.13.2
實現此功能可以通過禁用 server_tokens 指令:
server_tokens off;
附錄 :: Let’s Encrypt
安裝
最新的安裝包可以在這裡找到。
為了測試使用暫存環境,不排除速率限制。
生成新證書
certbot certonly --webroot --webroot-path /var/www/netguru/current/public/ \\
-d foo.netguru.co \\
-d bar.netguru.co
確保能夠正確更新。
certbot renew --dry-run
確保你在 crontab 添加了自動更新。運行 crontab -e,同時添加下邊一行代碼
3 * * * /usr/bin/certbot renew --quiet --renew-hook "/usr/sbin/nginx -s reload"
檢查 SSL 是否能夠通過 ssllabs 正常運行。
閱讀更多 java程序員工程師 的文章