如何定位并修復(fù) HttpCore5 中的 HTTP2 流量控制問題
發(fā)布時(shí)間:2022-01-11 點(diǎn)擊數(shù):1149
開篇吹一波阿里云性能測(cè)試服務(wù) PTS[1],PTS 在 2021 年 5 月份已經(jīng)上線了對(duì) HTTP2 協(xié)議的支持(底層依賴 httpclient5),在壓測(cè)時(shí)會(huì)通過與服務(wù)端協(xié)商的結(jié)果來(lái)決定使用 HTTP1.1 或者 HTTP2 協(xié)議。
01
背景
Cloud Native
寫這篇文章的原因是某天某個(gè)客戶找過來(lái),問我們是不是不支持 HTTP2,因?yàn)樗?XX 云上購(gòu)買了 2 個(gè)域名,其中一個(gè)開啟了 HTTP2,而在 PTS 壓測(cè)過程中,支持 HTTP2 的接口總是報(bào)錯(cuò):
起初懷疑是 HTTP2 支持的問題,通過在本地強(qiáng)制使用 HTTP2 協(xié)議,訪問淘寶主頁(yè),發(fā)現(xiàn)是沒問題的,懷疑是用戶在 XX 云上的配置問題,但緊接著通過在本地 Postman、curl 以及壓測(cè)引擎強(qiáng)制使用 HTTP1.1 協(xié)議時(shí)都能夠正常訪問該網(wǎng)頁(yè),意識(shí)到大概率是 PTS 引擎?zhèn)鹊膯栴}。
通過本地 debug,看到是因?yàn)檎?qǐng)求 URL 時(shí),客戶端窗口大小被調(diào)整為大于 2^32 -1 導(dǎo)致的異常。
那正好借這個(gè)機(jī)會(huì)看下這里的窗口大小指的是什么。
02
HTTP2 流控
Cloud Native
提到窗口,就要提到 HTTP2 相比于 HTTP1.1 支持的新特性:流控(Flow Control),其實(shí) HTTP1.1 依賴于傳輸層 TCP 的滑動(dòng)窗口一樣可以實(shí)現(xiàn)流控,那么為什么 HTTP2 要在應(yīng)用層再實(shí)現(xiàn)一個(gè)流控呢?原因在于 HTTP2 引入了流和多路復(fù)用,通過流控可以達(dá)到使多個(gè)流協(xié)同的效果。
一些流控的基本概念:
-
流控是針對(duì)連接而言的,不是針對(duì)端到端的,而是在兩端中的每一跳;主要指有代理的情況下,代理與兩端都存在流控
-
流控是基于WINDOW_UPDATE 幀的,接收者可以通過流控控制發(fā)送者的速度
-
流控既可以作用于 stream 也可以作用于 connection
-
對(duì)于連接與所有新開啟的流而言,流控窗口大小默認(rèn)都是 65535,且最大值為 2^32 - 1
-
流控?zé)o法禁用
為了便于理解,先簡(jiǎn)單列一下 HTTP2 幀的類型:
-
DATA:攜帶請(qǐng)求或響應(yīng)中的數(shù)據(jù)
-
HEADERS:用于新建一個(gè)流(請(qǐng)求或響應(yīng)),包含對(duì)應(yīng)的 Headers
-
PRIORITY:用于配置流的優(yōu)先級(jí)
-
RST_STREAM:強(qiáng)制結(jié)束某個(gè)流,僅用于某一端取消流,并不適用于正常流的結(jié)束
-
SETTINGS:H2 建聯(lián)的一些配置
-
PUSH_PROMISE:服務(wù)端推送響應(yīng)到客戶端
-
PING:向遠(yuǎn)端發(fā)送一條 PING,遠(yuǎn)端必須返回該 PING
-
GOAWAY:用于某一端將要結(jié)束連接
-
WINDOW_UPDATE:更新流控窗口大小
-
CONTINUATION:如果 headers 過大,單個(gè) HEADERS 幀難以攜帶,通過該幀發(fā)送額外的 headers
接下來(lái),我們重點(diǎn)看下流控相關(guān)的幀,主要是 SETTING 與 WINDOW_UPDATE,在連接建立時(shí)會(huì)通過 SETTINGS 幀來(lái)調(diào)整對(duì)方的窗口大小,之后在傳輸過程中,窗口大小會(huì)隨著數(shù)據(jù)的發(fā)送逐漸減小,直到收到對(duì)方發(fā)送的 WINDOW_UPDATE 幀,從而更新窗口大小。SETTINGS 幀主要包含以下內(nèi)容:
-
SETTINGS_HEADER_TABLE_SIZE:HPACK(一種header壓縮算法) header 表的最大長(zhǎng)度,默認(rèn)值 4096
-
SETTINGS_ENABLE_PUSH:客戶端發(fā)向服務(wù)端的配置,若設(shè)置為 true,客戶端將允許服務(wù)端推送響應(yīng),默認(rèn)值 true
-
SETTINGS_MAX_CONCURRENT_STREAMS:同時(shí)打開的 stream 最大數(shù)量,通常意味著同一時(shí)刻能夠同時(shí)響應(yīng)的請(qǐng)求數(shù)量,默認(rèn)無(wú)限
-
SETTINGS_INITIAL_WINDOW_SIZE:流控的初始窗口大小,默認(rèn)值 65535
-
SETTINGS_MAX_FRAME_SIZE:對(duì)端能夠接受幀的最大長(zhǎng)度,默認(rèn)值16384
-
SETTINGS_MAX_HEADER_LIST_SIZE:對(duì)端能夠接受的 header 列表最大長(zhǎng)度,默認(rèn)不限制
流控的實(shí)現(xiàn)如上所述,每發(fā)送一批 DATA 幀,即將窗口大小減小。需要注意的是流控僅針對(duì) DATA 幀。
前面提到流控既可以作用于 stream 又可以作用于 connection,那具體是怎么執(zhí)行的呢?connection 的流控與 上述 stream 流控邏輯類似,每次發(fā)送 DATA 幀,connection 與 stream 窗口都會(huì)減小,但不同的是,WINDOW_UPDATE 要么單獨(dú)作用于 stream,要么單獨(dú)作用于 connection(streamid 為 0 時(shí),表示作用于 connection)。
03
問題定位
Cloud Native
那么回到開篇的問題,我們以 URL https://www.sysgeek.cn/ 為例,通過在本地做代碼 debug 發(fā)現(xiàn),最終拋異常的原因在于接收到 WINDOW_UPDATE 幀后,更新后窗口大小值大于 2^32 - 1 導(dǎo)致拋異常:
而從這里的代碼可以看出,524288 是當(dāng)前窗口大小,而delta是對(duì)方告知的 WINDOW_UPDATE 大小,通過分析,發(fā)現(xiàn) 524288 這個(gè)值不同于默認(rèn)值 65535,那繼續(xù)看這個(gè)值是什么時(shí)間改動(dòng)的:
發(fā)現(xiàn)是接收 SETTINGS 指令后,初始化窗口大小時(shí)修改的,但這里與 RFC 7540 [2]的描述(connection 窗口大小僅在接收到 WINDOW_UPDATE 后才可能修改)是沖突的:
因此我們斷定是 httpcore5 的源代碼有 bug,在刪除標(biāo)記的這行代碼后,請(qǐng)求可以正常執(zhí)行了。
遺憾的是在準(zhǔn)備給 httpcore5 提 PR 的過程中發(fā)現(xiàn)這個(gè) bug 已經(jīng)在 commit 中被修復(fù)了。
