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