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"skynet.start(function() local mynode = skynet.getenv("node") if mynode == "node1" then 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 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 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")