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

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

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