天天看點

優化 Tengine HTTPS 握手時間

背景

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

TCP_NODELAY

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

優化 Tengine HTTPS 握手時間
優化 Tengine HTTPS 握手時間

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

TCP Delay Ack

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

優化 Tengine HTTPS 握手時間

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

優化 Tengine HTTPS 握手時間

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

優化 Tengine HTTPS 握手時間

預設寫 buffer 大小是 4k,當證書比較大時,就容易分多次寫核心,進而觸發用戶端的 delay ack。

接下來檢視 tengine 有沒有調整這個 buffer 的地方,還真有(下圖第903行):

優化 Tengine HTTPS 握手時間

那不應該有 delay ack 啊……

無奈之下隻能上 gdb 大法了,調試之後發現果然沒有調用到 BIO_set_write_buffer_size,原因是 rbio 和 wbio 相等了,那為啥以前沒有這種情況現在才有呢?難道是更新 openssl 的原因?繼續查 openssl-1.0.2 代碼:

優化 Tengine HTTPS 握手時間

openssl-1.1.1 的 SSL_get_wbio 有了變化:

優化 Tengine HTTPS 握手時間

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

優化 Tengine HTTPS 握手時間

重新編譯打包後測試,問題得到了修複。使用新版 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位元組)一緻           

(全文完)

繼續閱讀