上周末,我对 skynet 的 cluster 模块做了一点优化。
cluster 模式 是 skynet 的一种集群方案,用于将多台机器更为弹性的组成一个集群。我们将每台机器都赋予一个名字,然后就可以在集群间用这个名字向对方推送消息或发起请求。
集群的管理是一项非常复杂的工作,skynet 作为一个轻量化的框架,只实现了最基本的基础设施。cluster 这个基础设置已实现的部分并不复杂。在每个 skynet 进程中,我们启动了一个叫 clusterd 的服务,专门用于集群间的通讯。由 clusterd 再启动了一个 gate 服务,监听其它节点连过来的连接;同时,当前节点如果要对其它节点发送数据,也通过 clusterd 向外连接。业务要使用 cluster 的时候,都是通过 require "cluster" 这个库,然后这个库中的 api 负责和本节点的 clusterd 交换数据。
我们正在开发的一个 mmorpg 项目重度依赖了 cluster ,在过去的多次压力测试中,发现 clusterd 是测试中单位时间内消耗 CPU 最多的服务。它是整个 skynet 节点中的一个性能热点。所以我考虑了对这个 clusterd 做一些优化。
clusterd 在转发内部请求到外部的过程中,由于涉及消息转发,所以有几处地方需要拷贝/序列化消息。这个可以考虑优化为内存指针的传递。有些环节好做,有些环节不好做。我先把好做的部分改掉了。不好做的地方涉及对更底层 socket 模块的改造,为了避免优化改动涉及面太大,暂且放在一边。单单优化实现未必能获得特别的效益。
skynet 框架的目的就是为了充分发挥多核硬件的优势。在同一时间能把工作分摊到更多核心上处理,且不增加总体的工作量,才是最大的优化。从这个角度看,最容易做的优化是把 clusterd 负责对外请求的部分和接收外部请求的部分分离开。降低单个服务的 cpu 占用量就可以提高硬件对同时在线玩家的承载能力,且减少每个玩家消息处理的响应速度。
接收外部请求的部分是比较容易剥离的部分,因为它不需要了解 cluster 网络上各个节点的名字和地址的对应关系,而这个对应表可以是动态的,在 clusterd 中统一维护。接收请求只需要获得外部连接的网络包,解析出请求,转发给当前节点中的对应服务,然后把回应原路返回即可。
之前的版本中已经启用了一个 gate 来接受外部连接,只是过去把所有的请求又转回了 clusterd 。我改动成针对每个连接都启动一个新的 clusteragent 服务,然后让 gate 把数据转给它就行了。至于处理数据包的代码,简单的从 clusterd 中移到了新服务 clusteragent 中。由于每个连接都分开了,原来处理大数据包请求的部分还可以做一些简化。
周一,就这个修改,我们在已有的项目上做了压力测试。从数据看效果比较明显。由于我们的组网从功能上是对等结构,每个节点是等价的,所以在压力测试中 cluster 向外请求和接收请求数量是基本相同的。压测一个小时,发现 cluster 相关服务在处理的消息数量上是基本相同的。即过去被 clusterd 单一服务处理消息被分摊到了多个服务上。clusterd 保留了对外请求的功能,所以大致是原来 50% 的处理量。而剩下的 50% 则被分摊到了多个 clusteragent 上,数量正是节点数量减一。
由于 clusteragent 的相关消息转发环节还做了一些优化处理,减少了消息内存的拷贝,对消息转发的处理速度也比 clusterd 的转发速度快了一倍。经测试,在我们这台虚拟机上,差不多达到了 7 万条每秒。因为每个请求都要转发一个外部网络包,以及把内部的回应包转发出去,也就是可以达到每节点 3.5 万 qps 。
我们压力测试用的硬件 E5-2620 两块,有 24 核心,CPU 主频 2GHz 。这应该是 Intel 26 系列中最低档次的 CPU 。我们采购的比较早,如果是现在采购新的硬件,应该比它性能要好一些。我们的压力测试并没有完全跑满硬件的处理能力(cluster 相关服务的消息队列并未堆积过载)。我想 3.5 万 qps 应该能作为之后项目中, cluster 针对两个节点间互通的处理能力下限参考。这个处理能力和 CPU 单个核心的处理能力有关,和核心数量无关。
对于很多不对等的组集群的方式,很多节点的业务单一,处理着外部大量节点的请求,这次优化的意义可能更大。比如中心的登录及在线状态维护,信箱,等等业务都可以放在集群的一个单独节点上。cluster 的这次优化可以帮助这类节点消除之前 clusterd 是单一服务的热点问题。