為什麼要優化 Ngin HTTPS 延遲
Nginx 常作為最常見的伺服器,常被用作負載均衡 (Load Balancer)、反向代理 (Reverse Proxy),以及網關 (Gateway) 等等。一個配置得當的 Nginx 伺服器單機應該可以期望承受住 50K 到 80K 左右每秒的請求,同時将 CPU 負載在可控範圍内。
但在很多時候,負載并不是需要首要優化的重點。比如對于卡拉搜尋來說,我們希望使用者在每次擊鍵的時候,可以體驗即時搜尋的感覺,也就是說,每個搜尋請求必須在 100ms - 200ms 的時間内端對端地傳回給使用者,才能讓使用者搜尋時沒有“卡頓”和“加載”。是以,對于我們來說,優化請求延遲才是最重要的優化方向。
這篇文章中,我們先介紹 Nginx 中的 TLS 設定有哪些與請求延遲可能相關,如何調整才能最大化加速。然後我們用優化Nginx 伺服器的執行個體來分享如何調整 Nginx TLS/SSL 設定,為首次搜尋的使用者提速 30% 左右。我們會詳細讨論每一步我們做了一些什麼優化,優化的動機和效果。希望可以對其它遇到類似問題的同學提供幫助。
TLS 握手和延遲
很多時候開發者會認為:如果不是絕對在意性能,那麼了解底層和更細節的優化沒有必要。這句話在很多時候是恰當的,因為很多時候複雜的底層邏輯必須包起來,才能讓更高層的應用開發複雜度可控。比如說,如果你就隻需要開發一個 APP 或者網站,可能并沒有必要關注彙編細節,關注編譯器如何優化你的代碼——畢竟在蘋果或者安卓上很多優化在底層就做好了。
那麼,了解底層的 TLS 和應用層的 Nginx 延遲優化有什麼關系呢?
答案是多數情況下,優化網絡延遲其實是在嘗試減少使用者和伺服器之間的資料傳輸次數,也就是所謂的 roundtrip。由于實體限制,北京到雲南的光速傳播差不多就是要跑 20 來毫秒,如果你不小心讓資料必須多次往返于北京和雲南之間,那麼必然延遲就上去了。
是以如果你需要優化請求延遲,那麼了解一點底層網絡的上下文則會大有裨益,很多時候甚至是你是否可以輕松了解一個優化的關鍵。本文中我們不深入讨論太多 TCP 或者 TLS 機制的細節,如果有興趣的話請參考 High Performance Browser Networking 一書,可以免費閱讀。
舉個例子,下圖中展示了如果你的服務啟用了 HTTPS,在開始傳輸任何資料之前的資料傳輸情況。

在傳輸資料前資料已經跑了好幾個來回 roundtrip
可以看到,在你的使用者拿到他需要的資料前,底層的資料包就已經在使用者和你的伺服器之間跑了 3 個來回。
假設每次來回需要 28 毫秒的話,使用者已經等了 224 毫秒之後才開始接收資料。
同時這個 28 毫秒其實是非常樂觀的假設,在國内電信、聯通和移動以及各種複雜的網絡狀況下,使用者與伺服器之間的延遲更不可控。另一方面,通常一個網頁需要數十個請求,這些請求不一定可以全部并行,是以幾十乘以 224 毫秒,頁面打開可能就是數秒之後了。
是以,原則上如果可能的話,我們需要盡量減少使用者和伺服器之間的往返程 (roundtrip),在下文的設定中,對于每個設定我們會讨論為什麼這個設定有可能幫助減少往返程。
Nginx 中的 TLS 設定
那麼在 Nginx 設定中,怎樣調整參數會減少延遲呢?
開啟 HTTP/2
HTTP/2 标準是從 Google 的 SPDY 上進行的改進,比起 HTTP 1.1 提升了不少性能,尤其是需要并行多個請求的時候可以顯著減少延遲。在現在的網絡上,一個網頁平均需要請求幾十次,而在 HTTP 1.1 時代浏覽器能做的就是多開幾個連接配接(通常是 6 個)進行并行請求,而 HTTP 2 中可以在一個連接配接中進行并行請求。HTTP 2 原生支援多個并行請求,是以大大減少了順序執行的請求的往返程,可以首要考慮開啟。
如果你想自己看一下 HTTP 1.1 和 HTTP 2.0 的速度差異,可以試一下:https://www.httpvshttps.com/。我的網絡測試下來 HTTP/2 比 HTTP 1.1 快了 66%。
HTTP 1.1 與 HTTP 2.0 速度對比
在 Nginx 中開啟 HTTP 2.0 非常簡單,隻需要增加一個 http2 标志即可
listen 443 ssl;
# 改為
listen 443 ssl http2;
如果你擔心你的使用者用的是舊的用戶端,比如 Python 的 requests,暫時還不支援 HTTP 2 的話,那麼其實不用擔心。如果使用者的用戶端不支援 HTTP 2,那麼連接配接會自動降級為 HTTP 1.1,保持了後向相容。是以,所有使用舊 Client 的使用者,仍然不受影響,而新的用戶端則可以享受 HTTP/2 的新特性。
如何确認你的網站或者 API 開啟了 HTTP 2
在 Chrome 中打開開發者工具,點開 Protocol 之後在所有的請求中都可以看到請求用的協定了。如果 protocol 這列的值是 h2 的話,那麼用的就是 HTTP 2 了
用 Chrome 确認 HTTP/2 已經打開
當然另一個辦法是直接用 curl 如果傳回的 status 前有 HTTP/2 的話自然也就是 HTTP/2 開啟了。
➜ ~ curl --http2 -I https://kalasearch.cn
HTTP/2 403
server: Tengine
content-type: application/xml
content-length: 264
date: Tue, 22 Dec 2020 18:38:46 GMT
x-oss-request-id: 5FE23D363ADDB93430197043
x-oss-cdn-auth: success
x-oss-server-time: 0
x-alicdn-da-ups-status: endOs,0,403
via: cache13.l2et2[148,0], cache10.l2ot7[291,0], cache4.us13[360,0]
timing-allow-origin: *
eagleid: 2ff6169816086623266688093e
調整 Cipher 優先級
盡量挑選更新更快的 Cipher,有助于減少延遲:
# 手動啟用 cipher 清單
ssl_prefer_server_ciphers on; # prefer a list of ciphers to prevent old and slow ciphers
ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
啟用 OCSP Stapling
在國内這可能是對使用 Let's Encrypt 證書的服務或網站影響最大的延遲優化了。如果不啟用 OCSP Stapling 的話,在使用者連接配接你的伺服器的時候,有時候需要去驗證證書。而因為一些不可知的原因(這個就不說穿了)Let's Encrypt 的驗證伺服器并不是非常通暢,是以可以造成有時候數秒甚至十幾秒延遲的問題,這個問題在 iOS 裝置上特别嚴重
解決這個問題的方法有兩個:
- 不使用 Let's Encrypt,可以嘗試替換為阿裡雲提供的免費 DV 證書
- 開啟 OCSP Stapling
開啟了 OCSP Stapling 的話,跑到證書驗證這一步可以省略掉。省掉一個 roundtrip,特别是網絡狀況不可控的 roundtrip,可能可以将你的延遲大大減少。
在 Nginx 中啟用 OCSP Stapling 也非常簡單,隻需要設定:
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /path/to/full_chain.pem;
如何檢測 OCSP Stapling 是否已經開啟?
可以通過以下指令
openssl s_client -connect test.kalasearch.cn:443 -servername kalasearch.cn -status -tlsextdebug < /dev/null 2>&1 | grep -i "OCSP response"
來測試。如果結果為
OCSP response:
OCSP Response Data:
OCSP Response Status: successful (0x0)
Response Type: Basic OCSP Response
則表明已經開啟。參考 HTTPS 在 iPhone 上慢的問題 一文
調整 ssl_buffer_size
sslbuffersize 控制在發送資料時的 buffer 大小,預設設定是 16k。這個值越小,則延遲越小,而添加的報頭之類會使 overhead 會變大,反之則延遲越大,overhead 越小。
是以如果你的服務是 REST API 或者網站的話,将這個值調小可以減小延遲和 TTFB,但如果你的伺服器是用來傳輸大檔案的,那麼可以維持 16k。關于這個值的讨論和更通用的 TLS Record Size 的讨論,可以參考:Best value for nginx's sslbuffersize option
如果是網站或者 REST API,建議值為 4k,但是這個值的最佳取值顯然會因為資料的不同而不一樣,是以請嘗試 2 - 16k 間不同的值。在 Nginx 中調整這個值也非常容易
ssl_buffer_size 4k;
啟用 SSL Session 緩存
啟用 SSL Session 緩存可以大大減少 TLS 的反複驗證,減少 TLS 握手的 roundtrip。雖然 session 緩存會占用一定記憶體,但是用 1M 的記憶體就可以緩存 4000 個連接配接,可以說是非常非常劃算的。同時,對于絕大多數網站和服務,要達到 4000 個同時連接配接本身就需要非常非常大的使用者基數,是以可以放心開啟。
這裡 ssl_session_cache 設定為使用 50M 記憶體,以及 4 小時的連接配接逾時關閉時間 ssl_session_timeout
# Enable SSL cache to speed up for return visitors
ssl_session_cache shared:SSL:50m; # speed up first time. 1m ~= 4000 connections
ssl_session_timeout 4h;
卡拉搜尋如何減少 30% 的請求延遲
卡拉搜尋是國内的 Algolia,緻力于幫助開發者快速搭建即時搜尋功能(instant search),做國内最快最易用的搜尋即服務。
開發者接入後,所有搜尋請求通過卡拉 API 即可直接傳回給終端使用者。為了讓使用者有即時搜尋的體驗,我們需要在使用者每次擊鍵後極短的時間内(通常是 100ms 到 200ms)将結果傳回給使用者。是以每次搜尋需要可以達到 50 毫秒以内的引擎處理時間和 200 毫秒以内的端對端時間。
我們用豆瓣電影的資料做了一個電影搜尋的 Demo,如果感興趣的話歡迎體驗一下即時搜尋,嘗試一下搜尋“無間道”或者“大話西遊”體驗一下速度和相關度:https://movies-demo.kalasearch.cn/
對于每個請求隻有 100 到 200 毫秒的延遲預算,我們必須把每一步的延遲都考慮在内。
簡化一下,每個搜尋請求需要經曆的延遲有
卡拉搜尋的端對端延遲圖示
總延遲 = 使用者請求到達伺服器(T1) + 反代處理(Nginx T2) + 資料中心延遲(T3) + 伺服器處理 (卡拉引擎 T4) + 使用者請求傳回(T3+T1)
在上述延遲中,T1 隻與使用者與伺服器的實體距離相關,而 T3 非常小(參考Jeff Dean Number)可以忽略不計。
是以我們能控制的大緻隻有 T2 和 T4,即 Nginx 伺服器的處理時間和卡拉的引擎處理時間。
Nginx 在這裡作為反向代理,處理一些安全、流量控制和 TLS 的邏輯,而卡拉的引擎則是一個在 Lucene 基礎上的倒排引擎。
我們首先考慮的第一個可能性是:延遲是不是來自卡拉引擎呢?
在下圖展示的 Grafana 儀表盤中,我們看到除了幾個時不時的慢查詢,搜尋的 95% 伺服器處理延遲小于 20 毫秒。對比同樣的資料集上 benchmark 的 Elastic Search 引擎的 P95 搜尋延遲則在 200 毫秒左右,是以排除了引擎速度慢的可能。
Search Grafana
而在阿裡雲監控中,我們設定了從全國各地向卡拉伺服器發送搜尋請求。我們終于發現 SSL 處理時間時常會超過 300 毫秒,也就是說在 T2 這一步,光處理 TLS 握手之類的事情,Nginx 已經用掉了我們所有的請求時間預算。
同時檢查之後我們發現,在蘋果裝置上搜尋速度格外慢,特别是第一次通路的裝置。是以我們大緻判斷應該是因為我們使用的 Let's Encrypt 證書的問題。
我們按照上文中的步驟對 Nginx 設定進行了調整,并将步驟總結出來寫了這篇文章。在調整了 Nginx TLS 的設定後,SSL 時間從平均的 140ms 降低到了 110ms 左右(全國所有省份聯通和移動測試點),同時蘋果裝置上首次通路慢的問題也消失了。
調整後延遲
在調整過後,全國範圍内測試的搜尋延遲降低到了 150 毫秒左右。
總結
調整 Nginx 中的 TLS 設定對于使用 HTTPS 的服務和網站延遲有非常大的影響。本文中總結了 Nginx 中與 TLS 相關的設定,詳細讨論各個設定可能對延遲的影響,并給出了調整建議。之後我們會繼續讨論 HTTP/2 對比 HTTP 1.x 有哪些具體改進,以及在 REST API 使用 HTTP/2 有哪些優缺點,請繼續關注