游戏服务器研究三:bigworld 的 load balance 算法

1. 前言

bigworld 的 load balance 算法的大致思路是知道的,即 动态区域分割 + 动态边界调整。但具体是怎么实现的,不清楚,网上也不找到相关的文章介绍,所以只能自己看代码进行分析。

本文大致记录我所分析到的算法实现,基于 bigworld 的这个开源版本:2014 BigWorld Open-Source Edition Code,更具体的信息可在我另一篇文章找到 《游戏服务器研究一:bigworld 开源代码的编译与运行》 。

本文应该会持续更新,因为算法有很多细节,看得越多,了解越多。而这些细节对于这类算法是很重要的,属于生产实践上的微调,离开这些微调,load balance 可能工作得不如预期,甚至在一些边角的情况下,可能会表现得特别差。

如有错误,欢迎指出。


2. bigworld 服务器架构

与 load balance 相关的服务器是 cellapp 和 cellappmgr,其中 cellapp 可以有很多个,而 cellappmgr 全局只有一个。


图1:bigworld 服务器架构[1]


3. load balance 基本算法

一张地图,bigworld 用一个 Space 类来表示,根据负载情况,动态分割成 n 个 区域(cell),这些 cell 的面积不是固定的,会根据地图上的实体(entity)的 cpu 使用率(cpu load)的分布情况来动态调整。

Space 以及 cell 相关的分割信息,由全局唯一的 cellappmgr 服务器管理。具体的 cell 运行在 cellapp 服务器上,整个集群会有多个 cellapp。

一个 space 可能会分割成多个 cell,但是在同一个 cellapp 上,只能运行这个 space 的一个 cell(否则负载均衡就没有意义了)。所以,一个 space 分割成 n 个 cell,就需要有 n 个 cellapp 来运行这些 cell。

bigworld 的 load balance 基本算法是 动态区域分割 + 动态边界调整。

动态区域分割:space 所使用的一组 cellapp 的平均 cpu load 已经超过阈值,无法通过改变 cell 的边界来减轻负载,只能通过增加 cell 的个数解决。

动态边界调整:当前 cell 个数不变,通过改变各个 cell 的管辖范围,即移动 cell 之间的边界,来使得 cell 之间的 cpu load 处于阈值之内,且相对平衡。


3.1 算法过程

下面大致描述一种可能的分割情况。


1、一开始的时候,一个 Space 只包含一个 cell,这个 cell 占据了整个 space 的面积。



2、当一个 space 所使用的一组 cellapp 的平均负载超过阈值时,cellappmgr 会决定增加一个 cell,并把这个 cell 放到组外的 cellapp 上运行,以此分担压力。



3、如果可以通过调整边界来使得各个 cell 的负载在阈值之内,并且负载相差最小,则直接调整边界。

值得指出的是,上面第 2 步中,新增的 cell3,一开始它的面积是 0,在动态的调整中,会慢慢增加它的占用面积,直到它上面运行的 entity 的负载之和与 cell2 相当。这个过程不是一步到位的,这样做的好处是整个过程变得很平滑,不会一下子需要从 cell2 迁移大量的 entity 到 cell3 上面。



4、如果调整边界仍然无法解决负载过高的问题,则继续增加 cell,但 cell 是采取 geometric tessellation(几何镶嵌)的方式分割的,横向(Horizontal)与纵向(Vertical)交织着分割。



5、依上述方法,经过多次分割后,可能演变成如下。




4. load balance 代码分析

bigworld 的代码质量很高,模块划分比较清晰。但是如果不了解一些核心概念,那么看 load balance 相关的代码就会很吃力。我也是硬看了一段时间,才理清大致的脉络。

下面的分析不会按照小白的方式,进行有条有理的叙述,只会写一些对于理解整个算法最关键的要点,具体逻辑要自己看代码。


4.1 cell

cell 是平面的,尽管地图是 3d 的,但 cell 只取平面的信息。


4.2 bsptree 的概念

bsptree 即 Binary Space Partioning Tree,实际上这里并不需要深入理解这种 tree,把它当成一棵二叉树即可,不会影响对整个算法的理解。


4.3 bsptree 相关的数据结构

在 cellappmgr 目录下:

类名说明文件
BSPNodebsp节点基类bsp_node.cpp
CellDatabsp叶子节点类,继承自BSPNodecell_data.cpp
InternalNodebsp中间节点类,继承自BSPNodeinternal_node.cpp

4.4 bsptree 的构造过程

以下讨论的都是 cellappmgr 目录下的类。

1、数据结构
bsptree 的根结点保存在 Space 类中,即 CM::BSPNode * pRoot_

2、根结点的初始化
根结点的初始化很容易找到,它的调用链路是:

CellAppMgr::createEntityInNewSpace 
-> Space::addCell() 
-> Space::addCell( CellApp & cellApp, CellData * pCellToSplit )

强调一下,此时创建出来的 pRoot_CellData 类型的(即 bsptree 的叶子节点类型)。它需要等到第一次分裂之后,才会变成 InternalNode 类型(即 bsptree 的中间节点类型)。

3、根节点的第一次分裂
这是隐藏得很深的,我找了挺久才捋清楚。

它的调用链路是:

CellAppMgr::metaLoadBalance()
-> CellAppGroups::checkForOverloaded( float addCellThreshold )
-> overloadedGroups.addCells();
-> CellAppGroup::addCell()
-> Space::addCell()
-> Space::addCell( CellApp & cellApp, CellData * pCellToSplit )

到此为止,看看 Space::addCell( CellApp & cellApp, CellData * pCellToSplit ) 的内部,此时传递的参数 pCellToSplit 是 null 的。

里面执行到这句的时候 pRoot_ = (pRoot_ ? pRoot_->addCell( pCellData ) : pCellData); ,由于 pRoot_ 此前已经初始化过,所以非空,那么就会执行 pRoot_->addCell( pCellData ),也就是调用 CellData::addCell( CellData * pCell, bool isHorizontal ),这里面就产生了分裂。

经过这次分裂,pRoot_ 正式变为 InternalNode 类型。


4.5 cpu 负载(cpu load)的计算

负载不是简单的使用 entity 的数量来衡量的,而是精细到每个 entity 的 cpu load。每个 entity 上面都有一个 profiler,在涉及到具体的 entity 处理的地方,基本上都调用这个 profiler 进入 profiling。

以下讨论的是 cellapp 目录下的类。

几个关键点

1、Entity 上挂着的 profiler 是 EntityProfiler profiler_;

2、与 EntityProfiler 关系密切的是这个 AutoScopedHelper 类,它是个简单的类,利用 RAII 机制来调用 profiler;会在构造函数里调用 pEntity->profiler().start();,在析构函数里调用 pEntity_->profiler().stop();

3、AUTO_SCOPED_ENTITY_PROFILEAUTO_SCOPED_THIS_ENTITY_PROFILE 这两个宏是对 AutoScopedHelper 的封装,使用这两个宏的地方都是对 entity 进行 profiling 的地方,在代码中搜索一下,可以发现一大堆。

4、每个 gametick,都会调用 EntityProfiler::tick 以重新计算每个 entity 的 cpu load,调用链路是:

CellApp::handleGameTickTimeSlice()
-> CellApp::updateLoad()
-> CellApp::tickProfilers( uint64 lastTickInStamps )
-> Cells::tickProfilers( uint64 tickDtInStamps, float smoothingFactor )
-> Cell::tickProfilers( uint64 tickDtInStamps, float smoothingFactor )

4.6 动态边界调整

动态边界调整的目标是使得 bsptree 的左右子树的 cpu load 处于相对平衡的状态,让两棵子树的 cpu load 之差尽可能达到最小。它是自上而下调整的,一级级都做调整。

以下讨论的都是 cellappmgr 目录下的类。

调用链是:

CellAppMgr::handleTimeout( TimerHandle /*handle*/, void * arg )
-> CellAppMgr::loadBalance()
-> Space::loadBalance()

进入 Space::loadBalance() 之后,就要看 pRoot_ 的状态了。

如果当前 pRoot_ 是 CellData 类型的,则调用的是 CellData::balance,没什么特别的事情好做的。

如果当前 pRoot_ 是 InternalNode 类,则调用的是 InternalNode::balance,这里面就比较复杂了,会自顶向下的尝试对各个层级的边界进行调整。


4.7 动态区域分割

动态区域分割的原因是,space 所使用的一组 cellapp 的平均 cpu load 已经超过阈值,无法通过改变 cell 的边界来减轻负载,只能通过增加 cell 的个数解决。

以下讨论的都是 cellappmgr 目录下的类。

调用链是:

CellAppMgr::handleTimeout( TimerHandle /*handle*/, void * arg )
-> CellAppMgr::metaLoadBalance()
-> CellAppGroups::checkForOverloaded( float addCellThreshold )

CellAppGroups 以及 checkForOverloaded 的逻辑都比较直,容易分析,这里就不细说了。


4.8 EntityBoundLevels 的作用是什么?

这一小段会有点长,要解释清楚这个概念并不容易。

概念解释

BSPNode 里面有个成员变量 EntityBoundLevels entityBoundLevels_;

最开始看这个的时候很费解,搞不懂它的作用,但它在 loadbalance 的时候会被使用,是个很重要的变量。后面仔细研究,终于搞懂了。

它实际上就是对于一个 cell 上的 entity 的 cpu load 分布情况的一个刻画,而且是从横向(左->右,右->左),纵向(上->下,下->上)总 4 个方向都进行了刻画。因为相邻 cell 的边界调整可以是向左或向右,向上或向下的,需要准备好这 4 个方向的数据,提供算法决策的依据。

举个例子,两个挨在一起的 cell:cell1 和 cell2,在执行 loadbalance 的时候,它们的 cpu load 分别是 load1 和 load2,load1 小于 load2。

那么这时候要怎么移动边界,让 cell1 和 cell2 的 cpu load 接近相等呢?答案是向右移动。

但要移动多少呢?这时候就需要 cell2 从左到右的 cpu load 的分布情况,而这就刚好是 cell2 的 entityBoundLevels_ 变量保存的信息。它并不是一个完整的信息,而是一个压缩后的信息,只记录了 5 个 level 的 cpu load 分布,注意,level 越大 cpu load 值越小。



举个例子,如果 diff = (load2-load1)/2,而 diff >= entityBoundLevels_[左到右][level5]diff < entityBoundLevels_[左到右][level4],那么把边界移动到 level5 对应的线就行了,这里只是尽量做到负载平衡,而不是百分百平衡。


代码说明

1、cellapp 端

向 cellappmgr 发送 EntityBoundLevels 等数据,调用链是:

CellApp::handleGameTickTimeSlice()
-> CellApp::updateBoundary()
-> CellAppMgrGateway::updateBounds( const Cells & cells )
-> Cells::writeBounds( BinaryOStream & stream )
-> Cell::writeBounds( BinaryOStream & stream )
-> Space::writeBounds( BinaryOStream & stream )
-> Space::writeEntityBounds( BinaryOStream & stream )

最后就是在 Space::writeEntityBounds 里面,把 cell 4 个方向的 entity cpu load 信息都写入了。

void Space::writeEntityBounds( BinaryOStream & stream ) const
{// This needs to match CellAppMgr's CellData::updateEntityBounds// Args are isMax and isYthis->writeEntityBoundsForEdge( stream, false, false ); // Leftthis->writeEntityBoundsForEdge( stream, false, true  ); // Bottomthis->writeEntityBoundsForEdge( stream, true,  false ); // Rightthis->writeEntityBoundsForEdge( stream, true,  true  ); // Top
}

2、cellappmgr 端

接收 cellapp 发上来的 update 数据,接收逻辑是:

CellApp::updateBounds( BinaryIStream & data )

这个其实被定义在 cellappmgr_interface.hpp 里面的。

BW_STREAM_MSG( CellApp, updateBounds );

3、细究 writeEntityBoundsForEdge

为何这个函数能方便的从4个方向统计 entity 的 cpu load 分布呢?因为 bigworld 使用了十字链表法来实现 aoi。

这样一来,只要从左到右或从右到左扫描 x 轴,就可以得到横向的分布信息;从上到下或从下往上扫描 y 轴,就可以得到纵向的分布信息。


4.9 chunkBounds_ 的作用是什么?

BSPNode 里面有个成员变量 BW::Rect chunkBounds_;

它表示自己所在的 cellapp 上,自己对应的 space 已经加载的地图的边界范围,通过把自己的 range_chunkBounds_ 取交集,就可以判断自己所在的区域是否已经完成地图数据的加载的,这也就是 CellData::calculateAreaNotLoaded() 所做的事情。


4.10 smooth 的意义

有很多变量前都加了 smooth 作为前缀,比如 smoothedLoad_,它的意义就是数学上说的“平滑”。

比如下面这个函数里面计算 smoothedLoad_,就是使用了指数平滑法,其中 bias 就是指数平滑法用的参数。平滑的作用就是让变量不会抖动的太厉害,相对平缓一些。

void CellApp::informOfLoad( const CellAppMgrInterface::informOfLoadArgs & args )
{lastReceivedLoad_ = args.load;float addedArtificialLoad = 0.f;for (Cells::const_iterator it = cells_.begin();it != cells_.end();++it){addedArtificialLoad +=(*it)->space().artificialMinLoadCellShare( lastReceivedLoad_ );}currLoad_ = lastReceivedLoad_ + addedArtificialLoad;float bias = CellAppMgrConfig::loadSmoothingBias();smoothedLoad_ = ((1.f - bias) * smoothedLoad_) + (bias * currLoad_);estimatedLoad_ = smoothedLoad_;numEntities_ = args.numEntities;
}

指数平滑法的计算公式为:

S t = a Y t + ( 1 − a ) S t − 1 S_{t} = aY_{t}+(1−a)S_{t−1} St=aYt+(1a)St1

S t S_t St 是时间 t 的平滑值
Y t Y_t Yt 是时间 t 的实际值
S t − 1 S_{t−1} St1 是时间 t-1 的平滑值
a 是平滑常数,取值范围 [0,1]


5. 一些问题

5.1 cellapp 是怎么找到 cellappmgr 的

通过本机的 bwmachined2 这个进程查询得到 cellappmgr 的地址,然后向 cellappmgr 注册。


5.2 cellapp 上面 entity 的消息是怎么处理的

1、消息是收到立即处理的,但如果下一帧即将到来( app.nextTickPending() ),则不能因为处理这个消息导致下一帧被延迟执行,所以需要先把消息先放到 cellapp 的 buffered 队列中: bufferedEntityMessages,bufferedInputMessages。

2、这些 buffered 队列里的消息,会在下一帧开头的函数 CellApp::handleGameTickTimeSlice() 中被处理,即

this->bufferedEntityMessages().playBufferedMessages( *this );
this->bufferedInputMessages().playBufferedMessages( *this );

6. 总结

bigworld 的整个 load balance 算法实现是比较精细的,但在分布式环境下,如何保证这套算法的稳健运行,还需要再深入研究,亲自动手实验一下。


7. 参考

[1] bigworld. BigWorld Technology Server Whitepaper. https://sourceforge.net/p/bigworld/code/HEAD/tree/trunk/docs/pdf/BigWorld%20Technology%20Server%20Whitepaper.pdf.


系列文章:

  • 游戏服务器研究一:bigworld 开源代码的编译与运行

  • 游戏服务器研究二:大世界的 scale 问题

  • 游戏服务器研究三:bigworld 的 load balance 算法

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/35928.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Appium+python自动化(二十八)- 滑呀滑,滑到奈何桥喝碗孟婆汤 - 高级滑动(超详解)

简介   奈何桥上叹奈何&#xff0c;三生石前憾三生&#xff0c;彼岸花下非彼岸&#xff0c;奈何三生彼岸人。 相传过了鬼门关便上一条路叫黄泉路&#xff0c;路上盛开着只见花&#xff0c;不见叶的彼岸花。花叶生生两不见&#xff0c;相念相惜永相失&#xff0c;路尽头有一条…

【JavaScript】流程控制和函数

目录 一、分支语句 1、if语句&#xff1a; 2、switch语句&#xff1a; 二、循环语句 1、while循环语句 2、for循环语句 三、函数声明 1、function 函数名(形参列表){ 函数体 } 2、var 函数名function(形参列表){函数体} 一、分支语句 1、if语句&#xff1a; if(表达式){ }else …

Vue-cli搭建项目----基础版

什么是Vue-cli 全称:Vue command line interface 是一个用于快速搭建Vue.js项目的标准工具,他简化了Vue.js应用的创建和管理过程,通过命令工具帮助开发者快速生成,配置和管理Vue项目. 主要功能 同一的目录结构本地调试热部署单元测试集成打包上线 具体操作 第一步创建项目:…

第三届人工智能、物联网与云计算技术国际会议(AIoTC 2024)

第三届人工智能、物联网与云计算技术国际会议(AIoTC 2024)将于2024年9月13日-15日在中国武汉举行。本次会议由华中师范大学伍伦贡联合研究院与南京大学联合主办、江苏省大数据区块链与智能信息专委会承办、江苏省概率统计学会、江苏省应用统计学会、Sir Forum、南京理工大学、南…

西门子智能电气阀门定位器在冶金生产控制的应用

西门子智能电气阀门定位器在冶金生产控制的应用 1 前 言 在自动化程度越来越高的冶金行业中 ,调节阀起着至关重要的作用,一旦其发生故障, 轻则出现生产事故,停机,停炉影响各级生产指标,生产任务,影响装置的安全运行。重则可能出现人身安全事故,将直接影响家庭的幸福和企…

Android SurfaceFlinger——动画进程销毁(十七)

在动画播放完成后&#xff0c;对动画相关资源释放的同时还需要销毁动画进程。这里我们就来分析一下动画进程的销毁流程。 一、动画进程销毁 动画进程的销毁一般是在桌面进程准备显示的时候&#xff0c;而桌面准备显示是在桌面 Activity 的 Resume 生命周期&#xff0c;我们来看…

美团校招机试 - 小美的平衡矩阵(20240309-T1)

题目来源 美团校招笔试真题_小美的平衡矩阵 题目描述 小美拿到了一个 n * n 的矩阵&#xff0c;其中每个元素是 0 或者 1。 小美认为一个矩形区域是完美的&#xff0c;当且仅当该区域内 0 的数量恰好等于 1 的数量。 现在&#xff0c;小美希望你回答有多少个 i * i 的完美…

redis哨兵模式(Redis Sentinel)

哨兵模式的背景 当主服务器宕机后&#xff0c;需要手动把一台从服务器切换为主服务器&#xff0c;这就需要人工干预&#xff0c;费事费力&#xff0c;还会造成一段时间内服务不可用。这不是一种推荐的方式。 为了解决单点故障和提高系统的可用性&#xff0c;需要一种自动化的监…

暑假本科生、研究生怎么学?来看详细的AI夏令营规划

Datawhale夏令营 发布&#xff1a;2024 AI 夏令营 学习规划 「学习内容详览」 01机器学习方向&#xff1a;2024/7/1~7/7 「Datawhale」邀请想入门人工智能领域并实践机器学习算法的学习者和我们一起来学习~ 详细学习规划如下&#xff1a; 02大模型技术方向&#xff1a;2024/7…

基于springboot、vue汽车租赁系统

设计技术&#xff1a; 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringbootMybatisvue工具&#xff1a;IDEA、Maven、Navicat 主要功能&#xff1a; 用户进入系统可以查看首页、个人中心、车辆信息管理、租赁订单列表管理、还车记录管理等操作 管理…

串级PID控制算原理及法详解

文章目录 1. PID 2. 串级PID 3. 串级PID的物理量 4. C语言实现单极PID 5. C语言实现串极PID 6. 模拟仿真 1. PID PID是应用最广泛的闭环控制方法之一&#xff0c;是一种常用的反馈控制方法&#xff0c;对于每个PID控制器由三个部分组成&#xff1a;比例控制&#xff08;…

自然语言处理——英文文本预处理

高质量数据的重要性 数据的质量直接影响模型的性能和准确性。高质量的数据可以显著提升模型的学习效果&#xff0c;帮助模型更准确地识别模式、进行预测和决策。具体原因包括以下几点&#xff1a; 噪音减少&#xff1a;高质量的数据经过清理&#xff0c;减少了无关或错误信息…

Wp-scan一键扫描wordpress网页(KALI工具系列三十)

目录 1、KALI LINUX 简介 2、Wp-scan工具简介 3、信息收集 3.1 目标IP&#xff08;服务器) 3.2kali的IP 4、操作实例 4.1 基本扫描 4.2 扫描已知漏洞 4.3 扫描目标主题 4.4 列出用户 4.5 输出扫描文件 4.6 输出详细结果 5、总结 1、KALI LINUX 简介 Kali Linux 是一…

《梦醒蝶飞:释放Excel函数与公式的力量》6.1 DATE函数

6.1 DATE函数 第一节&#xff1a;DATE函数 1&#xff09;DATE函数概述 DATE函数是Excel中的一个内置函数&#xff0c;用于根据指定的年、月、日返回对应的日期序列号。这个函数非常有用&#xff0c;尤其是在处理日期数据时&#xff0c;它可以帮助你构建特定的日期&#xff0…

pycharm工具回退键调出

pycharm工具调出回退键。 View->Appearance->Toolbar,即可调出 调不出的可以使用快捷键&#xff1a;ctrlalt向左箭头 但是这个快捷键容易和电脑屏幕旋转冲突。可将电脑的快捷键关掉&#xff0c;即可。 ctrlalt向上箭头&#xff1a;将屏幕旋转到正常&#xff08;横向&am…

【面试干货】final、finalize 和 finally 的区别

【面试干货】final、finalize 和 finally 的区别 1、final1.1 修饰类1.2 修饰方法1.3 修饰变量 2、finally3、finalize4、总结 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 在Java编程语言中&#xff0c;final、finalize和finally都是关键…

汽车免拆诊断案例 | 2016 款吉利帝豪EV车无法加速

故障现象 一辆2016款吉利帝豪EV车&#xff0c;累计行驶里程约为28.4万km&#xff0c;车主反映车辆无法加速。 故障诊断 接车后路试&#xff0c;行驶约1 km&#xff0c;踩下加速踏板&#xff0c;无法加速&#xff0c;车速为20 km/h左右&#xff0c;同时组合仪表上的电机及控制…

设备驱动框架之LED

文章目录 前言一、什么是驱动框架二、使用步骤1.注册LED设备2.卸载LED设备3.内核中申请内存4.container_of5.platform_get_drvdata 和 platform_set_drvdata6.module_platform_driver 三、驱动示例总结 前言 为了尽量降低驱动开发者难度以及接口标准化&#xff0c;就出现了设备…

面试-Java线程池

1.利用Excutors创建不同的线程池满足不同场景的需求 分析&#xff1a; 如果并发的请求的数量非常多&#xff0c;但每个线程执行的时间非常短&#xff0c;这样就会频繁的创建和销毁线程。如此一来&#xff0c;会大大降低系统的效率。 可能出现&#xff0c;服务器在为每个线程创建…

利用powershell开展网络钓鱼

要确保人们打开我们的恶意文件并执行它们&#xff0c;我们只需让微软努力工作多年来赢得人们的信任&#xff0c;然后将一些危险的宏插入到幻灯片中。 本博文将介绍如何通过屏幕顶部的一个友好的警告提示&#xff0c;在用户启用宏后立即运行您的宏。 首先&#xff0c;我们需要打…