« 记录一个并发引起的 bug | 返回首页 | Skynet 设计综述 »

开发笔记(25) : 改进的 RPC

自从动了重新实现 skynet 的念头,最近忙的跟狗一样。每天 10 点醒来就忙着写代码,一句废话都不想说,一直到晚上 11 点回家睡觉。连续干了快一个月了。

到昨天,终于把全部代码基本移植到了新框架下,正常启动了起来。这项工作算是搞一段落。庆幸的是,我这个月的工作,并没有影响到其他人对游戏逻辑的开发。只是我单方面的同步不断新增的逻辑逻辑代码。

Skynet 的重写,实际上在半个月前就已经完成。那时,已经可以用新的服务器承载原有的独立的用户认证系统了。那么后半个月的这些琐碎工作,其实都是在移植那些游戏逻辑代码。

在 Skynet 原始设计的时候,api 是比较简洁的,原则上讲,是可以透明替换。但实际上,在使用中,增加了许多阴暗角落。一些接口层的小变动,增加的隐式特性,使得并不能百分百兼容。另外,原来的一些通讯协议和约定不算太合理,在重新制作时,我换掉了部分的方案,但需要编写一个兼容的链路层。

比如:以前,我们把通过 tcp 接入的 client 和 server 内部同进程内的服务等同处理。认为它们都是通过相同的二进制数据包协议通讯。但是,同进程内的服务间通讯明显是可以被优化的,他们可以通过 C 结构而不是被编码过的数据包交换信息,并可以做到由发起请求方分配内存,接受方释放内存,减少无谓的数据复制。在老的版本中,强行把两者统一了起来,失去了许多优化空间。在新版本里,我增加了较少的约定,修改了一点接口,就大幅度提升了进程内服务间信息交换的效率。

另一方面,一旦固定采用单进程多线程方案,之前的多进程共享数据的模块就显得过于厚重了。新的方案更为轻量,也更适合 lua 使用。这项工作在上一篇 blog 中提到过。这和 skynet 的重写原本是两件事情,但我强行放在一起做迁移,增加了许多难度。但考虑到,原本我就需要梳理一次我们的全部服务器端代码(包括大量我没有 review 过的),就把这两件事情同时做了。

在这个过程中,可以剔除许多冗余代码,去掉一些我们曾经以为会用到,到实际废弃的模块。彻底解决一些历史变更引起的问题。过程很痛苦,但很值得。新写的代码各种类型检查更严格,就此发现了老的逻辑层代码中许多隐藏的 bug 。一些原有用 erlang 实现的模块,重新用 lua 实现了一遍,混合太多语言做开发,一些很疼的地方,经历过的人自然清楚。以后如非必要,尽量不用 lua 之外的语言往这个系统里增加组件了。

btw, 新系统还没有经过压力测试。一些优化工作也没有展开。但初步看起来,还是卓有成效的。至少,改进了数据共享模块,以及提出许多冗余后,整个系统的内存占用量下降到原来的 1/5 不到。CPU 占用率也有大幅度的下降。当然,这几乎不关 C 还是 Erlang 做开发的事,重点得益于经过半年的需求总结,以及我梳理了大部分模块后做的整体改进。


今天想重点谈谈下面一段时间我希望做的改进。是关于服务间 RPC 的。

目前能找到年初的一篇记录 ,经过大半年的演化,已经不完全是记录的那个样子了。但大体上的思路一直沿用着。

这个方案的优点在于,使用通用的 google proto buffer 协议做严格的 RPC 协议定义。但缺点也是很明显的,最麻烦的地方在于,在一个需要大量服务间交互的应用环境内,新实现一组 RPC 需要做大量的工作,这对程序员是一个负担。

程序员需要:一,在一个协议描述文件中,定义出协议名以及对应的协议号;二,在对应名字的 proto buffer 文件中,定义出协议对应的输入参数和输出参数列表;三,在特定的位置,创建特定名字的 lua 源文件,在里面实现特定名字的协议过程。

有时,这个流程是好事,它能够在 lua 这种弱类型系统中,辅助检查出潜在的错误;但对开发效率的影响也是显著的。同时,这个长长的流程,以及对应的各种编解码工作,也引起了部分性能损失。

我希望在以后的系统中,引入更为方便简洁的 RPC 机制。

简单的说,我的最终需求是:程序员不需要为 RPC 调用和本地调用写不同的代码。程序员需要心里了解一次调用是远程调用,但他不必为实现这些方法做额外的事情。在他不需要把一些方法定义为远程方法时,只需要把源文件换个位置,或是重新组织一下代码加载的过程,就可以轻松的完成。远程对象和本地对象对调用者来说,也应该尽可能的透明。

我今天把这个想法基本实现了。

经过重写 skynet ,我对这类事务的处理稍微建立了一点模式。首先,应该把同进程内的服务间 RPC 调用同跨进程的调用区分开。

跨进程(跨机)调用,可以通过增加一个特定的服务,在链路层把它们接起来即可。应该专心实现同进程内,不同服务间的高性能 RPC 才是重点。等这一步完成,只需要为异地对象建立一个本地副本做消息中转就够了。

我们应该专心考虑 Lua State 实现的服务,而不必过于考虑不同语言间的 RPC 调用。一起以 Lua 为主语言来考虑功能。

首先我引入了之前实现好的 Lua 数据序列化模块 。并对它做了一些改动 ,合并到 skynet 项目中。

这个改动是,让序列化模块底层理解远程对象。增加了一个远程对象类型。因为所有的 Lua State 其实是在一个系统进程内的,数据交换工作通过 Lua 的 C 扩展库就可以完成。在同个进程内,每个远程对象都有唯一的数字 id 。传递远程对象,只需要传递这个 id 即可。

每个 Lua State 中,维护一张表,记录所有在这个 State 中创建出来的远程对象,以及对应的 id 。在序列化模块中,一旦发现提到的远程对象是自己进程内的,就自动翻译成本地对象。

序列化模块本身不处理任何远程对象的特殊行为。它把这个工作交给外部注入。skynet 模块把远程调用的方法注入到远程对象中。

所谓 RPC 调用,就是一个远程对象加一个方法名,加上若干参数。通过序列化方法,打包成一个数据包,查询到远程对象所在的服务地址,发送过去即可。

而所谓远程对象,其实只是在 lua table 里设置一个叫 __remote 的字段,设入数字 handle 就可以让序列化模块识别了。

这个模块并不复杂,我实现在了 skynet.lua 中,不到 100 行代码。

作为一个范例,我实现了一个叫 root 的远程对象,让它在一个独立服务 中启动。它可以提供一个基本的名字服务。在一个简单的 test 程序 中,我们可以看到一个远程对象把自己注册到 root 里,别的服务从 root 拿到这个对象,就可以向本地对象一样调用上面的方法了。

Comments

灵活与不安全共存,不知道外挂检测的代价有多大?

RPC本来就是remote,没有必要再把inner-proc的概念归并到RPC,remote的数据串化肯定要值拷贝。逻辑多线程并发服务器,并发的是IO以及针对所有逻辑实体的IO触发,而针对单个逻辑实体可以采取顺序执行(当前只能被一根线程执行)。这也就是erlang里的概念吧,Actor Model。

一定要用RPC么,见识过的项目代码都木有用到,去开发这个是不是有点儿奢侈?

越来月看不动

年初的时候看到描述的rpc,当时觉得为每个rpc调用都定义一个proto buffer的协议,是为支持跨语言的rpc调用。

我是觉得如果rpc只在lua里面进行使用,那只需要定义一个对lua数据类型描述的协议就行了,这样对rpc的编写者/调用者来说都是透明的。

我也感觉profbuf实现起来有些麻烦,以及效率问题,所以我的RPC方案就是序列化+类型反射

一直排斥将rpc和本地调用透明化,说不上理由,只是觉得味道不对吧。

个人觉得rpc主要的设计要点很简单,第一个是网络io与应用层分离,这样多个应用逻辑可以复用io(socket),第二个是协议分层设计,底层协议需要严格的变动很小的逻辑,上层应用层就可以灵活多变了。
我觉得你没有关注第二点,没做分层,只在严格-宽松之间进行了衡量,容易两极化。

什么时候,终结者病毒会自主产生呢

Post a comment

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