前言為什麼要最佳化 Ngin HTTPS 延遲
Nginx 常作為最常見的伺服器,常被用作負載均衡 (Load Balancer)、反向代理 (Reverse Proxy),以及閘道器 (Gateway) 等等。一個配置得當的 Nginx 伺服器單機應該可以 期望承受住 50K 到 80K 左右 [1] 每秒的請求,同時將 CPU 負載在可控範圍內。
但在很多時候,負載並不是需要首要最佳化的重點。比如對於卡拉搜尋來說,我們希望使用者在每次擊鍵的時候,可以體驗即時搜尋的感覺,也就是說, 每個搜尋請求必須在 100ms - 200ms 的時間 內端對端地返回給使用者,才能讓使用者搜尋時沒有“卡頓”和“載入”。因此,對於我們來說,最佳化請求延遲才是最重要的最佳化方向。
這篇文章中,我們先介紹 Nginx 中的 TLS 設定有哪些與請求延遲可能相關,如何調整才能最大化加速。然後我們用最佳化 卡拉搜尋 [2] Nginx 伺服器的例項來分享如何調整 Nginx TLS/SSL 設定,為首次搜尋的使用者提速 30% 左右。我們會詳細討論每一步我們做了一些什麼最佳化,最佳化的動機和效果。希望可以對其它遇到類似問題的同學提供幫助。
TLS 握手和延遲很多時候開發者會認為:如果不是絕對在意效能,那麼瞭解底層和更細節的最佳化沒有必要。這句話在很多時候是恰當的,因為很多時候複雜的底層邏輯必須包起來,才能讓更高層的應用開發複雜度可控。比如說,如果你就只需要開發一個 APP 或者網站,可能並沒有必要關注彙編細節,關注編譯器如何最佳化你的程式碼——畢竟在蘋果或者安卓上很多最佳化在底層就做好了。
那麼,瞭解底層的 TLS 和應用層的 Nginx 延遲最佳化有什麼關係呢?
答案是多數情況下,最佳化網路延遲其實是在嘗試減少使用者和伺服器之間的資料傳輸次數,也就是所謂的 roundtrip。由於物理限制,北京到雲南的光速傳播差不多就是要跑 20 來毫秒,如果你不小心讓資料必須多次往返於北京和雲南之間,那麼必然延遲就上去了。
因此如果你需要最佳化請求延遲,那麼瞭解一點底層網路的上下文則會大有裨益,很多時候甚至是你是否可以輕鬆理解一個最佳化的關鍵。本文中我們不深入討論太多 TCP 或者 TLS 機制的細節,如果有興趣的話請參考 High Performance Browser Networking [4] 一書,可以免費閱讀。
舉個例子,下圖中展示瞭如果你的服務啟用了 HTTPS,在開始傳輸任何資料之前的資料傳輸情況。
在傳輸資料前資料已經跑了好幾個來回 roundtrip
可以看到,在你的使用者拿到他需要的資料前,底層的資料包就已經在使用者和你的伺服器之間跑了 3 個來回。
假設每次來回需要 28 毫秒的話,使用者已經等了 224 毫秒之後才開始接收資料。
同時這個 28 毫秒其實是非常樂觀的假設,在國內電信、聯通和移動以及各種複雜的網路狀況下,使用者與伺服器之間的延遲更不可控。另一方面,通常一個網頁需要數十個請求,這些請求不一定可以全部並行,因此幾十乘以 224 毫秒,頁面開啟可能就是數秒之後了。
Nginx 中的 TLS 設定那麼在 Nginx 設定中,怎樣調整引數會減少延遲呢?
開啟 HTTP/2HTTP/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.cnHTTP/2 403server: Tenginecontent-type: application/xmlcontent-length: 264date: Tue, 22 Dec 2020 18:38:46 GMTx-oss-request-id: 5FE23D363ADDB93430197043x-oss-cdn-auth: successx-oss-server-time: 0x-alicdn-da-ups-status: endOs,0,403via: cache13.l2et2[148,0], cache10.l2ot7[291,0], cache4.us13[360,0]timing-allow-origin: *eagleid: 2ff6169816086623266688093e
調整 Cipher 優先順序儘量挑選更新更快的 Cipher,有助於 減少延遲 [5] :
# 手動啟用 cipher 列表ssl_prefer_server_ciphers on; # prefer a list of ciphers to prevent old and slow ciphersssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
啟用 OCSP Stapling
在國內這可能是對使用 Let's Encrypt 證書的服務或網站影響最大的延遲優化了。如果不啟用 OCSP Stapling 的話,在使用者連線你的伺服器的時候,有時候需要去驗證證書。而因為一些不可知的原因(這個就不說穿了) Let's Encrypt 的驗證伺服器並不是非常通暢 [6] ,因此可以造成有時候 數秒甚至十幾秒延遲的問題 [7] ,這個問題在 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 上慢的問題 [8] 一文。
調整 ssl_buffer_sizessl_buffer_size 控制在傳送資料時的 buffer 大小,預設設定是 16k。這個值越小,則延遲越小,而新增的報頭之類會使 overhead 會變大,反之則延遲越大,overhead 越小。
因此如果你的服務是 REST API [9] 或者網站的話,將這個值調小可以減小延遲和 TTFB,但如果你的伺服器是用來傳輸大檔案的,那麼可以維持 16k。關於這個值的討論和更通用的 TLS Record Size 的討論,可以參考: Best value for nginx's ssl*buffer*size option [10]
如果是網站或者 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 visitorsssl_session_cache shared:SSL:50m; # speed up first time. 1m ~= 4000 connectionsssl_session_timeout 4h;
卡拉搜尋如何減少 30% 的請求延遲卡拉搜尋是國內的 Algolia [11] ,致力於幫助開發者快速搭建即時搜尋功能(instant search),做國內最快最易用的搜尋即服務。
開發者接入後,所有搜尋請求透過卡拉 API 即可直接返回給終端使用者。為了讓使用者有即時搜尋的體驗,我們需要在使用者每次擊鍵後極短的時間內(通常是 100ms 到 200ms)將結果返回給使用者。因此每次搜尋需要可以達到 50 毫秒以內的引擎處理時間和 200 毫秒以內的端對端時間。
對於每個請求只有 100 到 200 毫秒的延遲預算,我們必須把每一步的延遲都考慮在內。
簡化一下,每個搜尋請求需要經歷的延遲有
卡拉搜尋的端對端延遲圖示
總延遲 = 使用者請求到達伺服器(T1) + 反代處理(Nginx T2) + 資料中心延遲(T3) + 伺服器處理 (卡拉引擎 T4) + 使用者請求返回(T3+T1)
在上述延遲中,T1 只與使用者與伺服器的物理距離相關,而 T3 非常小(參考 Jeff Dean Number [12] )可以忽略不計。
所以我們能控制的大致只有 T2 和 T4,即 Nginx 伺服器的處理時間和卡拉的引擎處理時間。
Nginx 在這裡作為反向代理,處理一些安全、流量控制和 TLS 的邏輯,而卡拉的引擎則是一個在 Lucene 基礎上的倒排引擎。
我們首先考慮的第一個可能性是:延遲是不是來自卡拉引擎呢?
在下圖展示的 Grafana 儀表盤 [13] 中,我們看到除了幾個時不時的慢查詢,搜尋的 95% 伺服器處理延遲小於 20 毫秒。對比同樣的資料集上 benchmark 的 Elastic Search 引擎的 P95 搜尋延遲則在 200 毫秒左右,所以排除了引擎速度慢的可能。
Search Grafana
而在阿里雲監控中,我們設定了從全國各地向卡拉伺服器傳送搜尋請求。我們終於發現 SSL 處理時間時常會超過 300 毫秒,也就是說在 T2 這一步,光處理 TLS 握手之類的事情,Nginx 已經用掉了我們所有的請求時間預算。
同時檢查之後我們發現,在蘋果裝置上搜索速度格外慢,特別是第一次訪問的裝置。因此我們大致判斷應該是因為我們使用的 Let's Encrypt 證書的問題。
我們按照上文中的步驟對 Nginx 設定進行了調整,並將步驟總結出來寫了這篇文章。在調整了 Nginx TLS 的設定後,SSL 時間從平均的 140ms 降低到了 110ms 左右(全國所有省份聯通和移動測試點),同時蘋果裝置上首次訪問慢的問題也消失了。
調整後延遲
在調整過後,全國範圍內測試的搜尋延遲降低到了 150 毫秒左右。