skynet cluster集群笔记

skynet cluster集群笔记

  • 前言
  • cluster相关方法说明
  • 集群设计方案:
  • 集群中常遇到的问题:
  • 注意事项:

前言

skynet 是一个基于事件驱动的分布式游戏服务器框架,支持构建高性能、高并发的网络程序。在 skynet中,集群是指将多个节点连接在一起,共同协作完成任务的一个系统,一个skynet集群架构中涉及的一些名词如下:

1. 节点: skynet 中的节点是指运行着 skynet 实例的独立服务器。每个节点都有自己的地址和唯一标识符,可以运行不同的服务。节点之间通过网络连接进行通信和协作。
2. 服务: 在 skynet 中,服务是指运行在节点上的具体功能模块。每个服务都有一个唯一的服务名标识符,可以通过该标识符进行访问和通信。服务可以在同一节点上运行,也可以跨节点部署。
3. 集群: skynet 中的集群是指多个节点连接在一起,共同组成一个分布式系统。集群中的节点可以相互通信,共享资源,协同完成任务。集群可以通过网络连接,也可以根据配置在同一台物理机器上运行多个节点。
4. 节点发现: 节点发现是指节点之间相互发现和连接的过程。skynet 提供了节点发现的功能,可以通过配置文件(例如clustername)或者其他方式指定节点的地址和端口,使节点能够找到彼此并建立连接。
5. 服务发现: 集群中的服务发现是指服务之间相互发现和通信的过程。通过服务发现,可以在集群中查找和访问指定的服务,实现分布式系统中的模块化和协作。

cluster相关方法说明

cluster.call: 用于在集群中向指定节点的服务发送请求并等待其回应。它可以用于远程调用其他节点上的服务。

-- 调用名为 "testservice" (也可以是address)的服务的 "hello" 函数,并传递参数 "arg1" 和 "arg2",等待返回结果
local ret = cluster.call("node1", ".testservice", "hello", "arg1", "arg2")
print(ret) 

cluster.send: 用于在集群中向指定节点的服务发送消息,不等待回应。它通常用于异步通信或者发送不需要返回结果的消息。

-- 向名为 "testservice" (也可以是address)的服务发送消息通知,传递参数 "message"
cluster.send("node2", ".testservice", "notify", "message")

cluster.open: 打开节点端口, 通知网关监听节点端口。用于节点发现,这个节点中的服务可以被其他节点调用(前提需要使用cluster.register或cluster.proxy获取到服务句柄)。

-- 在当前节点中打开一个名为 "node1" 的服务,并指定处理函数
cluster.open("node1")

cluster.reload: 用于加载集群节点配置

-- 加载node2,用于集群中的节点发现(node1配置关闭状态)
-- __nowaiting设置true,表示使用cluster.call时是阻塞等待,若设置为false,cluster.call不等待加载完成,而立即返回
-- __nowaiting默认为true
cluster.reload {
            __nowaiting = true,
            node1 = false, -- node1 is down
            node2 = "127.0.0.1:2529"
        }

cluster.proxy: 用于在当前节点中创建一个代理对象,代理指定节点上的服务。这样可以方便地通过代理对象调用远程服务。(可以直接使用skynet.call代替使用cluster.call直接调用远端服务地址)

-- 将远程节点node2上的db1服务在本地创建一个代理对象
local proxy = cluster.proxy "node2@db1"   -- cluster.proxy("node2", "@.db1")
-- 使用代理对象调用远程服务的方法
skynet.call(proxy, "lua", ...)

cluster.snax: 用于在当前节点中创建一个 SNAX 服务实例,该实例可以管理指定节点上的 SNAX 服务。

-- 创建一个 SNAX 服务实例,管理名为 "testservice" 的 SNAX 服务,位于 "node1" 节点上
local instance = cluster.snax("node1", "testservice")

cluster.register: 用于将addr注册为在cluster中可见的字符串名字name,如果不传addr,如默认将自身注册为name

-- 在集群中注册当前节点的db1服务
cluster.register(".db1")

cluster.unregister: 用于取消cluster.register的注册。

cluster.unregister(".db1")

cluster.query: 用于查询指定节点上注册的服务名字,返回该名字对应的服务数字地址。若不存在,则抛出error

-- 查询节点node1上的.db1服务的address,并返回其对应的服务地址
local address = cluster.query("node1", ".db1")
print(address)  -- 打印查询结果

源方法使用的注意点:
cluster.call、cluster.send、cluser.proxy接口可以使用@加字符串的方式来调用通过cluster.register注册的服务(官方推荐用法

集群设计方案:

  1. 将需要在集群中被服务发现的服务,通过cluster.register注册,并将远端需要使用到的服务通过cluster.proxy代理
-- 在服务初始化时注册需要被服务发现的服务
cluster.register("myservice")

-- 将远端节点node2上的service1服务进行代理,用于节点内使用
local node2_service1 = cluster.proxy("node2", "@service1")
  1. 根据节点服务配置将需要加入到集群中的服务的数值地址(newservice、proxy返回值),按照一定规则添加到表list中,并将list通过内存共享机制(例如sharetable)加入到共享内存中,各服务可以按照一定路由规则(例如对指定类型服务的address列表取模)实现集群中的服务调度
-- 创建一个共享表用于存储服务配置信息
local serviceConfig = {
    newservice = {...}, -- 服务配置信息
    proxy = {...} -- 代理信息
}

-- 将配置信息添加到共享内存中
local sharetable = require "skynet.sharetable"
sharetable.loadtable("serviceConfig", serviceConfig)
  1. 添加一个集群唯一的监控服务(cluster_center),并添加心跳,用于监控各节点状态
-- 创建并启动一个监控服务cluster_center
skynet.start(function()
    skynet.fork(function()
        while true do
            -- 发送心跳消息给其他节点
            skynet.send("cluster_center", "lua", "heartbeat")
            skynet.sleep(500)  -- 每隔5秒发送一次心跳
        end
    end)
end)
  1. 在每个节点添加一个local的节点内的监控服务,用于结合cluster_center同步各节点的启动状态或服务状态
-- 在节点内添加本地监控服务
skynet.start(function()
    skynet.fork(function()
        while true do
            -- 同步节点状态等
            skynet.send("cluster_center", "lua", "sync", skynet.self(), ...)
            skynet.sleep(100)  -- 每隔1秒进行一次状态同步
        end
    end)
end)
  1. 合理的分配节点服务,根据具体业务需要和服务压力,结合集群规模及服务之间的依赖关系进行节点服务的划分与分配,可以根据需要进行动态调整,并保持集群的平衡与高效运行

以上代码片段为伪代码示例,具体需要根据实际业务需求修改~

集群中常遇到的问题:

1. require时序问题: 常遇到A服务启动时,需要依赖B服务,但此时B服务还未加载完成。
解决方案: 可以在newservice节点服务时,使用fork处理服务启动中的require部分,待监听(例如使用setenv和getenv的方式)到节点中基础服务(包括依赖的服务)都成功返回newservice后再执行服务中的require部分

2. 服务数据时序问题: 服务启动后优先执行的逻辑中依赖其他服务的数据。
解决方案: 可以使用skynet.timeout的方式,将服务启动后需要优先执行的逻辑丢到定时器中,因为定时器中的逻辑执行,需要优先保证当前服务的正常启动,通过可以设置时间参数>服务最大启动消耗时间,例如节点上服务在监听到节点上的基础服务都newservice后1~2s内完成服务的require,只需将timeout注册的时间设置为大于2s即可

3. 动态创建的本地服务调用问题: 经常会出现需要再某个服务中动态创建其他业务服务的情况,例如需要在hall服务中动态创建一些room服务,这些服务可能会比较频繁的创建与销毁,如果每次创建销毁都走cluster.register和cluster.unregister会增加服务路由策略设计的复杂度。
解决方案: 通过记录这些动态服务的node和address信息,例如记录在缓存或redis中,在调度处通过判断是否为本地节点选择使用skynet.call或cluster.call调度,从而可以动态的维护此类服务

4. 网络通信问题: 在分布式系统中,网络通信是至关重要的。由于网络延迟、丢包等原因,可能导致消息传递失败、服务调用超时等问题。
解决方案: 使用消息队列或者基于 RPC 的通信方式,并且实现重试机制,以处理网络通信中的丢包和延迟问题。另外,可以使用心跳机制来检测节点的健康状态,及时发现网络故障。

---@file cluster_node.lua
-- 定义心跳间隔时间(单位:秒)
local heartbeat_interval = 10

-- 记录节点最后一次心跳的时间戳
local last_heartbeat_time = {}

-- 启动心跳检测
local function start_heartbeat_detection(node)
    last_heartbeat_time[node] = skynet.now() -- 记录节点当前时间
    skynet.fork(function()
        while true do
            skynet.sleep(heartbeat_interval * 100) -- 等待心跳间隔时间
            local current_time = skynet.now()
            local last_time = last_heartbeat_time[node] or 0
            if current_time - last_time > heartbeat_interval * 100 then
                handle_node_failure(node) -- 节点心跳超时,处理节点故障
                break
            end
        end
    end)
end

-- 更新节点心跳时间
local function update_heartbeat_time(node)
    last_heartbeat_time[node] = skynet.now()
end

-- 接收来自其他节点的心跳消息
skynet.dispatch("lua", function(session, source, ...)
    local message_type, node = ...
    if message_type == "heartbeat" then
        update_heartbeat_time(node) -- 更新节点心跳时间
    end
end)

---@file node_X.lua
-- 发送心跳消息给集群管理服务
local function send_heartbeat_message(node)
    skynet.send(cluster_node_addr, "lua", "heartbeat", node)
end

-- 主动发送心跳消息
local function send_heartbeat_periodically(node)
    while true do
        send_heartbeat_message(node)
        skynet.sleep(heartbeat_interval * 100)
    end
end

-- 启动心跳发送
skynet.fork(send_heartbeat_periodically, node_to_monitor)

5. 负载均衡: 当集群中的节点数量增加时,需要有效地分配和管理负载,以确保各个节点的负载均衡,避免某些节点负载过重而影响系统性能。
解决方案: 使用一致性哈希算法或轮询算法或取模等负载均衡算法,将请求均匀地分发到集群中的各节点上。

6. 故障恢复: 分布式系统中的节点可能会由于网络故障、硬件故障等原因而宕机,因此需要实现故障检测和自动恢复机制,以保证系统的高可用性。
解决方案: 实现监控和自动恢复机制,当节点出现故障时,自动将其替换为其他可用节点,并通知相关服务重新分配负载。

7. 数据一致性: 在分布式系统中,数据一致性是一个复杂的问题。由于数据分布在不同的节点上,可能出现数据不一致的情况,需要采取合适的数据同步和一致性协议来解决这一问题。
解决方案: 使用分布式事务、副本同步等机制确保数据在集群中的一致性,例如使用 Paxos 或者 Raft 算法。

-- 使用 Paxos 算法实现分布式事务
if paxos_commit(data) then
    -- 数据提交成功
else
    -- 数据提交失败,进行回滚操作
end

8. 节点管理: 随着集群规模的扩大,节点的管理和监控变得更加复杂。需要实现节点的动态扩容、缩容,以及节点状态的监控和报警机制。
解决方案: 实现动态扩容和缩容机制,监控节点状态并自动进行调整,同时提供管理界面方便手动干预。

-- 监控节点状态并进行动态扩容或缩容
if need_scale_out() then
    scale_out()
elseif need_scale_in() then
    scale_in()
end

9. 性能调优: 随着系统的运行,可能需要进行性能调优,优化系统的吞吐量、延迟等性能指标,以提升系统的性能和稳定性。
解决方案: 分析系统瓶颈,对关键路径进行优化,包括减少网络通信开销、提高算法效率、优化数据库查询等。

注意事项:

1. 实现细节:
a. 考虑设置local服务,local服务只暴露于当地节点中,例如对于一些无状态的服务,例如db,可以设置为local,节点中可以优先使用节点内的db服务,从而减少rpc调度,提升性能。
b. 注意不同节点中一些业务数据的唯一性,例如在不同节点上都会动态创建一些room服务,根据具体需求,考虑是否会出现不同节点上房间编号相同的情况,例如可以在定义房间编号时引入节点id,保证唯一性。
c. skynet.call、skynet.send服务间调用考虑使用adress代替使用name,因为使用adress在c层跳过走一遍二分查找法的逻辑,可以提升效率,同时还有一个好处就是使用adress路由,服务不存在是不会阻塞,会直接报错,可以及时的暴露异常。
d. skyent.name只支持字符长度16,超过会有问题,使用时需要注意。
e. 集群设计用cluster.register代替skynet.register,减少C层路由逻辑 同时cluster.proxy使用@调用提升性能。

2. 系统拓扑结构: 确定集群的拓扑结构,包括节点之间的通信方式、数据流向、负载均衡策略等。合理的拓扑结构能够提高系统的性能和可扩展性。

3. 容错机制: 实现容错机制,包括故障检测、故障恢复和数据备份等,以提高系统的稳定性和可靠性。

4. 性能监控: 部署性能监控系统,及时监控集群各个节点的性能指标,发现潜在问题并进行优化调整。

5. 版本管理: 统一管理集群中的软件版本,确保各个节点的代码版本一致,避免因为版本不一致而导致的兼容性问题。

6. 数据分区: 合理划分数据分区,避免数据倾斜和热点问题,以及提高系统的并行度和吞吐量。

7. 容量规划: 根据业务需求和预期负载情况,进行容量规划,确保集群有足够的资源支撑业务的发展和变化。

8. 扩展性设计: 考虑到系统未来的扩展性,设计灵活的架构和接口,方便后续的功能扩展和性能优化。

9. 文档和培训: 编写详细的文档和培训资料,使得团队成员能够理解集群的设计原理和运行机制,提高团队的协作效率和系统的可维护性。

10. 容器化和自动化: 考虑使用容器化技术和自动化部署工具,简化集群的管理和维护,提高部署效率和系统的灵活性。

11. 安全防护: 加强集群的安全防护措施,包括防火墙设置、访问控制、漏洞修复等,保护集群不受恶意攻击和数据泄露。

你可能感兴趣的:(skynet,笔记,服务器,lua,系统架构)