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

0. 背景

我以前在"如何评价ry(Ryan Dahl)的新项目deno?"的回答中曾经写到:javascript

我比较好奇的是 deno 使用了 Protobuf,而没有使用 Mojo。既然目标是要兼容浏览器,却不使用 Mojo...

...html

可是从 issue 中能够看出,Ryan Dahl 以前是没有据说过 Mojo 的,可是他看完 Mojo 以后,依然以为 Protobuf 是正确的的选择。java

Ryan Dahl 最初选择了 golang,后来又将 golang 从 deno 中完全删除。前几天 Protobuf 的做者 Kenton Varda(kentonv) 开了一个 issue:Protobuf seems like a lot of overhead for this use case? #269,在文中 kentonv 指出:git

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 性能。github

1. Cap'n Proto 性能

kentonv 离开 Google 以后开发了 Cap'n Proto。golang

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。

相关文章
相关标签/搜索