1. 程式人生 > 程式設計 >優化 Tengine HTTPS 握手時間

優化 Tengine HTTPS 握手時間

背景

網路延遲是網路上的主要效能瓶頸之一。在最壞的情況下,客戶端開啟一個連結需要DNS查詢(1個 RTT),TCP握手(1個 RTT),TLS 握手(2個RTT),以及最後的 HTTP 請求和響應,可以看出客戶端收到第一個 HTTP 響應的首位元組需要5個 RTT 的時間,而首位元組時間對 web 體驗非常重要,可以體現在網站的首屏時間,直接影響使用者判斷網站的快慢,所以首位元組時間(TTFB)是網站和伺服器響應速度的重要指標,下面我們來看影響 SSL 握手的幾個方面:

TCP_NODELAY

我們知道,小包的載荷率非常小,若網路上出現大量的小包,則網路利用率比較低,就像客運汽車,來一個人發一輛車,可想而知這效率將會很差,這就是典型的 TCP 小包問題,為瞭解決這個問題所以就有了 Nigle 演演算法,演演算法思想很簡單,就是將多個即將傳送的小包,快取和合併成一個大包,然後一次性發送出去,就像客運汽車滿員發車一樣,這樣效率就提高了很多,所以核心協議棧會預設開啟 Nigle 演演算法優化。Night 演演算法認為只要當傳送方還沒有收到前一次傳送 TCP 報文段的的 ACK 時,傳送方就應該一直快取資料直到資料達到可以傳送的大小(即 MSS 大小),然後再統一合併到一起傳送出去,如果收到上一次傳送的 TCP 報文段的 ACK 則立馬將快取的資料傳送出去。雖然效率提高了,但對於急需交付的小包可能就不適合了,比如 SSL 握手期間互動的小包應該立即傳送而不應該等到傳送的資料達到 MSS 大小才傳送,所以,SSL 握手期間應該關閉 Nigle 演演算法,核心提供了關閉 Nigle 演演算法的選項: TCP_NODELAY,對應的 tengine/nginx 程式碼如下:



需要注意的是這塊程式碼是2017年5月份才提交的程式碼,使用老版本的 tengine/nginx 需要自己打 patch。

TCP Delay Ack

與 Nigle 演演算法對應的網路優化機制叫 TCP 延遲確認,也就是 TCP Delay Ack,這個是針對接收方來講的機制,由於 ACK 包是有效 payload 比較少的小包,如果頻繁的發 ACK 包也會導致網路額外的開銷,同樣出現前面提到的小包問題,效率低下,因此延遲確認機制會讓接收方將多個收到資料包的 ACK 打包成一個 ACK 包返回給傳送方,從而提高網路傳輸效率,跟 Nigle 演演算法一樣,核心也會預設開啟 TCP Delay Ack 優化。進一步講,接收方在收到資料後,並不會立即回覆 ACK,而是延遲一定時間,一般ACK 延遲傳送的時間為 200ms(每個作業系統的這個時間可能略有不同),但這個 200ms 並非收到資料後需要延遲的時間,系統有一個固定的定時器每隔 200ms 會來檢查是否需要傳送 ACK 包,這樣可以合併多個 ACK 從而提高效率,所以,如果我們去抓包時會看到有時會有 200ms 左右的延遲。但是,對於 SSL 握手來說,200ms 的延遲對使用者體驗影響很大,如下圖:


9號包是客戶端的 ACK,對 7號伺服器端發的證書包進行確認,這兩個包相差了將近 200ms,這個就是客戶端的 delay ack,這樣這次 SSL 握手時間就超過 200ms 了。那怎樣優化呢?其實只要我們儘量少傳送小包就可以避免,比如上面的截圖,只要將7號和10號一起傳送就可以避免 delay ack,這是因為核心協議棧在回覆 ACK 時,如果收到的資料大於1個 MSS 時會立即 ACK,核心原始碼如下:


知道了問題的原因所在以及如何避免,那就看應用層的傳送資料邏輯了,由於是在 SSL 握手期間,所以應該跟 SSL 寫核心有關係,檢視 openssl 的原始碼:


預設寫 buffer 大小是 4k,當證書比較大時,就容易分多次寫核心,從而觸發客戶端的 delay ack。
接下來檢視 tengine 有沒有調整這個 buffer 的地方,還真有(下圖第903行):


那不應該有 delay ack 啊……
無奈之下只能上 gdb 大法了,除錯之後發現果然沒有呼叫到 BIO_set_write_buffer_size,原因是 rbio 和 wbio 相等了,那為啥以前沒有這種情況現在才有呢?難道是升級 openssl 的原因?繼續查 openssl-1.0.2 程式碼:


openssl-1.1.1 的 SSL_get_wbio 有了變化:


原因終於找到了,使用老版本就沒有這個問題。就不細去看 bbio 的實現了,修復也比較簡單,就用老版本的實現即可,所以就打了個 patch:


重新編譯打包後測試,問題得到了修復。使用新版 openssl 遇到同樣問題的同學可以在此地方打 patch。

Session 複用

完整的 SSL 握手需要2個 RTT,SSL Session 複用則只需要1個 RTT,大大縮短了握手時間,另外 Session 複用避免了金鑰交換的 CPU 運算,大大降低 CPU 的消耗,所以伺服器必須開啟 Session 複用來提高伺服器的效能和減少握手時間,SSL 中有兩種 Session 複用方式:

  • 服務端 Session Cache
    大概原理跟網頁 SESSION 類似,服務端將上次完整握手的會話資訊快取在伺服器上,然後將 session id 告知客戶端,下次客戶端會話複用時帶上這個 session id,即可恢復出 SSL 握手需要的會話資訊,然後客戶端和伺服器採用相同的演演算法即可生成會話金鑰,完成握手。這種方式是最早優化 SSL 握手的手段,在早期都是單機模式下並沒有什麼問題,但是現在都是分散式叢集模式,這種方式的弊端就暴露出來了,拿 CDN 來說,一個節點內有幾十臺機器,前端採用 LVS 來負載均衡,那客戶端的 SSL 握手請求到達哪臺機器並不是固定的,這就導致 Session 複用率比較低。所以後來出現了 Session Ticket 的優化方案,之後再細講。那服務端 Session Cache 這種複用方式如何在分散式叢集中優化呢,無非有兩種手段:一是 LVS 根據 Session ID 做一致性 hash,二是 Session Cache 分散式快取;第一種方式比較簡單,修改一下 LVS 就可以實現,但這樣可能導致 Real Server 負載不均,我們用了第二種方式,在節點內部署一個 redis,然後 Tengine 握手時從 redis 中查詢是否存在 Session,存在則複用,不存在則將 Session 快取到 redis 並做完整握手,當然每次與 redis 互動也有時間消耗,需要做多級快取,這裡就不展開了。核心的實現主要用到 ssl_session_fetch_by_lua_file 和 ssl_session_store_by_lua_file,在 lua 裡面做一些操作 redis 和快取即可。
  • Session Ticket
    上面講到了服務端 Session Cache 在分散式叢集中的弊端,Session Ticket 是用來解決該弊端的優化方式,原理跟網頁的 Cookie 類似,客戶端快取會話資訊(當然是加密的,簡稱 session ticket),下次握手時將該 session ticket 通過 client hello 的擴充套件欄位傳送給伺服器,伺服器用配置好的解密 key 解密該 ticket,解密成功後得到會話資訊,可以直接複用,不必再做完整握手和金鑰交換,大大提高了效率和效能,(那客戶端是怎麼得到這個 session ticket 的呢,當然是伺服器在完整握手後生成和用加密 key 後給它的)。可見,這種方式不需要伺服器快取會話資訊,天然支援分散式叢集的會話複用。這種方式也有弊端,並不是所有客戶端或者 SDK 都支援(但主流瀏覽器都支援)。所以,目前服務端 Session Cache 和 Session Ticket 都會存在,未來將以 Session Ticket 為主。Tengine 開啟 Session Ticket 也很簡單:
    ssl_session_tickets on;
    ssl_session_timeout 48h;
    ssl_session_ticket_key ticket.key;  #需要叢集內所有機器的 ticket.key 內容(48位元組)一致複製程式碼

(全文完)


本文作者:金九

原文連結

本文為雲棲社群原創內容,未經允許不得轉載。