7月31日同步更新

This commit is contained in:
MCNeteaseDevs
2025-07-31 17:53:14 +08:00
parent f5c6bdba2e
commit cf061270d3
799 changed files with 27437 additions and 494 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -0,0 +1,257 @@
---
front: https://mc.res.netease.com/pc/zt/20201109161633/mc-dev/assets/img/help_debug_01.1be8e5f5.png
hard: 进阶
time: 20分钟
---
# 插件调试技巧集锦
## 定位内存增长
* 使用运营指令【/check-memory-run】检查服务器脚本层内存泄漏。需要执行两次指令第一次生成快照第二次生成同第一次的diff。
参数为:
关键字 | 数据类型| 说明
---|:---|---:
serverId |int |
useList |list | 通常是 ["tracemalloc", "objreport"]
objNames |list | 通常是空
示例:
* 第一步ssh到控制服所在的服务器上执行以下指令其中4000为lobby服的服务器ID
```bash
curl -X POST '42.186.17.79:8014/check-memory-run' -H 'Content-Type: application/json' --data-raw '{"serverId" : 4000,"useList":["tracemalloc","objreport"],"objNames":[]}'
```
* 第二步通过studio登录一个客户端
* 第三步,客户端登录完成后,关闭客户端
* 第四步,客户端完全退出之后,再次执行
```bash
curl -X POST '42.186.17.79:8014/check-memory-run' -H 'Content-Type: application/json' --data-raw '{"serverId" : 4000,"useList":["tracemalloc","objreport"],"objNames":[]}'
```
此时从lobby4000的服务端日志上可以看到
```
[2021-05-06 16:27:23 INFO] Python:[2021-05-06 16:27:23,359] [INFO][Engine] run_check use_list:['tracemalloc', 'objreport'] obj_names:[]
[2021-05-06 16:27:23 INFO] Python:[2021-05-06 16:27:23,399] [INFO][Engine] run_tracemalloc traceback
[2021-05-06 16:27:23 INFO] Python:[2021-05-06 16:27:23,422] [INFO][Engine] Top 10 differences
[2021-05-06 16:27:23 INFO] Python:redirect.py:127: size=195 KiB (+195 KiB), count=1860 (+1860), average=107 B
[2021-05-06 16:27:23 INFO] Python:mod/server/memory/obj_report.py:43: size=48.0 KiB (+48.0 KiB), count=1 (+1), average=48.0 KiB
[2021-05-06 16:27:23 INFO] Python:/usr/local/lib/python2.7/re.py:261: size=12.0 KiB (+12.0 KiB), count=1 (+1), average=12.0 KiB
[2021-05-06 16:27:23 INFO] Python:mod/common/system/eventHandler.py:14: size=9928 B (+9928 B), count=20 (+20), average=496 B
[2021-05-06 16:27:23 INFO] Python:lib/msgpack/fallback.py:945: size=8832 B (+8832 B), count=16 (+16), average=552 B
[2021-05-06 16:27:23 INFO] Python:lib/msgpack/fallback.py:840: size=7728 B (+7728 B), count=14 (+14), average=552 B
[2021-05-06 16:27:23 INFO] Python:mod/server/serverrpchandler.py:17: size=7320 B (+7320 B), count=5 (+5), average=1464 B
[2021-05-06 16:27:23 INFO] Python:lib/msgpack/fallback.py:600: size=5808 B (+5808 B), count=11 (+11), average=528 B
[2021-05-06 16:27:23 INFO] Python:mod/server/component/compFactoryServer.py:36: size=5080 B (+5080 B), count=18 (+18), average=282 B
[2021-05-06 16:27:23 INFO] Python:mod/server/memory/obj_report.py:45: size=4960 B (+4960 B), count=5 (+5), average=992 B
[2021-05-06 16:27:23 INFO] Python:[2021-05-06 16:27:23,494] [INFO][Engine] [QA] [DIFF_MORE]
[2021-05-06 16:27:23 INFO] Python:+205 <type 'function'>
[2021-05-06 16:27:23 INFO] Python:+41 <type 'dict'>
[2021-05-06 16:27:23 INFO] Python:+22 <type 'tuple'>
[2021-05-06 16:27:23 INFO] Python:+18 <type 'property'>
[2021-05-06 16:27:23 INFO] Python:+15 <type 'list'>
[2021-05-06 16:27:23 INFO] Python:+11 <type 'weakref'>
[2021-05-06 16:27:23 INFO] Python:+11 <type 'type'>
[2021-05-06 16:27:23 INFO] Python:+9 <type 'module'>
[2021-05-06 16:27:23 INFO] Python:+8 <type 'getset_descriptor'>
[2021-05-06 16:27:23 INFO] Python:+7 <type 'set'>
[2021-05-06 16:27:23 INFO] Python:+6 <type 'builtin_function_or_method'>
[2021-05-06 16:27:23 INFO] Python:+5 <class 'redis.connection.Token'>
[2021-05-06 16:27:23 INFO] Python:+4 <class 'server.component.engineTypeCompServer.EngineTypeComponentServer'>
[2021-05-06 16:27:23 INFO] Python:+4 <class 'neteaseBattleScript.battleCommon.battleMob.BattleMob'>
[2021-05-06 16:27:23 INFO] Python:+4 <class 'server.component.nameCompServer.NameComponentServer'>
[2021-05-06 16:27:23 INFO] Python:+1 <type 'cell'>
[2021-05-06 16:27:23 INFO] Python:+1 <class 'threading._RLock'>
[2021-05-06 16:27:23 INFO] Python:+1 <class 'redis.connection.SocketBuffer'>
[2021-05-06 16:27:23 INFO] Python:+1 <class 'redis.connection.Connection'>
[2021-05-06 16:27:23 INFO] Python:+1 <class 'redis.connection.Encoder'>
[2021-05-06 16:27:23 INFO] Python:+1 <class 'redis.connection.PythonParser'>
[2021-05-06 16:27:23 INFO] Python:+1 <class 'common.system.eventHandler.EventHandlerNoArgs'>
[2021-05-06 16:27:23 INFO] Python:+1 <type '_io.BytesIO'>
[2021-05-06 16:27:23 INFO] Python:+1 <class 'serverhttp.HttpPool'>
[2021-05-06 16:27:23 INFO] Python:+1 <class 'redis.selector.PollSelector'>
[2021-05-06 16:27:23 INFO] Python:+1 <class 'http_util.CHttpPool'>
[2021-05-06 16:27:23 INFO] Python:+1 <class 'socket._socketobject'>
[2021-05-06 16:27:23 INFO] Python:[2021-05-06 16:27:23,495] [INFO][Engine] [QA] [DIFF_LESS]
```
* 【[INFO][Engine] Top 10 differences】这句日志下面的10行代表了脚本层在两次【/check-memory-run】请求之间新申请内存最多的10行代码。注意是新申请而不是新增也就是只计算内存申请的总数不考虑是否在之后释放。
比如说【Python:redirect.py:127: size=195 KiB (+195 KiB), count=1860 (+1860), average=107 B】代表redirect.py的127行的语句一共申请了195KB的内存这一行一共执行了1860次平均每次申请107B的内存当然这些新申请的内存很可能很快就释放了一般来说top10的新申请内存都会是python的系统调用或者是引擎的调用。
* 【[INFO][Engine] [QA] [DIFF_MORE]】这句日志下面的内容,代表了脚本层在两次【/check-memory-run】请求之间新增的python对象也就是第一次调用【/check-memory-run】时还不存在但是在第二次调用【/check-memory-run】时引用计数大于零的对象这里列出的对象就是新增并且没有释放引用的对象。
比如说【Python:+4 <class 'neteaseBattleScript.battleCommon.battleMob.BattleMob'>】说明玩家登录后再退出python层新增了4个【neteaseBattleScript.battleCommon.battleMob.BattleMob】对象假如每次有玩家登录后退出都会新增4个这种对象就说明这个对象的引用管理上存在泄漏在玩家退出之后依旧保留了引用
* 【[INFO][Engine] [QA] [DIFF_LESS]】这句日志下面的neritic代表了脚本层在两次【/check-memory-run】请求之间减少的python对象也就是第一次调用【/check-memory-run】时引用计数大于零但是在第二次调用【/check-memory-run】时引用计数已经清零的对象一般来说这个信息可以用于排除一些疑似泄漏的对象可能部分对象的释放存在延时会干扰了内存泄漏的定位
## 实时定位性能问题
* 使用运营指令【/profile】测量python函数占用cpu时间。需要执行两次指令第一次开始profile第二次生成性能数据文件。性能数据文件放到可执行文件所在目录下的profile子目录中。性能数据文件名的格式profile+生成文件的时间戳
参数为:
关键字 | 数据类型| 说明
---|:---|---:
serverId |int |服务器对应ID。0表示为master-1表示所有服务器其他表示lobby/game/service的服务器ID
bBegin |bool |true开始profilefalse完成profile|
示例:
* 第一步ssh到控制服所在的服务器上执行以下指令其中4000为lobby服的服务器ID
```bash
curl -X POST '42.186.17.79:8008/profile' -H 'Content-Type: application/json' --data-raw '{"serverId" : 4000,"bBegin":true}'
```
* 第二步通过studio登录一个客户端
* 第三步,客户端登录完成后,关闭客户端
* 第四步,客户端完全退出之后,再次执行
```bash
curl -X POST '42.186.17.79:8008/profile' -H 'Content-Type: application/json' --data-raw '{"serverId" : 4000,"bBegin":false}'
```
此时可以从【netgame/app/{gameid}/lobby/lobby_lobby_4000/profile】目录中找到对应的profile_xxx文件文件内容为
```
727372 function calls (725719 primitive calls) in 1.543 seconds
Ordered by: internal time
ncalls tottime percall cumtime percall filename:lineno(function)
1356 0.259 0.000 1.169 0.001 baseSystemManager.py:137(Tick)
56989 0.107 0.000 0.107 0.000 utils.py:4(GetKey)
61020 0.096 0.000 0.182 0.000 baseSystem.py:207(GetNeedUpdate)
1400 0.066 0.000 1.543 0.001 eventBus.py:132(Post)
69156 0.056 0.000 0.056 0.000 collections.py:90(__iter__)
61051 0.051 0.000 0.066 0.000 serverSystem.py:264(_GetApp)
1356 0.050 0.000 1.341 0.001 serverApp.py:263(TickApp)
1356 0.038 0.000 0.039 0.000 interface.py:89(Tick)
62755 0.025 0.000 0.025 0.000 {method 'get' of 'dict' objects}
6780 0.023 0.000 1.393 0.000 eventHandler.py:25(Call)
1356 0.021 0.000 0.056 0.000 netServerApp.py:51(TickApp)
61020 0.020 0.000 0.020 0.000 baseApp.py:114(GetNeedUpdate)
68093 0.017 0.000 0.017 0.000 {method 'has_key' of 'dict' objects}
1356 0.017 0.000 0.029 0.000 neteaseBattleScript.battleGameObjMgrServer:73(Tick)
1356 0.015 0.000 0.022 0.000 hurtSysServer.py:17(UpdateHurt)
61067 0.015 0.000 0.015 0.000 game.py:30(GetServer)
1356 0.014 0.000 0.032 0.000 healthSysServer.py:17(UpdateHealth)
1400 0.013 0.000 0.019 0.000 eventConf.py:907(GetScriptServerEventIDList)
1356 0.013 0.000 0.052 0.000 gameSysServer.py:67(Update)
1356 0.012 0.000 0.016 0.000 netgameApp.py:10(Tick)
1275/26 0.011 0.000 0.023 0.001 fallback.py:741(_pack)
1356 0.011 0.000 0.019 0.000 mobSpawnSysServer.py:16(UpdateSystemMobSpawnServer)
1356 0.011 0.000 0.042 0.000 neteaseBattleScript.battleServerSystem:51(Update)
1356 0.010 0.000 0.057 0.000 moveSysServer.py:53(Update)
1400 0.010 0.000 0.028 0.000 serverEventBus.py:12(GetScriptEventIDList)
1356 0.009 0.000 0.010 0.000 neteaseBattleScript.battleCommon.battleGameObjMgr:89(Tick)
1360 0.009 0.000 0.013 0.000 Queue.py:93(empty)
```
* 【727372 function calls in 1.543 seconds】监控到727372次函数调用总消耗cpu时间为1.543秒
* 【ncalls】函数调用的次数
* 【tottime】函数的总的运行时间除掉函数中调用子函数后的运行时间
* 【percall】第一个等于 tottime/ncalls
* 【cumtime】表示该函数及其所有子函数的调用运行的时间即函数开始调用到返回的时间
* 【percall】第二个即函数运行一次的平均时间等于 cumtime/ncalls
* 【filename:lineno(function)】函数所在的代码位置;
## 获取性能分析火焰图
* 使用API【StartProfile】和【StopProfile】可以获取python脚本层的性能分析火焰图注意性能统计的开销较大不是很适合在正式服务器长期运行且由于当前仅有API才能驱动所以假如需要实时运行需要自己封装成运营指令。
代码示例:
```python
def DoProfile(second):
serverApi.StartProfile()
def finishProfile():
timestamp = int(time.time())
filename = "profile_%d.svg" % timestamp
serverApi.StopProfile(filename)
comp = serverApi.GetEngineCompFactory().CreateGame(serverApi.GetLevelId())
comp.AddTimer(second, finishProfile)
```
* 通过聊天或其他方式驱动上文函数后能够在【netgame/app/{gameid}/lobby/lobby_lobby_4000】目录中找到对应的profile_xxx.svg文件从服务器下载此文件后拖动到【Google Chrome】浏览器后可以看到python脚本的调用火焰图
![](./images/help_debug_01.png)
* 如火焰图所示,竖直方向表示调用栈,每一层都是一个函数。调用栈越深,火焰就越高,顶部就是正在执行的函数,下方都是它的父函数。分析性能时主要看火焰图的宽度(其中颜色没有特别意义),火焰图越宽,表示该函数对整体性能的消耗越大。因此需要对该函数进行优化。
![](./images/help_debug_02.png)
* 将鼠标放在一个函数块上时,下方会显示当前函数对应的详细信息,以冒号隔开,其中每一项分别代表:所在文件行数、函数名称、占总性能的百分比、调用次数、函数净消耗时间、函数总消耗时间。
![](./images/help_debug_03.png)
* 内存火焰图中的详细信息类似,分别代表:所在文件行数、当前选函数的这一行代码的运行内存占该函数内存的百分比、自身和调用总内存消耗、调用次数。
* 优化的核心主要是减少调用次数以及优化函数的写法。其中对于开发者而言只需要关注开发者开发的代码即可对于部分函数调用到mod框架或者引擎顶层框架进而导致性能消耗较大的可以尝试通过减少调用次数来进行优化。
* 另外火焰图支持通过右上方的Search框或者“F3”快捷键对函数关键词进行搜索。同时可以点击函数缩放查看对应的调用栈。
## 获取脚本收发包信息
* 使用API【StartRecordEvent】和【StopRecordEvent】可以获取python脚本层的收发包统计信息注意收发包统计的开销较大不是很适合在正式服务器长期运行且由于当前仅有API才能驱动所以假如需要实时运行需要自己封装成运营指令。
代码示例:
```python
def DoProfileEvent(self, second):
serverApi.StartRecordEvent()
def finishProfile():
result = serverApi.StopRecordEvent()
for eventName, data in result.iteritems():
head = "event[{}]".format(eventName)
head = head.ljust(20)
print "{} sendNum={} sendSize={} recvNum={} recvSize={}".format(head, data["send_num"], data["send_size"], data["recv_num"], data["recv_size"])
comp = serverApi.GetEngineCompFactory().CreateGame(serverApi.GetLevelId())
comp.AddTimer(second, finishProfile)
```
* 通过聊天或其他方式驱动上文函数后,能够在对应服务器进程的日志中,看到下面的日志。
```
[2021-05-07 11:03:48 INFO] Python:event[neteaseBattle:neteaseBattleDev:S2CRpcCall] sendNum=5 sendSize=1136 recvNum=0 recvSize=0
[2021-05-07 11:03:48 INFO] Python:event[neteaseBattle:neteaseBattleBeh:C2SRpcCall] sendNum=0 sendSize=0 recvNum=4 recvSize=315
[2021-05-07 11:03:48 INFO] Python:event[neteaseJewel:neteaseJewelDev:DisplayJewelBoardEvent] sendNum=1 sendSize=1008 recvNum=0 recvSize=0
```
* 日志中每一行都是一个脚本事件的收发统计服务端向客户端发送的包视为Send客户端向服务端发送的包视为Recv
* 【neteaseJewel:neteaseJewelDev:DisplayJewelBoardEvent】用【:】分开的三个字符串第一个【neteaseJewel】是脚本事件的namespace第二个【neteaseJewelDev】是脚本事件的systemName第三个【DisplayJewelBoardEvent】脚本事件的eventName。
* 【sendNum=5】代表在统计时间内此脚本事件一共发送了5次【sendSize=1136】代表在统计时间内通过此脚本事件一共发送了【1136】个字节的信息仅代表逻辑上的大小不代表实际经过网络层加密和压缩之后的传输量但依旧可以作为流量统计的标的
* 【recvNum=4】代表在统计时间内此脚本事件一共接收到了4次【recvSize=315】代表在统计时间内通过此脚本事件一共接收了【315】个字节的信息
## 获取引擎收发包信息
* 使用API【StartRecordPacket】和【StopRecordPacket】可以获取引擎层的收发包统计信息注意收发包统计的开销较大不是很适合在正式服务器长期运行且由于当前仅有API才能驱动所以假如需要实时运行需要自己封装成运营指令。
代码示例:
```python
def DoProfilePacket(self, second):
serverApi.StartRecordPacket()
def finishProfile():
result = serverApi.StopRecordPacket()
for packetName, data in result.iteritems():
head = "packet[{}]".format(packetName)
head = head.ljust(20)
print "{} sendNum={} sendSize={} recvNum={} recvSize={}".format(head, data["send_num"], data["send_size"], data["recv_num"], data["recv_size"])
comp = serverApi.GetEngineCompFactory().CreateGame(serverApi.GetLevelId())
comp.AddTimer(second, finishProfile)
```
* 通过聊天或其他方式驱动上文函数后,能够在对应服务器进程的日志中,看到下面的日志。
```
[2021-05-07 11:25:54 INFO] Python:packet[NeteaseNetGameTransferPacket] sendNum=29 sendSize=1525 recvNum=271 recvSize=14162
[2021-05-07 11:25:54 INFO] Python:packet[MovePlayerPacket] sendNum=0 sendSize=0 recvNum=4 recvSize=120
[2021-05-07 11:25:54 INFO] Python:packet[LevelChunkPacket] sendNum=15 sendSize=360 recvNum=0 recvSize=0
[2021-05-07 11:25:54 INFO] Python:packet[LevelSoundEventPacket] sendNum=18 sendSize=596 recvNum=18 recvSize=556
[2021-05-07 11:25:54 INFO] Python:packet[ActorEventPacket] sendNum=2 sendSize=8 recvNum=0 recvSize=0
[2021-05-07 11:25:54 INFO] Python:packet[MoveActorDeltaPacket] sendNum=447 sendSize=5275 recvNum=0 recvSize=0
[2021-05-07 11:25:54 INFO] Python:packet[PyRpcPacket] sendNum=5 sendSize=945 recvNum=5 recvSize=1025
[2021-05-07 11:25:54 INFO] Python:packet[SetActorMotionPacket] sendNum=32 sendSize=448 recvNum=0 recvSize=0
[2021-05-07 11:25:54 INFO] Python:packet[PlayerAuthInputPacket] sendNum=0 sendSize=0 recvNum=200 recvSize=10601
[2021-05-07 11:25:54 INFO] Python:packet[LevelEventPacket] sendNum=24 sendSize=417 recvNum=0 recvSize=0
[2021-05-07 11:25:54 INFO] Python:packet[RemoveActorPacket] sendNum=5 sendSize=30 recvNum=0 recvSize=0
[2021-05-07 11:25:54 INFO] Python:packet[UpdateBlockPacket] sendNum=1 sendSize=8 recvNum=0 recvSize=0
[2021-05-07 11:25:54 INFO] Python:packet[NetworkChunkPublisherUpdatePacket] sendNum=2 sendSize=10 recvNum=0 recvSize=0
[2021-05-07 11:25:54 INFO] Python:packet[ClientCacheMissResponsePacket] sendNum=2 sendSize=6 recvNum=0 recvSize=0
[2021-05-07 11:25:54 INFO] Python:packet[TextPacket] sendNum=1 sendSize=45 recvNum=1 recvSize=24
[2021-05-07 11:25:54 INFO] Python:packet[NetMultiUserTransferPacket] sendNum=582 sendSize=14290 recvNum=0 recvSize=0
[2021-05-07 11:25:54 INFO] Python:packet[UpdateAttributesPacket] sendNum=6 sendSize=478 recvNum=0 recvSize=0
[2021-05-07 11:25:54 INFO] Python:packet[AnimatePacket] sendNum=0 sendSize=0 recvNum=25 recvSize=75
[2021-05-07 11:25:54 INFO] Python:packet[ClientCacheBlobStatusPacket] sendNum=0 sendSize=0 recvNum=2 recvSize=40
[2021-05-07 11:25:54 INFO] Python:packet[SetTimePacket] sendNum=1 sendSize=4 recvNum=0 recvSize=0
[2021-05-07 11:25:54 INFO] Python:packet[PlayerActionPacket] sendNum=0 sendSize=0 recvNum=17 recvSize=119
[2021-05-07 11:25:54 INFO] Python:packet[SetActorDataPacket] sendNum=50 sendSize=609 recvNum=0 recvSize=0
```
* 日志中的每一行都是一个引擎消息包的收发统计服务端向客户端发送的包视为Send客户端向服务端发送的包视为Recv注意由于Apollo网络服实际上客户端是通过proxy的转发才能和特定的服务端进程交互的【NeteaseNetGameTransferPacket】就是转发消息包的包名所以这个消息包的数量基本会等于下面所有包的总和。
* 【LevelSoundEventPacket】就是引擎消息包的包名
* 【sendNum=18】代表在统计时间内此引擎消息包一共发送了18次【sendSize=596】代表在统计时间内通过引擎消息包一共发送了【596】个字节的信息仅代表逻辑上的大小不代表实际经过网络层加密和压缩之后的传输量但依旧可以作为流量统计的标的
* 【recvNum=18】代表在统计时间内此引擎消息包一共接收到了18次【recvSize=556】代表在统计时间内通过此引擎消息包一共接收了【556】个字节的信息
## Q&A
### 服务端的Mod似乎完全没有生效怎么办
studio的日志显示和日志搜索功能都是优先拉取最近一段时间的日志由于服务端进程启动的时候会刷新大量日志有可能一些早期的traceback日志都没有拉取到所以需要主动把日志窗口的滚轮滚动到顶部触发拉取更早时间的日志可能在Mod加载的时候就出现了traceback最终导致整个服务端的Mod都没有生效。修正对应的traceback后重新部署就可以了。
### 客户端的Mod似乎完全没有生效怎么办
* 检查Mod的【worlds/level/】目录下的【world_behavior_packs.json】是否存在并且其中的uuid和【behavior_packs】中的【manifest.json】中的uuid是否一致
* 检查Mod的【worlds/level/】目录下的【world_resource_packs.json】是否存在并且其中的uuid和【resource_packs】中的【manifest.json】中的uuid是否一致
```
lovecraftGuild
│ mod.sql
│ readme.txt
│ server.properties
├─behavior_packs
└─lovecraftGuildBehavior
│ manifest.json // uuid需要与worlds/level/world_behavior_packs.json一致
└─lovecraftGuildScript
├─developer_mods
├─resource_packs
└─lovecraftGuildRes
| manifest.json // uuid需要与worlds/level/world_resource_packs.json一致
└─ui
└─textures
├─worlds
└─level
| world_behavior_packs.json
| world_resource_packs.json
└─studio_res
```

View File

@@ -0,0 +1,25 @@
---
front:
hard: 进阶
time: 20分钟
---
# 性能开关
性能开关分统一和单独两种类型。
整体关闭/打开预定义的游戏原生逻辑所有的逻辑默认状态均为【开】也就是is_disable=False 只有当调用此接口关闭之后,才会进入到【关】的状态,关闭这类原生逻辑能够提高服务器的性能,承载更高的同时在线人数,同时也会使一些生存服的玩法失效。另外,强烈建议在服务器初始化时调用此接口,同时不要在服务器运行中途修改
关闭/打开某个游戏原生逻辑所有的逻辑默认状态均为【开】也就是is_disable=False
只有当调用此接口关闭之后,才会进入到【关】的状态,关闭这类原生逻辑能够提高服务器的性能,
承载更高的同时在线人数,同时也会使一些生存服的玩法失效。另外,强烈建议在服务器初始化时调用此接口,同时不要在服务器运行中途修改
性能开关<a href="../../../mcdocs/2-Apollo/4-SDK/6-大厅与游戏服API.html#性能开关" rel="noopenner"> 介绍 </a>

View File

@@ -0,0 +1,84 @@
---
front:
hard: 进阶
time: 20分钟
---
# 快速切服
### 应用场景
不同服务器之间跳转时,需要消耗一段等待时间。对于需要频繁切服的游戏类型(例如小游戏),快速切服具有重要的意义。
Apollo为以下两类跳转提供了快速切服方案。
### 跳转的服务器具有相同的mod
应用场景举例假设该游戏具有两类game服gameA、gameB部署了相同的插件。需要实现gameA到gameA或gameA到gameB之间的快速切服。
详见接口介绍:
<a href="../../../mcdocs/1-ModAPI/接口/通用/调试.html#setkeepresourcewhentransfer" rel="noopenner"> SetKeepResourceWhenTransfer </a>
<a href="../../../mcdocs/1-ModAPI/接口/通用/调试.html#getkeepresourcewhentransfer" rel="noopenner"> GetKeepResourceWhenTransfer </a>
### 跳转的服务器具有不同的mod
#### 方案1使用SetResourceFastload
设置资源快速加载,在进入游戏时快速加载资源,没有不同服资源一致的前提限制,但是速度没有使用`SetKeepResourceWhenTransfer`那么快
应用前提物品和方块的自定义贴图需要定义在item_texture.json和terrain_texture.json中
详见接口介绍:
<a href="../../../mcdocs/1-ModAPI/接口/通用/调试.html#setresourcefastload" rel="noopenner"> SetResourceFastload </a>
<a href="../../../mcdocs/1-ModAPI/接口/通用/调试.html#getresourcefastload" rel="noopenner"> GetResourceFastload </a>
#### 方案2使用SetKeepResourceWhenTransfer
合并不同服的资源,并把合并后的资源包部署到所有服,使得不同服的资源包一致,达到`SetKeepResourceWhenTransfer`的前提条件
应用前提:不同服务器之间,对原版资源的修改保持一致。(例如原生界面调整,自定义天空盒、太阳、月亮)
应用场景举例假设该游戏具有两类game服gameA部署了官方neteaseGuild插件gameB部署了官方neteaseAppear插件大厅服没有加载插件。需要实现三者之间的快速切服。
- 步骤1右键点击需要快速切服功能的服务器弹出菜单后选择“应用快速切服”
#### ![](./images/quick04.png)
- 步骤2选择应用快切之后的服务器命名将点击“应用快速切服”将生成快切网络。
![](./images/quick01.png)
- 步骤3新生成的快切网络服中大厅服、游戏服将额外生成一个commonRes的Mod并自动勾选。
![](./images/quick03_1.png)
![](./images/quick03_2.png)
![](./images/quick03_3.png)
- 步骤4调用接口<a href="../../../mcdocs/1-ModAPI/接口/通用/调试.html#setkeepresourcewhentransfer" rel="noopenner"> SetKeepResourceWhenTransfer </a>,完成快速切服设置。
- 步骤5部署完成后该网络服将拥有快速切服功能。
特别说明:
- 此方案应用的快速切服将改变原有网络服的mod结构建议在新生成的mod上完成开发调试。
- 应用快速切服实质上是对原有的mod的资源包进行合并。举例说明合并规则modA的资源包路径为modA/resource_packs/modAResmodB资源包路径为modB/resource_packs/modBRes这两部分合并到commonRes/resource_packs目录下。
- 合并过程中如果存在相对路径一样的文件如modA/resource_packs/modARes/textures/blocks/a.jpg和modB/resource_packs/modBRes/textures/blocks/a.jpg这部分资源将合并失败并在日志窗口进行报错提示。

View File

@@ -0,0 +1,226 @@
# 数据库优化小技巧
## 实用Q&A
### 如何遍历几十万行数据的大表
* 一次性读取一个拥有几十万数据的大表,很有可能会因为数据缓冲区不足导致失败,此时应该利用**ORDER BY**和**LIMIT**关键字进行分段查询
* 假设有那么一个数据表,记录了每个玩家的金币数,建表语句为:
```SQL
CREATE TABLE IF NOT EXISTS `neteaseUidToMoney` (
`uid` INT UNSIGNED NOT NULL COMMENT '玩家',
`money` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '金币',
`updateTime` INT UNSIGNED NOT NULL COMMENT '最后更新时间',
PRIMARY KEY (uid) COMMENT '主键',
INDEX `updateTime_index` (`updateTime`) COMMENT '更新时间索引'
)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
* 需要遍历此表格,示例代码为:
```Python
self.mUidToMoney = {}
baseLimit = 5000
def queryCallback(dataList):
if dataList is None:
print "[ERROR] query uidToMoney failed."
return
maxUpdateTime = 0
for data in dataList:
uid, money, updateTime = data
self.mUidToMoney[uid] = money
maxUpdateTime = max(maxUpdateTime, updateTime)
if len(dataList) < baseLimit: # 查询结果数量小于baseLimit认为是查询完成了
return
continueQueyr(maxUpdateTime, baseLimit)
# 继续查询,这里使用>=是为了防止因为Limit的原因部分updateTime=maxUpdateTime的账号还没有被查询到
# 注意事项:这里查询的结果中可能存在部分之前的查询中已经查询出的数据,加载时应该以最新的数据为准
# 重复数据的来源1之前一次查询中updateTime=maxUpdateTime的账号
# 重复数据的来源2多次查询过程中更新了自己的money数据的账号
def continueQueyr(updateTime, limit):
sql = "SELECT uid, money, updateTime FROM neteaseUidToMoney WHERE updateTime>={} ORDER BY updateTime LIMIT {}".format(updateTime, limit)
mysqlPool.AsyncQueryWithOrderKey("UidToMoney", sql, (), queryCallback)
#
sql = "SELECT uid, money, updateTime FROM neteaseUidToMoney ORDER BY updateTime LIMIT {}".format(baseLimit)
mysqlPool.AsyncQueryWithOrderKey("UidToMoney", sql, (), queryCallback)
```
### 查询结果显示错乱怎么办
* 有时候我们使用命令行工具执行SELECT操作的时候发现输出的结果乱掉了。屏幕的显示很奇怪连 MySQL 的表格分割线 | 都不见了
* 这种情况,一般都是因为表格中有字符串或者 BLOB 类型并且输出的结果中包含了一些特殊字符大部分的SSH终端会解析这些特殊字符导致显示混乱。
* 举例:
```bash
// 特殊字符【\r】表示回车。回车的意思是把光标移动到当前行的最开始(但是不换行)
echo -e "abcdefg\rzx"
// 最终的输出为
zxcdefg
// 因为【\r】会把光标回到最开头,然后继续输出 zxzx 就把 ab 覆盖掉了
```
* 一般来说简单一点的解决办法是把结果保存为文件然后用VIM打开查看
```bash
mysql -e "SELECT xxx FROM xxx" > result.txt
vim result.txt
// 对于不可见的特殊字符,在 vim 中,会以一种特殊颜色(一般为蓝色),^ 开头的符号来表示。比如如果看到了蓝色的 ^@,表示文本中出现了一个 NULL 字符
// 有少部分特殊字符,连 vim 都会解析(比如 tab 字符vim 就会解析)。在 vim 中输入命令 :set list可以让 vim 不解析所有的特殊字符,全部直接显示出来。
```
## 建表小贴士
### 字符集
* 提供给服主的数据库,默认的内部操作字符集、客户端来源数据使用的字符集、连接层字符集、查询结果字符集等这些全部默认统一使用的是 utf8mb4
* **强烈建议**建表语句中统一使用utf8mb4字符集
```SQL
CREATE TABLE IF NOT EXISTS `playerShortcut` (
`uid` INT UNSIGNED NOT NULL COMMENT '玩家',
`nickname` VARCHAR(40) NOT NULL DEFAULT '' COMMENT '昵称',
`createTime` INT UNSIGNED NOT NULL COMMENT '首次登录时间',
PRIMARY KEY (uid) COMMENT '主键'
)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- nickname是肯定需要支持中文的目前utf8mb4能够最大限度地支持中文甚至支持部分表情符号
```
### 精确存储浮点数
* 涉及需要精确数据时,**建议**使用 DECIMAL 而非 FLOAT 来存储精确浮点数,以避免精度丢失问题
```SQL
CREATE TABLE IF NOT EXISTS `Salary` (
`_id` INT UNSIGNED NOT NULL auto_increment COMMENT '唯一ID自增',
`uid` INT UNSIGNED NOT NULL COMMENT '玩家',
`salary` DECIMAL(19,4) NOT NULL DEFAULT 0 COMMENT '工资',
`updateTime` INT UNSIGNED NOT NULL COMMENT '最后更新时间',
PRIMARY KEY (_id) COMMENT '主键'
)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- DECIMAL(19,4)代表19位整数+4位小数保证加减之后的结果四舍五入能够精确到2位小数刚好精确到分
```
### 分表
* 超大的表会严重拖慢 MySQL 的读写效率,甚至对其他表的读写效率也造成影响
* **强烈建议**单表不要超过 5 千万条数据
* 为了避免 MySQL 完全卡死甚至崩溃,单表数据库**必须**不能超过 1 亿,如果超过,业务**必须**自行对数据做分表
```SQL
CREATE TABLE IF NOT EXISTS `playerChatHistroy` (
`_id` INT UNSIGNED NOT NULL auto_increment COMMENT '唯一ID自增',
`uid` INT UNSIGNED NOT NULL COMMENT '玩家',
`content` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '聊天文本',
`chatTime` INT UNSIGNED NOT NULL COMMENT '聊天发送时间',
PRIMARY KEY (_id) COMMENT '主键'
INDEX `uid_index` (`uid`) COMMENT '玩家uid索引方便检索属于某玩家的聊天记录'
INDEX `time_index` (`chatTime`) COMMENT '时间索引,方便清理过期内容'
)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 假如需要长时间保存聊天数据,那么可以创建多个分表
-- 根据uid%N的结果把聊天数据分布到对应的分表里面~~~~
```
### 单表字段数
* 对于单表的字段数,没有硬性的限制,但是**建议**单表字段数不超过 30 个。太多字段的话建议考虑垂直分表,字段遵循少、精、短的原则。
* 较少的单表字段数方便做冷热数据分离和大字段分离
* 较少的单表字段数能让内存缓存更多有效数据,从而提高 IO 效率,提高业务性能
* 后期如果需要变更表结构,较少的单表字段数让操作会更快
### 显示指定主键
* 建议每个表都显式指定主键
```SQL
CREATE TABLE IF NOT EXISTS `playerPayHistroy` (
`_id` INT UNSIGNED NOT NULL PRIMARY KEY auto_increment COMMENT '唯一ID自增', -- 显示指定主键方式一
`uid` INT UNSIGNED NOT NULL COMMENT '玩家',
`pay` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '聊天文本'
)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
--
CREATE TABLE IF NOT EXISTS `playerPayHistroy` (
`_id` INT UNSIGNED NOT NULL auto_increment COMMENT '唯一ID自增',
`uid` INT UNSIGNED NOT NULL COMMENT '玩家',
`pay` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '聊天文本',
PRIMARY KEY (_id) COMMENT '主键' -- 显示指定主键方式二
)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
### 字段特征限制
* 所有字段**建议**均显式定义为 NOT NULL如果确实有必要存 NULLNULL 浪费空间,且影响性能),则建议用 0、特殊值或空串代替 NULL 值进行逻辑处理
* 字段类型在满足需求的条件下**建议**越小越好(类型最短原则)
* **建议**业务不要往 MySQL 里面存放二进制数据,尤其是大的二进制数据,因为 MySQL 处理二进制数据的性能很低,可以使用 base64 等工具将二进制数据转换成字符型数据后再存储。
## 索引小贴士
* **建议**业务针对常见查询添加索引
* 如下的建表语句,每次玩家升级都新增一条记录,记录了每个玩家到达每个等级的时间。
```SQL
CREATE TABLE IF NOT EXISTS `playerLevelHistroy` (
`_id` INT UNSIGNED NOT NULL auto_increment COMMENT '唯一ID自增',
`uid` INT UNSIGNED NOT NULL COMMENT '玩家',
`level` INT UNSIGNED NOT NULL COMMENT '等级',
`reachTime` INT UNSIGNED NOT NULL COMMENT '达到目标等级的时间',
PRIMARY KEY (_id) COMMENT '主键'
)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
* 假如想查询2021年9月1日前等级已经升到10级的全部玩家
```SQL
SELECT uid, reachTime FROM playerLevelHistroy WHERE level=10 AND reachTime<1630425600;
```
* 添加索引前
![image.png](./images/mysql001.png)
* 添加索引后
```SQL
ALTER TABLE playerLevelHistroy ADD INDEX lv_time_index (`level`, `reachTime`);
```
![image.png](./images/mysql002.png)
### 活用EXPLAIN
* 任何新上线的 SQL**强烈建议**先EXPLAIN一下看看索引使用情况避免全表扫描。
* 还是上面的playerLevelHistroy表假如想查询指定uid的玩家每次升级的时间点
```SQL
SELECT level, reachTime FROM playerLevelHistroy WHERE uid=2147585444;
```
* EXPLAIN之后的结果为
![image.png](./images/mysql003.png)
* 没有索引的情况下性能堪虑,这里就需要添加索引
```SQL
ALTER TABLE playerLevelHistroy ADD INDEX uid_index (`uid`);
```
* 重新EXPLAIN的结果为
![image.png](./images/mysql004.png)
### 索引字段数限制
* 单个索引字段数**强烈建议**不要超过 5 个
### 单表索引数限制
* 单表索引数量**强烈建议**不超过 20 个,尽量避免冗余索引
* 索引并不是越多越好,有时在加速读的同时也引入了一些额外的写和锁开销,降低写入能力
### 索引字段特征限制
* **建议**选择区分度高的列作为索引。男女、性别这类索引基本占半的索引没用处
* 需要执行 ORDER BY和 GROUP BY 的字段**建议**建立合适的索引
* 多表 JOIN 时WHERE 条件**建议**充分利用同一张表上的索引
* **建议**不要出现超过 20 长度的 varchar 索引
## 查询/修改小贴士
### 显式指定 SELECT 的相关字段
* **强烈建议**业务逻辑中尽可能避免使用【SELECT *】
* 显式指定 SELECT 的相关字段能有效减少查询的数据总量仅SELECT 需要用到的字段)
```SQL
-- 还是上面的playerLevelHistroy表格查询指定uid的玩家每次升级的时间点
-- 使用
SELECT level, reachTime FROM playerLevelHistroy WHERE uid=2147585444;
-- 代替
SELECT * FROM playerLevelHistroy WHERE uid=2147585444;
-- 因为where限定了uid其实没有每条记录获取uid的必要同时自增长的_id也没有获取的必要
```
* 加入未来对表格进行了新增列等操作显示指定SELECT语句可以避免业务逻辑出现BUG
### 极简化使用事务
* **建议**能通过业务逻辑实现的功能,就不要使用事务
* **建议**业务尽量使用小事务而不要使用大而长的复杂事务。
### 使用IN代替OR
* 能用 IN 或者 OR 时,**建议**使用 IN 代替 OR
```SQL
SELECT uid, money FROM neteaseUidToMoney WHERE uid IN (1,2,3);
-- 代替
SELECT uid, money FROM neteaseUidToMoney WHERE uid=1 OR uid=2 OR uid=3;
```
* 如果 IN 的数量过多超过1000**建议**拆成批量的 SQL 语句
### 计数查询
* **建议**业务尽可能避免使用 SELECT COUNT 计数操作,因为该操作非常消耗资源
* 如果计数不需要非常准确,**建议**通过 SHOW TABLE STATUS 里面的 rows 值代替
```SQL
SHOW TABLE STATUS like 'playerLevelHistroy'\G;
```
![image.png](./images/mysql005.png)
* 如果计数需要非常准确,**建议**额外维护一张汇总表
### 模糊查询限制
* **强烈建议**避免左模糊或者全模糊的查找语句,这类语句无法使用索引
```SQL
-- %通配符在最右侧,那么此模糊查询可以使用索引(假如有的话)
SELECT uid, nickname FROM neteasePlayerCol WHERE nickname LIKE 'star%'
-- %通配符在左侧,那么此模糊查询就无法使用索引,只能全表扫描
SELECT uid, nickname FROM neteasePlayerCol WHERE nickname LIKE '%ship'
```
## 其他
### 判断业务需要的总连接数
* 在InitDB的时候输入的参数即为当前进程使用的数据库连接数上限假如多次调用InitDB那么上限就取多次调用的最大值
* 单个服务端进程使用的数据库连接数*服务端进程的数量,即可得出数据库连接数的上限
* 考虑到一定的余量可以在计算出的上限基础上加上300--500做为预估的数据库连接数上限
* 默认的数据库连接数上限为**5000**,假如预估的连接数上限超过这个数,那么请在正式服上线之前通知运营人员,预先调整好数据库的允许连接数

View File

@@ -0,0 +1,52 @@
# Redis缓存的使用
### 概述
Redis是一个业务逻辑为单线程的基于内存的键值数据存储可以理解为一个数据类型更简单但效率更高的service他可以实现的功能使用service也可以实现但是不需要额外编写service代码与占用额外的进程资源。
### 分布式锁
当某个业务可能在多个进程上执行又需要保证原子性时可以使用redis来添加锁。在业务前设置key获取锁业务完成后删除key释放锁。
可参考neteaseChunkRes插件
![redis1](./images/redis1.png)
### 保存全局数据
对于一些需要多个服同时频繁修改与访问的全局数据可以存放在redis上。如果一次修改或访问需要多条redis操作则还需要配合分布式锁使用。
例如
- 玩家的在线状态
可参考neteaseFriend插件玩家登录与登出时设置key打开好友界面查询好友在线状态时获取key
- 全局弹幕
可参考neteaseDanmu插件。
玩家发的弹幕缓存在server内存然后每一段时间插入到netease:danmu:list。
server每一段时间从netease:danmu:list取一部分弹幕到netease:danmu:latest分布式锁保证每7秒执行一次
server每一段时间从netease:danmu:latest取弹幕并推送到客户端。
### 作为sql的缓存
相比于直接使用server内存作为sql缓存在中间加入一层redis的好处有避免server崩溃丢失大量数据更多地减少sql请求的频率以及降低server内存峰值。
例如neteaseQuest插件
- 玩家任务进度有更新,先更新到内存
- 每隔4秒将内存数据整合写入redis
- 每隔444秒将redis数据整合写入sql

View File

@@ -0,0 +1,390 @@
# 性能优化小贴士
## 定位性能瓶颈
### 定位有性能问题的服务器
* 登录到服务器
* 使用**ps命令**可以查看当前服务器进程
```bash
ps -aux | grep mcpe
```
* 输出如下
```txt
fuzhu 12325 8.5 0.2 4177016 280364 ? Sl 11月17 91:44 /home/admin/netgame/app/1494/lobby/lobby_lobby_4000/mcpe_1494_lobby_lobby_4000 -m 8G
.
.
.
```
* 第一列为启动该进程的用户名,服务器进程一般都用**fuzhu**账号启动
* 第二列为该进程的**PID**,需要进一步查询信息需要用到
* 第三列为该进程使用掉的CPU资源百分比一般此数字最高的就是性能最有问题的
* 第四列为该进程所占用的物理内存百分比,有时候内存占用过多也可能导致卡顿
* 第五列为该进程使用掉的虚拟内存量 (Kbytes),一般无需过多关注
* 第六列为该进程占用的固定的内存量 (Kbytes),第四列很高的时候这一项一般也很高
* 最后一列为该进程的启动路径,从启动路径最后的可执行文件名,可以定位该进程是哪个服务器
* mcpe_1494_lobby_lobby_4000**mcpe**后的第一个数字1494对应的就是Studio上的项目名
![image.png](./images/profile001.png)
* mcpe_1494_lobby_lobby_4000位于**最后的数字**对应的就是Studio上的此项目的具体的服务器ID
![image.png](./images/profile002.png)
* 然后可以通过配置看到这个服务器到底使用了哪些插件加载了哪些Mod
![image.png](./images/profile003.png)
### 通过Studio获取服务端脚本性能直方图
* 部署服务器完毕后,点击【更多】
![image.png](./images/profile008.png)
* 在菜单中选中【性能监测】
![image.png](./images/profile009.png)
* 再点击【监测服务端脚本】
![image.png](./images/profile010.png)
* 左侧服务器列表中选中想要监测脚本性能的服务器这里选择了大厅服ID4000
* 然后填写性能监测的持续时间,并点击【开始监测】按钮
![image.png](./images/profile011.png)
* 监测开始后,中间显示了监测的持续时间和总进度,此时就可以登录服务器触发需要监测性能的玩法
* 可以点击下方的【结束监测】按钮随机结束监测(此时依旧能够获取到已执行时间内的监测结果)
![image.png](./images/profile012.png)
* 结束监测后,会自动切换到【查看脚本性能】分页,此时点击【查看火焰图】
![image.png](./images/profile013.png)
* 首次查看火焰图需要选择打开火焰图的浏览器这里选择【Google Chrome】即可
![image.png](./images/profile014.png)
* 在浏览器上可以查看脚本运行的火焰图,定位性能热点
![image.png](./images/profile015.png)
### 通过Studio获取客户端脚本性能直方图
* 部署服务器完毕后点击开发测试启动PC客户端登录服务器后也可以启动手机客户端然后点击【更多】
![image.png](./images/profile016.png)
* 在菜单中选中【性能监测】
![image.png](./images/profile017.png)
* 再点击【监测客户端脚本】
![image.png](./images/profile018.png)
* 左侧列表中选中想要监测脚本性能的客户端
* 然后填写性能监测的持续时间,并点击【开始监测】按钮
![image.png](./images/profile019.png)
* 监测开始后,中间显示了监测的持续时间和总进度,此时就可以在客户端触发需要监测性能的玩法
* 可以点击下方的【结束监测】按钮随机结束监测(此时依旧能够获取到已执行时间内的监测结果)
![image.png](./images/profile020.png)
* 结束监测后,会自动切换到【查看脚本性能】分页,此时点击【查看火焰图】
![image.png](./images/profile021.png)
* 首次查看火焰图需要选择打开火焰图的浏览器这里选择【Google Chrome】即可
![image.png](./images/profile014.png)
* 在浏览器上可以查看脚本运行的火焰图,定位性能热点
![image.png](./images/profile022.png)
- 特别说明:
1客户端脚本火焰图需要有账号进入游戏才开始监测。
2为保证客户端性能监测的准确性建议只开启一个客户端账号进行监测。
## 客户端常见性能问题
### ScrollView中的元素过多
* 客户端的UI界面的性能表现与UI界面上原始控件button、label、image、panel等的总数成正比一般超过1000个就会有明显可感知的卡顿感
* 隐藏使用API【SetVisible】不需要的控件可以提升整体性能但是不隐藏仅仅是不可见的控件依旧会影响UI界面的性能
* 最常遇到问题的就是ScrollVIew中因为滚动出可见窗口而不可见的部分因为一般ScrollView中不可见的控件一般远多于可见的所以需要尽量控制ScrollView中Clone的单元最好不要超过50个
### 限制贴图尺寸
* 客户端有很大的一部分内存和CPU都消耗在了解压、加载、同步贴图资源上而贴图的尺寸则是影响性能的关键无论是在UI还是在模型特效领域都是如此
* 贴图尺寸是指贴图的分辨率一张1024x1024的贴图比512x512的贴图多占用4倍的资源哪怕都只是纯色的贴图。
## 数据库与存档
### 根据需求设计合适的存储方案
* 在绝大多数的情景中,合适的设计方案都比编码层面的优化更有效,在需要持久化存储,涉及数据库操作的层面更是如此。
* 假设有这么一个需求:需要记录每个玩家每日的杀怪数量,并根据杀怪数量给予阶梯性奖励。
* 使用key-value的形式存储杀怪的数量不同的设计如下所示
* 方法1
```Python
def GetKillMob(uid):
key = "killMob:{}".format(uid)
value = dbGet(key)
if not value:
value = 0
return value
def OnKillMob(uid, number):
key = "killMob:{}".format(uid)
value = dbIncrby(key, number)
# 根据玩家杀怪进度给予奖励
CheckBonus(uid, value)
# 记录uid杀过怪Set形式的存储重复的uid在add时会被无视
globalKey = "killMob:allUser"
dbSetAdd(globalKey, uid)
# 每天的日期变更点,需要情况当日的杀怪记录
def OnMidNight():
globalKey = "killMob:allUser"
allUsers = dbSetGetall(globalKey)
if not allUsers:
return
for uid in allUsers:
key = "killMob:{}".format(uid)
dbSet(key, 0)
```
* 方法2
```Python
import time
def GetKillMob(uid):
local = time.localtime()
key = "killMob:{}:{}-{}-{}".format(uid, local.tm_year, local.tm_mon, local.tm_mday)
value = dbGet(key)
if not value:
value = 0
return value
def OnKillMob(uid, number):
local = time.localtime()
key = "killMob:{}:{}-{}-{}".format(uid, local.tm_year, local.tm_mon, local.tm_mday)
value = dbIncrby(key, number)
# 设置过期时间为1天
dbExpire(key, 86400)
# 根据玩家杀怪进度给予奖励
CheckBonus(uid, value)
```
* 很明显的方法1中的【OnMidNight】函数就是性能的瓶颈执行需要的时间和当日登陆过杀过怪物的玩家数量成正比而方法2中因为存储的key中就包含了日期就不需要日期变更点时清理杀怪数量只需要设置key过期时间为一天就行了假如是mysql也可以根据updateTime清理过期的key直接规避了最大的性能瓶颈。
### 使用定时+离线存档替代实时存档
* 根据实际更新的频率与数据重要程度,分离存档数据也是提升整体性能的妙招。
* 假设玩家有一个需要存档的数值为金币数每次杀怪都会掉落几堆金钱然后每分钟玩家都可能杀死十几只甚至几十只怪物玩法参考传奇、Diablo那么适合处理这个金币数的方式就是只在内存中更新并且每隔几分钟存档一次然后玩家离线时额外存档一次。否则多高性能的数据库也顶不住大量玩家在线时实时更新金币数的操作。
* 假设玩家有个物品栏每次关卡结束才有可能获取物品在NPC商店中才能买卖物品那么物品的变化就比较合适每次都同步到数据库因为丢失物品存档对玩家来说是非常严重且无法接受的上面的金币数没对上可能玩家都不会发觉而且物品的更新频率可能要好几分钟才一次。
## 已知的性能深坑
### 地图物品过多引发的性能问题
* 地图minecraft:filled_map是一种用于观看已探索地形的物品由于目前地图物品中的信息存储在服务端地图文件中并且向客户端同步信息的方式存在一定的问题每一个新的地图物品都会增加服务器进行的负载当一个服务器使用过在保存地图信息的情况下重启服务器依旧不会情况计数的地图物品过多时几百几千张地图会导致服务器卡顿
* 解决方法暂时只能通过禁用地图物品来避免此问题假如已经遇到了此问题那么可以通过API【ChangePerformanceSwitch】关闭地图信息记录同步来解决。
![image.png](./images/profile004.png)
![image.png](./images/profile006.png)
### 复活点与末地传送门距离过近
* 复活逻辑中,存在一些固有的限制,比如说,人物复活的地点不能离末地传送门太近
* 当前的复活逻辑中,定位复活点的方式是,在预设的复活点附近小范围内随机一个点,然后判定这个点是否符合复活的条件,假如不符合,那么就重新再随机一次,直到找到一个可用的复活点为止。但是如果附近就有末地传送门,按照人物不能复活在末地传送门附近的限制,所有可随机的点都无法复活,逻辑就会陷入死循环
* 解决方法:暂时只能限制地图中,复活点附近不能有末地传送门
### 不恰当使用API 【RegisterEntityAOIEvent】
* API【 RegisterEntityAOIEvent】在设置的区域过大时会因为扫描的范围过大导致每帧判定实体进出AOI范围时消耗很多CPU导致卡顿
![image.png](./images/profile006.png)
* 解决方法:慎用
### 不恰当使用API【SetAddArea】
* 引擎逻辑消耗的CPU基本与加载进内存的实体数量成正比而加载进内存的实体数量又和加载进内存的区块数量成正比一般来说加载进内存的区块等同于所有在线玩家的视野范围的并集但是API【SetAddArea】可以设置一些区块为常驻内存额外带来更多的运算量。
![image.png](./images/profile007.png)
* 解决方法:慎用
## 一些特定应用环境下的特殊优化方法
### 使用__slots__优化类
* 使用**__slots__**能够节省大规模节省内存占用,并且提升类属性的访问效率
#### __slots__用法举例
```Python
class A(object):
__slots__ = ['name', 'attr']
def __init__(self, name, attr):
self.name = name
self.attr = attr
```
* 定义了一个类A它有两个属性name和attr这样在A的实例中我们就可以使用name和attr这两个属性了但是使用一个没有包含在__slots__中的属性就会出错类似其他一些语言中预定义类的属性
#### 优化效果
* 使用**__slots__**的类,内存占用大概只有不使用的**三分之一**到**四分之一**
* 使用**__slots__**的类,属性访问的速度比不适用的快**10%**左右
* 使用**__slots__**的类,可以禁止外部调用胡乱给实例赋值定义之外的属性
#### 使用注意事项
* 要使用**__slots__**类必须继承自object而且为了不生成**__dict__**,子类必须定义**__slots__**
* 使用**__slots__**的类不适合多继承
#### 合适的应用场景
* 需求层面上,类的属性较多但固定,并且实例众多
## 高效Python编码建议
### 缓存属性访问值
* Python的**.**和**[]**取值效率较低,在复杂循环中,适当地使用临时变量缓存属性;或者在类中,适当使用成员变量缓存属性有助于提升性能:
```Python
# -*- coding: utf-8 -*-
import timeit
buff_data = dict({'CommonEffectArgs': 'EffectCom/buff_shutup_jh:biped Head:-1:110000:p0,-0.1,-0.5:s1.2,1.1,1.1', 'Desc': '沉默,无法释放部分技能。', 'Effect': 2, 'Icon': 'UI_bufficon_cm.png', 'Name': '沉默', 'NegType': 1, 'SubType': 1, 'Type': 2, 'iNoSkill': 2})
class Buff(object):
def __init__(self):
self._data = buff_data
self.dataGetter = buff_data.get
@property
def data(self):
return self._data
a = Buff()
b = a.dataGetter
n = 10000000
print timeit.Timer("a.data.get('CommonEffectArgs', None)", 'from __main__ import a').timeit(n)
print timeit.Timer("a._data.get('CommonEffectArgs', None)", 'from __main__ import a').timeit(n)
print timeit.Timer("a.dataGetter('CommonEffectArgs', None)", 'from __main__ import a').timeit(n)
print timeit.Timer("b('CommonEffectArgs', None)", 'from __main__ import b').timeit(n)
# 3.05556253623
# 1.56452551984 提升49%
# 1.12835684232 提升63%
# 0.834673416222 提升73%
```
### 减少函数调用层次
* Python的**函数调用开销**比较大,适当减少一些**粒度较小**的函数有助于提升性能
```Python
import timeit
def func():
1 + 1
n = 10000000
print timeit.Timer('func()', 'from __main__ import func').timeit(n)
print timeit.Timer('1 + 1', 'from __main__ import func').timeit(n)
# 0.942378586137
# 0.160280201029
```
### xrange比range更快
```Python
import timeit
print timeit.Timer('for i in range(100000000): pass', '').timeit(1)
print timeit.Timer('for i in xrange(100000000): pass', '').timeit(1)
print '*' * 50
print timeit.Timer('for i in range(10): pass', '').timeit(10000000)
print timeit.Timer('for i in xrange(10): pass', '').timeit(10000000)
print '*' * 50
print timeit.Timer("""
for i in range(1000000):
if i == 1000:
break""", '').timeit(1)
print timeit.Timer("""
for i in xrange(1000000):
if i == 1000:
break""", '').timeit(1)
# 2.50074970539
# 1.42963639366 提升43%
# **************************************************
# 4.08494359559
# 3.33126372555 提升19%
# **************************************************
# 1.0912625188 提升99.9999%
# 2.64364066034e-05
```
### 分支逻辑改成dict lookup进行分发
```Python
# 使用分支逻辑,性能较低
def GetDataFunc(eType):
if eType == 1:
return Func1
elif eType == 2:
return Func2
elif eType == 3:
return Func3
elif eType == 4:
return Func4
else:
return Func5
# 使用字典分发,分支越多,调用次数越多,性能差距越大
_FuncMap = {
1: Func1,
2: Func2,
3: Func3,
4: Func4,
}
def GetDataFunc(eType):
return _FuncMap.get(eType, Func5)
```
### 分支逻辑把命中概率的判断放在前面
* 不得已使用分支结构时,应该把命中概率高的判断放在前面,这样可以截断进入其他分支判断,提升效率
```Python
import random
# 写法1四个分支长期多次运行后概率分别为10%15%25%和50%,效率最低
number = random.randint(1, 100)
if number > 90:
DoThing1()
elif number > 75:
DoThing2()
elif number > 50:
DoThing3()
else:
DoThing4()
# 写法2四个分支长期多次运行后概率分别为50%25%15%和10%,效率最高
number = random.randint(1, 100)
if number <= 50:
DoThing4()
elif number <= 75:
DoThing3()
elif number <= 90:
DoThing2()
else:
DoThing1()
```
### 使用in而不是has_key
```Python
import timeit
n = 10000000
L = {'name': 'coco'}
print timeit.Timer('L.has_key("name")', 'from __main__ import L').timeit(n)
print timeit.Timer('"name" in L', 'from __main__ import L').timeit(n)
# 0.759608778504
# 0.362127141699 提升52%
```
### 尽量不用getattr和__getattr__
```Python
import timeit
class A(object):
def __init__(self):
self.name = 'coco'
n = 10000000
a = A()
print timeit.Timer('getattr(a, "name")', 'from __main__ import a').timeit(n)
print timeit.Timer('a.name', 'from __main__ import a').timeit(n)
# 1.11034120692
# 0.449406160554 # 提升60%
```
### 性能敏感的不用map、zip、filter
```Python
import timeit
def func(x):
pass
n = 10000000
lst1 = [1, 2, 3]
print timeit.Timer('map(func, lst1)', 'from __main__ import func, lst1').timeit(n)
print timeit.Timer("""
for x in lst1:
func(x)""", 'from __main__ import lst1, func').timeit(n)
# 6.45036315847
# 3.2444904196 提升50%
```
### 尽量避免/减少函数默认参数和拓展参数
```Python
import timeit
def func1(x, y, z):
pass
def func2(x=True, y=True, z=True):
pass
def func3(*args):
pass
def func4(**kwargs):
pass
n = 10000000
print timeit.Timer('func1(True, True, True)', 'from __main__ import func1').timeit(n)
print timeit.Timer('func2(True, True, True)', 'from __main__ import func2').timeit(n)
print timeit.Timer('func3(True, True, True)', 'from __main__ import func3').timeit(n)
print timeit.Timer('func4(x=True, y=True, z=True)', 'from __main__ import func4').timeit(n)
# 1.3778375206 提升46%
# 1.47306705548 提升42%
# 1.77330221509 提升30%
# 2.54251300749
```

View File

@@ -0,0 +1,72 @@
# 服务器部署优化
## 概述
通过对服务器部署的优化,可以最大限度地利用机器资源,提升全服承载力,同时减少服务器的卡顿,提升玩家的体验。
这篇文章提供一些服务器部署的最大人数,进程数量的建议,以及一些调整的思路来实现该目的。
一般来说proxy与game/lobby都会部署多个可以调整每个服的最大人数来提升服务器体验与cpu利用率。
## 服务器最大人数建议
- 每个proxy可承载500人左右
- 没有开启任何性能开关的生存服game/lobby最大人数建议为20~30人
- 根据不同性能开关的开启情况game/lobby可承载100~200人
因此假设全服预计同时在线人数最多为3000人game与lobby内人数上限都定为2000人则可以部署6个proxy10~20个lobby67~100个game。
## 服务器线程数据
一台物理机一般有很多个核心通过调整一台物理机上的进程数量可以充分利用cpu核心并且不超出机器的承载能力。
- Master与Service
可以认为只有一个主线程其他线程可以忽略一个master或一个service最多占用一个核心
- game与lobby
可以认为只有一个主线程其他线程可以忽略一个game或一个lobby最多占用一个核心
- proxy
可以认为有两个加解密线程两个raknet线程一个主线程加起来最多占用3个核心
因此假设有一台32核的物理机则可以部署6个proxy+14个game/lobby或者32个game/lobby。
## 服务器最大人数调优
上述只给出了一个大致的建议,但是不同玩法的服务器承载人数都不一样,因此需要根据正式上线后的性能指标来进行调优。
选择一个高峰期一般为周末晚上八点到九点观察Grafana监控的在线人数挑一个人数最多的gamelobby以及proxy
- game/lobby
登录到机器上,使用`top -Hp <进程pid>`观察线程的占用。应该保证cpu最大的线程占用率不能长时间超过80%建议常驻cpu定在70%左右。
因此假设该线程现在的常驻cpu在50%左右承载了50人那么可以调整该服最大人数为70人。如果该类型服同时在线预计2000人则一共部署29个该类型服。
而如果该线程常驻cpu已经高于70%,那么不建议增加最大人数。如果实际体验已经感到卡顿,则需要适当降低最大人数。
![image-20221103181309868](./images/opt01.png)
- proxy
登录到机器上,使用`top -Hp <进程pid>`观察线程的占用。应该保证cpu最大的线程人数多时一般会是WrapPool线程占用率不能长时间超过80%建议常驻cpu定在70%左右。
因此假设最大负载线程现在的常驻cpu在50%左右承载了300人那么一个proxy可以承载420人如果预计全服最大同时在线人数为3000人则一共部署8个proxy。
而如果最大负载线程常驻cpu已经高于70%那么不建议缩减proxy数量。如果实际体验已经感到卡顿则应该适当增加proxy数量。
![image-20221103182254187](./images/opt02.png)