« skynet 的网关模块的一点修改 | 返回首页 | 用栈方式管理 Lua 中的 C 对象 »

MongoDB 的 Lua Driver

最近听从同事建议想尝试一下 MongoDB 。

前年,图灵的同学送过我一本《MongoDB权威指南》 ,当时我花了两个晚上看完。我所有的认知就是这本书了。我们最近的合作项目 狂刃 也是用的 MongoDB ,最近封测阶段,关于数据库部分也出过许多问题。蜗牛同学在帮助成都的同学做调优,做了不少工作。总是能在办公室里听到关于 MongoDB 的话题。

我打算为 skynet 做一个 MongoDB 的 Driver 。

Skynet 默认是用 lua 做开发语言的。那么为什么不直接用 luamongo 呢?

因为 skynet 需要一个异步库,不希望一个 service 在做数据库操作的时候被阻塞住。那么,我们就不可能直接把 luamongo 作为库的形式提供给 lua 使用。

一个简单的方法是 skynet 目前对 redis 做的封装那样(当然,skynet 中的 redis 封装也是非阻塞的),提供一个独立的 service 去访问数据库,然后其它服务器向它发送异步请求。如果我直接使用 luamongo 就会出现一个问题:

我需要先把请求从 lua table 序列化,发送给和 mongoDB 交互的位置,反序列化后再把 lua table 打包成 bson 。获得 MongoDB 的反馈后,又需要逆向这个流程。这是非常低效的事情。如果我们可以直接让请求方生成 bson 对象,这样就可以直接把 bson 对象的指针发过到 交互模块就够了( skynet 是单进程模型,可以在服务内直接交换 C 指针)。这就需要我定制一套 lua moogodb 的 driver 了。


我是从昨天正式开始进行这个工作的。原以为,有 MongoDB 的 C Driver 打底,应该也就是一天的工作量。结果却不是这样。

首先我研究了 bson 这种数据格式。以前也听过它,以为就是 json 的 binary 形式而已,是一种简单通用的结构化数据交换格式。研究后的结论是,bson 并非一种通用数据格式,而是为 MongoDB 专门设计的,虽然它希望成为通用格式,但协议设计中却充满了 MongoDB 特有的东西。这让我小小不爽。

MongoDB 的 C Driver 是官方维护的。一开始我是挺信任它的代码的。但做下来非常失望。首先文档跟不上更新,当然这也不太所谓,反正我不怎么读文档,都是直接看 .h 文件做的。我试图在这个 C Driver 提供的 bson 库的基础上封装 lua 接口。做到快结束的时候发现它没有支持 bson 标准中的 minkey 和 maxkey 两个东西。虽然加上是举手之劳,但我一直没有找到方便的提交问题的渠道。从 github 上找到维护人的 email ,写了封邮件去问,还没有得到反馈。不过最终我还是把这个东西做好了。

接下来做 mongo 其它部分的封装。发现了更多的问题。由于文档比较老,我在用 api 的时候都是直接读 .c 里的实现的。

比如 mongo_set_write_concern 可以给连接设置一个默认的 write concern 对象,但它的实现里只是保存了 mongo_write_concern 指针,而无视了这个对象的生命期。

如果说这个问题不太严重的话,这个接口可能更能说明问题:

MONGO_EXPORT void 
mongo_write_concern_set_mode( mongo_write_concern *write_concern, const char* mode ){
    write_concern->mode = mode;
}

mode 这个字符串并没有复制。一般 C 程序用的时候可能不太能暴露问题,但如果做 lua 的绑定的话, mode 就一定是从 lua state 里传过来的 lua string 了。一旦 string 被 gc 回收,就会导致悬空指针的问题。

我现在没用一个 api 就很不放心的读一遍实现。照这个做法,真不如放弃掉封装 C Driver 的想法,从协议层直接去实现呢。

结论是 MongoDB 的 C Driver 的代码质量很糟糕。


下面我谈谈关于 Lua 封装库的一些问题:

对于 bson 中的几个复杂对象,例如 objectid timestamp 等类型,luamongo 里是做了一个 full userdata ,并且附加了 metatable 。我觉得这种方式并不好。会导致每次对 mongoDB 的消息处理都生成许多的 userdata 。为了回避这个问题,我想了两一个方法。

bson 的字符串类型要求是 UTF-8 编码的,所以不是 UTF-8 的串都需要用 binary 格式编码。我们可以利用这一点,构造一个非 UTF-8 的 string 来表示别的类型的对象。

我的方法是:所有 00 XX 开头的字符串都是扩展类型。这里 XX 是扩展类型的 type 。这样,我把 bson 需要的扩展类型,包括 binary 、timestamp 、date、minkey、maxkey、objectid 等等都编码成了 lua 的 string ,回避了 userdata 的开销。构造这些 mongo 扩展类型就是构造一个特殊的 lua string 。编码解码的成本都相当的低。

我打算重新按 bson 协议实现一遍 mongoDB 的 lua driver ,所以目前已经完成的 C Driver 的 lua 封装暂时就不开源了。

Comments

题外话,PostgreSQL的C客户端库倒是支持异步的,非常容易集成到基于select、epoll,、kqueue之类的循环机制中。
MongoDB C启动质量不高,我估计MongoDB是C++的写的,不合习惯C开发了:)。其实MongoDB的Java驱动其实性能也有问题,我当时进行性能测试时,发现再加上Safe和索引后,性能下降很厉害,后来在论坛上问询,有人说是MongoDB Java驱动的问题,用他们的异步驱动测试一下,发现性能很不错。
MongoDB 的质量离 MySQL 等还差不少, 最好不用用它. 应该能够搜到一些 Don't use mongodb 的讨论
去年我也做了一个lua的mongodb驱动:https://github.com/bigplum/lua-resty-mongol,是在openresty/ngx-lua平台上开发的。 当时找了一下lua的驱动,发现daurnimator/mongol这个驱动比较符合需求,纯lua实现,使用luasocket做通信。移植到openresty/ngx-lua上只修改了几行代码,就能跑通。 云风可以看看ngx-lua这个项目,如果skynet也朝这个方向走,应该能做成一个很通用的游戏平台。
@tiansheng li 我们用 erlang 开发了半年. 积累了大量代码 最终因为性能严重不足,重写了全部 erlang 代码.
最近重看了一下博主关于游戏开发的文章,个人认为博主用C+lua开发游戏服务器有些不合适,很多功能可以使用Erlang的OTP库简单解决(花的时间也更少,可靠性也更好),而且基于Erlang的Mnesia非常适合游戏运行时的数据库(Erlang也支持MongoDB数据库,可以用MongoDB储存玩家信息,Mnesia作为服务器运行时的实时数据库),个人建议以后的游戏服务器可以考虑用Erlang(也可以重写现有的),而且这门语言的学习成本并不高
Mongo C driver 的开发太不积极了,而且PR的合并也很慢,结果就是个恶性循环,太多必须的功能不支持。但是上次邮件问项目负责人提到貌似准备在开发新的c driver。不知道官方究竟怎么打算的
@ack string 和 userdata 的 GC 成本不一样(string 可以被直接 mark , userdata 需要检查是否有 metatable 和 uservalue)。 有兴趣可以读我写过的 lua gc 的分析。且一般(比如 luamongo 的实现) 还需要给 userdata 附加 metatable 。 另外,对于相同值的 userdata 是不能正确比较的,比如相同的 objectid 可能是两个 userdata. 比较它们就不相等。 如果是 string 就可以被正确的比较,也可以正确的作为 table 的 key 。
呃,lua string和lua userdata的开销有什么区别吗?好像一个级别啊。

Post a comment

非这个主题相关的留言请到:留言本