【筆記】golang中使用protocol buffers的底層庫直接解碼二進制數據


背景

一個簡單的代理程序,發現單核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的底層庫來實現更高效率更少內存(但是非常非常難用)的庫!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM