gRPC Assembly

如果要排查網(wǎng)絡(luò)問(wèn)題泻红,tcpdump 或者 Wireshark 是非常好的工具负敏,但有時(shí)候我們想要更多。譬如前一段時(shí)間我們?cè)谂挪橐粋€(gè)奇怪的問(wèn)題城菊,TiKV 在處理一個(gè)請(qǐng)求的時(shí)候報(bào)了 key not in range 的 bug备燃,直觀的說(shuō),就是我們想在某段 range 里面處理一個(gè) key凌唬,但實(shí)際這個(gè) key 根本不是這個(gè) range 里面的并齐。通常出現(xiàn)這種錯(cuò)誤,無(wú)非就是兩種情況,要不是 TiKV 自己內(nèi)部有個(gè) bug况褪,導(dǎo)致了 key 錯(cuò)亂撕贞,要不就上層的 TiDB 給 TiKV 的請(qǐng)求錯(cuò)了,發(fā)過(guò)來(lái)一個(gè)錯(cuò)誤的 key窝剖。

然后 TiDB 這邊麻掸,也可能有幾種情況,要不就是自己內(nèi)部有個(gè) bug赐纱,導(dǎo)致了 key 錯(cuò)亂(可能給 PD 或者 TiKV 發(fā)送了錯(cuò)誤的 key)脊奋,要不就是從 PD 獲取這個(gè) key 的路由信息的時(shí)候錯(cuò)了。

為了確定到底是哪個(gè)組件出了問(wèn)題疙描,我們需要登錄到三個(gè)組件诚隙,詳細(xì)的排查日志,但多數(shù)時(shí)候起胰,請(qǐng)求是沒(méi)有日志的久又,因?yàn)樗麄兌际钦_的請(qǐng)求,只有最終出問(wèn)題那里才有效五。于是另一個(gè)做法就是將日志的級(jí)別調(diào)成 debug地消,這樣是能看到很多請(qǐng)求了,但也會(huì)面臨日志太多畏妖,不容易排查的問(wèn)題脉执。或者干脆重新定制一個(gè)加了特殊 log 的版本戒劫,但這樣多數(shù)時(shí)候在用戶那邊是不可能的半夷。

所以這時(shí)候,如果我們能直接監(jiān)控三個(gè)組件的數(shù)據(jù)交互迅细,那么就能快速的確定到底是哪個(gè)組件出了問(wèn)題巫橄。具體到我們這邊,各個(gè)組件是使用 gRPC 交互的茵典,所以我們只需要將 gRPC 的協(xié)議給解析出來(lái)湘换,最好能解碼成我們實(shí)際定義的 protobuf,這事情就成了统阿。但比較悲催的是彩倚,并沒(méi)有這樣的工具。不過(guò)對(duì)于程序員來(lái)說(shuō)砂吞,沒(méi)有就擼一個(gè)唄署恍,于是就有了 gRPC assembly(后面簡(jiǎn)稱 assembly)。

要實(shí)現(xiàn)一個(gè) assembly蜻直,其實(shí)比較簡(jiǎn)單盯质,無(wú)非幾步:

  1. 捕獲各個(gè)模塊之間的 TCP 數(shù)據(jù)流
  2. 將這個(gè) TCP 數(shù)據(jù)通過(guò) HTTP/2 協(xié)議解析
  3. 通過(guò) HTTP/2 協(xié)議映射到對(duì)應(yīng)的 gRPC service
  4. 通過(guò)我們定義的 protobuf 格式解析出實(shí)際的數(shù)據(jù)

gopacket

對(duì)于網(wǎng)絡(luò)包的捕獲袁串,Go 里面已經(jīng)有一個(gè)非常方便的庫(kù) - gopacket,這個(gè)庫(kù)方便到你通過(guò)這個(gè)庫(kù)能很快速的自己擼一個(gè) tcpdump 出來(lái)呼巷。

在 gopacket 里面囱修,有一個(gè) httpassembly,我們可以直接解析 HTTP/1 的協(xié)議王悍。自然破镰,對(duì)我來(lái)說(shuō),一個(gè)非常簡(jiǎn)單的做法就是直接在這個(gè)基礎(chǔ)上面压储,弄一個(gè)我們自己的 assembly 了鲜漩,當(dāng)然還是需要有一些變動(dòng)的。

這里我就不多介紹 gopacket 的使用集惋,大家自行 Google孕似,反正也是這玩意也是 Google 弄的。

HTTP/2

通過(guò)第一步刮刑,我們可以得到一個(gè) TCP stream喉祭,那么下一步自然就是將這個(gè) TCP stream 解析成 HTTP/2 了。這里我們先簡(jiǎn)單的說(shuō)說(shuō) HTTP/2雷绢,不同于 HTTP/1泛烙,HTTP2 的消息是基于 frame 的,大家可以詳細(xì)的去參考 RFC7540翘紊。所以對(duì)我們來(lái)說(shuō)蔽氨,解析 HTTP/2,其實(shí)就是解析 frame霞溪。

這里孵滞,我們使用的是 Go 的另一個(gè)庫(kù) http2 中捆。但這個(gè)庫(kù)鸯匹,Google 其實(shí)并沒(méi)有想讓外面直接使用,所以除了 API泄伪,幾乎沒(méi)任何的例子殴蓬。幸運(yùn)的是,Go 的 gRPC 使用的是這個(gè)庫(kù)蟋滴,自然染厅,我們就能知道如何用了,主要在 http_util.go 這個(gè)文件里面津函。將 frame 的使用放到之前的 HTTP assembly 里面肖粮,類似如下:

func (h *httpStream) run() {
    buf := bufio.NewReader(&h.r)
    framer := http2.NewFramer(ioutil.Discard, buf)
    framer.MaxHeaderListSize = uint32(16 << 20)
    framer.ReadMetaHeaders = hpack.NewDecoder(4096, nil)

因?yàn)槲覀兺瑯有枰P(guān)注 HTTP/2 的 header,所以這里需要用 HPACK 的 encoder尔苦,關(guān)于 HPACK涩馆,大家可以參考 RFC7541行施。

創(chuàng)建了 framer,下一步自然就是 for 循環(huán)調(diào)用 framer.ReadFrame() 了魂那。但這里其實(shí)是有一個(gè)問(wèn)題的蛾号,對(duì)于 HTTP/2 的第一次握手來(lái)說(shuō),其實(shí)并不是走的 frame涯雅,所以如果直接調(diào)用 ReadFrame鲜结,會(huì)發(fā)現(xiàn)程序直接出錯(cuò)了,這也是我最開(kāi)始犯的錯(cuò)誤活逆【ⅲ看了看 HTTP/2 的 starting 這章節(jié),我才知道了出現(xiàn)問(wèn)題蔗候。

幸運(yùn)的是贬养,我們整個(gè)集群因?yàn)槿渴褂玫?gRPC 協(xié)議,所以使用的是 Connection Preface 這種方式琴庵,我們只需要先讓 buf peek 幾個(gè)字符误算,判斷下是不是 PRI * HTTP/2.0 這樣的就可以了,然后我們就可以直接使用 ReadFrame 來(lái)處理剩下的數(shù)據(jù)迷殿。

這里還需要注意儿礼,對(duì)于 PD 來(lái)說(shuō),同一個(gè)端口可以支持 gRPC庆寺,也同時(shí)提供 HTTP/1 的訪問(wèn)蚊夫,所以我們除了要判斷是否是 HTTP/2 的開(kāi)始握手消息,也需要判斷是否是 HTTP/1 的消息懦尝,這個(gè)就比較簡(jiǎn)單了知纷,對(duì)于 HTTP/1 來(lái)說(shuō),request 最開(kāi)始無(wú)非就是 GET陵霉,POST琅轧,PUT 這些,而 response 則是以 HTTP 開(kāi)頭的踊挠。

gRPC

當(dāng)我們拿到了 HTTP/2 的 frame 之后乍桂,我們自然就能映射成對(duì)應(yīng)的 gRPC 協(xié)議了。大家可以詳細(xì)的看看 gRPC over HTTP2 這篇文檔效床。對(duì)于我們來(lái)說(shuō)睹酌,最開(kāi)始關(guān)注的 frame 當(dāng)然是 MetaHeadersFrame 以及 DataFrame。

對(duì)于 MetaHeadersFrame剩檀,如果它里面包含 :path憋沿,那么它其實(shí)可以認(rèn)為是一個(gè) request,也就是 client 發(fā)給 server 的沪猴,如果里面包含 :status辐啄,則可以認(rèn)為是一個(gè) response甥绿,也就是 server 回復(fù)給 client 的。對(duì)我們來(lái)說(shuō)则披,拿到 :path 非常的重要共缕,這樣我們就能知道實(shí)際定義的 gRPC service 以及對(duì)應(yīng)的 method 到底是哪一個(gè),然后才能用對(duì)應(yīng)的 protobuf 解析出來(lái)士复。譬如 :path/pdpb.PD/StoreHeartbeat图谷,那么我們就知道,這個(gè)是一個(gè)定義在 pdpb 文件里面的 PD 的 service阱洪,而函數(shù)是 StoreHeartbeat便贵,然后我們就能夠知道它的 request 和 response 對(duì)應(yīng)的具體的 protobuf,這樣在后面的 DataFrame 里面我們就能夠?qū)?shù)據(jù)解析出來(lái)了冗荸。

通常承璃,如果我們碰到了 :path,我們需要將這個(gè) frame 的 stream ID 跟 :path 綁定起來(lái)蚌本,這樣 response 的時(shí)候盔粹,我們才能根據(jù) stream ID 找到這個(gè) :path 了。這里程癌,我再次犯了一個(gè)錯(cuò)誤舷嗡,之前我一直以為 stream ID 是全局唯一的,所以我用了一個(gè)全局的 map 來(lái)保存上面的映射關(guān)系嵌莉,但后來(lái)發(fā)現(xiàn)貌似有問(wèn)題进萄,才突然明白,只有對(duì)于單個(gè)連接锐峭,這個(gè) stream ID 是唯一的中鼠。

而對(duì)于 DataFrame,數(shù)據(jù)的第一個(gè)字符表明是否壓縮沿癞,如果是 1援雇,則壓縮算法是在前面 header 里面的 grpc-encoding 字段定義,這里我還沒(méi)考慮抛寝,多數(shù)時(shí)候我們也沒(méi)開(kāi)壓縮熊杨,后面再說(shuō)吧曙旭。

Protocol Buffer

當(dāng)做完上面一步盗舰,其實(shí)我們大部分工作已經(jīng)完成了,但實(shí)際我們還是會(huì)遇到一些問(wèn)題桂躏,主要就是如果這個(gè) assembly 是中途開(kāi)始監(jiān)控網(wǎng)絡(luò)傳輸钻趋,那么極大概率是拿不到 :path 的,如果沒(méi)有 :path剂习,我們就沒(méi)法解出來(lái)對(duì)應(yīng)的 protobuf message蛮位。

之所以會(huì)出現(xiàn)這樣的問(wèn)題较沪,主要就是因?yàn)?HTTP/2 的 HPACK,HPACK 會(huì)將一些重復(fù)的 header 壓縮省去失仁。對(duì)于 gRPC 來(lái)說(shuō)尸曼,因?yàn)?client 可能在一個(gè) stream 上面多次調(diào)用相同的 method,而這個(gè) :path 都是一樣的萄焦,自然會(huì)被 HPACK 給去掉了控轿。

因?yàn)槲覀儧](méi)辦法知道實(shí)際的 protobuf,所以唯一的辦法就是將這個(gè)數(shù)據(jù)按照 protobuf 的編碼格式給打印出來(lái)拂封,詳細(xì)的編碼可以參考 encoding茬射。對(duì)于 Go 來(lái)說(shuō),我們可以直接使用 DebugPrint冒签。

但 DebugPrint 這個(gè)函數(shù)輸出來(lái)的東西我不怎么喜歡在抛,于是還是重新自己擼了一個(gè),畢竟很簡(jiǎn)單萧恕,首先 DecodeVarint刚梭,然后得到這個(gè) field 實(shí)際的 tag number 以及 Wire Type,然后在根據(jù)對(duì)應(yīng)的 wire type 打印出值票唆。但這里望浩,因?yàn)槲覀兡貌坏綄?shí)際的 protobuf,所以并不清楚這些值的具體含義惰说。譬如對(duì)于 wire type 為 0 的數(shù)據(jù)磨德,我們并不清楚它到底是 int32,還是 int64吆视,或者是 sint32禁荸。

而對(duì)于 wire type 為 2 的數(shù)據(jù)棚愤,我們也不知道它到底是 string,還是 embedded struct,這里我稍微做了一點(diǎn)優(yōu)化蟋座,會(huì)嘗試去先按照 embedded struct 解碼,如果能正常解開(kāi)假勿,表明是一個(gè) embedded struct这吻,否則就不是。但對(duì)于 Repeated Fields般堆,我暫時(shí)還沒(méi)啥太好的想法在孝。

后面大概的輸出類似這樣:

tag=1 struct
  tag=1 varint=2037210783374497686
  tag=2 varint=13195394291058371180
  tag=3 varint=244
  tag=4 varint=2
tag=2 varint=1
tag=3 struct
  tag=2 struct
    tag=1 struct
      tag=3 varint=244

雖然多數(shù)時(shí)候還是看不出來(lái)啥,但聊勝于無(wú)吧淮摔。不過(guò)如果我們大概能猜到是什么樣的 protobuf私沮,還是能直接匹配上去的。

Demo

最后大概的效果類似如下和橙,啟動(dòng)程序仔燕,監(jiān)控 TiKV 的 20160 端口:

go run assembly.go -f "port 20160" -i lo0
2018/12/29 20:17:17 Starting capture on interface "lo0"
2018/12/29 20:17:17 reading in packets
2018/12/29 20:17:26 127.0.0.1:64989 -> 127.0.0.1:20160 /tikvpb.Tikv/KvPrewrite context:<region_id:2 region_epoch:<conf_ver:1 version:1 > peer:<id:3 store_id:1 > > mutations:<key:"usertable:a" value:"\010\000\002\0020" > primary_lock:"usertable:a" start_version:405297128206237697 lock_ttl:3000
2018/12/29 20:17:26 127.0.0.1:20160 -> 127.0.0.1:64989 /tikvpb.Tikv/KvPrewrite
2018/12/29 20:17:26 127.0.0.1:64995 -> 127.0.0.1:20160 /tikvpb.Tikv/KvCommit context:<region_id:2 region_epoch:<conf_ver:1 version:1 > peer:<id:3 store_id:1 > > start_version:405297128206237697 keys:"usertable:a" commit_version:405297128206237698
2018/12/29 20:17:26 127.0.0.1:20160 -> 127.0.0.1:64995 /tikvpb.Tikv/KvCommit
2018/12/29 20:17:29 127.0.0.1:64999 -> 127.0.0.1:20160 /tikvpb.Tikv/KvGet context:<region_id:2 region_epoch:<conf_ver:1 version:1 > peer:<id:3 store_id:1 > > key:"usertable:a" version:405297128901443585
2018/12/29 20:17:29 127.0.0.1:20160 -> 127.0.0.1:64999 /tikvpb.Tikv/KvGet value:"\010\000\002\0020"

通過(guò)這個(gè)工具造垛,我們還是能非常方便的看出來(lái)各個(gè)組件之間到底在干啥,對(duì)查問(wèn)題還是有幫助的晰搀。當(dāng)然五辽,這只是一個(gè)小 demo,如果哪位有興趣外恕,歡迎聯(lián)系我奔脐,一起加入把這個(gè)東西給做完善,tl@pingcap.com吁讨。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末髓迎,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子建丧,更是在濱河造成了極大的恐慌排龄,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,561評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件翎朱,死亡現(xiàn)場(chǎng)離奇詭異橄维,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)拴曲,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門争舞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人澈灼,你說(shuō)我怎么就攤上這事竞川。” “怎么了叁熔?”我有些...
    開(kāi)封第一講書人閱讀 157,162評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵委乌,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我荣回,道長(zhǎng)遭贸,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 56,470評(píng)論 1 283
  • 正文 為了忘掉前任心软,我火速辦了婚禮壕吹,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘删铃。我一直安慰自己耳贬,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,550評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布泳姐。 她就那樣靜靜地躺著效拭,像睡著了一般。 火紅的嫁衣襯著肌膚如雪胖秒。 梳的紋絲不亂的頭發(fā)上缎患,一...
    開(kāi)封第一講書人閱讀 49,806評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音阎肝,去河邊找鬼挤渔。 笑死,一個(gè)胖子當(dāng)著我的面吹牛风题,可吹牛的內(nèi)容都是我干的判导。 我是一名探鬼主播,決...
    沈念sama閱讀 38,951評(píng)論 3 407
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼沛硅,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼眼刃!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起摇肌,我...
    開(kāi)封第一講書人閱讀 37,712評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤擂红,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后围小,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體昵骤,經(jīng)...
    沈念sama閱讀 44,166評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,510評(píng)論 2 327
  • 正文 我和宋清朗相戀三年肯适,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了变秦。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,643評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡框舔,死狀恐怖蹦玫,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情刘绣,我是刑警寧澤钳垮,帶...
    沈念sama閱讀 34,306評(píng)論 4 330
  • 正文 年R本政府宣布,位于F島的核電站额港,受9級(jí)特大地震影響饺窿,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜移斩,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,930評(píng)論 3 313
  • 文/蒙蒙 一肚医、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧向瓷,春花似錦肠套、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,745評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春刁赖,著一層夾襖步出監(jiān)牢的瞬間搁痛,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,983評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工宇弛, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留鸡典,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,351評(píng)論 2 360
  • 正文 我出身青樓枪芒,卻偏偏與公主長(zhǎng)得像彻况,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子舅踪,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,509評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • 經(jīng)過(guò)很長(zhǎng)一段時(shí)間的開(kāi)發(fā)纽甘,TiDB 終于發(fā)了 RC3。RC3 版本對(duì)于 TiKV 來(lái)說(shuō)最重要的功能就是支持了 gRP...
    siddontang閱讀 41,903評(píng)論 7 80
  • gRPC 是一個(gè)高性能抽碌、通用的開(kāi)源RPC框架悍赢,其由 Google 主要面向移動(dòng)應(yīng)用開(kāi)發(fā)并基于HTTP/2 協(xié)議標(biāo)準(zhǔn)...
    劉琨_10f5閱讀 1,287評(píng)論 4 6
  • gRPC 是基于 HTTP/2 協(xié)議的,要深刻理解 gRPC咬展,理解下 HTTP/2 是必要的泽裳。本篇文章會(huì)先簡(jiǎn)單介紹...
    tcgx閱讀 1,277評(píng)論 1 1
  • 稻子就像兒子 我家農(nóng)民的兒子 弓腰哈氣 擺身隨風(fēng) 金黃總在秋季 落地伴隨夕陽(yáng) 過(guò)去是木頭 如今是鐵皮 泥土逃不掉烏...
    擁抱思想閱讀 242評(píng)論 0 1
  • 清晨木葉上鋪上一層寒霜祷舀,寒冷的空氣吸入肺里瀑梗,猶如嘴中放入了木糖醇一般清爽。 我站在陽(yáng)臺(tái)裳扯,伸了個(gè)懶腰...
    汛然閱讀 304評(píng)論 0 1