« November 2013 | Main | January 2014 »

December 30, 2013

Lua 远程调试器

我们现在的手游完全用 Lua 开发,这就有了调试的需要。

今年曾写过一个 lua 代码跟踪器,主要是用于服务器开发。服务器程序不适合完全 stop the world 慢慢调试,以输出 log 为主。但现在在客户端,那么一个类 gdb 的调试环境更好一些。

lua 的调试器在我还在网易时就做过 。从网易出来后没带代码,需要用就要重新写了。好在 lua 的 debug 接口非常全,今天花了 2 个小时就重新实现了一个简陋的雏形。

这是一个远程调试器,启动程序的时候利用 ldebug.start 监听一个调试端口,程序跑起来后使用 nc 或 telnet 就可以连上去调试。

我们可以在 lua 代码中用 ldebug.probe 放一些硬断点。平常是不激活的,也没有什么额外开销。

调试端口连接上后,只能注入一些 lua 代码运行,看一些全局变量。但在调试控制台上可以输入 stop 让程序在最近的 ldebug.probe 调用处停下来。然后我们就可以用类似 gdb 的指令单步运行程序,或是观察一些边变量。

其实在停止程序后,还可以做一些断点或条件断点的功能。今天没空做了,等用到再说。

代码放在 github 上,不定期维护。有兴趣的同学可以完善它。

December 28, 2013

Ejoy2D 开源

我们的第一个手游差不多做完了,预计在明年 1 月初推广,目前内测的情况非常不错,我们也可以考虑开始下一步在手游领域立新项目了。

上个项目做的太匆忙,今年 4 月份才开始。因为决定做一个 2d 游戏,我觉得在 2d 游戏引擎方面我有超过 15 年的经验,使用一个流行的开源引擎,比如大家都在用的 Cocos2d-X 还不如自己写。这样对引擎的可控性更强,可以方便的定制自己需要的功能,并在性能上做针对性的优化。手机设备的硬件性能远不如 PC 机,即使程序性能足够,我们也要考虑硬件的能耗,让电池用的更久一点,让设备不那么放烫。优化引擎也是游戏程序员的乐趣之一。

我们这个项目还是做的太急了,只花了一个月时间做引擎,然后在上面做了太多应急的各种修改,到项目完成(10 月)时,已经很不堪了。我不想把这块东西带到下一个产品,所以打算重新把这块代码理一下。

开源是一开始就想做的事情,可以帮助到别人,也同时督促自己把代码写的更整洁。从这周开始,我满头写了一周代码,从头写了 4000 行代码,就有了这么一个项目:ejoy2d

当然它还很不完整,有兴趣的同学可以跟踪这个项目,我和我的同事会逐步完善它。同时欢迎其他同学推送 Pull-request 。下面对 ejoy2d 做一个简单的介绍:


首先,它仅仅是一个图形引擎,所以你在里面不可能看到诸如音频处理,网络处理等等和图形渲染无关的部分。而且更相当的称呼可能是基于 OpenGL ES 的图形库,它的设计目标是很容易的嵌入你自己的游戏引擎中。

那么,一个为移动设备开发的引擎,目前为什么只能在 Windows 和 Linux 平台上运行呢?

因为,我还没有想好以什么形式提供 iOS 和 Andriod 平台开发的工程文件和相关代码。如果你熟悉 iOS 或 Andriod 开发,把这个库嵌入进去也不是很难的事情。不久以后,我也会提供主流移动平台的相关模块。btw, 稍微修改一下 linux 的编译流程,应该可以 port 到 MacOS 上,暂时我还没有时间做这个工作。


可能和你见过的其他更庞大的游戏引擎不同,ejoy2d 甚至没有提供一个场景管理模块。这是引擎,我不觉得场景管理模块对于图形引擎是必需品。提供一个模块的场景管理模块反而会限制二次开发人员的想像力。ejoy2d 只提供绘制 sprite 的 API 。如果你希望更底层的控制渲染器,还可以使用 ejoy2d.shader 子模块中的多边型渲染 API 。

ejoy2d 采用 C + Lua 开发,一切高层 API 都是以 Lua 函数和对象形式提供的。二次开发人员完全可以用 Lua 来开发游戏,由于设计之初就把 lua 做为唯一上层开发语言,ejoy2d C 模块的 lua binding 模块都尽量保证高效。

sprite 是 ejoy2d 里最重要的数据类型,但引擎几乎不提供运行时的 sprite 构造方法,一切都描述在资源文件里。查看 examples/asset/sample.lua 你会看到一个资源包的描述文件的示范。

每个 sprite 都由一个 lua 表描述,type 字段表示了它的类型。目前 sprite 有四种类型:四边型图片(picture),多边形(polygon),动画(animation),文字框(label)。

id 字段是这个 sprite 在一个包文件里的唯一 id 。id 可以从 0 到很大的数字,一个包内的 id 不要求连续,但不建议散的太开。因为引擎是用一个数组来索引这些 sprite 的,这个数组的大小取决于最大的 id 号。

picture/polygon 表示了一个多边型从贴图到屏幕的映射关系。ejoy2d 为四边型做了一定优化,但同时也支持多边型。注:每个 picture/polygon 对象可以由不只一块多边型构成。排在前面的多边型绘制的时候更先渲染(在更下面)。

每个描述文件配了一张或多张贴图,开发的时候应该尽量把小图片拼合在同一张贴图内。游戏采用尽量少的贴图有利于手机平台的游戏性能。我们公司购买了 TexturePacker 放在我们的开发工具链中,当然你自己写一个工具做这个图片拼合工作也不算太难。一个资源包可以配多张贴图,在 picture 数据结构中写明引用的哪一张即可。这里遵循的 lua 习惯,贴图号是 base 1 的。

picture 的 src 字段 8 个整数表示的是 4 组贴图坐标,以像素为单位。screen 字段的 8 个整数则表示对应的屏幕坐标,是以屏幕像素为单位,但放大了 16 倍。你也可以认为这是一种定点数表达。


animation 是 ejoy2d 中最复杂的数据结构。一个 animation 是由若干 component 构成的。每个 component 可以用 id 引用其它的 sprite 对象(不限于静态图片组还是另一组动画),但你得保证不能成环。

animation 可以又多组动画序列,每组是一个 action 。当然你也可以只有一组默认动画组,这样不需要给 action 起名字。当有多组的时候,需要在描述数据里加上 action 字段,给一个字符串名字。

一组动画序列(action)是由若干 frame 组成的。每个 frame 可以由这个 animation 中定义的 component 任意组合。我们可以在资源文件中描述组合关系,如果简单引用一个 componenent ,就写上其编号( base 0 )。也可以加上一个变换矩阵,以及两个颜色值( color ,additive )。渲染的时候引擎会把这个 component 的颜色乘上 color 加上 additive 。

{
    type = "animation",
    export = "mine",
    id = 38,
    component = {
        {id = 34 },
        {id = 35 },
        {id = 33 },
        {id = 36 },
        {id = 37 },
        {id = 23, name = 'resource' },
    },
    {
        {   0,1,2,3,4,5 },
    },
},

这是从 sample.lua 中截取出来的一段示例:它描述了一个动画 sprite ,只有一个默认 action ,这个 action 只有一帧 。这一帧由 6 个组件构成,依次是 id = 34, id = 35, id = 33, id = 36, id = 37, id = 23 的 sprite 。每个组件都没有特殊的位置变换,以原有的坐标渲染。这个 sprite 被命名为 mine ,这样,开发人员可以通过 mine 这个名字从资源包中检索到它,而不需要记住 id = 38 这个特殊数字。

id = 23 这个特殊组件被命名为 resource 。一个被命名的组件可以在运行时从父节点对象中取出,并对它做一些运行时操作。

ejoy2d 的 sprite 渲染遵循如下原则:

如果一个组件没有命名,那么在设置这个 sprite 的当前 frame (被渲染的帧),所有匿名组件都会相应的改变。这可以方便美术人员组合做好的动画,而不用在程序里干预。而如果一个组件有名字,则必须在程序中去设置它的帧号,而不会自动跟随父节点的帧变化。

在 sample.lua 里还可以看到这样的帧描述信息:

{   { index = 3, mat = { -1024,0,0,1024,0,0 } } },

它表示引用第 4 个 component ( base 0 ),并需要在渲染的时候附加一个变换矩阵。这是一个 2*3 的矩阵,可以描述 2D 空间的旋转、位移、斜切、缩放等变换。为了提高运行效率,引擎采用的是定点数,把实际小数放大了 1024 倍。所以 -1024,0,0,1024,0,0 实际是一个 (-1,1) 的缩放变换,也就是横向做一个镜像。

注意:不推荐在这里利用变换矩阵做镜像。更好的做法是在 picture 结构里就把 screen 的坐标点翻过来,这可以在运行时节省一次矩阵乘法。这里这样写仅仅是为了示范。一般这个变换矩阵只用于有一个部件想被多个动画公用的情况。


关于资源打包:

ejoy2d 目前只提供了一个叫 simplepackage 的模块,它简单的把描述文件,图片文件结合在一起。阅读代码 可以看到,它仅仅是在文件目录下按一定规则去读相关的文件。

这只是方便开发,在产品发行时不应该这样做。我不想提供一个模块的包文件格式和相关代码,这块可以留给用的人自己写。

sprite pack 提供了低阶接口,可以把一个以 lua 代码形式提供的描述文件变成一个二进制数据块(ejoy2d.spritepack.pack),然后再用 ejoy2d.spritepack.import 导入这个二进制数据块。你可以在发布工具链上,写一个简单的脚本把你的描述文件从 lua 转换成二进制格式,然后在产品发布后,运行时直接导入二进制数据块。这样可以使加载速度快的多(或许你还需要加一点代码对数据压缩、加密)。

贴图文件目前只支持二进制 ppm 和 pgm 格式,分别描述 RGB 的颜色通道和 Alpha 通道。这是因为 ppm 的格式最容易处理,只需要不到 200 行代码。我不想嵌入一个 libpng 这样的库去处理类似 tga png jpg 等等更多的图片格式。这些工作很容易做,不需要放在这个引擎中。而且通常游戏发行时不需要支持这么多图片格式。更有可能的是,你需要诸如 pvr 压缩贴图的支持,根据项目需要增加一个 pvr 数据加载模块要有意义的多。


Ejoy2d 的编译和使用流程:

你需要安装 lua 5.2 以上的库,以及最新版的 glew 。如果你需要在 Linux 下开发,可能还需要安装 freetype 库。

Windows 下我只测试了 Mingw 的编译环境。直接 make 就会在项目目录下生成 ej2d.exe 这个执行文件。如果编译不过,可能是依赖库的路径设置不对。

引擎运行需要 ej2d.exe 这个执行文件,ejoy2d 目录必须和这个执行文件在同一目录下。ejoy2d 目录下放置的是引擎的 lua 部分。

用 ej2d 带一个脚本名就可以做测试了。比如 ej2d examples/ex01.lua

我的 example 写的相当简陋,需要慢慢完善。sample 需要的资源在 examples/asset 目录下。图片看起来会比较粗糙,我是从我们上一个手游项目中截取出来的。只是起一个示范作用,为了不至让代码仓库体积过大,我把原图缩小了 16 倍。

在 lua 脚本中 require ejoy2d 就可以用了。你需要编写 update/drawframe/touch 三个函数,用 ejoy2d.start 注入到引擎,就开始工作了。

update 会以每秒 30 次的固定频率调用,而每次引擎接到系统的渲染请求时都会调用 drawframe 函数。注意:当程序处理性能不够时,依然会尽量保证每秒 30 次的 update 调用,但不保证 drawframe 被调用多少次。所以不要在 drawframe 里做游戏逻辑。

touch 函数是用来处理触摸消息的。Windows 只简单用鼠标点击模拟了一下。未来需要做更多的多点触摸和手势的处理。移到真正的移动设备上后,需要做更复杂的处理。

引擎还需要提供更多的系统消息,比如系统暂停,系统恢复,系统退出等等。这些在未来我会逐步加上。


Sprite API

sprite 对象从资源文件中构造。使用 ejoy2d.sprite(包名,导出名) 创建。创建出来后,你可以用 spr:draw 来绘制它到屏幕。

draw 函数可以带一个 SRT 的表,描述这个 sprite 最终绘制的位置变换。通常只需要填写屏幕 x,y 。如果你需要做整个场景的缩放,scale 也是经常会用到的。当然我也提供了 rot 来旋转 sprite 。

每个 sprite 都可以设置它自己的局部变换矩阵。ejoy2d 提供了一个简单的 matrix 库( ejoy2d.matrix ),用 spr.mat = mat 来设置变换矩阵。注意:这个设置的 matrix 和资源文件里给组件附加的变换是不冲突的。渲染时引擎会逐级把变换矩阵乘在一起。没有 API 可以取到资源数据中为 sprite 预设的变换。

sprite 也有 color 属性。它通常用来实现 sprite 的半透明。当然你可以做一些变色特效。同样,颜色变换在资源描述里也有一份,和运行时设置的变换颜色相互独立。

在做一些特殊效果(比如对按钮加亮)时,光用颜色乘法还不够,这个使用可以设置 additive 数据,累加一个颜色上去。

在引擎内部,每个 sprite 都由一颗树构成。如果在资源描述文件中指定了组件(子树)的名字,那么用 sprite:fetch(名字) 就可以取到这个子节点。匿名子节点是没有办法在运行时获取到的。如果子节点的名字和固有属性名(mat/color 等) 不重复的话,你也可以只用用 sprite["name"] 取到它。但记住,fetch 子节点是一个略微重量的操作。如果要反复操作一个子节点,建议只获取一次,放在 local 变量中待用。

引擎不支持动态增加子节点。但可以用 sprite:mount("name", child) 把一个 sprite 挂接到资源文件中描述的位置上,替换掉默认的子树。同样为了方便,也可以直接用 sprite["name"] = child 来做这件事情。

简单说,ejoy2d 不提供运行时构建 sprite 的方法,但可以通过灵活的资源文件来弥补这一点。(你也可以通过 ejoy2d.spritepack 的 API 在内存中动态创建资源包)


关于粒子系统:

我们的新项目想使用 Cocos2d 官配的粒子编辑器。所以我移植了 Cocos2d-X 中的粒子系统的代码。雷雨同学这两天把这个模块整合到了 ejoy2d 中。但我对这个粒子系统并不算满意,有机会会重新写一遍。

从 Cocos2d 移植过来的粒子系统我已经把渲染相关的代码剥离,变成一个纯粹的数据处理器.它只负责计算每个粒子片的变换矩阵。除了在运行时利用这些矩阵渲染粒子片以外,你也可以用这些数据生成 sprite 包的描述文件,这样就不必要在运行时去计算粒子了。损失一些运行时的随机性,但可以提高一些效率。


关于 UI 系统:

UI 对象可以完全用 sprite 来实现。但除了渲染,你需要的是获取 UI 消息。ejoy2d 提供了一个类似 draw 的方法 test 来做到这一点。test 和 draw 不同。它不真正把 sprite 画出来,而是判定一个坐标点是否落在它的包围矩形中。你可以设置 sprite 的某个子节点的 message 属性为 true 来捕获 test 消息。如果 test 的坐标落在任意一个 message 属性为 true 的对象(包括它的子节点)上,函数会返回捕获到消息的对象。

test 方法为实现一个完整的 UI 提供了可能。但把我们公司项目中 UI 模块(完全用 Lua 实现的)转移到 ejoy2d 这个开源项目里来还需要一点时间。


关于字体渲染:

这部分开源代码目前还很粗糙,刚刚能用而已。我们自己的项目使用了另一套相关模块,但暂时还没有精力搬过来。这部分需要逐步完善。


关于编辑器:

要把 ejoy2d 用起来是需要许多工具的。这方面我们也在积累,自己开发了一些编辑器,同时也用改造一些开源工具来输出 ejoy2d 能识别的数据。这部分暂时没有开源的计划。系统有热心的同学可以为它发起编辑器的开源项目。


关于文档:

有空再慢慢写吧,最近恐怕是项目压身,没那么多时间了。有问题可以在 github 的 issue 里提出来。

December 20, 2013

skynet lua 服务的内存管理优化

前两天一直困扰我的问题是并发启动 lua state 比串行启动它们要慢的多。而启动 lua state 的操作相互是完全无关的,没有任何应用层的锁。原本我以为多核同时做这些事情,即使不比单核快,也不至于慢一倍吧?

昨天有同学在 google talk 上和我讨论这个问题,说要不要考虑下是内存资源方面的问题。比如,在大量线程同时申请并读写大量内存时,有可能引起操作系统在映射物理内存到虚拟地址空间这个操作上出现性能问题。

虽然最后确认不是这个原因,但这启发我可能内存分配器在多核上工作并不顺畅。

我们使用 skynet 的项目利用 jemalloc 替换了默认 glibc 的分配器。tcmalloc 也应该是一个好的选择。但无论是哪个,都有多线程锁的问题。而 lua 可以自定义内存管理器,我们在 lua 服务启动时,若预分配 1M 内存,那么在这 1M 内存内的内存管理就完全没有线程安全的顾虑了,理论上这种定制的内存管理器会比一切通用管理器表现更好。

昨天我花了 3 个小时实现了一个简单的 lua 内存管理器,提交到 github 上。默认是关闭的,有兴趣使用的同学可以在 service_lua.c 里把

#define PREALLOCMEM (1024 * 1024)

的注释去掉。这样就可以启用为 skynet 专配的 lua 内存分配器了。

实测这个分配器会比 glibc 的版本好 40% 以上,比 jemalloc 好 5% 左右。它虽然依然不能解释多核并发初始化 lua state 反而更慢的问题,但本身还是对性能有不少改进的。

December 18, 2013

skynet 服务启动优化

我们开发 6 个月的手游即将上线,渠道要求我们首日可以承受 20 万同时在线,100 万活跃用户的规模。这是一个不小的挑战,我们最近在对服务器做压力测试。

我们的服务器基于 skynet 构架,之前并没有实际跑过这么大用户量的应用,在压力测试时许多之前理论预测的问题都出现了,也发现了一些此前没有推测到的现象。

首先,第一个性能瓶颈出现在数以万计的机器人同时登陆系统的时候。这是我们预测到的,之前有为此写过一篇 blog

为了解决这个拥塞问题,我的建议是用这样一个系列的方案:

  1. 用户认证不要接入 agent 服务。即,不要因为每个新连接接入都启动一个新的 agent 为它做认证过程。而应该统一在 watchdog 分发认证请求。当然这样就不可以用 skynet 默认提供的 watchdog 了。skynet 的源代码库中之所以实现了一份简单的 watchdog ,更多的是一个简单的示范。我们自己开发的两个项目都自己定制了它。

  2. 认证的具体业务逻辑(例如需要接入数据库等),实现在一个独立的服务中,做成无状态服务,可以任意启动多份。由 watchdog 用简单的均匀负载的方式来使用。如有需要,再实现一套排队流程(参考 1, 参考 2 ),也由 watchdog 调度。

  3. 我们目前这个项目的设计是唯一大服,所有用户在一个服务器中,要求承担百万级用户同时在线。所以我们在每台物理机上都配备了一个 watchdog ,通过内部消息在中心服务器统一协调。如果不这样设计,watchdog 会实现的更简单。watchdog 只负责维护用户在线状态,没有具体的计算压力,所以很难成为性能热点。

  4. 当用户认证成功后,watchdog 启动一份 agent ,通知 gate (连接网关) 把用户连接重定向到 agent 上。后续用户的业务逻辑,都有一对一的 agent 为它服务。

由于 agent 是 lua 编写的,所以启动 agent 始终是一个开销很大的过程。加载 lua 代码比加载一个 C 编写的动态库要慢上不只一个数量级。在实际测试中,agent 的启动环节还需要通过 skynet 消息向一个中心(目前是 service manager)索取其它服务的地址。我刻意没有使用 skynet 提供的名字服务,因为 service manager 是用 lua 编写的,更容易定制。但这也会使启动 agent 更容易拥塞在某个单点。

启动 agent 过慢这个性能热点出现前,我们已经预备了一个方案。印证它的确是一个问题后,我们启动的预案:

我们在整个服务启动过程中,预算启动了 1000~5000 个 agent 待用。启动完毕后才开启对外端口。用一个定时器检查备用 agent 池是否枯竭,定期补充。这个方案只需要不到 100 行 lua 代码就可以完成,简单有效。躲过开服高峰期后,这就不会再是热点了。

注:不需要把 agent 池实现成可回收的。即不必在 agent 退出时归还。这浪费了 lua 作为沙盒的好处(用户断开连接就清理所有相关状态),也没有带来什么明显的性能好处,还增加了 agent 池实现的复杂度。


我们启动 agent 池方案后,发现预启动 1000 个 agent 在我们的服务器上居然长达 40 秒。平均每个居然要 40ms 秒之多。

我们的 agent 启动过程比较复杂,为了观测到热点在哪里,我 增加了 boot time 的统计 。不出所料,95% 以上的时间是花在 lua 脚本的加载上。我们的 1000 个 agent 加载的是相同的脚本,但是加载到不同的沙盒中。每个都要调用文件 IO 且 parser 源码。这两天我花了点时间把一直想做没做的功能实现了:在 skynet 中 cache 加载过的 lua 代码文件 ,不用每次都通过文件 IO 读取,并可以 cache 住源代码 parser 的结果。我用的是非侵入式方案,把自己写的 loader 注入到 lua 的 package.searchers 里。这个依赖 lua 5.2 的特性,可能在 luajit 上会有一点小问题。我们的项目没有使用 luajit ,所以暂时不会完善它。

做了这个简单的 code cache 后,启动时间从 40 秒下降到 20 秒,提高了一倍。


另一个困扰我两天,得不到合理解释的奇怪现象是:

如果我串行启动 1000 个 agent ,每个启动完毕才启动下一个;比我并发启动 1000 个 agent ,不用等待成功回应,居然要快一倍!

我剖析了启动时间,那 95% 的启动时间花在把 lua opcode 加载到 lua state 中。我们知道独立的 lua state 之间是没有任何关联的,不会有任何形式的锁,理论上并行不会有任何冲突。

我已经把 lua 服务的启动做成二步式,从 skynet 发起启动一个新的 lua state 和在 lua state 上加载代码是两个过程。所以 launcher 启动一个 agent 后,会有另一个工作线程去完成加载代码的工作。当启动串行时,可以大致看成 A 线程发起启动,创建 skynet service ;B 线程顺着在新启动的 service 上装载 lua 代码。

如果并行启动 1000 个 agent ,势必让所有工作线程都同时启动,以流水线方式装载初始化这些沙盒。这些工作都是相互独立的,在多核环境下,理论上应该快一些;但实际上却更慢,且慢了整整一倍。

实际运行时分别在串行和并行环境下测试,并行环境下 CPU 负荷也高的多,但最终实际消耗的时间却长的多(人可以直接感受到时间差别)。排除了 skynet 中少量的 spin lock 可能造成的浪费,我不太明白何以造成这样的结果。难道 是源于 CPU L1 Cache 的利用率不同?接下来我想花点时间仔细研究一下为什么。


最终我们的压力测试结果还是很让人满意的。我们配置有 64G 内存 6 core * 2 的服务器 6 台,可以轻松支撑 10 万用户在线,且游戏操作感觉流畅。我们单台机器的上限大约在 3 万用户(受限于内存),远远超出一开始的设计容量(之前我们希望可以做到单台机器 1 万用户就可以了)当然实际情况要等游戏上线才能明确了。

December 09, 2013

Skynet 的服务监控及远程调用

基于 Actor 模式的框架,比较难解决的问题是当一个 actor 异常退出后如何善后的问题。

Erlang 的做法简单粗暴,它提供了 spawn_link 方法 , 当一个 process (Erlang 的 Actor) 退出后,可以把和它关联的 process 也同时退出。

在 skynet 中,一开始我并不想在底层解决这个问题。我希望所有的 service 都是稳定的。如果一个 service 可能中途退出,那么在上层协调好这个关系。

而且 skynet 借助 lua 的 coroutine 机制,事实上在同一个 lua service 里跑着多个 actor 。一个 lua coroutine 才是一个 actor 。粗暴的将几个 service 在底层绑定生命期不太合适。

但是,如果有 service 有中途退出的可能,那么利用 skynet.call 调用上面的远程方法就变得不可靠。简单的用 timeout 来解决我认为只是回避了问题,而且会带来更多的复杂性。这是我在设计 skynet 时就想避免的。所以我在处理 service 生命期监控的问题上,做的比较谨慎。


前段时间我在 skynet 底层加上了 monitor 机制,可以把服务退出的消息从框架层抛出来,让上层逻辑可以感知到。但我并不想固定处理服务退出事件的逻辑,所以加了一个 skynet.monitor 方法把一个特殊服务注册入框架,让它全权负责处理这类消息。

btw, 这里我刻意回避了使用命名服务的机制,没有把 monitor 起一个特定名字。(例如:早期我就给 launcher 起了名字。)这是因为我觉得具名服务不应该在框架底层实现,而应该放在上层机制中。目前 skynet 支持的名字服务仅仅是出于历史兼容原因存在。新增加的代码我也不想再依赖这个特性了。

同时,我写了一个简单的 monitor 做为示范。在我们自己的项目中,对应的服务逻辑要复杂的多。


显然,光有 monitor 机制仅仅提供了解决问题的可能,把 skynet.call 做的更完备是不够的。今天的 skynet 更新 patch 中,我尝试完善了一下。

首先,增加了 skynet.watch 这个 api ,用于显式关注一些可能不稳定的服务。我不想给原有的 skynet.call 加太多的负担,毕竟大多数服务都是稳定不会退出的。(一旦意外退出,整个系统都会不可用,基于保持运行也意义不大)

skynet.watch 会向 monitor 汇报,当 monitor 发现服务消失的那一刻,将发消息通知。

skynet.call 在调用一个被 watch 的服务上的方法时,把 session/service 记录在一张表里。当收到 monitor 的反馈消息时,将从表中找到需要恢复的 coroutine ,把它唤醒并抛出异常。这样,skynet.call 就不会因为调用服务在处理远程请求过程中异常退出而无法返回的情况了。


目前还没有实现异常传递的机制。也就是当一个 coroutine 发生异常,而它又是被远程调用的方法,那么最好是能把这个异常传递回去。这套机制在我们的项目中有实现,但我还没有想好怎样合并到 github 的 skynet 核心代码树上。

12 月 10 日补充:

想了一下, 发现异常传播可以利用上面的通道进行,只需要增加几行代码即可. 所以今天的 patch 把这个功能加上了。这样,一但 skynet.call 调用的远程方法发生了异常后,调用者这边也会同样抛出一个异常,而不是挂起。