字節(jié)跳動(dòng)踩坑記:一知半解protobuf

本篇寫個(gè)小坑,別期望太高…
在廣告系統(tǒng)里,對(duì)延遲是毫秒必爭(zhēng)(畢竟省下來的每一毫秒都可以用在后端優(yōu)化效果),因此我們和外部媒體之間的通信往往使用 protobuf 。
相比 json、xml,protobuf 確實(shí)節(jié)省了不少編解碼的時(shí)間以及網(wǎng)絡(luò)開銷,不過相應(yīng)的代價(jià)是犧牲了便利性,不能用 vi 等文本編輯器查看/修改,遇到問題時(shí)排查也比較麻煩。
- 入坑 -
比如 7 月份,某媒體希望一次請(qǐng)求中拉到多條廣告(用于信息流場(chǎng)景),因此在 imp 添加一個(gè) ads_count 字段,用于標(biāo)識(shí)本次請(qǐng)求需要的廣告數(shù)量。
過程是這樣,在?xxx.proto 里給?Impression 類型添加一個(gè)新字段:
package com.xxx;message BidRequest {??string?id?=?1;??int32?ver?=?2;message Impression {????...????int32?ads_count?=?9;}??Impression?imp?=?3;...}
然后用 protoc 編譯,生成新版的?xxx.pb.go?
protoc --go_out=. xxx.proto看起來挺簡(jiǎn)單一個(gè)流程,結(jié)果還是出了問題:不論媒體請(qǐng)求中填了什么值,這邊 decode 出來,imp.GetAdsCount() 得到的總是 1 。
- 排查?-
由于我方代碼是自測(cè)過的,能夠正常取到 ads_count 的值,因此猜測(cè)是對(duì)方請(qǐng)求有點(diǎn)啥問題。
于是將對(duì)方的請(qǐng)求錄下來,存到文件 req.pb 中,然后用?protoc 暴力解碼:
?protoc?--decode-raw?req.pb1 {6: 0x3938373635343332}2: 13 {1: 12: "6f63bd4df111480"3: 1}...
可以看到,我們什么也沒看懂。

不過還好我們有 xxx.proto,借助已知信息,可以更好地解碼請(qǐng)求:
?protoc?--decode=com.xxx.BidRequest?xxx.proto??id: "123456789"ver: 1imp {??id:?1??...ads_count: 110: 3}...
看到了點(diǎn)不太對(duì)的東西。
- 填坑?-
在 imp 里面,除了 ads_count 之外,還看到了個(gè) "10: 3"。
由于 protobuf 的變量名不能是純數(shù)字,所以這應(yīng)當(dāng)是某個(gè)在類型定義里沒有出現(xiàn)的字段,decode時(shí)只能用其序號(hào)代替,由此可知,應(yīng)該是雙方的 proto?文件應(yīng)該有些差異。
經(jīng)過溝通,媒體確實(shí)在 ads_count 之前還加了另一個(gè)字段(可能是和其他合作方使用到的);雙方對(duì)齊以后,問題順利解決:
修正 ads_count 的序號(hào):
message Impression {????...????int32?ads_count?=?10;}
用正確的 proto 來 decode:
?protoc?--decode=com.xxx.BidRequest?xxx.proto??id: "123456789"ver: 1imp {??id:?1...??ads_count:?3}...
MISSION COMPLETED.
- encoding?-
問題是解決了,但是只寫這些就顯得太應(yīng)付了,就再介紹下 proto 文件是怎么編解碼的吧。
官方有一篇很詳細(xì)的文檔介紹了編碼的過程(詳見文末“閱讀原文”),這里摘一些重點(diǎn)。
以一個(gè)簡(jiǎn)單的類型為例:
message Test1 {??optional?int32 a = 1;}
如果給 a 賦值 150 并序列化,會(huì)得到3個(gè)字節(jié)(16進(jìn)制):
08?96?01其中第一個(gè)字節(jié)(08)是一個(gè) varint(每個(gè)字節(jié)的最高位 = 1 表示該 int 還需要拼上后續(xù)字節(jié)的低 7 bits),其內(nèi)容包含了第一個(gè)元素的序號(hào)(field number)和類型(wire type)。
將 08 的二進(jìn)制 "0000 1000" 拆分成三部分來解釋:
0
表示這個(gè) varint 到這個(gè)字節(jié)就結(jié)束了
0001
表示其序號(hào)是1
000
表示其值類型也是個(gè) varint
注意,不管這個(gè) varint 有多大,其末3位總是用于表示類型(wire type),可能的取值有:
0:?varint
1:?64-bit,如 fixed64, sfixed64, double
2:?指定長(zhǎng)度類型,如 string, bytes, 內(nèi)嵌類型
5:?32-bit,如 fixed32, sfixed32, float
第2、3個(gè)字節(jié)(96?01)是 a 的值,其二進(jìn)制表示是
0000 0001第 2 字節(jié)的最高位是 1 ,我們知道這個(gè) varint 還沒結(jié)束;而第 3 字節(jié)的最高位是 0 ,這個(gè) varint 就到此結(jié)束了。
將兩個(gè)最高位去掉,拼出一個(gè)完整的二進(jìn)制數(shù):
= 150注意:varint 按字節(jié)序是小端存儲(chǔ),因此第 3 個(gè)字節(jié)的 0000001 放在高位。
- signed integers?-
varint 看起來是個(gè)好東西,因?yàn)閷?shí)踐中經(jīng)常會(huì)用到一些枚舉值,可能的取值范圍很小,使用 varint 只需要少量的空間。
不過如果我們需要用 -1 的時(shí)候怎么辦呢?不管是用反碼還是補(bǔ)碼,都需要考慮符號(hào)位的問題? —— 對(duì)于 int32/int64,負(fù)數(shù)的編碼總是要占用 10 個(gè)字節(jié)。
protobuf 的解決方案是為 sint32/sint64 引入 "ZigZag encoding",簡(jiǎn)單來說就是交替使用 0,1,2,3,... 來表示 0,-1,1,-2,...,從而將較小的負(fù)數(shù)編碼為較小的無符號(hào)數(shù),再使用 varint 編碼。
- 沒了?-
就這樣吧,更多細(xì)節(jié)(string、內(nèi)嵌類型以及數(shù)組的編碼),請(qǐng)參考官方文檔(文末“閱讀原文”)。
最后一個(gè)小問題,下面這個(gè)編碼后的消息,表示什么意思呢?
36 36推薦閱讀
站長(zhǎng) polarisxu
自己的原創(chuàng)文章
不限于 Go 技術(shù)
職場(chǎng)和創(chuàng)業(yè)經(jīng)驗(yàn)
Go語言中文網(wǎng)
每天為你
分享 Go 知識(shí)
Go愛好者值得關(guān)注
