Redis , Google Protobuffer , ZeroMQ
http://blog.codingnow.com/2011/11/dev_note_3.html
这周的工作主要是写代码。
开发计划制定好后,我们便分头写代码去了。我们希望一期早点做出可以运行的东西来,一切都从简。整体的代码量并不多,如果硬拆成很多份让很多人来做的话,估计设计拆分方案,安排工作,协调每个人写的东西这些比一个人全部实现一遍的工作量还要大的多。
所以,最终就是两个人在做。怪物公司在弄客户端的东西,蜗牛同学包干了服务器。好吧,基本没我的事了,我就是那个打酱油的,好听点说,就是设计方案。当然,事情没多少,空下来的时间也可以干活。训练自己可以找到事情做,并真的做有用的事情,还是很难的。
话说回来,我们在这么一个简单的框架下,一开始确定了要采用一些现成的技术方案,即要用到 Redis , Google Protobuffer , ZeroMQ 。
Redis 和 ZeroMQ 是我最早选的,想了很久。
采用 Redis 是因为历史上,我参与的项目都没有大规模使用 SQL 数据库的传统。这和 MMO 这种特定应用有关。在 Web 开发中,面对的用户是临时的,不依赖固定连接的。你不确定用户在不在那里,你不确定同时要面对的用户有多少。你需要从小到大,采用一种可扩展容量的方案。这个时候,成熟的 SQL 数据库方案是首选。
从软件开发角度看,数据库是 MVC 模式中的 M 。以 MVC 模式解决问题,M 如何实现,采用 SQL 方案只是一种可能,绝不是唯一选择。换个角度考虑,如果是一个桌面软件,为什么大部分的 M 却没有采用数据库,而更多的是在内存中直接构建数据结构呢?性能恐怕只是一个原因,更重要的原因是面对的用户的行为不同。
为什么 MMORPG 服务器,至少在网易历史上的多款游戏,没有使用 SQL 服务做 M 。一部分原因是,网易游戏的开发源头是 Mud ,Mudos 并没有使用 SQL 作为 M ,另一部分原因是,MMORPG 面对的,同时需要服务的用户有限。而用户需要操作的数据大部分限制在用户相关的数据体内。之外的数据体非常少。及时数据总量很大,但一层索引简单(以用户 id 为索引)。每个用户都是为它持续服务,数据易变。这种行为下,从 MVC 角度看,更接近网络应用之前的软件设计。
当然你甚至可以把 M 只在逻辑上划分出来,物理上并不切换,也就是没有独立意义上的数据库服务器。这样绝对性能最高,其实只是实现了一个简单的单机游戏,允许通过网络输入多条操作流,并把行为反馈通过网络发送回去罢了。甚至比单机游戏更简单,因为没有图形控制部分。
如果程序不出问题,机房不停电,可以一直的跑下去,不用考虑数据储存问题。数据持久化不过是为了容灾罢了。把一些结构化数据持久化到硬盘上,最简洁的方案就是写操作系统层面的文件,一定比再使用一个数据库要轻量,干净的多。
有了以上背景,就不难理解,为什么我对 Redis 天生有好感。我们并没有改变设计思路,它是一直延续下来的。Redis 帮助了我们将数据服务拆分出来。当然,MMORPG 也在发展,以上提到的用户应用环境也在逐步变化,我们在软件设计上也会跟进这些变化。这些就是后话了。
ZeroMQ 呢,我是希望有一个稳健,简洁的多进程通讯方案的基础。ZeroMQ 是不错的一个。至少比 OS 的 Socket 库要实用的多。它提供了更好的模式 。这是我最为看中的。另外,这是一个 C 接口的库,容易 binding 到不同的语言下使用。
在这个问题上,蜗牛同学是反对使用 ZeroMQ 的。对他的所有反对意见,我都持保留态度。但我尊重开发人员的意愿。毕竟许多代码不会是我自己来写。蜗牛同学希望采用 Erlang + C Driver 的方式来驱动整个框架。也就是用 Erlang 来做通讯上的数据交换。其它可能采用的开发语言,都通过 Driver 的方式插入到 Erlang 的框架中去。
我个人觉得这样做的确可行,加上蜗牛同学有好几年 Erlang 开发经验,他能担负起实现框架的责任。我不是特别喜欢这个方案是因为,Erlang 这个东西还是太庞大了。我对庞大的东西天生反感。虽然以蜗牛同学的原话说,Erlang 以及他的 OTP 能写出这么多行代码来有他的必要性。我们用 C/C++(Python/Lua/Golang 等等) + ZeroMQ 实现一个拙劣的方案出来,只会漏洞百出。
我个人是不以为然的,毕竟已经做了 10 年的网络游戏,对这个领域已经很熟悉了。对于陌生领域,我们会面对许多未知的问题;但在熟悉领域,无论怎么做,方案都不可能太拙劣。只要保持最基本的简洁,我是有信心保证可以解决 MMORPG 中的各种需求的。做出来的东西也能很稳定。关键点在于,它会足够简单,能轻松的理解实现的每一行代码。
不过争论都放在一边。我的原则是,最终采用实现者自选的方案,只要它没有明显的问题。
我们最终不采用 ZeroMQ 。
Google Protobuffer 我不是很喜欢的。但采用它是多个角度妥协的结果。其实我更愿意自定义一套协议。而只是裁剪 GPB 的功能。
我认为,GPB 在最底层的协议编码定义上,做的还是不错的。改进它是多余的。作为一种协议定义,协议描述语言也算定义的不错。但只到此为止。接下来的部分就不甚满意。
GPB 协议本身,默认也是用 GPB 本身定义编码出来的。这看起来很 Cool ,但我不甚喜欢。当然所有完备的类似协议都应该有描述自己的能力。对于描述自己这件事情来说,GPB 还是稍显复杂了。
比如,如果你不借助已经有的 GPB 代码和工具,很难解析一个 GPB 协议。就好比,如果世界上第一款 C 编译器,就是最难实现的 C 编译器。因为大部分的 C 编译器是用 C 写的,实现一个 C 编译器,就陷入了先有鸡还是先有蛋的问题。
在这类问题上,据说 Lisp 比 C 要干的漂亮。不过我还是和世界上大多数程序员一样,用 C 多一些。好吧,我们还是继续用 GPB 好了。
google 在实现和使用 GPB 的时候,默认采用了一种为每个协议,生成一组代码的方式。而不是提供一套 C/C++ 库,供其它语言做 binding 。这也是我所不喜的。或许是为了性能考虑,但总觉得别扭。如果把 GPB 换成正则表达式来看,你就能理解我的心情。
现存的大多数正则表达式的实现,都是提供一组 API ,允许使用者把需要的模式以一种人类可读的串形式,编译为另一种计算机方便处理的数据结构。当你需要的时候,使用这个数据结构,交给库,就可以匹配,替换字符串了。如果默认的选择是把正则表达式编译成 C 代码,然后你用的时候再 link 到你的项目中,恐怕用的人要疯掉了。当然,生成代码这种可以带来更高的运行性能。
唔,其实这只是怎样使用 GPB 这种协议的问题,和协议定义关系不大。可惜 google 在开源之初就给出了官方的方案,引导其它语言也如法泡制,成了 GPB 的惯用法。老实说,对于 C++ ,这么做性能是不错的(其实也未必)。换到 Python 里,就非常低下了。去年我按我的思路实现了 lua 的 protobuf 解析库 ,性能可以达到和 C++ 版本差距不到一个数量级,甚至快过 java 版。
这周的剩余时间我都在写一个纯粹的 C 版的 protobuf 库,不依靠代码生成器的。希望能够作为它语言使用 GPB 的基础。别的语言只需要做 binding 就够了。这玩意挺难写,光接口设计我就改了两版。今天终于快收工了。过两天再写一篇文章专门谈谈这个问题吧。当然,还有开源。
既然在 GPB 上花了这么多功夫,当然,采用 GPB 就是最后的决定了。