Nginx 配置之性能篇

mb2x 9年前發布 | 57K 次閱讀 Nginx Web服務器

這篇文章繼續介紹與性能有關的一些配置。WEB 性能優化是一個系統工程,涵蓋很多方面,做好其中某個環節并不意味性能就能變好,但可以肯定地說,如果某個環節做得很糟糕,那么結果一定會變差。

首先說明下,本文提到的一些 Nginx 配置,需要較高版本 Linux 內核才支持。往往在實際生產環境中,升級服務器內核并不是一件容易的事。為了獲得最好的性能,有些升級還是必須的。但往往服務器運維和項目開發并不在一個 團隊,一方追求穩定不出事故,另一方希望提升性能,本來就是矛盾的。好在我們折騰自己 VPS 時,可以無視這些限制。

TCP 優化

Nginx 關于 TCP 的優化基本都是修改系統內核提供的配置項,所以跟具體的 Linux 版本和系統配置有關,我對這一塊還不是非常熟悉,這里只能簡單介紹下:

NGINXhttp {
    sendfile           on;
    tcp_nopush         on;
    tcp_nodelay        on;

    keepalive_timeout  60;
    ... ...
}

第一行的sendfile配置可以提高 Nginx 靜態資源托管效率。sendfile 是一個系統調用,直接在內核空間完成文件發送,不需要先 read 再 write,沒有上下文切換開銷。

TCP_NOPUSH 是 FreeBSD 的一個 socket 選項,對應 Linux 的 TCP_CORK,Nginx 里統一用tcp_nopush來控制它,并且只有在啟用了 sendfile 之后才生效。啟用它之后,數據包會累計到一定大小之后才會發送,減小了額外開銷,提高網絡效率。

TCP_NODELAY 也是一個 socket 選項,啟用后會禁用 Nagle 算法,盡快發送數據,可以節約 200ms。Nginx 只會針對處于 keep-alive 狀態的 TCP 連接才會啟用tcp_nodelay。

可以看到 TCP_NOPUSH 是要等數據包累積到一定大小才發送,TCP_NODELAY 是要盡快發送,二者相互矛盾。實際上,它們確實可以一起用,最終的效果是先填滿包,再盡快發送。

關于這部分內容的更多介紹可以看這篇文章:NGINX OPTIMIZATION: UNDERSTANDING SENDFILE, TCP_NODELAY AND TCP_NOPUSH

配置最后一行用來指定服務端為每個 TCP 連接最多可以保持多長時間。Nginx 的默認值是 75 秒,有些瀏覽器最多只保持 60 秒,所以我統一設置為 60。

另外,還有一個 TCP 優化策略叫 TCP Fast Open(TFO),這里先介紹下,配置在后面貼出。TFO 的作用是用來優化 TCP 握手過程。客戶端第一次建立連接還是要走三次握手,所不同的是客戶端在第一個 SYN 會設置一個 Fast Open 標識,服務端會生成 Fast Open Cookie 并放在 SYN-ACK 里,然后客戶端就可以把這個 Cookie 存起來供之后的 SYN 用。下面這個圖形象地描述了這個過程:

Nginx 配置之性能篇

關于 TCP Fast Open 的更多信息,可以查看 RFC7413,或者這篇文章:Shaving your RTT with TCP Fast Open

開啟 Gzip

我們在上線前,代碼(JS、CSS 和 HTML)會做壓縮,圖片也會做壓縮(PNGOUT、Pngcrush、JpegOptim、Gifsicle 等)。對于文本文件,在服務端發送響應之前進行 GZip 壓縮也很重要,通常壓縮后的文本大小會減小到原來的 1/4 - 1/3。下面是我的配置:

NGINXhttp {
    gzip             on;
    gzip_vary        on;

    gzip_comp_level  6;
    gzip_buffers     16 8k;

    gzip_min_length  1000;
    gzip_proxied     any;
    gzip_disable     "msie6";

    gzip_types       text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript;
    ... ...
}

這部分內容比較簡單,只有兩個地方需要解釋下:

gzip_vary用來輸出 Vary 響應頭,用來解決某些緩存服務的一個問題,詳情請看我之前的博客:HTTP 協議中 Vary 的一些研究

gzip_disable指令接受一個正則表達式,當請求頭中的 UserAgent 字段滿足這個正則時,響應不會啟用 GZip,這是為了解決在某些瀏覽器啟用 GZip 帶來的問題。特別地,指令值msie6等價于MSIE [4-6]\.,但性能更好一些。另外,Nginx 0.8.11 后,msie6并不會匹配 UA 包含SV1的 IE6(例如 Windows XP SP2 上的 IE6),因為這個版本的 IE6 已經修復了關于 GZip 的若干 Bug。

開啟緩存

優化代碼邏輯的極限是移除所有邏輯;優化請求的極限是不發送任何請求。這兩點通過緩存都可以實現。

服務端

我的博客更新并不頻繁,評論部分也早就換成了 Disqus,所以完全可以將頁面靜態化,這樣就省掉了所有代碼邏輯和數據庫開銷。實現靜態化有很多種方案,我直接用的是 Nginx 的 proxy_cache:

NGINXproxy_cache_path  /home/jerry/cache/nginx/proxy_cache_path levels=1:2 keys_zone=pnc:300m inactive=7d max_size=10g;
proxy_temp_path   /home/jerry/cache/nginx/proxy_temp_path;
proxy_cache_key   $host$uri$is_args$args;

server {
    location / {
        resolver                  127.0.0.1;  
        proxy_cache               pnc;
        proxy_cache_valid         200 304 2h;
        proxy_cache_lock          on;
        proxy_cache_lock_timeout  5s;
        proxy_cache_use_stale     updating error timeout invalid_header http_500 http_502;

        proxy_http_version        1.1;

        proxy_ignore_headers      Set-Cookie;
        ... ...
    }
    ... ...
}

首先,在配置最外層定義一個緩存目錄,并指定名稱(keys_zone)和其他屬性,這樣在配置 proxy_pass 時,就可以使用這個緩存了。這里我對狀態值等于 200 和 304 的響應緩存了 2 小時。

默認情況下,如果響應頭里有 Set-Cookie 字段,Nginx 并不會緩存這次響應,因為它認為這次響應的內容是因人而異的。我的博客中,這個 Set-Cookie 對于用戶來說沒有用,也不會影響輸出內容,所以我通過配置proxy_ignore_header移除了它。

客戶端

服務端在輸出響應時,可以通過響應頭輸出一些與緩存有關的信息,從而達到少發或不發請求的目的。HTTP/1.1 的緩存機制稍微有點復雜,這里簡單介紹下:

首先:服務端可以通過響應頭里的Last-Modified(最后修改時間) 或者ETag(內容特征) 標記實體。瀏覽器會存下這些標記,并在下次請求時帶上If-Modified-Since: 上次 Last-Modified 的內容或If-None-Match: 上次 ETag 的內容, 詢問服務端資源是否過期。如果服務端發現并沒有過期,直接返回一個狀態碼為 304、正文為空的響應,告知瀏覽器使用本地緩存;如果資源有更新,服務端返回狀態碼 200、新的 Last-Modified、Etag 和正文。這個過程被稱之為 HTTP 的協商緩存,通常也叫做弱緩存。

可以看到協商緩存并不會節省連接數,但是在緩存生效時,會大幅減小傳輸內容(304 響應沒有正文,一般只有幾百字節)。另外為什么有兩個響應頭都可以用來實現協商緩存呢?這是因為一開始用的Last-Modified有兩個問題:1)只能精確到秒,1 秒內的多次變化反映不出來;2)時間采用絕對值,如果服務端 / 客戶端時間不對都可能導致緩存失效。HTTP/1.1 并沒有規定ETag的生成規則,而一般實現者都是對資源內容做摘要,能解決前面兩個問題。

另外一種緩存機制是服務端通過響應頭告訴瀏覽器,在什么時間之前(Expires)或在多長時間之內(Cache-Control: Max-age=xxx),不要再請求服務器了。這個機制我們通常稱之為 HTTP 的強緩存。

一旦資源命中強緩存規則后,再次訪問完全沒有 HTTP 請求(Chrome 開發者工具的 Network 面板依然會顯示請求,但是會注明 from cache;Firefox 的 firebug 也類似,會注明 BFCache),這會大幅提升性能。所以我們一般會對 CSS、JS、圖片等資源使用強緩存,而入口文件(HTML)一般使用協商緩存或不緩存,這樣可以通過修改入口文件中對強緩存資源的引入 URL 來達到即時更新的目的。

這里也解釋下為什么有了Expire,還要有Cache-Control。也有兩個原因:1)Cache-Control 功能更強大,對緩存的控制能力更強;2)Cache-Control 采用的 max-age 是相對時間,不受服務端 / 客戶端時間不對的影響。

另外關于瀏覽器的刷新(F5 / cmd + r)和強刷(Ctrl + F5 / shift + cmd +r):普通刷新會使用協商緩存,忽略強緩存;強刷會忽略瀏覽器所有緩存(并且請求頭會攜帶 Cache-Control:no-cache 和 Pragma:no-cache,用來通知所有中間節點忽略緩存)。只有從地址欄或收藏夾輸入網址、點擊鏈接等情況下,瀏覽器才會使用強緩存。

默認情況下,Nginx 對于靜態資源都會輸出Last-Modified,而ETag、Expire和Cache-Control則需要自己配置:

NGINXlocation ~ ^/static/ { root /home/jerry/www/blog/www; etag on; expires max;
}

expires指令可以指定具體的 max-age,例如10y代表 10 年,如果指定為max,最終輸出的Expires會是 2037 年最后一天,Cache-Control的max-age會是 10 年(準確說是 3650 天,315360000 秒)。

使用 SPDY(HTTP/2)

我的博客之前多次講到過 HTTP/2(SPDY),現階段 Nginx 只支持 SPDY/3.1,這樣配置就可以啟用了(編譯 Nginx 時需要加上 --with-http_spdy_module 和 --with-http_ssl_module):

NGINXserver {
    listen             443 ssl spdy fastopen=3;
    spdy_headers_comp  6;
    ... ...
}

那個fastopen=3用來開啟前面介紹過的 TCP Fast Open 功能。3 代表最多只能有 3 個未經三次握手的 TCP 鏈接在排隊。超過這個限制,服務端會退化到采用普通的 TCP 握手流程。這是為了減少資源耗盡攻擊:TFO 可以在第一次 SYN 的時候發送 HTTP 請求,而服務端會校驗 Fast Open Cookie(FOC),如果通過就開始處理請求。如果不加限制,惡意客戶端可以利用合法的 FOC 發送大量請求耗光服務端資源。

HTTPS 優化

建立 HTTPS 連接本身就慢(多了獲取證書、校驗證書、TLS 握手等等步驟),如果沒有優化好只能是慢上加慢。

NGINXserver {
    ssl_session_cache    shared:SSL:10m;
    ssl_session_timeout  10m;

    ssl_session_tickets  on;

    ssl_stapling         on;
    ssl_stapling_verify  on;
    resolver             8.8.4.4 8.8.8.8  valid=300s;
    resolver_timeout     10s;
    ... ...
}

我的這部分配置就兩部分內容:TLS 會話恢復和 OCSP stapling。

TLS 會話恢復的目的是為了簡化 TLS 握手,有兩種方案:Session Cache 和 Session Ticket。他們都是將之前握手的 Session 存起來供后續連接使用,所不同是 Cache 存在服務端,占用服務端資源;Ticket 存在客戶端,不占用服務端資源。另外目前主流瀏覽器都支持 Session Cache,而 Session Ticket 的支持度一般。

ssl_stapling開始的四行用來配置 OCSP stapling 策略。瀏覽器可能會在建立 TLS 連接時在線驗證證書有效性,從而阻塞 TLS 握手,拖慢整體速度。OCSP stapling 是一種優化措施,服務端通過它可以在證書鏈中封裝證書頒發機構的 OCSP(Online Certificate Status Protocol)響應,從而讓瀏覽器跳過在線查詢。服務端獲取 OCSP 一方面更快(因為服務端一般有更好的網絡環境),另一方面可以更好地緩存。有關 OCSP stapling 的詳細介紹,可以看這里

好了,我的博客關于安全和性能兩部分 Nginx 配置終于都寫完了。實際上很多策略沒辦法嚴格區分是為了安全還是性能,比如 HSTS 和 CHACHA20_POLY1305,兩方面都有考慮,所以寫的時候比較糾結,早知道就合成一篇來寫了。

本文鏈接:https://www.imququ.com/post/my-nginx-conf-for-wpo.html

--EOF--

 本文由用戶 mb2x 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!