Go 尺寸语义

解释如何(不)使用 proto.Size

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.Sizeproto.Marshal 之间可能会“缩小”

  1. 启用了延迟解码
  2. 并且消息以 非最小线格式 形式到达
  3. 并且在调用 proto.Size 之前未访问消息,这意味着它尚未解码
  4. 并且在 proto.Size(但在 proto.Marshal 之前)之后访问消息,导致其被延迟解码

解码导致任何后续的 proto.Marshal 调用编码消息(而不是仅仅复制其线格式),这会导致隐式规范化为 Go 如何编码消息,目前以最小线格式(但不要依赖于此!)。

如您所见,这种情况相当特殊,但尽管如此,**最佳实践是将 proto.Size 结果视为上限**,并且永远不要假设结果与实际编码的消息大小匹配。

背景:非最小线格式

在编码 Protobuf 消息时,有一个 *最小线格式大小* 和许多解码为相同消息的更大的 *非最小线格式*。

非最小线格式(有时也称为“非规范化线格式”)是指非重复字段多次出现、非最佳变长整数编码、在网络上以非打包形式出现的打包重复字段等场景。

我们可以在不同的场景中遇到非最小线格式

  • **有意地。** Protobuf 支持通过连接其线格式来连接消息。
  • **意外地。** (可能是第三方)Protobuf 编码器没有理想地编码(例如,在编码变长整数时使用了比必要更多的空间)。
  • **恶意地。** 攻击者可以专门设计 Protobuf 消息以触发网络崩溃。