Go 大小语义
proto.Size
函数返回 proto.Message 线格式编码的大小(以字节为单位),方法是遍历其所有字段(包括子消息)。
特别是,它返回 Go Protobuf 将如何编码消息 的大小。
典型用法
识别空消息
检查 proto.Size
是否返回 0 是一种识别空消息的简便方法
if proto.Size(m) == 0 {
// No fields set (or, in proto3, all fields matching the default);
// 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 消息以触发网络上的崩溃。