詳解PROTOCOL BUFFERS



1. 前言

Protocal Buffers是google推出的一種序列化協議。由於它的編碼和解碼的速度,已經編碼后的大小控制的較好,因此它常常被用在RPC調用中,傳遞參數和結果。比如gRPC

Protocal Buffers的實現非常簡單,本文將對比JSON協議,來聊聊Protocol Buffers的實現以及它高性能的秘密

2. 正篇

2.1 減少傳輸量(字段名和定界符)

汽車類在Golang中的定義

type Car struct {
    Age   int32   `json:"age"`
    Color string  `json:"color"`
    Price float32 `json:"price"`
}

JSON字符串表示

{
    "age": 10,
    "color": "red",
    "price": 15.2568983
}

1)”{” 、”}”、”[“, “]”、 雙引號、”,” 、”:” 是為了把字段與字段之間,以及字段的名稱和值分隔開。它們不是必須的。
2)字段的名稱”age”、”color”、”price”也不是必須的。
如果發送方和接收方都對對象的定義是明晰的,那么字段的名稱也不要傳遞

Protocol Buffers對象定義

message Car {
    int32 age = 1;
    string color = 2;
    double price = 3;
}

每個字段都有一個編號,比如在例子中,age是1,color是2,price是3
接收方只要拿到編號,就可以知道需要解析的是哪個字段,它對應的名字甚至是字段值的長度

下圖是對Protocol buffers編碼的說明 圖1

image_1d7gtlrraorc1duvncp11m238s23.png-21.9kB

Protocol buffers有點TLV的意思(type-length-value)

FieldInfo 包含了存儲field_number(字段編號), data_type表示字段類型

Type Meaning Used For
0 Varint int32, int64, uint32, uint64, sint32, sint64, bool, enum
1 64-bit fixed64, sfixed64, double
2 Length-delimited string, bytes, embedded messages, packed repeated fields
3 Start group groups (deprecated)
4 End group groups (deprecated)
5 32-bit fixed32, sfixed32, float
  • 對於 64-bit 32-bit得到類data_type,也就得到了長度
  • 對於 Varint 可以在解析的過程得到value
  • 對於 類似Length-delimited 稍微有點特殊,有額外的字段length表示value字節的長度

注 Varint是對整型的變長表示,它與ES中使用的整型壓縮算法是完全一致的。參見我的文章VINT–針對INT型的壓縮格式
由於Protocol Buffers 有type和length信息的存在,因此無需字段名稱和JSON中的”{“等定界符

2.2 減少傳輸量(整型和浮點數)

由於JSON屬於文本型協議,因此它傳輸的數據都是字符

  • 對於較大的整數,var int32 age = 123456789 傳輸時會變成”123456789″ 需要消耗9個字節
  • 對於浮點數,如果出現小數部分 var float32 price = 15.2568983
    傳輸時,會變成”15.2568983″

Protocol Buffers中,int32按Varint存儲,平均開銷不到3個字節,而float32按照固定4字節存儲,這樣一來就比JSON少了不少

2.3字段可選

Protocol Buffers中允許指定某個字段是optional(可選的)。如果該字段沒有值,則編碼時,這個字段不會占用任何字節。

在一些語言的JSON庫包中,如果解碼時,該字段在JSON字符串中不存在,則會直接報錯。

2.4 解碼時的優勢

2.4.1 跳過數據結構

JSON 是一個沒有 header 的格式。因為沒有 header,JSON 需要掃描每個字節才可以定位到所需的字段上。中間可能要掃過很多不需要處理的字段。

message PbTestWriteObject {
  repeated string field1 = 1;
  message Field2 {
    repeated string field1 = 1;
    repeated string field2 = 2;
    repeated string field3 = 3;
  }
  Field2 field2 = 2;
  string field3 = 3;
}
message PbTestReadObject {
  string field3 = 3;
}

消息用 PbTestWriteObject 來編碼,然后用 PbTestReadObject 來解碼。field1 和 field2 的內容應該被跳過。

這是一個非常極端的例子,回顧圖1中的示例,在Protocol Buffers中除了Varint類型,其余類型,都能直接得到長度信息,因此可以直接跳過不需要解析的字節,效率大大提高

2.4.2 字符串的處理

對於string類型的數據,JSON一般而言還需要支持unicode和UTF8 2種編碼
對於Golang,string本身就是UTF8編碼的字節,因此在解碼時,直接做memcopy就行

3. 總結

 

編解碼數字的時候,JSON 仍然是非常慢的。Jsoniter 把這個差距從 10 倍縮小到了 3 倍多一些。

JSON 最差的情況是下面幾種:

  • 跳過非常長的字符串:和字符串長度線性相關。
  • 解碼 double 字段:Protobuf 優勢明顯,是 Jsoniter的 3.27 倍,是 Jackson 的 13.75 倍。
  • 編碼 double 字段:如果不能接受只保留 6 位小數,Protobuf 是 Jackson 的 12.71 倍。如果接受精度損失,Protobuf 是 Jsoniter 的 1.96 倍。
  • 解碼整數:Protobuf 是 Jsoniter 的 2.64 倍,是 Jackson 的 8.51 倍。

如果你的生產環境中的 JSON 沒有那么多的 double 字段,都是字符串占大頭,那么基本上來說替換成 Protobuf 也就是僅僅比 Jsoniter 提高一點點,肯定在 2 倍之內。如果不幸的話,沒准 Protobuf 還要更慢一點。

Protocol Buffers在極端場景下對JSON的速度優勢,可以達到5倍左右,但是它本身與Gzip等比較,不算是一種壓縮算法。它可以被表述為更為緊湊的序列化協議。對於針對它序列化的結果,再使用其它壓縮算法進行一步壓縮。

4. 代碼參考

對於不同類型字段的序列化(編碼)主要在
table_marshal.go 中的typeMarshaler函數

針對 32-bit 的編碼

func appendFixedS32Ptr(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) {
    p := ptr.getInt32Ptr()
    if p == nil {
        return b, nil
    }
    b = appendVarint(b, wiretag)
    b = appendFixed32(b, uint32(*p))
    return b, nil
}

針對 string 的編碼

func appendStringValue(b []byte, ptr pointer, wiretag uint64, _ bool) ([]byte, error) {
    v := *ptr.toString()
    b = appendVarint(b, wiretag) //
    b = appendVarint(b, uint64(len(v)))
    b = append(b, v...)
    return b, nil
}

 

 

 

參考資料

    1. Protocol Buffers-encoding
    2. wikipedia–Protocol_Buffers
    3. 陶文-Protobuf 有沒有比 JSON 快 5 倍?


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM