背景
一個簡單的代理程序,發現單核QPS達到2萬/s左右就上不去了,40%的CPU消耗在pb的decode/encode上面。
於是我想,對於特定的場景,直接從[]byte中取出字段,而不用完全的把整個結構在內存展開,豈不是要快很多。
so, 溫習了一些PB二進制格式的知識。
pb的二進制格式:
參考的文章有:
幾個關鍵點總結如下:
- 5 bit的 field index
- 3 bit的wire type
- wire type的定義如下:google.golang.org/protobuf/encoding/protowire/wire.go
const (
VarintType Type = 0 //int , float等全在這里
Fixed32Type Type = 5
Fixed64Type Type = 1
BytesType Type = 2 //字符串,或者嵌套的子類型
StartGroupType Type = 3 //廢棄
EndGroupType Type = 4 //廢棄
// Map 類型呢 ?
)
- 如果wire type 是 2, 則后續緊接着是長度信息
- bit 0 開頭,說明用一個字節表示長度
- bit 10開頭,說明2個字節表示長度
- bit 110開頭,說明3個字節表示長度
- 以此類推……
- 如果wire type是 1或5,則很簡單,后續的4字節或8字節是值
- 這個值被理解成int / uint / float等,就要看元數據的定義了
- 如果wire type 是 0,這里非常復雜
- 如果以 bit 0開頭,只有 7 bit 表示值
- 如果以bit 10開頭,后續的 14 bit 表示值
- 如果以bit 110開頭,后續的 21 bit表示值
- 以此類推
- 值的內容以 Zigzag 編碼 來表示
- 注意:二進制格式中唯一的元數據就是field index,除此之外不包含任何元數據信息。需要靠額外的元數據信息來指導如何decode這些二進制數據。
實操
PB二進制生成的代碼:
import (
"github.com/golang/protobuf/proto"
"github.com/prometheus/prometheus/prompb"
"google.golang.org/protobuf/encoding/protowire"
)
func Test_make_pb(t *testing.T){
wr := &prompb.WriteRequest{
Timeseries: []prompb.TimeSeries{
{
Labels: []prompb.Label{
{
Name: "__name__",
Value: "test_metric_1",
},
{
Name: "job",
Value: "test1",
},
},
Samples: []prompb.Sample{
{
Value: 123.456,
Timestamp: int64(time.Now().UnixNano()) / 1000000,
},
},
},
},
Metadata: nil,
}
t.Logf("%s", wr.String())
buf, _ := proto.Marshal(wr)
t.Logf("\n%s\nlen=%d",
stringutil.HexFormat(buf), len(buf))
}
pb對應的二進制數據為:
0a 3b 0a 19 0a 08 5f 5f 6e 61 6d 65 5f 5f 12 0d | ; __name__
74 65 73 74 5f 6d 65 74 72 69 63 5f 31 0a 0c 0a | test_metric_1
03 6a 6f 62 12 05 74 65 73 74 31 12 10 09 77 be | job test1 w
9f 1a 2f dd 5e 40 10 a7 c6 90 f9 bd 2f | / ^@ /
假設我以JSON來描述上面的結構:
{
"id" :1,
"wire_type":2,
"body_len" : 55,
"child":[
{
"id" :1,
"wire_type":2,
"idx": 0,
"body_len" : 25,
"child":[
{
"id" :1,
"wire_type":2,
"body_len" : 8,
"value": "__name__",
},
{
"id":2,
"wire_type":2,
"body_len" : 13,
"value": "test_metric_1",
}
],
},
{
"id" : 1, //這個理解為屬於第一組。這個節點和上個節點的ID都是1,因此反推出這兩個節點屬於repeated類型
"body_len" : 12,
"idx": 1,
"child":[
{
"id":1,
"body_len" : 3,
"value":"job"
},
{
"id":2,
"body_len" : 5,
"value":"test1"
},
]
},
{
"id": 2,
"wire_type":2,
"idx": 2,
"body_len": 12,
"child":[
{
"id":1,
"wire_type": 1, //64bit, float64
"value":"\x77\xbe\x9f\x1a\x2f\xdd\x5e\x40", //123.456
},
{
"id":2,
"wire_type":0, //timestamp
"value": "\xa7\xc6\x90\xf9\xbd\x2f"
}
]
}
]
}
后續打算基於PB的底層庫來實現更高效率更少內存(但是非常非常難用)的庫!