• 微信公众号:美女很有趣。 工作之余,放松一下,关注即送10G+美女照片!

【笔记】golang中使用protocol buffers的底层库直接解码二进制数据

开发技术 开发技术 4小时前 1次浏览

背景

一个简单的代理程序,发现单核QPS达到2万/s左右就上不去了,40%的CPU消耗在pb的decode/encode上面。
于是我想,对于特定的场景,直接从[]byte中取出字段,而不用完全的把整个结构在内存展开,岂不是要快很多。
so, 温习了一些PB二进制格式的知识。

pb的二进制格式:

参考的文章有:

  • Google Protobuf 编码原理
  • Protocol Buffers(3):阅读一个二进制文件

几个关键点总结如下:

  • 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().Nanosecond()) / 1000000,
					},
				},
			},
		},
		Metadata: nil,
	}
	t.Logf("%s", wr.String())
	buf, _ := proto.Marshal(wr)
	t.Logf("n%snlen=%d",
		stringutil.HexFormat(buf), len(buf))
}

pb对应的二进制数据为:

0a 37 0a 19 0a 08 5f 5f 6e 61 6d 65 5f 5f 12 0d  |  7    __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 0c 09 77 be  |  job  test1   w 
9f 1a 2f dd 5e 40 10 be 03                       |   / ^@          

假设我以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":"x77xbex9fx1ax2fxddx5ex40",  //毫秒数
                },
                {
                    "id":2,
                    "wire_type":0,
                    "value": "xbex03"
                    //  123.456 居然压缩为两个字节!还没搞懂是怎么解码的!
                }
            ]
        }
    ]
}

后续打算基于PB的底层库来实现更高效率更少内存(但是非常非常难用)的库!


喜欢 (0)