Go 大小語義
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.Size 和 proto.Marshal 之間可能會“縮小”:
- 啟用了延遲解碼
- 並且訊息以 非最小有線格式 到達
- 並且在呼叫
proto.Size之前未訪問訊息,這意味著它尚未解碼 - 並且在
proto.Size之後(但在proto.Marshal之前)訪問訊息,導致其被延遲解碼
解碼會導致任何後續的 proto.Marshal 呼叫編碼訊息(而不是僅僅複製其有線格式),這會導致隱式規範化為 Go 編碼訊息的方式,目前是最小有線格式(但不要依賴這一點!)。
如您所見,這種情況相當特殊,但儘管如此,將 proto.Size 結果視為上限是最佳實踐,切勿假設結果與實際編碼訊息的大小匹配。
背景:非最小有線格式
在編碼 Protobuf 訊息時,存在一種*最小有線格式大小*和許多更大的*非最小有線格式*,它們解碼為相同的訊息。
非最小有線格式(有時也稱為“非規範化有線格式”)指的是非重複欄位多次出現、非最優 varint 編碼、在線上出現非打包的打包重複欄位以及其他情況。
我們會在不同場景中遇到非最小有線格式
- 故意為之。 Protobuf 支援透過拼接其有線格式來拼接訊息。
- 意外為之。 一個(可能是第三方的)Protobuf 編碼器編碼不理想(例如,在編碼 varint 時使用比必要更多的空間)。
- 惡意為之。 攻擊者可以專門製作 Protobuf 訊息,以透過網路觸發崩潰。