百万在线:大型游戏服务端开发
上QQ阅读APP看书,第一时间看更新

2.8 使用节点集群建立分布式系统

一台物理机的承载量有限,现代服务端都采用分布式服务模式。Skynet提供了cluster集群模式,可让不同节点中的服务相互通信。

2.8.1 功能需求

此处的需求是将2.7节的ping程序改成分布式。如图2-32所示,先在节点2开启两个ping服务(ping1和ping2),然后再开启另一个ping服务(ping3),让ping1和ping2分别向ping3发送消息,ping3给予回应,如此往复。

图2-32 分布式ping

2.8.2 学习集群模块

图2-33展示了Skynet的cluster集群模式。在该模式中,用户需为每个节点配置cluster监听端口(即图中的7001和7002),Skynet会自动开启gate、clusterd等多个服务,用于处理节点间通信功能。假如图2-33的ping1要发送消息给另一个节点的ping3,流程是节点1先和节点2建立TCP连接,消息经由Skynet传送至节点2的clusterd服务,再由clusterd转发给节点内的ping3。

图2-33 cluster集群示意图

skynet.cluster模块提供节点间通信的API如表2-8所示。

表2-8 cluster集群的API

更多API参见https://github.com/cloudwu/skynet/wiki/Cluster。从图2-33也可看出,节点间通信有着较大的代价,不仅消息传递速度慢,安全性也得不到保障(如某个节点突然挂掉)。从Skynet的特点来看,如果CPU运算能力不足,选用更多核心的机器远比增加物理机性价比高。切记,任何企图抹平服务运行位置差异的设计都需要慎重考虑。

2.8.3 节点配置

本节程序会开启两个节点,意味着需要用到两份节点配置文件。复制两份配置模板(2.2.2节),分别命名为Pconfig.c1和Pconfig.c2,将主服务改为“Pmain”,再添加node这一项,指定节点名称。

examples/Pconfig.c1中新增的内容如下:


node= "node1"

examples/Pconfig.c2中新增的内容如下:


node= "node2"

2.8.4 代码实现

1.主服务

每个节点都是从主服务开始运行的,主服务负责节点初始化并开启其他服务。对照图2-32来看,节点1开启了两个ping服务,节点2开启了另外一个ping服务。

代码2-12展示了主服务的代码写法,在执行cluster.reload之后,主服务先判断当前的节点名称(mynode,skynet.getenv表示从节点配置中读取项目),如果是节点1则进入“mynode=="node1"”的分支,否则进入另一分支。每个节点都会调用cluster.open开启集群监听。

如果是节点1,主服务会开启ping1和ping2这两个服务,然后通过skynet.send发送start指令。由于是分布式程序,因此相比于2.3节的程序,传递的参数要增加一个,代表让ping1和ping2向node2节点的pong服务发送消息。如果是节点2,则开启一个ping服务,并用skynet.name把它命名为“pong”。

代码2-12 examples/Pmain.lua

(资源:Chapter2/6_cluster_main.lua)


local skynet = require "skynet"
local cluster = require "skynet.cluster"
require "skynet.manager"
 
skynet.start(function()
    cluster.reload({
        node1 = "127.0.0.1:7001",
        node2 = "127.0.0.1:7002"
    })
    local mynode = skynet.getenv("node")

    if mynode == "node1" then
        cluster.open("node1")
        local ping1 = skynet.newservice("ping")
        local ping2 = skynet.newservice("ping")
        skynet.send(ping1, "lua", "start", "node2", "pong")
        skynet.send(ping2, "lua", "start", "node2", "pong")
    elseif mynode == "node2" then
        cluster.open("node2")
        local ping3 = skynet.newservice("ping")
        skynet.name("pong", ping3)
    end
end)

2.ping服务

集群的ping服务与2.3节的ping服务相似,程序结构如代码2-13所示。变量mynode保存节点名称(如“node1”)。

代码2-13 examples/ping.lua的程序结构

(资源:Chapter2/6_cluster_ping.lua)


local skynet = require "skynet"
local cluster = require "skynet.cluster"
local mynode = skynet.getenv("node")

local CMD = {}
skynet.start(function()
    ...... 略
end)

ping服务包含ping和start这两个消息处理方法,如代码2-14所示。

在start方法中,参数source代表消息源,target_node和target分别代表目标服务的节点和地址,由主服务传入。然后通过cluster.send向target_node节点的target服务发送名为ping的消息,这里带有3个参数,其中mynode和skynet.self()代表自己所在的节点和地址,“1”是一个计数值。

在ping方法中,参数source_node、source_srv和count分别对应start方法的3个参数,前两个参数代表消息发送方的节点、地址,最后一个参数count代表计数值。最后,通过cluster.send给发送方回应消息,并把计数值加1。

代码2-14 examples/ping.lua中的部分内容


function CMD.ping(source, source_node, source_srv, count)
    local id = skynet.self()
    skynet.error("["..id.."] recv ping count="..count)
    skynet.sleep(100)
    clus ter.send(source_node, source_srv,  "ping",  mynode,  skynet.
self(), count+1)
end

function CMD.start(source, target_node, target)
    cluster.send(target_node, target, "ping", mynode, skynet.self(), 1)
end

2.8.5 运行结果

先开启节点2,再开启节点1。节点2的运行结果如图2-34所示,它会打印出“recv ping count=xxx”,由于节点1的两个ping服务都会向节点2发送消息,因此同一计数值会出现两次。节点1的运行结果如图2-35所示,两个服务分别收到节点2的回应。

图2-34 节点2的运行结果

图2-35 节点1的运行结果

2.8.6 使用代理

代码2-15展示的是代理的使用方法,先将节点2的pong服务作为代理(变量pong),之后便可以将它视为本地服务,在此方法中通过skynet.send或skynet.call发送消息。

代码2-15 examples/Pmain.lua中的重要内容


    if mynode == "node1" then
        cluster.open("node1")
        local ping1 = skynet.newservice("ping")
        local ping2 = skynet.newservice("ping")
        local pong = cluster.proxy("node2", "pong")
        skynet.send(pong, "lua", "ping", "node1", "ping1", 10)