由于本人对集群这块应用的不多,现只对其内部调用机制作简单的剖析。
集群源码包括两个文件cluster.lua和clusterd.lua。cluster.lua其实是对clustered.lua的api的封装。我们用到集群时只需要使用cluster.lua的api即可。
使用集群时必须引用cluster.lua文件,他会单例启用clusterd服务。而clusterd服务最开始就会调用loadconfig()加载配置。
配置的项为cluster= './path/clustername.txt',配置文件形如:
db = "192.168.0.183:2528"
db2 = "192.168.0.136:2529"
将配置表解析为名称对应节点地址端口号表。
要想使用集群,首先得打开集群的地址,即监听集群的地址端口号。使用API cluster.open("name")即可。例如调用cluster.open("db"),就会在该进程中监听2528的端口号。
对应的clusterd是command.listen:
function command.listen(source, addr, port)
local gate = skynet.newservice("gate")
if port == nil then
addr, port = string.match(node_address[addr], "([^:]+):(.*)$")
end
skynet.call(gate, "lua", "open", { address = addr, port = port })
skynet.ret(skynet.pack(nil))
end
source是调用服务的handle,这里没有用。可以看到这段代码的作用是启动一个gate服务,然后监听地址和端口号。gate服务前面已经讲过。
远程进程要想访问刚才的那个服务,也必须配置要访问的地址和端口号,配置发和上面一样。然后调用:
cluster.call("db", ".simpledb", "GET", "a")
"db"是本进程配置的远程地址和端口号,".simpledb"是远程进程的服务名。
看一下这个api:
function cluster.call(node, address, ...)
-- skynet.pack(...) will free by cluster.core.packrequest
return skynet.call(clusterd, "lua", "req", node, address, skynet.pack(...))
end
我们看其实现:
local function send_request(source, node, addr, msg, sz)
local session = node_session[node] or 1
-- msg is a local pointer, cluster.packrequest will free it
local request, new_session, padding = cluster.packrequest(addr, session, msg, sz)
node_session[node] = new_session
-- node_channel[node] may yield or throw error
local c = node_channel[node]
return c:request(request, session, padding)
end
function command.req(...)
local ok, msg, sz = pcall(send_request, ...)
if ok then
if type(msg) == "table" then
skynet.ret(cluster.concat(msg))
else
skynet.ret(msg)
end
else
skynet.error(msg)
skynet.response()(false)
end
end
在send_request中,addr是想要调用服务的地址,字符串和handle都可以。msg,sz 是经skynet.pack打包过后的数据。刚开始没有session,则为1。
关于session,在集群cluster中是非常重要的概念。为什么这么说呢?集群的目的是为了远程调用服务,意图和本地调用服务差不多。以下是云风大大的说法:
在与外部服务交互式时,请求回应模式是最常用模式之一。通常的协议设计方式有两种。每个请求包对应一个回应包,由 TCP 协议保证时序。
发起每个请求时带一个唯一 session 标识,在发送回应时,带上这个标识。这样设计可以不要求每个请求都一定要有回应,且不必遵循先提出的请求先回应的时序。对于第一种模式,用 skynet 的 Socket API 很容易实现,但如果在一个 coroutine 中读写一个 socket 的话,由于读的过程是阻塞的,这会导致吞吐量下降。
对于第二种模式,需要用 skynet.fork 开启一个新线程来收取回响应包,并自行和请求对应起来,实现比较繁琐。所以skynet 提供了一个更高层的封装:socket channel。
socket channel有这两种模式的实现。cluster使用了第二种模式。因为第一种模式虽然发送消息不受限制,但是消息是严格按照顺序收到的,如果前面消息处理时间很长,势必会影响本次消息的接收。
关于这个socket channel,大致看了下源码,还是有点小复杂,有时间再来剖析。
cluster发送数据时都会带上session,最后会根据session匹配收到的消息。
由于这里的网络数据是在gate服务中监听的,当收到消息时会command.socket会被调用(具体分析请看skynet watchdog,gate源码分析),在这里他会给目的服务发送消息。待得到回应后会发送给自己,这样cluster服务就可以收到响应的值了。