skynet框架:批量服务管理方案

skynet很经典的用法是一个节点会有批量的服务跑相同的模块逻辑。服务的生命周期管理显然是跟业务强关联的,需要根据实际业务对应做适配的生命周期管理方案。显然最直接的方案就是服务常驻,跟进程的生命周期同步,当服务的数量级不大时,认为消耗可控,方案是适用的,也避免过度设计。

这里想谈的是单节点数千服务的场景下对应的服务管理,以游戏项目中常用基础模块为例;

管理器需要处理的核心逻辑是,服务的创建销毁和服务消息的转发;

根据不同的服务类型定义出业务内唯一的id,比如玩家代理服务的id是玩家角色id,聊天频道代理服务是频道序号等;

local skynet = require("skynet")

local agent_manager = {
	_id_addr = {} -- {业务id:服务地址}
}

-- 创建/查询服务
function agent_manager.get_or_load_agent(agent_name, id)
	local addr = agent_manager._id_addr[id]
	if addr then
		return addr
	end
	addr = skynet.newservice(agent_name, id)
	agent_manager._id_addr[id] = addr
	return addr
end

-- 销毁服务
function agent_manager.destroy_agent(id)
	local addr = agent_manager._id_addr[id]
	if not addr then
		return
	end
	agent_manager._id_addr[id] = nil
	if addr then
		agent_manager.agent_call(id, "exit")
	end
end

-- 转发消息:call
function agent_manager.agent_call(id, cmd, ...)
	local addr = agent_manager.get_or_load_agent(agent_name, id)
	if not addr then
		return
	end
	return skynet.call(addr, "lua", cmd, ...)
end

-- 转发消息:send
function agent_manager.agent_send(id, cmd, ...)
	local addr = agent_manager.get_or_load_agent(agent_name, id)
	if not addr then
		return
	end
	skynet.send(addr, "lua", cmd, ...)
end

-- 广播消息
function agent_manager.broadcast_agent(ids, cmd, ...)
	local online_ids = online_filter(ids)
	for _, id in ipairs(online_ids) do
		local addr = agent_manager.get_or_load_agent(agent_name, id)
		if addr then
			skynet.send(addr, "lua", cmd, ...)
		end
	end
end

return agent_manager

玩家代理服务(client):显然服务的生命周期需要跟玩家上线下线同步,即玩家上线时创建分配服务,下线时销毁服务,当然实际方案通常会多加一些处理,比如优化重登情况:下线时冻结服务指定一段时间,超时才销毁退出,优化玩家频繁上线下线造成的性能消耗;同时玩家代理服务还持有网络套接字连接,需要处理数据存盘和句柄释放等操作;

代理服务的销毁流程是:(玩家下线—>关闭socket—>数据存盘—>通知管理器冻结服务—>重登唤醒激活/超时未重登释放)

-- user agent
function user_agent.logout(fd, dbdata)
	socket.close(fd)
	db.save(dbdata)
	skynet.call(agent_manager_addr, "agent_freeze", id)
end

-- agent manager
local EXPIRE_SEC = 60 * 3 -1
function agent_manager.agent_freeze(id)
	local function timeout_closure()
		return agent_manager.destroy_agent(id)
	end
	skynet.sleep(EXPIRE_SEC, timeout_closure)
end

聊天频道代理服务(chat):聊天频道的特点是玩家聚集度明显,分布不均。这里以 为每个频道创建独立服务 的方案来讨论。那么不同的服务访问热度是不同的,请求会集中在少数的频道服务中。这时候对服务使用闲置销毁的管理方案,一定时间内没有请求到达则自动销毁退出服务,每次请求都检查服务是否存在,否则先创建服务;

-- agent manager
local agent_manager = {
    _id_addr = {},
    _id_t = {},
}

local AGENT_EXPIRE_SEC = 60 * 10
function agent_manager.check_agent_expire(id)
	local addr = agent_manager._id_addr[id]
	if not addr then
		return
	end
	local last_call_t = agent_manager._id_t[id]
	if last_call_t and last_call_t + AGENT_EXPIRE_SEC <= skynet.now() then
		agent_manager.destroy_agent(id)
	end
end

-- 转发消息:call
function agent_manager.agent_call(id, cmd, ...)
	local addr = agent_manager.get_or_load_agent(agent_name, id)
	if not addr then
		return
	end
    agent_manager._id_t[id] = skynet.now()
	return skynet.call(addr, "lua", cmd, ...)
end

-- send/broadcast 同理

btw,这个方案可能需要关注的地方:1. 服务创建动作由请求触发,当请求存在并发场景时,需要关注处理临界区情况;2. 定时销毁的间隔(上述的AGENT_EXPIRE_SEC)需要根据实际业务来评估合适的值,不合理的销毁间隔反而会适得其反增大性能压力;

周期赛事服务(activity):赛事活动的特点是,服务有明显的有效期,在一段固定的时间内生效,那么适用于集中创建销毁的管理方案,在赛事开始时批量创建拉起服务,在赛事结束时批量销毁;当服务量级过大时,可以考虑引入上述闲置销毁处理方案;

function agent_manager.multi_load_agent(ids)
	for _, id in ipairs(ids) do
		local addr = agent_manager._id_addr[id]
		if not addr then
			addr = skynet.newservice(agent_name, id)
			agent_manager._id_addr[id] = addr
		end
	end
end

function agent_manager.multi_destroy_agent(ids)
	for _, id in ipairs(ids) do
		local addr = agent_manager._id_addr[id]
		if addr then
			agent_manager.destroy_agent(id)
		end
	end
end

团队代理服务(team):每个团队使用独立服务代理,服务应该是常驻的,但量级是O(N),N是团队总数量,上限不可控,且团队节点通常是单点节点,系统存在理论上限。团队服务的特点是活跃度差异大,这里适用于 lru 管理方案,评估系统安全承载的服务数量上限,按活跃度规则进行有序管理,末位淘汰销毁超出上限的冷服务。

local AGENT_CAPACITY = 30000
local lru = Lru.new(AGENT_CAPACITY)

function agent_manager.get_or_load_agent(id)
	local addr = agent_manager._id_addr[id]
	if addr then
		return addr
	end
	addr = skynet.newservice(agent_name, id)
	agent_manager._id_addr[id] = addr
	lru.push(id, addr) -- 加入lru管理
	return addr
end

function agent_manager.check_lru_capacity()
	if not lru.full() then
		return
	end
	local id = lru.pop()
	agent_manager.destroy_agent(id)
end

-- 更新访问活跃度
function agent_manager.agent_call(id, cmd, ...)
	local addr = agent_manager.get_or_load_agent(agent_name, id)
	if not addr then
		return
	end
    lru:update(id, skynet.now())
    agent_manager.check_lru_capacity()
	return skynet.call(addr, "lua", cmd, ...)
end

-- send/broadcast 同理
posted @ 2024-09-29 11:50  linxx-  阅读(161)  评论(0)    收藏  举报