skynet集群 --- cluster 模式
skynet本身解决的核心问题是充分利用同一台机器的多核的处理能力。云风在描述集群时,强调说skynet只提供了构建集群的组件。那是因为不是所有项目遇到的问题都能够用统一的解决方案的。还提出任何企图抹平服务运行位置差异的设计都需要慎重考虑,很可能存在设计问题,因为集群协作不与单机多服务工作,集群中可能对方的服务并未启动,而单机工作中,可以认为不会只有一部分出错(即不会说当前功能正在运行,但是本机目标服务未启动的情况)。
skynet自带两种集群基础架构,我们接下来要拜读的是cluster 方式。
相对于master/slave 模式,cluster模式有较大的弹性。它不需要配置harbor_id,harbor_id需要填0,那么就意味着,集群节点不限制于256个。
跟master/slave 不同,cluster的配置需要在每台机子上(或者缓存到redis中,每台机子获取到的配置表需要一致)配置,需要新建一份文件,如skynet示例,又或者在cluster.reload时传入一个table配置
1 __nowaiting = true -- If you turn this flag off, cluster.call would block when node name is absent 2 3 db = "127.0.0.1:2528" 4 db2 = "127.0.0.1:2529"
1 -- cluster1.lua 2 skynet.start(function() 3 cluster.reload { 4 db = "127.0.0.1:2528", 5 db2 = "127.0.0.1:2529", 6 } 7 ... 8 end)
接下来我们主要看几个问题:集群的启动过程,cluster.send()与cluster.call()的执行过程。
cluster集群的启动过程
当我们需要使用cluster模式时,我们必须要在一个地方进行 require "cluster",可以在main里,也可以在项目中的负载均衡里等等地方。当require "cluster"时,会执行如下代码:
1 -- cluster.lua 2 skynet.init(function() 3 clusterd = skynet.uniqueservice("clusterd") 4 end)
在init中,会启动一个 clusterd的服务,用来对集群的维护,其实cluster.lua只是对集群操作的一个浅封装。
在require启动完clusterd服务后,需要进行 cluster.reload({})的操作,对集群中的机子进行初始化,并且启动当前节点对配置表中的机子的 clustersender 发送服务,也就是说当前节点会有n个集群节点的发送服务(有多少个集群节点,就有多少个服务),当reload完成后,需要对各节点的监听操作(cluster.open "name"),这里cluster.open的主要操作是生成一个对应目标机子的gateserver并监听端口,如果有消息触发gateserver,则表示对应的监听机子有消息过来,如果是对方连接过来,则会生成一个对应的clusteragent(接收消息服务):
1 -- cluster1.lua 2 skynet.start(function() 3 cluster.reload { 4 db = "127.0.0.1:2528", 5 db2 = "127.0.0.1:2529", 6 } 7 ... 8 cluster.open "db" 9 cluster.open "db2" 10 ... 11 end)
首先看下对集群节点发送消息的服务的创建与其功能:

1 -- clustersender.lua 2 skynet.start(function() 3 channel = sc.channel { 4 host = init_host, 5 port = tonumber(init_port), 6 response = read_response, 7 nodelay = true, 8 } 9 skynet.dispatch("lua", function(session , source, cmd, ...) 10 local f = assert(command[cmd]) 11 f(...) 12 end) 13 end)
注意,此时只是创建了clustersender的服务,并没有与对应的机子进行open操作,实际上与对应节点建立连接是在对节点进行通讯的时候才会去判断是否已经连接成功,如果没有连接则进行连接,这里每次通讯都会去判断是因为连接是tcp的,可能会中断。在创建服务时,可以看到对socketchannel 的对象进行了初始化,host与port是必须要初始化的,这里的response进行赋值是为了告诉channel是使用哪种通信逻辑,具体的socketchannel支持的通信逻辑,可查看官方wiki:SocketChannel · cloudwu/skynet Wiki · GitHub。
同理,对节点进行接收消息的 clusteragent 服务的创建大同小异。
现在,对每个节点都创建了 clustersender, clusteragent 两个服务后,我们看下cluster.call的执行过程是怎样的。
cluster.call 的执行过程
当执行cluster.call时,首先先访问当前缓存是否有对应节点的发送服务信息,如果没有先将此次消息插入到节点的消息队列中,然后会跟clusterd获取对应clustersender,如果对应服务未启动,那么会触发连接去新建一个tcp连接的服务 ,如果创建失败则会将所有消息丢弃掉,如果服务创建完成,则会对所有消息进行依次执行,如果本身已经有缓存信息,则直接给发送服务发送 req 命令:

1 -- cluster.lua 2 function cluster.call(node, address, ...) 3 -- skynet.pack(...) will free by cluster.core.packrequest 4 local s = sender[node] 5 if not s then 6 local task = skynet.packstring(address, ...) 7 return skynet.call(get_sender(node), "lua", "req", repack(skynet.unpack(task))) 8 end 9 return skynet.call(s, "lua", "req", address, skynet.pack(...)) 10 end
clustersender 发送消息服务接收req命令并执行操作,首先会对消息进行编码,然后才执行发送操作:

1 -- clustersender.lua 2 local function send_request(addr, msg, sz) 3 -- msg is a local pointer, cluster.packrequest will free it 4 local current_session = session 5 local request, new_session, padding = cluster.packrequest(addr, session, msg, sz) -- 对消息进行打包,padding是区分消息是否为长消息,大于32k为长消息 6 ... 7 return channel:request(request, current_session, padding) -- 对消息发送 8 end 9 10 function command.req(...) 11 local ok, msg = pcall(send_request, ...) 12 ... 13 end
chanel:request 函数,主要是对消息的发送,然后区分使用的模式是否为一问一答,还是不要求每一个请求都有一个回应进行,但是两种模式都会触发等待消息的情况:

1 -- socketsender.lua 2 local function push_response(self, response, co) 3 if self.__response then 4 -- response is session 5 self.__thread[response] = co 6 else 7 -- response is a function, push it to __request 8 table.insert(self.__request, response) 9 table.insert(self.__thread, co) 10 if self.__wait_response then 11 skynet.wakeup(self.__wait_response) 12 self.__wait_response = nil 13 end 14 end 15 end 16 17 local function wait_for_response(self, response) 18 local co = coroutine.running() 19 push_response(self, response, co) 20 skynet.wait(co) 21 ... 22 end 23 24 function channel:request(request, response, padding) 25 assert(block_connect(self, true)) -- connect once 26 local fd = self.__sock[1] 27 ... 28 return wait_for_response(self, response) 29 end
执行channel:request 会挂起当前协程等待消息(而channel在connect时就会fork一个协程进行消息的分发。),对应节点会在clusteragent服务接收到消息并且执行,然后将消息原路返回给原节点。
节点收到消息回应,如果是dispatch_by_session则会根据session获取到对应的协程然后唤起,使得之前的wait_for_response协程进行消息并且返回消息给对应的调用者。

1 -- socketchannel.lua 2 local function connect_once(self) 3 ... 4 self.__dispatch_thread = skynet.fork(function() 5 pcall(dispatch_function(self), self) 6 -- clear dispatch_thread 7 self.__dispatch_thread = nil 8 end) 9 ... 10 end
以上则是cluster.call的调用过程。
cluster.send 的执行过程
相对于cluster.call而言,send的主要区别则是在channel:request时会直接返回函数,不会挂起协程进行等待。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)