Go 语言中的大小语义
proto.Size
函数通过遍历 proto.Message
的所有字段(包括子消息),返回其线路格式编码的字节大小。
具体来说,它返回的是 **Go Protobuf 将如何编码该消息**的大小。
关于 Protobuf Editions 的说明
通过 Protobuf Editions,.proto
文件可以启用改变序列化行为的功能。这可能会影响 proto.Size
返回的值。例如,设置 features.field_presence = IMPLICIT
会导致设置为其默认值的标量字段不被序列化,因此不会计入消息的大小。
典型用法
识别空消息
检查 proto.Size
是否返回 0 是检查空消息的常用方法。
if proto.Size(m) == 0 {
// No fields set; skip processing this message,
// or return an error, or similar.
}
限制程序输出的大小
假设您正在编写一个批处理管道,它为另一个系统(在本例中我们称之为“下游系统”)生成工作任务。下游系统配置用于处理中小型任务,但负载测试表明,当面对超过 500 MB 的工作任务时,系统会发生级联故障。
最好的修复方法是为下游系统添加保护(参见 https://cloud.google.com/blog/products/gcp/using-load-shedding-to-survive-a-success-disaster-cre-life-lessons)),但当实现负载削减不可行时,您可以决定在管道中添加一个快速修复。
func (*beamFn) ProcessElement(key string, value []byte, emit func(proto.Message)) {
task := produceWorkTask(value)
if proto.Size(task) > 500 * 1024 * 1024 {
// Skip every work task over 500 MB to not overwhelm
// the brittle downstream system.
return
}
emit(task)
}
不正确的用法:与 Unmarshal 无关
因为 proto.Size
返回的是 Go Protobuf 将如何编码消息的字节数,所以在对传入的 Protobuf 消息流进行解组(解码)时使用 proto.Size
是不安全的。
func bytesToSubscriptionList(data []byte) ([]*vpb.EventSubscription, error) {
subList := []*vpb.EventSubscription{}
for len(data) > 0 {
subscription := &vpb.EventSubscription{}
if err := proto.Unmarshal(data, subscription); err != nil {
return nil, err
}
subList = append(subList, subscription)
data = data[:len(data)-proto.Size(subscription)]
}
return subList, nil
}
当 data
包含一个非最小化线路格式的消息时,proto.Size
返回的大小可能与实际解组的大小不同,这会导致解析错误(最好情况)或在最坏情况下导致数据解析不正确。
因此,这个示例只有在所有输入消息都由(同一版本的)Go Protobuf 生成时才能可靠工作。这是令人意外的,并且很可能不是本意。
提示: 请改用 protodelim
包来读写带有大小分隔的 Protobuf 消息流。
高级用法:预设缓冲区大小
proto.Size
的一个高级用法是在编组前确定缓冲区所需的尺寸。
opts := proto.MarshalOptions{
// Possibly avoid an extra proto.Size in Marshal itself (see docs):
UseCachedSize: true,
}
// DO NOT SUBMIT without implementing this Optimization opportunity:
// instead of allocating, grab a sufficiently-sized buffer from a pool.
// Knowing the size of the buffer means we can discard
// outliers from the pool to prevent uncontrolled
// memory growth in long-running RPC services.
buf := make([]byte, 0, opts.Size(m))
var err error
buf, err = opts.MarshalAppend(buf, m) // does not allocate
// Note that len(buf) might be less than cap(buf)! Read below:
请注意,当启用延迟解码时,proto.Size
返回的字节数可能比 proto.Marshal
(以及像 proto.MarshalAppend
这样的变体)将要写入的字节数要多!因此,当您将编码后的字节放到线路上传输(或存入磁盘)时,请务必使用 len(buf)
,并丢弃之前从 proto.Size
获得的结果。
具体来说,在 proto.Size
和 proto.Marshal
调用之间,一个(子)消息可能会“缩小”,当:
- 启用了延迟解码
- 且消息以非最小化线路格式到达
- 且在调用
proto.Size
之前消息未被访问,意味着它尚未被解码 - 且在调用
proto.Size
之后(但在proto.Marshal
之前)消息被访问,导致其被延迟解码
解码会导致任何后续的 proto.Marshal
调用都会对消息进行编码(而不是仅仅复制其线路格式),这会隐式地将其规范化为 Go 编码消息的方式,目前是最小化线路格式(但不要依赖这一点!)。
如您所见,这种情况相当特定,但尽管如此,**最佳实践是将 proto.Size
的结果视为一个上限**,永远不要假设该结果与实际编码的消息大小完全匹配。
背景:非最小化线路格式
在编码 Protobuf 消息时,存在一种*最小化线路格式大小*和多种解码为相同消息的、尺寸更大的*非最小化线路格式*。
非最小化线路格式(有时也称为“非规范化线路格式”)指的是诸如非重复字段出现多次、非最优的 varint 编码、打包的重复字段在线路上以非打包形式出现等情况。
我们可能在不同场景中遇到非最小化线路格式:
- 有意为之。 Protobuf 支持通过拼接消息的线路格式来拼接消息。
- 意外发生。 一个(可能是第三方的)Protobuf 编码器没有进行理想的编码(例如,在编码 varint 时使用了比必要更多的空间)。
- 恶意构造。 攻击者可能特意构造 Protobuf 消息,以通过网络触发崩溃。