优化 Tengine HTTPS 握手时间
背景
網(wǎng)絡(luò)延遲是網(wǎng)絡(luò)上的主要性能瓶頸之一。在最壞的情況下,客戶端打開(kāi)一個(gè)鏈接需要DNS查詢(1個(gè) RTT),TCP握手(1個(gè) RTT),TLS 握手(2個(gè)RTT),以及最后的 HTTP 請(qǐng)求和響應(yīng),可以看出客戶端收到第一個(gè) HTTP 響應(yīng)的首字節(jié)需要5個(gè) RTT 的時(shí)間,而首字節(jié)時(shí)間對(duì) web 體驗(yàn)非常重要,可以體現(xiàn)在網(wǎng)站的首屏?xí)r間,直接影響用戶判斷網(wǎng)站的快慢,所以首字節(jié)時(shí)間(TTFB)是網(wǎng)站和服務(wù)器響應(yīng)速度的重要指標(biāo),下面我們來(lái)看影響 SSL 握手的幾個(gè)方面:
TCP_NODELAY
我們知道,小包的載荷率非常小,若網(wǎng)絡(luò)上出現(xiàn)大量的小包,則網(wǎng)絡(luò)利用率比較低,就像客運(yùn)汽車(chē),來(lái)一個(gè)人發(fā)一輛車(chē),可想而知這效率將會(huì)很差,這就是典型的 TCP 小包問(wèn)題,為了解決這個(gè)問(wèn)題所以就有了 Nigle 算法,算法思想很簡(jiǎn)單,就是將多個(gè)即將發(fā)送的小包,緩存和合并成一個(gè)大包,然后一次性發(fā)送出去,就像客運(yùn)汽車(chē)滿員發(fā)車(chē)一樣,這樣效率就提高了很多,所以內(nèi)核協(xié)議棧會(huì)默認(rèn)開(kāi)啟 Nigle 算法優(yōu)化。Night 算法認(rèn)為只要當(dāng)發(fā)送方還沒(méi)有收到前一次發(fā)送 TCP 報(bào)文段的的 ACK 時(shí),發(fā)送方就應(yīng)該一直緩存數(shù)據(jù)直到數(shù)據(jù)達(dá)到可以發(fā)送的大小(即 MSS 大小),然后再統(tǒng)一合并到一起發(fā)送出去,如果收到上一次發(fā)送的 TCP 報(bào)文段的 ACK 則立馬將緩存的數(shù)據(jù)發(fā)送出去。雖然效率提高了,但對(duì)于急需交付的小包可能就不適合了,比如 SSL 握手期間交互的小包應(yīng)該立即發(fā)送而不應(yīng)該等到發(fā)送的數(shù)據(jù)達(dá)到 MSS 大小才發(fā)送,所以,SSL 握手期間應(yīng)該關(guān)閉 Nigle 算法,內(nèi)核提供了關(guān)閉 Nigle 算法的選項(xiàng): TCP_NODELAY,對(duì)應(yīng)的 tengine/nginx 代碼如下:?
需要注意的是這塊代碼是2017年5月份才提交的代碼,使用老版本的 tengine/nginx 需要自己打 patch。
TCP Delay Ack
與 Nigle 算法對(duì)應(yīng)的網(wǎng)絡(luò)優(yōu)化機(jī)制叫 TCP 延遲確認(rèn),也就是 TCP Delay Ack,這個(gè)是針對(duì)接收方來(lái)講的機(jī)制,由于 ACK 包是有效 payload 比較少的小包,如果頻繁的發(fā) ACK 包也會(huì)導(dǎo)致網(wǎng)絡(luò)額外的開(kāi)銷(xiāo),同樣出現(xiàn)前面提到的小包問(wèn)題,效率低下,因此延遲確認(rèn)機(jī)制會(huì)讓接收方將多個(gè)收到數(shù)據(jù)包的 ACK 打包成一個(gè) ACK 包返回給發(fā)送方,從而提高網(wǎng)絡(luò)傳輸效率,跟 Nigle 算法一樣,內(nèi)核也會(huì)默認(rèn)開(kāi)啟 TCP Delay Ack 優(yōu)化。進(jìn)一步講,接收方在收到數(shù)據(jù)后,并不會(huì)立即回復(fù) ACK,而是延遲一定時(shí)間,一般ACK 延遲發(fā)送的時(shí)間為 200ms(每個(gè)操作系統(tǒng)的這個(gè)時(shí)間可能略有不同),但這個(gè) 200ms 并非收到數(shù)據(jù)后需要延遲的時(shí)間,系統(tǒng)有一個(gè)固定的定時(shí)器每隔 200ms 會(huì)來(lái)檢查是否需要發(fā)送 ACK 包,這樣可以合并多個(gè) ACK 從而提高效率,所以,如果我們?nèi)プグ鼤r(shí)會(huì)看到有時(shí)會(huì)有 200ms 左右的延遲。但是,對(duì)于 SSL 握手來(lái)說(shuō),200ms 的延遲對(duì)用戶體驗(yàn)影響很大,如下圖:?
9號(hào)包是客戶端的 ACK,對(duì) 7號(hào)服務(wù)器端發(fā)的證書(shū)包進(jìn)行確認(rèn),這兩個(gè)包相差了將近 200ms,這個(gè)就是客戶端的 delay ack,這樣這次 SSL 握手時(shí)間就超過(guò) 200ms 了。那怎樣優(yōu)化呢?其實(shí)只要我們盡量少發(fā)送小包就可以避免,比如上面的截圖,只要將7號(hào)和10號(hào)一起發(fā)送就可以避免 delay ack,這是因?yàn)閮?nèi)核協(xié)議棧在回復(fù) ACK 時(shí),如果收到的數(shù)據(jù)大于1個(gè) MSS 時(shí)會(huì)立即 ACK,內(nèi)核源碼如下:?
知道了問(wèn)題的原因所在以及如何避免,那就看應(yīng)用層的發(fā)送數(shù)據(jù)邏輯了,由于是在 SSL 握手期間,所以應(yīng)該跟 SSL 寫(xiě)內(nèi)核有關(guān)系,查看 openssl 的源碼:?
默認(rèn)寫(xiě) buffer 大小是 4k,當(dāng)證書(shū)比較大時(shí),就容易分多次寫(xiě)內(nèi)核,從而觸發(fā)客戶端的 delay ack。
接下來(lái)查看 tengine 有沒(méi)有調(diào)整這個(gè) buffer 的地方,還真有(下圖第903行):?
那不應(yīng)該有 delay ack 啊……
無(wú)奈之下只能上 gdb 大法了,調(diào)試之后發(fā)現(xiàn)果然沒(méi)有調(diào)用到 BIO_set_write_buffer_size,原因是 rbio 和 wbio 相等了,那為啥以前沒(méi)有這種情況現(xiàn)在才有呢?難道是升級(jí) openssl 的原因?繼續(xù)查 openssl-1.0.2 代碼:?
openssl-1.1.1 的 SSL_get_wbio 有了變化:?
原因終于找到了,使用老版本就沒(méi)有這個(gè)問(wèn)題。就不細(xì)去看 bbio 的實(shí)現(xiàn)了,修復(fù)也比較簡(jiǎn)單,就用老版本的實(shí)現(xiàn)即可,所以就打了個(gè) patch:?
重新編譯打包后測(cè)試,問(wèn)題得到了修復(fù)。使用新版 openssl 遇到同樣問(wèn)題的同學(xué)可以在此地方打 patch。
Session 復(fù)用
完整的 SSL 握手需要2個(gè) RTT,SSL Session 復(fù)用則只需要1個(gè) RTT,大大縮短了握手時(shí)間,另外 Session 復(fù)用避免了密鑰交換的 CPU 運(yùn)算,大大降低 CPU 的消耗,所以服務(wù)器必須開(kāi)啟 Session 復(fù)用來(lái)提高服務(wù)器的性能和減少握手時(shí)間,SSL 中有兩種 Session 復(fù)用方式:
- 服務(wù)端 Session Cache
大概原理跟網(wǎng)頁(yè) SESSION 類(lèi)似,服務(wù)端將上次完整握手的會(huì)話信息緩存在服務(wù)器上,然后將 session id 告知客戶端,下次客戶端會(huì)話復(fù)用時(shí)帶上這個(gè) session id,即可恢復(fù)出 SSL 握手需要的會(huì)話信息,然后客戶端和服務(wù)器采用相同的算法即可生成會(huì)話密鑰,完成握手。
這種方式是最早優(yōu)化 SSL 握手的手段,在早期都是單機(jī)模式下并沒(méi)有什么問(wèn)題,但是現(xiàn)在都是分布式集群模式,這種方式的弊端就暴露出來(lái)了,拿 CDN 來(lái)說(shuō),一個(gè)節(jié)點(diǎn)內(nèi)有幾十臺(tái)機(jī)器,前端采用 LVS 來(lái)負(fù)載均衡,那客戶端的 SSL 握手請(qǐng)求到達(dá)哪臺(tái)機(jī)器并不是固定的,這就導(dǎo)致 Session 復(fù)用率比較低。所以后來(lái)出現(xiàn)了 Session Ticket 的優(yōu)化方案,之后再細(xì)講。那服務(wù)端 Session Cache 這種復(fù)用方式如何在分布式集群中優(yōu)化呢,無(wú)非有兩種手段:一是 LVS 根據(jù) Session ID 做一致性 hash,二是 Session Cache 分布式緩存;第一種方式比較簡(jiǎn)單,修改一下 LVS 就可以實(shí)現(xiàn),但這樣可能導(dǎo)致 Real Server 負(fù)載不均,我們用了第二種方式,在節(jié)點(diǎn)內(nèi)部署一個(gè) redis,然后 Tengine 握手時(shí)從 redis 中查找是否存在 Session,存在則復(fù)用,不存在則將 Session 緩存到 redis 并做完整握手,當(dāng)然每次與 redis 交互也有時(shí)間消耗,需要做多級(jí)緩存,這里就不展開(kāi)了。核心的實(shí)現(xiàn)主要用到 ssl_session_fetch_by_lua_file 和 ssl_session_store_by_lua_file,在 lua 里面做一些操作 redis 和緩存即可。
- Session Ticket
上面講到了服務(wù)端 Session Cache 在分布式集群中的弊端,Session Ticket 是用來(lái)解決該弊端的優(yōu)化方式,原理跟網(wǎng)頁(yè)的 Cookie 類(lèi)似,客戶端緩存會(huì)話信息(當(dāng)然是加密的,簡(jiǎn)稱 session ticket),下次握手時(shí)將該 session ticket 通過(guò) client hello 的擴(kuò)展字段發(fā)送給服務(wù)器,服務(wù)器用配置好的解密 key 解密該 ticket,解密成功后得到會(huì)話信息,可以直接復(fù)用,不必再做完整握手和密鑰交換,大大提高了效率和性能,(那客戶端是怎么得到這個(gè) session ticket 的呢,當(dāng)然是服務(wù)器在完整握手后生成和用加密 key 后給它的)。可見(jiàn),這種方式不需要服務(wù)器緩存會(huì)話信息,天然支持分布式集群的會(huì)話復(fù)用。這種方式也有弊端,并不是所有客戶端或者 SDK 都支持(但主流瀏覽器都支持)。所以,目前服務(wù)端 Session Cache 和 Session Ticket 都會(huì)存在,未來(lái)將以 Session Ticket 為主。
Tengine 開(kāi)啟 Session Ticket 也很簡(jiǎn)單:
ssl_session_tickets on;ssl_session_timeout 48h;ssl_session_ticket_key ticket.key; #需要集群內(nèi)所有機(jī)器的 ticket.key 內(nèi)容(48字節(jié))一致(全文完)
原文鏈接
本文為云棲社區(qū)原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載。
總結(jié)
以上是生活随笔為你收集整理的优化 Tengine HTTPS 握手时间的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: ArchSummit分享 | 高德地图A
- 下一篇: 蚂蚁金服OceanBase挑战TPCC