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) > 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 訊息以觸發網路上的崩潰。