1. 程式人生 > >從 Nginx 預設不壓縮 HTTP/1.0 說起

從 Nginx 預設不壓縮 HTTP/1.0 說起

提醒:本文最後更新於 1072 天前,文中所描述的資訊可能已發生改變,請謹慎使用。

臨近年關,明顯變忙,部落格也更新得慢了,以後儘量保證周更吧。今天這篇文章屬於計劃之外的更新,源自於白天看到的《一個基於 http 協議的優化》。在這篇文章中,作者描述了這樣一個現象:

在移動的 http 請求量和聯通不相上下的前提下,移動的 http response 帶來的網路流量是聯通的 2.5 倍。移動大概有 3 成的請求都沒有做壓縮,而聯通幾乎都是經過壓縮的。那些沒有經過壓縮的 http 會話都是走了 1.0 的協議,相反經過壓縮的 http 會話都是走了 http1.1 協議。

也就是說在相同的服務端配置下,移動運營商過來的流量中有 30% 走了 HTTP/1.0,而作者所使用的 HTTP Server,不對 HTTP/1.0 響應啟用 GZip。

為什麼在移動運營商網路下會有這麼高比例的 HTTP/1.0 請求,本文按下不表,總之這一定是移動的原因。直接看另外一個問題,也就是本文標題所寫:Nginx 為什麼預設不壓縮 HTTP/1.0?

那篇文章的作者並沒有說明他用什麼 HTTP Server,我這裡直接當成 Nginx 好了。後面會發現這個問題跟 HTTP 協議有關,所有 HTTP Server 都會遇上。

在 Nginx 的官網文件中,有這樣一個指令:

Syntax: gzip_http_version 1.0 | 1.1;
Default: gzip_http_version 1.1;
Context: http, server, location
Sets the minimum HTTP version of a request required to compress a response.

很明顯,這個指令是用來設定 Nginx 啟用 GZip 所需的 HTTP 最低版本,預設是 HTTP/1.1。也就是說 Nginx 預設不壓縮 HTTP/1.0 是因為這個指令,將它的值改為 1.0 就能解決問題。

對於文字檔案,GZip 的效果非常明顯,開啟後傳輸所需流量大約會降至 1/4 ~ 1/3。這麼好的事情,Nginx 改一下配置就可以支援,為什麼它預設不開啟?

Nginx 對於滿足條件(請求頭中有 Accept-Encoding: gzip,響應內容的 Content-Type 存在於 gzip_types 列表)的請求會採用即時壓縮(On-The-Fly Compression),整個壓縮過程在記憶體中流式完成。也就是說,Nginx 不會等檔案 GZip 完成再返回響應,而是邊壓縮邊響應,這樣可以顯著提高 TTFB(Time To First Byte,首位元組時間,WEB 效能優化重要指標)。這樣唯一的問題是,Nginx 開始返回響應時,它無法知道將要傳輸的檔案最終有多大,也就是無法給出 Content-Length

這個響應頭部。

我們還知道,HTTP/1.1 預設支援 TCP 持久連線(Persistent Connection),HTTP/1.0 也可以通過顯式指定 Connection: keep-alive 來啟用持久連線。HTTP 執行在 TCP 連線之上,自然也有著跟 TCP 一樣的三次握手、慢啟動等特性,要想提高 HTTP 效能,啟用持久連線就顯得尤為重要。

明白以上兩點,馬上就能水落石出了:對於 TCP 持久連線上的 HTTP 報文,客戶端需要一種機制來準確判斷結束位置。而在 HTTP/1.0 中,這種機制只有 Content-Length。於是,對於本文前面提出的情況,HTTP Server 只能要麼不壓縮,要麼不啟用持久連線(對於非持久連線,TCP 斷開就可以認為 HTTP 報文結束),而 Nginx 預設選擇的是前者。

那麼在 HTTP/1.1 中,這個問題解決了嗎?當然!我在之前的文章中講過,HTTP/1.1 新增的 Transfer-Encoding: chunked 所對應的分塊傳輸機制可以完美解決這類問題。有興趣的同學可以檢視我的這篇文章:HTTP 協議中的 Transfer-Encoding

在很久以前,我們為了降低服務端的 CPU 壓力,往往會把靜態資源預先壓縮為 .gz 檔案,這樣 HTTP Server 不需要即時壓縮檔案,也就不會遇到本文提到的這個問題。但隨著時間的推移,現在幾乎沒人會這麼幹了,想要了解預先壓縮細節的同學可以看下 Nginx 的 ngx_http_gzip_static_module 模組。

理論知識先寫到這裡,最後用實踐來驗證一下:

首先,不啟用 Nginx 的 HTTP/1.0 GZip 功能,使用 HTTP/1.0 請求報文測試:

http/1.0 without gzip

可以看到,儘管我的請求報文中指明瞭可以接受 GZip,但是返回的內容依然是未壓縮的;同時服務端響應了 Content-LengthConnection: keep-alive,連線並沒有斷開。也就是說對於 HTTP/1.0 請求,Nginx 為了儘可能啟用持久連線,放棄了 GZip,這是 Nginx 的預設策略。

然後,啟用 Nginx 的 HTTP/1.0 GZip 功能,使用 HTTP/1.0 請求報文測試:

http/1.0 with gzip

可以看到,這次的請求報文與上次完全一樣,但是結果截然不同:雖然返回的內容被壓縮了,但是連線也被斷開了,服務端返回了 Connection: close。原因就是之前說過的,動態壓縮導致無法事先得知響應內容長度,在 HTTP/1.0 中只能依靠斷開連線來讓客戶端知道響應結束了。

最後,使用 HTTP/1.1 請求報文測試:

http/1.1 with gzip

可以看到,由於請求報文是 HTTP/1.1 的,Nginx 能知道這個客戶端支援 HTTP/1.1 的 Transfer-Encoding: chunked,於是通過分塊傳輸解決了所有問題:既啟用了壓縮,也啟用了持久連線。

那麼,對於 HTTP/1.0 請求,我們是讓 Nginx 放棄持久連線好,還是放棄 GZip 好呢?

實際上,由於 HTML 文件或 JSON 介面一般都是用 PHP、Node.js 等服務端語言動態輸出,即使不壓縮,Nginx 也無法事先得知它的 Content-Length,在 HTTP/1.0 中無論如何都無法啟用持久連線,這時還不如啟用 GZip 省點流量。

對於 JS、CSS 等事先可以知道大小的靜態文字檔案,我的建議是,移動端首次訪問把重要的 JS、CSS 都內聯在 HTML 中,然後存入 localStorage,後續不輸出;不重要的 JS、CSS 通過外鏈引入,啟用 GZip,犧牲 keep-alive 來達到減少流量的目的。

而對於圖片類靜態資源,由於主流圖片格式都已經經過高度壓縮,實在沒必要再浪費服務端 CPU 來開啟 GZip,這樣也不會對 keep-alive 機制產生影響。

本文先寫到這裡,歡迎來部落格原文進行評論、交流。瀏覽器的 GZip 其實還有很多有趣的小故事,先賣個關子,以後有空再寫。

--EOF--

提醒:本文最後更新於 1072 天前,文中所描述的資訊可能已發生改變,請謹慎使用。