思考gRPC:為什麼是protobuf

橫雲斷嶺發表於2018-07-19

背景

談到RPC,就避免不了序列化的話題。

gRPC預設的序列化方式是protobuf,原因很簡單,因為兩者都是google發明的,哈哈。

在當初Google開源protobuf時,很多人就期待是否能把RPC的實現也一起開源出來。沒想到最終出來的是gRPC,終於補全了這一塊。

跨語言的序列化方案

事實上的跨語言序列化方案只有三個: protobuf, thrift, json。

  • json體積太大,並且缺少型別資訊,實際上只用在RESTful介面上,並沒有看到RPC框架會預設選json做序列化的。

國內一些大公司的使用情況:

  • protobuf ,騰迅,百度等

  • thrift,小米,美團等

  • hessian, 阿里用的是自己維護的版本,有js/cpp的實現,因為阿里主用java,更多是歷史原因。

序列化裡的型別資訊

序列化就是把物件轉換為二進位制資料,反序列化就把二進位制資料轉換為物件。

各種序列化庫層出不窮,其中有一個重要的區別:型別資訊存放在哪

可以分為三種:

  1. 不儲存型別資訊

    典型的是各種json序列化庫,優點是靈活,缺點是使用的雙方都要知道型別是什麼。當然有一些json庫會提供一些擴充套件,偷偷把型別資訊插入到json裡。

  2. 型別資訊儲存到序列化結果裡

    比如java自帶的序列化,hessian等。缺點是型別資訊冗餘。比如RPC裡每一個request都要帶上型別。因此有一種常見的RPC優化手段就是兩端協商之後,後續的請求不需要再帶上型別資訊。

  3. 在生成程式碼裡帶上型別資訊

    通常是在IDL檔案裡寫好package和類名,生成的程式碼直接就有了型別資訊。比如protobuf, thrift。缺點是需要生成程式碼,雙方都要知道IDL檔案。

型別資訊看起來是一個小事,但在安全上卻會出大問題,後面會討論。

實際使用中序列化有哪些問題

這裡討論的是沒有IDL定義的序列化方案,比如java自帶的序列化,hessian, 各種json庫。

  • 大小莫名增加,比如使用者不小心向map裡put了大物件。
  • 物件之間互相引用,使用者根本不清楚序列化到底會產生什麼結果,可能新加一個field就不小心被序列化了
  • enum類新增加的不能識別,當兩端的類版本不一致時就會出錯
  • 哪些欄位應該跳過序列化 ,不同的庫都有不同的 @Ignore ,沒有通用的方案
  • 很容易把一些奇怪的類傳過來,然後對端報ClassNotFoundException
  • 新版本jdk新增加的類不支援,需要序列化庫不斷升級,如果沒人維護就悲劇了
  • 庫本身的程式碼質量不高,或者API設計不好容易出錯,比如kryo

gRPC是protobuf的一個外掛

以gRPC官方的Demo為例:

package helloworld;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user`s name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

可以看到rpc的定義也是寫在proto檔案裡的。實際上gRPC是protobuf的一個擴充套件,通過擴充套件生成gRPC相關的程式碼。

protobuf並不是完美解決方案

在protobuf出來以後,也不斷出現新的方案。比如

protobuf的一些缺點:

  • 缺少map/set的支援(proto3支援map)
  • Varint編碼會消耗CPU
  • 會影響CPU快取,比如比較大的int32從4位元組用Varint表示是5位元組就不對齊了
  • 解碼時要複製一份記憶體,不能做原地記憶體引用的優化

    protobuf在google 2008年公開的,內部使用自然更早。當時頻寬還比較昂貴,現在人們對速度的關注勝過頻寬了。

protobuf需要生成程式碼的確有點麻煩,所以會有基於java annotation的方案:

同樣thrift有:

序列化被人忽視的安全性問題

序列化漏洞危害很大

  1. 序列化漏洞通常比較嚴重,容易造成任意程式碼執行
  2. 序列化漏洞在很多語言裡都會有,比如Python Pickle序列化漏洞。

很多程式設計師不理解為什麼反序列化可以造成任意程式碼執行。

反序列化漏洞到底是怎麼工作的呢?很難直接描述清楚,這些漏洞都有很精巧的設計,把多個地方的程式碼串聯起來。可以參考這個demo,跑起來除錯下就可以有直觀的印象:

這裡有兩個生成java序列化漏洞程式碼的工具:

常見的庫怎樣防止反序列化漏洞

下面來看下常見的序列化方案是怎麼防止反序列化漏洞的:

  1. Java Serialization

  2. jackson-databind

  3. fastjson

    • fastjson通過一個denyList來過濾掉一些危險類的package,參見ParserConfig.java
    • fastjson在新版本里denyList改為通過hashcode來隱藏掉package資訊,但通過這個DenyTest5可以知道還是過濾掉常見危險類的package
    • fastjson在新版本里預設把autoType的功能禁止掉了

所以總結下來,要麼白名單,要麼黑名單。當然黑名單機制不能及時更新,業務方得不斷升jar包,非常蛋疼。白名單是比較徹底的解決方案。

為什麼protobuf沒有序列化漏洞

這些序列化漏洞的根本原因是:沒有控制序列化的型別範圍

為什麼在protobuf裡並沒有這些反序列化問題?

  • protobuf在IDL裡定義好了package範圍
  • protobuf的程式碼都是自動生成的,怎麼處理二進位制資料都是固定的

protobuf把一切都框住了,少了靈活性,自然就少漏洞。

總結

  • 應該重視反序列化漏洞,畢竟Oracle都不得不考慮把java序列化廢棄了
  • 序列化漏洞的根本原因是:沒有控制序列化的型別範圍
  • 防止序列化漏洞,最好是使用白名單
  • protobuf通過IDL生成程式碼,嚴格控制了型別範圍
  • protobuf不是完美的方案,但是作為跨語言的序列化事實方案之一,IDL生成程式碼比較麻煩也不是啥大問題

連結


相關文章