优化 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字節)一致(全文完)
原文鏈接
本文為云棲社區原創內容,未經允許不得轉載。
總結
以上是生活随笔為你收集整理的优化 Tengine HTTPS 握手时间的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ArchSummit分享 | 高德地图A
- 下一篇: 蚂蚁金服OceanBase挑战TPCC