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) > 100 * 1024 * 1024 {
// Skip every work task over 100 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 消息时,有一个 *最小线格式大小* 和许多解码为相同消息的更大的 *非最小线格式*。
非最小线格式(有时也称为“非规范化线格式”)是指非重复字段多次出现、非最佳变长整数编码、在网络上以非打包形式出现的打包重复字段等场景。
我们可以在不同的场景中遇到非最小线格式
- **有意地。** Protobuf 支持通过连接其线格式来连接消息。
- **意外地。** (可能是第三方)Protobuf 编码器没有理想地编码(例如,在编码变长整数时使用了比必要更多的空间)。
- **恶意地。** 攻击者可以专门设计 Protobuf 消息以触发网络崩溃。