優化 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 代碼以下: nginx

須要注意的是這塊代碼是2017年5月份才提交的代碼,使用老版本的 tengine/nginx 須要本身打 patch。web

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。redis

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字節)一致

(全文完)服務器


原文連接
本文爲雲棲社區原創內容,未經容許不得轉載。網絡

相關文章
相關標籤/搜索