原文:https://imququ.com/post/my-nginx-conf-for-wpo.htmljavascript
在介紹完我博客(imququ.com)的 Nginx 配置中與安全有關的一些配置後,這篇文章繼續介紹與性能有關的一些配置。WEB 性能優化是一個系統工程,涵蓋不少方面,作好其中某個環節並不意味性能就能變好,但能夠確定地說,若是某個環節作得很糟糕,那麼結果必定會變差。css
首先說明下,本文提到的一些 Nginx 配置,須要較高版本 Linux 內核才支持。在實際生產環境中,升級服務器內核並非一件容易的事,但爲了得到最好的性能,有些升級仍是必須的。不少公司服務器運維和項目開發並不在一個團隊,一方追求穩定不出事故,另外一方但願提高性能,原本就是矛盾的。好在咱們折騰本身 VPS 時,能夠無視這些限制。html
Nginx 關於 TCP 的優化基本都是修改系統內核提供的配置項,因此跟具體的 Linux 版本和系統配置有關,我對這一塊還不是很是熟悉,這裏只能簡單介紹下:java
NGINXhttp { sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 60; ... ... }
第一行的 sendfile
配置能夠提升 Nginx 靜態資源託管效率。sendfile 是一個系統調用,直接在內核空間完成文件發送,不須要先 read 再 write,沒有上下文切換開銷。node
TCP_NOPUSH 是 FreeBSD 的一個 socket 選項,對應 Linux 的 TCP_CORK,Nginx 裏統一用 tcp_nopush
來控制它,而且只有在啓用了 sendfile 以後才生效。啓用它以後,數據包會累計到必定大小以後纔會發送,減少了額外開銷,提升網絡效率。nginx
TCP_NODELAY 也是一個 socket 選項,啓用後會禁用 Nagle 算法,儘快發送數據,某些狀況下能夠節約 200ms(Nagle 算法原理是:在發出去的數據還未被確認以前,新生成的小數據先存起來,湊滿一個 MSS 或者等到收到確認後再發送)。Nginx 只會針對處於 keep-alive 狀態的 TCP 鏈接纔會啓用 tcp_nodelay
。正則表達式
能夠看到 TCP_NOPUSH 是要等數據包累積到必定大小才發送,TCP_NODELAY 是要儘快發送,兩者相互矛盾。實際上,它們確實能夠一塊兒用,最終的效果是先填滿包,再儘快發送。算法
關於這部份內容的更多介紹能夠看這篇文章:NGINX OPTIMIZATION: UNDERSTANDING SENDFILE, TCP_NODELAY AND TCP_NOPUSH。數據庫
配置最後一行用來指定服務端爲每一個 TCP 鏈接最多能夠保持多長時間。Nginx 的默認值是 75 秒,有些瀏覽器最多隻保持 60 秒,因此我統一設置爲 60。json
另外,還有一個 TCP 優化策略叫 TCP Fast Open(TFO),這裏先介紹下,配置在後面貼出。TFO 的做用是用來優化 TCP 握手過程。客戶端第一次創建鏈接仍是要走三次握手,所不一樣的是客戶端在第一個 SYN 會設置一個 Fast Open 標識,服務端會生成 Fast Open Cookie 並放在 SYN-ACK 裏,而後客戶端就能夠把這個 Cookie 存起來供以後的 SYN 用。下面這個圖形象地描述了這個過程:
關於 TCP Fast Open 的更多信息,能夠查看 RFC7413,或者這篇文章:Shaving your RTT with TCP Fast Open。須要注意的是,現階段只有 Linux、ChromeOS 和 Android 5.0 的 Chrome / Chromium 才支持 TFO,因此實際用途並不大。
5 月 26 日發佈的 Nginx 1.9.1,增長了 reuseport
功能,意味着 Nginx 也開始支持 TCP 的 SO_REUSEPORT 選項了。這裏也先簡單介紹下,具體配置方法後面統一介紹。啓用這個功能後,Nginx 會在指定的端口上監聽多個 socket,每一個 Worker 都能分到一個。請求過來時,系統內核會自動經過不一樣的 socket 分配給對應的 Worker,相比以前的單 socket 多 Worker 的模式,提升了分發效率。下面這個圖形象地描述了這個過程:
有關這部份內容的更多信息,能夠查看 Nginx 的官方博客:Socket Sharding in NGINX Release 1.9.1。
咱們在上線前,代碼(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_http_version 1.0; 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。
默認 Nginx 只會針對 HTTP/1.1 及以上的請求才會啓用 GZip,由於部分早期的 HTTP/1.0 客戶端在處理 GZip 時有 Bug。如今基本上能夠忽略這種狀況,因而能夠指定 gzip_http_version 1.0
來針對 HTTP/1.0 及以上的請求開啓 GZip。
優化代碼邏輯的極限是移除全部邏輯;優化請求的極限是不發送任何請求。這兩點經過緩存均可以實現。
個人博客更新並不頻繁,評論部分也早就換成了 Disqus,因此徹底能夠將頁面靜態化,這樣就省掉了全部代碼邏輯和數據庫開銷。實現靜態化有不少種方案,我直接用的是 Nginx 的 proxy_cache(注:本博客爲了作更精細的靜態化,已經將緩存邏輯挪到 Web 應用裏實現了):
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 的內容
,詢問服務端資源是否過時。若是服務端發現並無過時,直接返回一個狀態碼爲 30四、正文爲空的響應,告知瀏覽器使用本地緩存;若是資源有更新,服務端返回狀態碼 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 來達到即時更新的目的。
這裏也解釋下爲何有了 Expires
,還要有 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
、Expires
和 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 秒)。
個人博客以前屢次講到過 HTTP/2(SPDY),現階段 Nginx 只支持 SPDY/3.1,這樣配置就能夠啓用了(編譯 Nginx 時須要加上 --with-http_spdy_module 和 --with-http_ssl_module):
NGINXserver { listen 443 ssl spdy fastopen=3 reuseport; spdy_headers_comp 6; ... ... }
那個 fastopen=3
用來開啓前面介紹過的 TCP Fast Open 功能。3 表明最多隻能有 3 個未經三次握手的 TCP 連接在排隊。超過這個限制,服務端會退化到採用普通的 TCP 握手流程。這是爲了減小資源耗盡攻擊:TFO 能夠在第一次 SYN 的時候發送 HTTP 請求,而服務端會校驗 Fast Open Cookie(FOC),若是經過就開始處理請求。若是不加限制,惡意客戶端能夠利用合法的 FOC 發送大量請求耗光服務端資源。
reuseport
就是用來啓用前面介紹過的 TCP SO_REUSEPORT 選項的配置。
創建 HTTPS 鏈接自己就慢(多了獲取證書、校驗證書、TLS 握手等等步驟),若是沒有優化好只能是慢上加慢。
NGINXserver { ssl_session_cache shared:SSL:10m; ssl_session_timeout 60m; ssl_session_tickets on; ssl_stapling on; ssl_stapling_verify on; ssl_trusted_certificate /xxx/full_chain.crt; 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 的詳細介紹,能夠看這裏。
這些策略設置好以後,能夠經過 Qualys SSL Server Test 這個工具來驗證是否生效,例以下圖就是本博客的測試結果(via):
在給 Nginx 指定證書時,須要選擇合適的證書鏈。由於瀏覽器在驗證證書信任鏈時,會從站點證書開始,遞歸驗證父證書,直至信任的根證書。這裏涉及到兩個問題:1)服務器證書是在握手期間發送的,因爲 TCP 初始擁塞窗口的存在,若是證書太長極可能會產生額外的往返開銷;2)若是服務端證書沒包含中間證書,大部分瀏覽器能夠正常工做,但會暫停驗證並根據子證書指定的父證書 URL 本身獲取中間證書。這個過程會產生額外的 DNS 解析、創建 TCP 鏈接等開銷。配置服務端證書鏈的最佳實踐是包含站點證書和中間證書兩部分。有的證書提供商簽出來的證書級別比較多,這會致使證書鏈變長,選擇的時候須要特別注意。
好了,個人博客關於安全和性能兩部分 Nginx 配置終於都寫完了。實際上不少策略沒辦法嚴格區分是爲了安全仍是性能,好比 HSTS 和 CHACHA20_POLY1305,兩方面都有考慮,因此寫的時候比較糾結,早知道就合成一篇來寫了。