« May 2013 | Main | July 2013 »

June 28, 2013

skynet 下的用户登陆问题

今天收到一个朋友的邮件,他们使用 skynet 框架的游戏上线后遇到一些问题。

引用如下:

我有一个线上的 server 用了skynet框架,用了gate watchdog agent这样的结构,当人数到达1000左右的时候发现很多新 socket 连接都阻塞在了 gate 或 watchdog 这里,导致用户登录不了,后面发现是客户端那边在登录的时候做了超时判断,如果超时了就断开连接,重新连 server 执行登录过程,因为 watchdog 是在收到客户端发来的第一个消息的时候创建agent,这就导致很多 cpu 消耗在创建销毁 agent 上,而 agent 服务创建的代价比较高,这就导致watchdog效率下降。后来取消掉了客户端的那个登录超时判断,情况有所好转,但在人多的时候延迟还是存在,接着又优化了server 这边的一些逻辑代码,现在还在观察中。


中午吃饭的时候,我和我们的开发人员讨论了这个问题。为什么我们在 1000 人级别时没有出现类似状况呢?我总结的原因如下:

我们把用户认证过程放在了 agent 启动之前,是由 watchdog 进行的。

这样,未认证连接在认证前是不会启动新的 agent 的。这避免了远超过系统承载能力的并发连接引起的问题。

这样做,会让 watchdog 的逻辑复杂一些,它需要先统一处理 gate 转发过来的数据包,直到认证结束,然后通知 gate 做包转发。watchdog 承载的任务也多一些,但这么做看起来是值得的。

agent 启动是一个 cpu 开销很大的操作。skynet 的 serivce 对象,本身是很轻量的。lua vm 的创建也不会有什么开销。但是在一个新的 lua vm 上初始化 lua 代码却会消耗大量的时间。

如果 agent 的启动全部由 watchdog 完成,就必然被串行化,并会阻塞住 watchdog 消化其它包。当多人同时登陆时势必是一个性能瓶颈。尤其是每个新建连接就立刻创建新的 agent 的策略,当用户登陆不上反复重试,更会将问题恶化。

最后,我修改了一下 skynet 中 lua 服务的启动流程,可能会对这种情况有所改善。

我将 lua 服务的启动流程改为两步,先创建出空的 lua vm 。然后注册一个专用于启动的消息处理函数,并立刻给自己发一个启动消息。这个消息一定是消息队列里的第一个消息。

接下来由这个启动消息来触发 lua vm 的进一步初始化过程。这样,就可以充分利用多核来处理并发登陆请求了。

这个修改可能引起的问题是,启动服务的调用无法侦测到启动失败。


最后,我认为,针对并发登陆的排队处理总是要做的。由 watchdog 转发登陆认证请求到一个排队服务器中去应该是一个更好的解决方案。

June 26, 2013

Hive , Lua 的 actor 模型

上个周末我一直在想,经过一年多在 skynet 上的开发,我已经有许多相关经验了。如果没有早期 erlang 版本的历史包袱以及刚开始设计 skynet 时的经验不足,去掉那些不必要的特性后的 skynet 应该是怎样的。

一个精简过代码的 skynet 不需要支持 Lua 之外的语言和通讯协议。如果某个服务的性能很关键,那么可以用 C 编写一个 Lua 库,只让 Lua 做消息分发。如果需要发送自定义协议的消息,可以把这个消息打包为一个 C 结构,然后把 C 结构指针编码在发送的消息中。

skynet 的内部控制指令全部可以移到一个系统服务中,用 Lua 编写。

跨机支持不是必要的。如果需要在多个进程/机器上运行多份协同工作,可以通过编写一个跨机通讯的服务来完成。虽然会增加一个间接层使跨进程通讯代价更大,但是可以简化许多代码。

广播也不是基础设施,直接用循环发送复制的消息即可。为了必要过大的消息在广播过程中反复拷贝,可以把需要广播的消息先打包为 C 对象,然后仅广播这个 C 对象的指针即可。

基于精简设计的理念,我花了两天时间重新实现了 skynet ,并赋于它新的名字 Hive 。这次一共不到 2000 行代码,其中 C 代码不到 1400 行,可以说达到我最初精简代码的目的。

对于不了解 skynet 的同学来说,最简单介绍 hive 的方法是,这是一个精简版的 erlang ,用 Lua 实现的 actor 模型。

你可以用标准的 Lua 5.2 解释器包含 hive 模块,并用 hive.start 启动它即可。

main.lua 这个文件会被首先运行,并启动在一个独立的 Lua state 中,这里把这个独立的 state 称为 cell 。同时,系统 system cell 一定存在,它可以向其他 cell 提供 timer 等基础服务。

hive 由很多 cell 构成,每个都是独立的 lua state ,并采用多线程并行工作。线程的总数可以在启动时设定,cell 的个数可以远大于线程数,它们将完全均匀的分配 cpu 资源。

每个 cell 都是一个消息收发器,可以处理别的 cell 发送过来的消息。处理消息可以是严格的请求回应模式,也可以只投递消息而不期待回应。每条消息都由一个独立的 coroutine 来处理。

目前 main.lua 演示了如何启动新的 cell pingpong.lua ,并给它发送消息。

感兴趣的同学可以在 github 上取得全部代码,我列了一个长长的 todo list ,大部分工作是从 skynet 移植过来。这不是一个工作项目,所以不保证最近有精力全部完成它们。如果你有兴趣,可以帮助我一起来完成。

June 21, 2013

重写了 skynet 中的 socket 库

前几天在做 skynet 的 mongodb driver ,感觉以前为 skynet 做的 socket 库不太好用。

以前 skynet 对外的网络连接是通过 connection 服务实现的,这个服务会自动推送绑定的 socket 的上行数据到指定的位置。接下来,需要自己用 coroutine 去分析这个数据流。这样做比设计一个询问应答接口来获取 socket 上的数据流要高效一些,但用起来很不方便。

我希望能有一个看起来和传统的 socket api 类似的接口,直接 read/write 即可,但又不想失去性能。而且 read 不能阻塞住系统线程。

这次重新实现了 connection 服务,就把接口也改过来了,新的服务叫 socket 以便于和之前区分。

新的 socket 服务除了读数据外,也加上了写缓冲。这个改动类似于前几天对 gate 服务的修改。修改过程中,我把 epoll 和 kqueue 相关的代码抽取出来,独立在一个 event.h 的头文件去了。

api 改动最大的是 socket.lua 这个库,增加了 read write readline 等接口。由于 skynet 的 service 在分发包的时候可以利用 coroutine 并发。如果直接用 read 去解析数据包的话,若有多个 coroutine 同时解析同一个 fd ,就有可能产生混乱。我增加了对 fd 的锁来回避这个问题。

当然,一般是不会直接使用 socket 这个底层库的。一个应用 socket 库的案例是重新编写的 redis 库

老版的 redis 是先建立一个 redis-cli.lua 的服务,统一和外部 redis server 交互。有了新版 socket 库后,直接去和外部 redis server 建立连接就可以了。

June 18, 2013

MongoDB lua driver

唔,我知道有人已经做了 MongoDB 的 lua driver ,比如这个 。但我不想仅仅是对 C++ API 的封装,而想从协议层做起,这样日后可以方便改为异步模型,也好整合到 skynet 中去。

这里还有一份纯 Lua 的实现,是从协议层做的封装。但有几个问题,一是依赖 lua-socket 库,二是纯 lua 实现不如 C 库性能好,三是特性没有支持完整。

曾经想在 C Driver 的基础上做封装。做的过程中发现 c driver 代码质量不高,且特性支持不完整,最终我考虑自己从协议层开始重新做一份。

mongo 的文档不算完整,我感觉协议定义也不是很严谨。所以在实现过程中遇到许多麻烦。原本认为一天能写完的,结果做了三天。今天终于可以发布一个初步的版本了。

https://github.com/cloudwu/lua-mongo

制作这个 driver 的思路是,把 bson 的编解码部分完全独立开,放到另一个项目 lua-bson 中。

这样,以后为 skynet 做特别版可以更灵活一些。因为 skynet 需要一个异步模型,bson 包的打包过程和 mongo 的 driver 可能在不同的 lua state 完成。

性能相关的部分我想放到 C 代码中完成,但 C 代码只提供最基本的 api ,功能性需求用 lua 实现即可。

目前只是一个初步的东西。因为我几乎没有用过 MongoDB ,对它的了解仅限于对协议的研究。希望有熟悉 mongo 的同学可以协助我来完善这个 driver 。

June 14, 2013

写了一个 lua bson 库

bson 的官方网站上链接了一个纯 Lua 实现的 bson 库,但是实现的不完整。

我用 C 实现了一个 Lua bson 库。

bson 是为 MongoDB 设计的结构化数据序列化协议,所以有很多设定是为 mongodb 服务的,如果单用于序列化结构化数据,那么那些不一定要实现。但我写这个的最终目的是做一个 lua 的 mongo driver ,所以就实现的比较完整了。

bson 结构中,有一些固定长度的字段,修改它们不必重新编码。这在 mongDB 的通讯协议中非常有用,所以我也加了对应的接口。还有许多特性可能会有用,比如把两个 bson document 连接成一个之类的,等我在写 mongoDB driver 的时候,视情况实现。


2014 : 3 月 12 日补充

因为 mongo 对有些 bson 文档要求 key 的次序, 而 lua 的 table 是无序的。所以增加了 bson.encode_order 按次序打包 document 。

June 09, 2013

用栈方式管理 Lua 中的 C 对象

最近思考了给 Lua 写 C 扩展的另一个问题。

我曾经总结过几种 Lua C 库中 C/C++ 对象的生命期管理问题 。最近想到另一个方案,虽然实现后并没有用到项目里,但值得记录一下。

Lua 没有 RAII ,一切对象的回收是依赖 GC 的。封装 C/C++ 对象则一般用 userdata 。userdata 比较重,作为临时对象使用总觉得有点别扭。比如封装 matrix 对象,如果我们为每个 matrix 对象都生成一个 userdata ,那么一些临时的 matrix 对象就会一直推迟到 GC 发生的时候才回收。而在 C/C++ 这样的语言中,临时对象通常是在离开调用层次时自动释放的。

对于某些 C 和 Lua 混合的业务也有这样的问题。某些较长的业务流程,一部分环节由于性能原因使用 C 来实现,另一部分更适合直接用 Lua 。我们必须用 userdata 来交换中间状态。比如处理一个 C 层次上产生的数据包或 C 结构数据,交由 Lua 处理后,C 对象就没有必要再存在了。但处理过程中,Lua 代码则需要反复引用和处理它。

多数情况下,我们不用太考虑这两者间的差别。但这并不妨碍我去考虑有没有可能在 Lua 中模拟一套栈对象的管理机制。它可能是 GC 系统之外的一种对象生命期管理的选择。


我试着用 C 实现了一个简单的栈结构,每个堆栈用一个数组来保存一系列相同的 C 对象(或指针)。由于 Lua 的 coroutine 的存在,没有 lua thread 都应该有一个独立的栈,所以我把这个结构封装成 userdata 放在一张表里,用当前的 lua thread (即 lua State 对象)索引。

用户可以主动的调用入栈或出栈,这有一点繁琐,可以考虑放在 debug hook 中实现,但我觉得手工调用更好。如果入栈和出栈调用不匹配的话,栈深度就很容易变成负数或超过设置的最大值。所以一旦写错,很容易被发现。

索引栈上的对象可以用一个组合起来的 id ,高位使用 stack frame 的编号,低位使用序号。这个 id 可以对应到唯一的对象。我采用单调递增的 stack frame 编号,也就是说,及时在同一层次的 stack frame 上,多次函数调用在同一位置产生的对象也有不同的 id 。

这个 id 就可以以 lightuserdata 的形式保存在 Lua 中的。Lua API 引用 lightuerdata 的时候,可以校验 id 是否还在当前栈上,并得到真正的对象。当离开调用层次时,C 库去释放掉那些过期的对象。

June 07, 2013

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 封装暂时就不开源了。