skynet:cluster master/slave 模式(局域网) cluster 模式

skynet跟mq扮演的角色类似,每个skynet进程维护了一个MQ,会dispatch msg到每个skynet_context的私有mq。有skynet就没必要再在自己项目里引入MQ了。

skynet 支持两种集群模式。


当单台机器的处理能力达到极限后,可以考虑通过内置的 master/slave 机制来扩展。具体的配置方法见 Config 。

集群服务用到的配置项:

  • cluster 它决定了集群配置文件的路径。
  • standalone 如果把这个 skynet 进程作为主进程启动(skynet 可以由分布在多台机器上的多个进程构成网络),那么需要配置standalone 这一项,表示这个进程是主节点,它需要开启一个控制中心,监听一个端口,让其它节点接入。
  • master 指定 skynet 控制中心的地址和端口,如果你配置了 standalone 项,那么这一项通常和 standalone 相同。

每个 skynet 进程都是一个 slave 节点。但其中一个 slave 节点可以通过配置 standalone 来多启动一个 cmaster 服务,用来协调 slave 组网。对于每个 slave 节点,都内置一个 harbor 服务用于和其它 slave 节点通讯。

每个 skynet 服务都有一个全网唯一的地址,这个地址是一个 32bit 数字,其高 8bit 标识着它所属 slave 的号码。即 harbor id 。在 master/slave 网络中,id 为 0 是保留的。所以最多可以有 255 个 slave 节点。

在 master/slave 模式中,节点内的消息通讯和节点间的通讯是透明的。skynet 核心会根据目的地址的 harbor id 来决定是直接投递消息,还是把消息转发给 harbor 服务。

不要把这个模式用于跨机房的组网。所有 slave 节点都应该在同一局域网内(最好在同一交换机下)。不应该把系统设计成可以任意上线或下线 slave 的模式。

slave 的组网机制也限制了这一点。如果一个 slave 意外退出网络,这个 harbor id 就被废弃,不可再使用。这样是为了防止网络中其它服务还持有这个断开的 slave 上的服务地址;而一个新的进程以相同的 harbor id 接入时,是无法保证旧地址和新地址不重复的。

cluster 模式


cluster 模块,它大部分用 lua 编写,只有通讯协议处理的部分涉及一个很小的 C 模块。

它的工作原理是这样的:

在每个 skynet 节点(单个进程)内,启动一个叫 clusterd 的服务。所有需要跨进程的消息投递都先把消息投递到这个服务上,再由它来转发到网络。

要使用它之前,你需要编写一个 cluster 配置文件,配置集群内所有节点的名字和对应的监听端口。并将这个文件事先部署到所有节点

db = "127.0.0.1:2528"

接下来,你需要在 db 的启动脚本里写上 cluster.open "db"

有两种方式可以访问到这个节点

  1. 可以通过 cluster.call(nodename, service, ...) 提起请求。这里 nodename 就是在配置表中给出的节点名。service 可以是一个字符串,或者直接是一个数字地址(如果你能从其它渠道获得地址的话)。当 service 是一个字符串时,只需要是那个节点可以见到的服务别名,可以是全局名或本地名。但更推荐是 . 开头的本地名,因为使用 cluster 模式时,似乎没有特别的理由还需要在那个节点上使用 master/slave 的架构(全局名也就没有特别的意义)。cluster.call 有可能因为 cluster 间连接不稳定而抛出 error 。但一旦因为 cluster 间连接断开而抛出 error 后,下一次调用前 cluster 间会尝试重新建立连接。

  2. 可以通过 cluster.proxy(nodename, service) 生成一个本地代理。之后,就可以像访问一个本地服务一样,和这个远程服务通讯。但向这个代理服务 send 消息,有可能因为 cluster 间的连接不稳定而丢失。详见 cluster.send 的说明。

  3. 如果想单向推送消息,可以调用 cluster.send(nodename, service, ...) 。但注意,跨越节点推送消息有丢失消息的风险。因为 cluster 基于 tcp 连接,当 cluster 间的连接断开,cluster.send 的消息就可能丢失。而这个函数会立刻返回,所以调用者没有机会知道发送出错。

Cluster 是去中心化的,所以需要在每台机器上都放置一份配置文件(通常是相同的)。通过调用 cluster.reload 可以让本进程重新加载配置。如果你修改了每个节点名字对应的地址,那么 reload 之后的请求都会发到新的地址。而之前没有收到回应的请求还是会在老地址上等待。如果你老的地址已经无效(通常是主动关闭了进程)那么请求方会收到一个错误。

某个节点配置多个通道

在skynet框架中使用cluster模式,经常有消息在节点之间传递。大部分情况,我们在节点A和节点B之间只需要建立一个连接通道,但是在有些时候我们希望让一些比较独立的业务能占用一条单独的通道进行处理,不希望跟到正常的业务逻辑去抢通道资源。这个时候,我们就需要为某个节点配置多个通道了。

比如,我们要在节点A中再开辟一条连接连通节点B的通道,由于一条通道就是一条tcp连接,所以我们需要为节点B再配置一个端口。我们打开集群的cluster配置文件,添加一个节点B的记录,新分配一个端口:

nodea = "127.0.0.1:50653"
nodeb "127.0.0.1:50654"
nodeb2"127.0.0.1:50655"

 然后重新启动节点,在节点B的启动脚本中,我们也需要在集群中打开nodeb2:

cluster.open("nodeb2")

然后,在节点A中,我们就可以进行跨节点访问了,这个时候,我们可以分别用nodeb和nodeb2进行访问,框架将使用2条tcp通道进行分别处理。你也可以使用netsta命令,查看nodeb和nodeb2的连接情况。

cluster.call("nodeb",".main","xxxxxx")
cluster.call("nodeb2",".main","xxxxxx")

关于cluster的实现

为什么要cluster:

除非你的业务本来就是偏重 IO 的,也就是你根本不打算利用单台硬件的多核心优势来增强计算力,抹平本机和网络的差异是没有意义的。无论硬件怎样发展,你都不可能看到主板上的总线带宽和 TCP 网络的带宽工作在同一数量级的那一天,因为这是物理基本规律决定的。

当你的业务需要高计算力,把 actor 放在一台机器上才可以正常的发挥 CPU 能力去合作;如果你的系统又需要分布式扩展,那么一定是有很多组独立无关的业务可以平行处理。这两类工作必须由构架系统的人自己想清楚,规划好怎么部署这些 actor ,而不可能随手把 actor 扔在分布式系统中,随便挑台硬件运行就够了。

恰巧网络游戏服务就是这种业务类型。多组服务器、多个游戏场景之间交互很弱,但其中的个体又需要很强的计算力。这就是 skynet 切合的应用场景。

实现:

skynet 的核心层之上,设计了 cluster 模块。它大部分用 lua 编写,只有通讯协议处理的部分涉及一个很小的 C 模块。用 Lua 编写可以提高系统的可维护性,和网络通讯的带宽相比,Lua 相对 C 在处理数据包的性能降低是微不足道的。

在每个 skynet 节点(单个进程)内,启动一个叫 clusterd 的服务。所有需要跨进程的消息投递都先把消息投递到这个服务上,再由它来转发到网络。

参考:

GettingStarted

skynet_cluster