Proto Buffers in Lua
Google 的 Jeff Dean 同学说,设计分布式系统一定要有 Protocol Description Language。
Google Proto Buffers 的意义在于,定义了一个不错的 PDL 。protobuffers 的实现反而不那么重要了。
这几天我一直在倒腾 lua 下的 proto buffers 的支持。一直在思考,怎样的接口才是最适合 lua 使用的。
大多数语言下的 proto buffers 实现,都是将编码的数据块展开成本地语言的数据结构。对于 C/C++ ,这是最高效的形式。但对于动态语言,那就未必了。虽然 google 为 python 做的 proto buffers 的官方实现也是如此,但我依然想考虑一下,是否有更高效的方式来做这件事。
lua ,最高效的数据处理交换是利用语言的栈。虽然 table 也足够快,但复杂且深层次的 table 往往容易成为最终的性能瓶颈。btw, google 的 js V8 引擎,在部分场合表现的比 lua jit 更为出色,很大程度上是因为它极大的加快了 table 的查询。这几乎会成为所有解释型动态语言的性能热点。而且不断了生成临时 table ,还会给 gc 造成较大的负担。那么,能不能找到一种方式可以回避在 lua 中构造复杂数据结构,而又能方便的使用由 proto buffers 定义出来的结构化数据呢?
我花了三四天初步实现了一个 lua proto buffers 的库,全部是以 C 代码完成的。最终提供给 lua 的接口只有四个。
load 能加载一个 proto buffers 的元数据,对协议本身做一个描述。并生成一个 C 对象 (userdata) 返回到 lua state 中。这个 C 对象包含了一组 proto 描述信息,除了其中的字符串信息外,全部储存在一块连续的内存中。作为 lua 的 userdata ,不需要任何 gc 方法。其中引用的字符串全部记录在这个 userdata 的环境表中,在 C 对象里只记录字符串表里的序号。这个方式可以很大的提高 lua 运行时的字符串传递性能。
pattern 可以为某个 message 生成一个模式对象 (还是 C 对象),这个模式对象可以用来解码或编码数据。pattern 的概念是整个库的核心概念。库会依据 pattern 来编码或解码数据。pattern 本身的生成比较重量,但在实际应用中,往往是一次性的,而不需要对每个数据包处理时都重新生成 pattern 。做一些小手脚,就可以在 lua 层面做进一步的简单封装。用字符串的形式来 cache 并索引 pattern 。
unpack 可以对一个数据块进行解包工作。unpack 必须提供数据块以及相应的 pattern 对象。它会按 pattern 的指示把结构化数据解到栈上。这种方式回避了每次解包都生成临时的 table 。当然,如果使用者还是喜欢构造一个 table 来表达数据块中的数据结构的话,依然可以通过几行 lua 封装代码来办到。
pack 可以根据一个 pattern 对数据块打包。同样从栈接收数据源。不需要构造临时的 table 用于打包。
我在做这个库之前,做了一些工作来解析 Google Proto Buffers 定义的协议描述文件。如上所说,我认为,协议定义及描述是整个 Proto Buffers 的核心,而其实现以及二进制编码规则则是次要的东西。
google 提供了开源工具,把文本协议描述文件编译成 proto buffers 自身形式的结构化数据。这做了很漂亮,也方便了对协议本身的解析。不过若是抛开二进制编码,若想让整套协议自举,而整个过程和具体编码形式无关的话,更有趣的做法是自己来实现文本协议描述的解析。
好在用 lua 来做这项工作并不太难,proto buffers 定义的语言也相当简单,容易解析。我大约写了 100 来行 lua 代码来完成基本的语义分析。当然,用了强大的 lpeg 库。话说,PEG 是个相当强大的东西,强烈推荐没学过的同学们好好学习一下。用顺手的话,为自己的项目定义 DSL 那是易如反掌。
整个解析的实际运行过程非常繁重,但最终只是生成一个很小的 C 数据对象。我用了一个有趣的方案来干这件事:单独开一个 lua State 来完成协议的解析。这个独立的 lua State 总共所消耗的内存可以事先预估,不会太大(跟 proto 文件的规模以及复杂度有关)。所以我为这个独立的 State 定制了一个只分配不释放的内存管理器。这个内存管理器工作在独立的系统内存页上,方便最后一次回收。这样,每次解析一个协议文件,只需要消耗一些临时内存,而在解析完后,能够绝对干净的清除痕迹。我统计了一下一个最终大约 70K 的 C 数据对象,处理过程消耗了大约 1.5M 内存。
灵活使用多个 lua State 应该是一个良好的习惯。尤其在较大规模的需要长期运行程序中,把各个模块分配到独立的 lua State 空间中运行,可以使任务完成后,内存得到准确的回收。
这几天做的这个东西,还很初步,所以就不细写了。等成熟后再考虑开源。
ps. 在 lua 社区,设计些小的模式语言来完成数据处理,似乎是一个很流行的做法。lua 自带的字符串匹配如此;cosmo 也是个有趣的小玩意;lpeg 也提供一个 The re Module 来模拟正则表达式的语法。等等这些启发我设计出这个 proto buffers 的 pattern 。
Comments
Posted by: Arthur_qi | (17) September 4, 2017 09:59 AM
Posted by: mrvon | (16) January 27, 2016 04:42 PM
Posted by: 风风雨雨 | (15) November 13, 2014 07:06 PM
Posted by: Anonymous | (14) May 16, 2014 09:44 AM
Posted by: pass86 | (13) June 23, 2012 01:05 AM
Posted by: bbp | (12) November 28, 2011 02:02 PM
Posted by: 西山老土匪 | (11) September 9, 2010 09:39 AM
Posted by: 西安电动轮 | (10) August 24, 2010 02:16 PM
Posted by: 西安电动轮 | (9) August 24, 2010 02:15 PM
Posted by: 12904 | (8) August 20, 2010 06:36 AM
Posted by: 童装 | (7) August 17, 2010 09:32 AM
Posted by: Anonymous | (6) August 16, 2010 01:42 PM
Posted by: Anonymous | (5) August 12, 2010 05:32 PM
Posted by: lichking | (4) August 11, 2010 03:52 PM
Posted by: 不正直的人 | (3) August 11, 2010 11:15 AM
Posted by: cat | (2) August 11, 2010 11:07 AM
Posted by: chinetman | (1) August 11, 2010 09:28 AM