在介紹完我博客(imququ.com)的 Nginx 配置中與安全有關的一些配置后,這篇文章繼續(xù)介紹與性能有關的一些配置。WEB 性能優(yōu)化是一個系統(tǒng)工程,涵蓋很多方面,做好其中某個環(huán)節(jié)并不意味性能就能變好,但可以肯定地說,如果某個環(huán)節(jié)做得很糟糕,那么結果一定會變差。 首先說明下,本文提到的一些 Nginx 配置,需要較高版本 Linux 內核才支持。在實際生產(chǎn)環(huán)境中,升級服務器內核并不是一件容易的事,但為了獲得最好的性能,有些升級還是必須的。很多公司服務器運維和項目開發(fā)并不在一個團隊,一方追求穩(wěn)定不出事故,另一方希望提升性能,本來就是矛盾的。好在我們折騰自己 VPS 時,可以無視這些限制。 TCP 優(yōu)化 Nginx 關于 TCP 的優(yōu)化基本都是修改系統(tǒng)內核提供的配置項,所以跟具體的 Linux 版本和系統(tǒng)配置有關,我對這一塊還不是非常熟悉,這里只能簡單介紹下:
第一行的 sendfile 配置可以提高 Nginx 靜態(tài)資源托管效率。sendfile 是一個系統(tǒng)調用,直接在內核空間完成文件發(fā)送,不需要先 read 再 write,沒有上下文切換開銷。 TCP_NOPUSH 是 FreeBSD 的一個 socket 選項,對應 Linux 的 TCP_CORK,Nginx 里統(tǒng)一用 tcp_nopush 來控制它,并且只有在啟用了 sendfile 之后才生效。啟用它之后,數(shù)據(jù)包會累計到一定大小之后才會發(fā)送,減小了額外開銷,提高網(wǎng)絡效率。 TCP_NODELAY 也是一個 socket 選項,啟用后會禁用 Nagle 算法,盡快發(fā)送數(shù)據(jù),可以節(jié)約 200ms。Nginx 只會針對處于 keep-alive 狀態(tài)的 TCP 連接才會啟用 tcp_nodelay。 可以看到 TCP_NOPUSH 是要等數(shù)據(jù)包累積到一定大小才發(fā)送,TCP_NODELAY 是要盡快發(fā)送,二者相互矛盾。實際上,它們確實可以一起用,最終的效果是先填滿包,再盡快發(fā)送。 關于這部分內容的更多介紹可以看這篇文章:NGINX OPTIMIZATION: UNDERSTANDING SENDFILE, TCP_NODELAY AND TCP_NOPUSH。 配置最后一行用來指定服務端為每個 TCP 連接最多可以保持多長時間。Nginx 的默認值是 75 秒,有些瀏覽器最多只保持 60 秒,所以我統(tǒng)一設置為 60。 另外,還有一個 TCP 優(yōu)化策略叫 TCP Fast Open(TFO),這里先介紹下,配置在后面貼出。TFO 的作用是用來優(yōu)化 TCP 握手過程??蛻舳说谝淮谓⑦B接還是要走三次握手,所不同的是客戶端在第一個 SYN 會設置一個 Fast Open 標識,服務端會生成 Fast Open Cookie 并放在 SYN-ACK 里,然后客戶端就可以把這個 Cookie 存起來供之后的 SYN 用。下面這個圖形象地描述了這個過程: 關于 TCP Fast Open 的更多信息,可以查看 RFC7413,或者這篇文章:Shaving your RTT with TCP Fast Open。需要注意的是,現(xiàn)階段只有 Linux、ChromeOS 和 Android 5.0 的 Chrome / Chromium 才支持 TFO,所以實際用途并不大。 5 月 26 日發(fā)布的 Nginx 1.9.1,增加了 reuseport 功能,意味著 Nginx 也開始支持 TCP 的 SO_REUSEPORT 選項了。這里也先簡單介紹下,具體配置方法后面統(tǒng)一介紹。啟用這個功能后,Nginx 會在指定的端口上監(jiān)聽多個 socket,每個 Worker 都能分到一個。請求過來時,系統(tǒng)內核會自動通過不同的 socket 分配給對應的 Worker,相比之前的單 socket 多 Worker 的模式,提高了分發(fā)效率。下面這個圖形象地描述了這個過程: 有關這部分內容的更多信息,可以查看 Nginx 的官方博客:Socket Sharding in NGINX Release 1.9.1。 開啟 Gzip 我們在上線前,代碼(JS、CSS 和 HTML)會做壓縮,圖片也會做壓縮(PNGOUT、Pngcrush、JpegOptim、Gifsicle 等)。對于文本文件,在服務端發(fā)送響應之前進行 GZip 壓縮也很重要,通常壓縮后的文本大小會減小到原來的 1/4 - 1/3。下面是我的配置:
這部分內容比較簡單,只有兩個地方需要解釋下: gzip_vary 用來輸出 Vary 響應頭,用來解決某些緩存服務的一個問題,詳情請看我之前的博客:HTTP 協(xié)議中 Vary 的一些研究。 gzip_disable 指令接受一個正則表達式,當請求頭中的 UserAgent 字段滿足這個正則時,響應不會啟用 GZip,這是為了解決在某些瀏覽器啟用 GZip 帶來的問題。特別地,指令值 msie6 等價于 MSIE [4-6]\.,但性能更好一些。另外,Nginx 0.8.11 后,msie6 并不會匹配 UA 包含 SV1 的 IE6(例如 Windows XP SP2 上的 IE6),因為這個版本的 IE6 已經(jīng)修復了關于 GZip 的若干 Bug。 開啟緩存 優(yōu)化代碼邏輯的極限是移除所有邏輯;優(yōu)化請求的極限是不發(fā)送任何請求。這兩點通過緩存都可以實現(xiàn)。 服務端 我的博客更新并不頻繁,評論部分也早就換成了 Disqus,所以完全可以將頁面靜態(tài)化,這樣就省掉了所有代碼邏輯和數(shù)據(jù)庫開銷。實現(xiàn)靜態(tài)化有很多種方案,我直接用的是 Nginx 的 proxy_cache(注:本博客為了做更精細的靜態(tài)化,已經(jīng)將緩存邏輯挪到 Web 應用里實現(xiàn)了):
首先,在配置最外層定義一個緩存目錄,并指定名稱(keys_zone)和其他屬性,這樣在配置 proxy_pass 時,就可以使用這個緩存了。這里我對狀態(tài)值等于 200 和 304 的響應緩存了 2 小時。 默認情況下,如果響應頭里有 Set-Cookie 字段,Nginx 并不會緩存這次響應,因為它認為這次響應的內容是因人而異的。我的博客中,這個 Set-Cookie 對于用戶來說沒有用,也不會影響輸出內容,所以我通過配置 proxy_ignore_header 移除了它。 客戶端 服務端在輸出響應時,可以通過響應頭輸出一些與緩存有關的信息,從而達到少發(fā)或不發(fā)請求的目的。HTTP/1.1 的緩存機制稍微有點復雜,這里簡單介紹下: 首先,服務端可以通過響應頭里的 Last-Modified(最后修改時間) 或者 ETag(內容特征) 標記實體。瀏覽器會存下這些標記,并在下次請求時帶上 If-Modified-Since: 上次 Last-Modified 的內容 或 If-None-Match: 上次 ETag 的內容,詢問服務端資源是否過期。如果服務端發(fā)現(xiàn)并沒有過期,直接返回一個狀態(tài)碼為 304、正文為空的響應,告知瀏覽器使用本地緩存;如果資源有更新,服務端返回狀態(tài)碼 200、新的 Last-Modified、Etag 和正文。這個過程被稱之為 HTTP 的協(xié)商緩存,通常也叫做弱緩存。 可以看到協(xié)商緩存并不會節(jié)省連接數(shù),但是在緩存生效時,會大幅減小傳輸內容(304 響應沒有正文,一般只有幾百字節(jié))。另外為什么有兩個響應頭都可以用來實現(xiàn)協(xié)商緩存呢?這是因為一開始用的 Last-Modified 有兩個問題: 1)只能精確到秒,1 秒內的多次變化反映不出來; 2)時間采用絕對值,如果服務端 / 客戶端時間不對都可能導致緩存失效 在輪詢的負載均衡算法中,如果各機器讀到的文件修改時間不一致,有緩存無故失效和緩存不更新的風險。HTTP/1.1 并沒有規(guī)定 ETag 的生成規(guī)則,而一般實現(xiàn)者都是對資源內容做摘要,能解決前面兩個問題。 另外一種緩存機制是服務端通過響應頭告訴瀏覽器,在什么時間之前(Expires)或在多長時間之內(Cache-Control: Max-age=xxx),不要再請求服務器了。這個機制我們通常稱之為 HTTP 的強緩存。 一旦資源命中強緩存規(guī)則后,再次訪問完全沒有 HTTP 請求(Chrome 開發(fā)者工具的 Network 面板依然會顯示請求,但是會注明 from cache;Firefox 的 firebug 也類似,會注明 BFCache),這會大幅提升性能。所以我們一般會對 CSS、JS、圖片等資源使用強緩存,而入口文件(HTML)一般使用協(xié)商緩存或不緩存,這樣可以通過修改入口文件中對強緩存資源的引入 URL 來達到即時更新的目的。 這里也解釋下為什么有了 Expire,還要有 Cache-Control。也有兩個原因:1)Cache-Control 功能更強大,對緩存的控制能力更強;2)Cache-Control 采用的 max-age 是相對時間,不受服務端 / 客戶端時間不對的影響。 另外關于瀏覽器的刷新(F5 / cmd + r)和強刷(Ctrl + F5 / shift + cmd +r):普通刷新會使用協(xié)商緩存,忽略強緩存;強刷會忽略瀏覽器所有緩存(并且請求頭會攜帶 Cache-Control:no-cache 和 Pragma:no-cache,用來通知所有中間節(jié)點忽略緩存)。只有從地址欄或收藏夾輸入網(wǎng)址、點擊鏈接等情況下,瀏覽器才會使用強緩存。 默認情況下,Nginx 對于靜態(tài)資源都會輸出 Last-Modified,而 ETag、Expire 和 Cache-Control 則需要自己配置:
expires 指令可以指定具體的 max-age,例如 10y 代表 10 年,如果指定為 max,最終輸出的 Expires 會是 2037 年最后一天,Cache-Control 的 max-age 會是 10 年(準確說是 3650 天,315360000 秒)。 使用 SPDY(HTTP/2) 我的博客之前多次講到過 HTTP/2(SPDY),現(xiàn)階段 Nginx 只支持 SPDY/3.1,這樣配置就可以啟用了(編譯 Nginx 時需要加上 --with-http_spdy_module 和 --with-http_ssl_module):
那個 fastopen=3 用來開啟前面介紹過的 TCP Fast Open 功能。3 代表最多只能有 3 個未經(jīng)三次握手的 TCP 鏈接在排隊。超過這個限制,服務端會退化到采用普通的 TCP 握手流程。這是為了減少資源耗盡攻擊:TFO 可以在第一次 SYN 的時候發(fā)送 HTTP 請求,而服務端會校驗 Fast Open Cookie(FOC),如果通過就開始處理請求。如果不加限制,惡意客戶端可以利用合法的 FOC 發(fā)送大量請求耗光服務端資源。 reuseport 就是用來啟用前面介紹過的 TCP SO_REUSEPORT 選項的配置。 HTTPS 優(yōu)化 建立 HTTPS 連接本身就慢(多了獲取證書、校驗證書、TLS 握手等等步驟),如果沒有優(yōu)化好只能是慢上加慢。
我的這部分配置就兩部分內容:TLS 會話恢復和 OCSP stapling。 TLS 會話恢復的目的是為了簡化 TLS 握手,有兩種方案:Session Cache 和 Session Ticket。他們都是將之前握手的 Session 存起來供后續(xù)連接使用,所不同是 Cache 存在服務端,占用服務端資源;Ticket 存在客戶端,不占用服務端資源。另外目前主流瀏覽器都支持 Session Cache,而 Session Ticket 的支持度一般。 ssl_stapling 開始的四行用來配置 OCSP stapling 策略。瀏覽器可能會在建立 TLS 連接時在線驗證證書有效性,從而阻塞 TLS 握手,拖慢整體速度。OCSP stapling 是一種優(yōu)化措施,服務端通過它可以在證書鏈中封裝證書頒發(fā)機構的 OCSP(Online Certificate Status Protocol)響應,從而讓瀏覽器跳過在線查詢。服務端獲取 OCSP 一方面更快(因為服務端一般有更好的網(wǎng)絡環(huán)境),另一方面可以更好地緩存。有關 OCSP stapling 的詳細介紹,可以看這里。 在給 Nginx 指定證書時,需要選擇合適的證書鏈。因為瀏覽器在驗證證書信任鏈時,會從站點證書開始,遞歸驗證父證書,直至信任的根證書。這里涉及到兩個問題:1)服務器證書是在握手期間發(fā)送的,由于 TCP 初始擁塞窗口的存在,如果證書太長很可能會產(chǎn)生額外的往返開銷;2)如果服務端證書沒包含中間證書,大部分瀏覽器可以正常工作,但會暫停驗證并根據(jù)子證書指定的父證書 URL 自己獲取中間證書。這個過程會產(chǎn)生額外的 DNS 解析、建立 TCP 連接等開銷。配置服務端證書鏈的最佳實踐是包含站點證書和中間證書兩部分。有的證書提供商簽出來的證書級別比較多,這會導致證書鏈變長,選擇的時候需要特別注意。 好了,我的博客關于安全和性能兩部分 Nginx 配置終于都寫完了。實際上很多策略沒辦法嚴格區(qū)分是為了安全還是性能,比如 HSTS 和 CHACHA20_POLY1305,兩方面都有考慮,所以寫的時候比較糾結,早知道就合成一篇來寫了。 |
|
來自: BENpudding > 《待分類》