Go 大小語義

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

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.Sizeproto.Marshal 之間可能會“縮小”:

  1. 啟用了延遲解碼
  2. 並且訊息以 非最小有線格式 到達
  3. 並且在呼叫 proto.Size 之前未訪問訊息,這意味著它尚未解碼
  4. 並且在 proto.Size 之後(但在 proto.Marshal 之前)訪問訊息,導致其被延遲解碼

解碼會導致任何後續的 proto.Marshal 呼叫編碼訊息(而不是僅僅複製其有線格式),這會導致隱式規範化為 Go 編碼訊息的方式,目前是最小有線格式(但不要依賴這一點!)。

如您所見,這種情況相當特殊,但儘管如此,proto.Size 結果視為上限是最佳實踐,切勿假設結果與實際編碼訊息的大小匹配。

背景:非最小有線格式

在編碼 Protobuf 訊息時,存在一種*最小有線格式大小*和許多更大的*非最小有線格式*,它們解碼為相同的訊息。

非最小有線格式(有時也稱為“非規範化有線格式”)指的是非重複欄位多次出現、非最優 varint 編碼、在線上出現非打包的打包重複欄位以及其他情況。

我們會在不同場景中遇到非最小有線格式

  • 故意為之。 Protobuf 支援透過拼接其有線格式來拼接訊息。
  • 意外為之。 一個(可能是第三方的)Protobuf 編碼器編碼不理想(例如,在編碼 varint 時使用比必要更多的空間)。
  • 惡意為之。 攻擊者可以專門製作 Protobuf 訊息,以透過網路觸發崩潰。