Skynet 集群及 RPC
这几天老在开会,断断续续的拖慢了开发进度。直到今天才把 Skynet 的集群部分,以及 RPC 协议设计实现完。
先谈谈集群的设计。
最终,我们希望整个 skynet 系统可以部署到多台物理机上。这样,单进程的 skynet 节点是不够满足需求的。我希望 skynet 单节点是围绕单进程运作的,这样服务间才可以以接近零成本的交换数据。这样,进程和进程间(通常部署到不同的物理机上)通讯就做成一个比较外围的设置就好了。
为了定位方便,我希望整个系统里,所有服务节点都有唯一 id 。那么最简单的方案就是限制有限的机器数量、同时设置中心服务器来协调。我用 32bit 的 id 来标识 skynet 上的服务节点。其中高 8 位是机器标识,低 24 位是同一台机器上的服务节点 id 。我们用简单的判断算法就可以知道一个 id 是远程 id 还是本地 id (只需要比较高 8 位就可以了)。
我设计了一台 master 中心服务器用来同步机器信息。把每个 skynet 进程上用于和其他机器通讯的部件称为 Harbor 。每个 skynet 进程有一个 harbor id 为 1 到 255 (保留 0 给系统内部用)。在每个 skynet 进程启动时,向 master 机器汇报自己的 harbor id 。一旦冲突,则禁止连入。
master 服务其实就是一个简单的内存 key-value 数据库。数字 key 对应的 value 正是 harbor 的通讯地址。另外,支持了拥有全局名字的服务,也依靠 master 机器同步。比如,你可以从某台 skynet 节点注册一个叫 DATABASE 的服务节点,它只要将 DATABASE 和节点 id 的对应关系通知 master 机器,就可以依靠 master 机器同步给所有注册入网络的 skynet 节点。
master 做的事情很简单,其实就是回应名字的查询,以及在更新名字后,同步给网络中所有的机器。
skynet 节点,通过 master ,认识网络中所有其它 skynet 节点。它们相互一一建立单向通讯通道。也就是说,如果一共有 100 个 skynet 节点,在它们启动完毕后,会建立起 1 万条通讯通道。
为了缩短开发时间,我利用了 zeromq 来做 harbor 间通讯,以及 master 的开发。蜗牛同学觉得更高效的做法是自己用 C 来写,并和原有的 gate 的 epoll 循环合并起来。我觉得有一定道理,但是还是先给出一个快速的实现更好。
我们的早一个 Erlang 版本,把 client 也看成了 skynet 系统中的特殊节点。这次看来,我认为是不必要的设计。
如果在同一个进程内,通讯和包转发足够廉价的话,完全没必要为统一这种特殊性而多做太多工作。所以在这次新实现中,client 被看成是 gate 这个服务才了解的细节。由 gate 收集 client 的数据流,并转发到内部的其它服务上。同时,我为发送数据单独启动了一类服务。为每个接入 gate 的 client 动态生成一个节点。只要向这个节点发送数据,都加上和 client 间约定的打包协议的包头转发给 client 。
把 client 独立出来,不当作是内部节点处理,可以使我们能专心 RPC 的问题。
skynet 的内部节点之间,有很大程度是请求回应模式的消息传递模式。这种请求回应模式,可以是 RPC 请求,也可以是一些更简单的通讯协议。
在前一个版本中,我们认为,skynet 只需要解决后消息包,如何有序的,从一个节点传输到另一个节点就够了。之后的细节是下一个层次考虑的问题。可是做下去我们发现,不同的服务间如果想协同工作,必须约定一些基本的通讯协议。每个服务使用独立不相同的通讯协议几乎是不可能的。这是因为,每个服务节点只有单一的输入消息源。虽然我们可以识别消息的来源地址,但根据来源地址来区分消息协议种类是不可能的事情。
结果,我们采用了 google proto buffer 。消息包必须用 protobuffer 打包,并有统一的一级结构。这反而是整个设计不那么简洁了。
这次,我归纳了这半年来的使用需求。发现,skynet 不应只处理单个包从某点发送到另一点的任务。既然我们不抽象出连接这个概念,那么就至少需要让 skynet 框架了解怎样回应一个特定的包。
所以,最终我把一个 31 位的 session id 放到了底层。每个服务节点内部都维护了一个单调递增的 session id 记数器。一旦它需要时,可以给它发出的消息携带一个唯一的 id ;同时约定,接收到这个包并加以处理的节点,如果想针对这个消息包做回应,它就应该把这个 session id 发送回来。
为了区分请求包和回应包。约定,请求包的 session id 为负数,回应包的 session id 为正数。不需要回应的包,可以用 0 做 session id 。
这样,我们就可以利用收到的 session id 做数据包的有限分发了。这并没有增加太多的协议上的约定,每个服务可以按自己喜爱的方式设计协议。它们可以要求请求它的服务的人按自己的协议发送请求,它可以正确的回应。同时,它需要使用其它远程服务时,则按对方服务的协议来通讯。
接下来,在 lua 的封装层做很少量的工作,就可以让这套机制运转起来了。也就是根据收到的 session id 做一下分发。利用 coroutine ,做远程请求时,记录下产生的 session id ,yield 出来,把线程挂起;待到收到携带有相同 session id 的反馈包,把挂起的线程唤醒即可。
我写了一个简单的 key-value 设置和查询的内存数据库作为范例。起名叫 SIMPLEDB ,可以从 client 发起查询请求:“ GET xxx ”,或是更新请求:“ SET xxx yyy ”。agent 收到请求后,会转发到 SIMPLEDB ,并把结果反馈给 client 。有兴趣的同学可以看看相关的 lua 代码。
当然,lua 在整套系统中并不是必备设施。如果你愿意,也可以写出相同功能的其它动态语言(例如 python )的对接模块来。
建议继续学习:
- HBase技术介绍 (阅读:6783)
- hadoop rpc机制 && 将avro引入hadoop rpc机制初探 (阅读:5107)
- Thrift简析 (阅读:4859)
- 解析Google集群资源管理系统Omega (阅读:4613)
- 快速构建实时抓取集群 (阅读:4383)
- Hadoop集群间Hadoop方案探讨 (阅读:3748)
- “集群和负载均衡”的通俗版解释 (阅读:3551)
- storm集群的监控 (阅读:3393)
- “集群和负载均衡”在实战当中的运用技巧 (阅读:3400)
- Linux下的NFS (阅读:3186)
扫一扫订阅我的微信号:IT技术博客大学习
- 作者:云风的 BLOG 来源: 云风的 BLOG
- 标签: RPC Skynet 集群
- 发布时间:2012-08-07 13:36:54
- [2528] 代理的加密部分
- [1329] 创业笔记 | 从0到1开公司是什么体验
- [649] vimgtd-在vim(gvim)中实现GT
- [574] 查找第K小的元素
- [73] Oracle MTS模式下 进程地址与会话信
- [65] Go Reflect 性能
- [64] 【社会化设计】自我(self)部分――欢迎区
- [64] 如何拿下简短的域名
- [63] 图书馆的世界纪录
- [61] android 开发入门