Protobuf 做者不建議在 Deno 中使用 Protobuf

0. 背景

我以前在"如何評價ry(Ryan Dahl)的新項目deno?"的回答中曾經寫到:javascript

我比較好奇的是 deno 使用了 Protobuf,而沒有使用 Mojo。既然目標是要兼容瀏覽器,卻不使用 Mojo...html

...java

若是想要兼容瀏覽器生態,選擇 Mojo 是個捷徑,而若是目標是高性能的服務器,那麼應該選擇非序列化的 zero-copy 庫。不管從哪一個角度看 protobuf 好像都不太適合 deno。git

可是從 issue 中能夠看出,Ryan Dahl 以前是沒有據說過 Mojo 的,可是他看完 Mojo 以後,依然以爲 Protobuf 是正確的的選擇。github

Ryan Dahl 最初選擇了 golang,後來又將 golang 從 deno 中完全刪除。前幾天 Protobuf 的做者 Kenton Varda(kentonv) 開了一個 issue:Protobuf seems like a lot of overhead for this use case? #269,在文中 kentonv 指出:golang

I was surprised by the choice of Protobuf for intra-process communications within Deno. Protobuf's backwards compatibility guarantees and compact wire representation offer no benefit here, while the serialize/parse round-trip on every I/O seems like it would be pretty expensive.編程

大概意思是:kentonv 對於 Deno 選擇 Protobuf 感到很吃驚,由於 Protobuf 的兼容性優點並非 Deno 須要的,相反,Protobuf 的序列化和反序列化很是消耗 I/O 性能。數組

1. Cap'n Proto 性能

kentonv 離開 Google 以後開發了 Cap'n Proto。瀏覽器

Cap'n Proto 相比 Protobuf 到底有多快呢?10 倍?100 倍?1000倍?官網給出了一張對比圖:安全

Cap'n Proto 的編碼解碼速度是 Protobuf 的 ∞ 倍。2333

其實這張圖是個標題黨,圖中對比了二者的編碼解碼,可是在 Cap'n Proto 中,是根本不須要編碼和解碼(encoding/decoding)的。Cap'n Proto 編碼後的數據格式直接存放在內存,數據結構跟在內存裏面的佈局保持一致,因此能夠直接將編碼好的結構根據字節存放到硬盤,或者經過網絡傳輸。

這是否是意味這 Cap'n Proto 編碼是特定於平臺的?

不!Cap'n Proto 採用的按字節編碼方案是獨立於任何平臺的,但在現在主流的通用 CPU 上面會有更好的性能。數據的組織相似於編譯器對 struct 的組織形式:固定寬度,固定偏移,以及內存對齊,對於可變的數組元素使用指針,而指針也是使用的偏移存放而不是絕對地址。整數使用的是小端序,由於大多數現代 CPU 都是小端序的,甚至大端序的 CPU 一般有讀取小端序數據的指令。

注:大端序(big-endian)和小端序(little-endian)統稱爲字節順序。對於多字節數據,例如 32 位整數佔據 4 字節,在不一樣的處理器中存放方式也不一樣,之內存中 0x0A0B0C0D 的存放方式爲例:

在大端序中,若是數據以 8bit 爲單位進行存儲,則最高位字節 0x0A 存儲在最低的內存地址處。

地址增加方向  →
0x0A, 0x0B, 0x0C, 0x0D
複製代碼

若是數據以 16bit 爲單位進行存儲,則最高的 16bit 單元 0x0A0B 存儲在低位:

地址增加方向  →
0x0A0B, 0x0C0D
複製代碼

而小端序則與此相反。目前大多數主流 CPU 都是小端序的,這也是 Cap'n Proto 採用小端序的緣由。

若是熟悉 C 或者 C++ 的結構體,能夠看到 Cap'n Proto 的編碼方式跟 struct 的內存結構很類似。即便在 V8 引擎內部,也是使用了相似的結構來進行屬性的快速讀取。相比使用 Hash Map 有很高的性能提高。

2. 序列化/反序列化

Protobuf 每次都會構建一個用於表示 message 的對象,而後將對象序列化爲 ArrayBuffer,在消息的接收方須要從緩衝區讀取 message,而後解析爲一個對應的對象,在以後的編程中使用該對象。而在 Cap'n Proto 中消息的結構直接存放在 ArrayBuffer 上,當咱們調用 message.setFoo(123) 時,實際上就相似於 uint32Array[offset] = 123,在消息的接收方,咱們能夠直接從緩衝區讀取這條消息。

Protobuf 可使用變寬的編碼,這樣對於某些場景能夠有更小的編碼長度。而 Cap'n Proto 爲了性能考慮會把整數編碼爲固定寬度,額外的字節使用 0 進行填充(這種存儲方式很相似於 memcached)。一個是以空間換時間,一個是以時間換空間。在經過網絡發送消息時,咱們但願消息體越小越好,可是若是在同一地址空間內通訊時,則咱們有無限帶寬。Cap'n Proto 的文檔中還指出,當帶寬真的很重要時,不管您使用何種編碼格式,都應該對消息體進行通用壓縮,如 zlib 或 LZ4。

3. FlatBuffers

deno 的做者 ry 也在 issue 中參與了討論,對你們的熱情關注 ry 感到十分感動,而後。。。。而後建立了一個 flatbuffers 分支 :P

FlatBuffers 一樣是一個 created at Google 的庫,具備更加完善的文檔以及 Benchmarks。而 Cap'n Proto 除了那個「無限倍速」的不公平測試外,沒有任何的基準測試數據。

而 kentonv 對基準測試的態度是:

關於基準測試 - 我花了不少時間對序列化系統進行基準測試,不幸的是,個人結論是基準測試結果幾乎老是毫無心義。

...

一個真正有意義的基準測試,須要使用兩種不一樣的序列化來編寫兩個版本的實際應用程序,並對它們進行比較......但這是幾乎沒有人作過的大量工做。

這確實是個大工程。相比而言,V8 和 Chrome 每次發佈都會進行 Real-world JavaScript performance

4. 安全

在當前 deno 的 protobuf 使用上,每一個消息都會建立一個副本。deno 使用 protobuf 只是爲了在 V8 和其餘特權代碼之間通信,即便真的明確須要一個消息副本,那麼也能夠直接使用 memcpy() 來達到更高的性能。

若是在同一個緩衝區(ArrayBuffer),當不一樣的線程同時操做時,則須要一個副原本防止 TOCTOU 漏洞,或者謹慎的處理 JavaScript 代碼,但這是不可控的,由於你不能防止第三方模塊也作相同的假設(若是第三方擴展也使用相同的通信機制的化)。

TOCTOU 的全程是「time of check to time of use」,TOCTOU 是競爭條件缺陷的一種。在多線程、多核系統中,這個漏洞很廣泛。當咱們訪問某個共享資源時,系統首先會檢測當前用戶或代碼是否有權限,而檢查(check)和使用(use)是分離的,並且不是原子的。當系統檢查資源被授予用戶權限後,攻擊者能夠臨時阻塞調用戶線程,而後在時間差內替換調資源,以達到越權訪問的目的。

舉個簡單的例子:

if (hasPermission("file")) {
    // (1)
    buffer = open("file");
    // (2) dosth
    write("file", buffer);
    // (3)
}
複製代碼

而攻擊者能夠在 (1) 處構造以下代碼:

// ...
// hasPermission 檢查經過
symlink("/etc/passwd", "file");
// 文件打開以前
// ...
複製代碼

這樣用戶就越權拿到了 "/etc/passwd" 的控制權。

上面只是一個簡單的例子,TOCTOU 有不少不一樣的形式。在類 Unix 系統上,/tmp/var/tmp 目錄常常會被錯誤地使用,從而致使競爭條件。

爲了安全而暫時損失性能是一種不得已的妥協,以前 V8 也遇到過,對於逃逸分析的漏洞直接致使了安全問題,Chrome 團隊不得不在下一個發行版中去除了逃逸分析。

5. 綜上

Deno 就像一個出生不久的孩子,Ryan Dahl 也在不停的探索,不免會走一些彎路。而做爲普通開發者的咱們,能夠關注 deno 的源碼以及 github 上的 commit。

對於一個很是成熟的項目,好比 Node.js,咱們很難讀懂他的所有源碼,甚至咱們都不知道從何讀起。而 deno 則是一個機會,咱們見證了 deno 的誕生,截至到我寫這篇文章,deno 一共纔有 249 次 commit。

相關文章
相關標籤/搜索