db系统遇到的问题
1. linux服务器cpu使用率异常高
现象:
内网服务器cpu使用率较高,达到80%多,启动game服务器非常慢。- top命令查看内网服务器的cpu使用率,发现排第一的是redis进程;其次是mysql进程;再次是一堆py进程(在内网linux服务器上,启动了25个左右的py脚本,每个py脚本就是一个db服务,对应负责1个game服务器的数据业务:CRUD。
- 观察py进程,分析哪个进程占用的cpu最多。检查该py进程对应的game服务器,数据或者业务上是否有什么异常。
- 发现该服务器对应的redis缓存数据有点异常,设置的缓存数量是20,但该服务器缓存了130条player数据。
- 所以是代码bug,导致缓存数量突破上限。随后复查代码,修正bug。
2. cpu使用率异常高
- 还是cpu使用率异常,但这次未发现明显异常的game服务器。
- 当game服务器发送请求到redis队列中,game服务器需要轮询来判断请求执行的状态(执行失败、执行成功等)。
- 在一般情况下,py脚本处理的速度较快,不会发生拥挤情况。但当多个服务器同时进行世界地图存盘操作时。(项目是slg类型,世界地图数据比较大,世界地图被划分为100个格子,以格子序号来分批进行存储。)
- 此时redis处理数据存储,本身就已经很耗性能了。而此时game服务器发送过来的查询请求,进一步增加了redis进程的负载,导致拥塞情况的加剧。(就好像你在写代码,本身已经在消耗脑力了。此时策划坐在你旁边,不断的问你,好了没有?好了没有?好了没有?)
- 解决方案:后续计划将请求队列的数据结构由sortset修改为list,因为list支持阻塞方式(blpop)查询请求是否执行完毕,而不需要dbsdk一直轮询来判断请求是否执行完毕。
- 之前设计上:
命令队列和结果队列,都是使用sortset来存放。
- 关于blpop,后面又发生了一个小插曲。(早期的py-redis库中,blpop实现又bug,可能队列中有数据,但是blpop会无法取出数据,从而导致直接卡死。后续解决方案是升级到最新的py-redis库,修复卡死。)
3. 命令被覆盖
- mgr和game都引用了dbsdk,在启动初期的数据库表和zcfg校验环节,有一些请求会出现覆盖的现象。
- 例如:show tables命令,进入request队列是被封装为下列结构:
{请求唯一oid,请求}
。由于request队列是sortset类型,val值相同(都是show tables
)的话,先放进去的show tables
会被后到来的直接覆盖掉。导致游戏上层没有收到该命令的执行结果,无法正常启动。 - 例如,game服务器发送请求
{1,show tables}
,此时py脚本还未取出请求。mgr服务器立马发来请求{2,show tables}
。此时{1,show tables}
被覆盖了。导致game服务器会一直等待{1,show tables}
的执行结果,但却再也等不到了。
4. rapidjson转换接口使用不正确
- cache队列中,save_time字段的最后面多了个\u000,导致py脚本在定时将redis cache队列中的数据,同步到mysql时,组装的sql语句执行失败。
5. 考虑内存不足的处理方案
select * from t_map
,在game启动时加载世界地图,当游戏后期数据量大的时候,python或game服务器内存不够,就可能导致加载失败,需要设计方案处理内存不足的情况:
- 批次载入:先count(*)一下,查询出t_map表一共有多少行数据。比如count=1000,那么将1000行数据分10次载入,每次载100行。
- 可能每次载100行数据所组成的数据包,其大小仍旧超过网络框架代码的限定值,那么此时就必须完善
分包传输机制
了。
6. redis监控
redis-cli monitor
:查看redis进程当前正在执行的命令
7.请求执行的唯一性
- 一条命令请求,应该只能被成功执行一次。
- 之前请求队列的数据结构是sortset,由于代码bug,导致:执行失败的命令,没有从请求队列中删除,导致该命令会被循重复执行。
- 现在请求队列是list类型,py脚本在Update中blpop弹出请求。此时会面临一个问题:当弹出的请求执行失败时(比如mysql断开连接了),我们要怎么办?因为请求已经从redis队列中弹出,我们没法再发起第2次执行行为。
8.命令队列、结构队列,修改为list结构,避免一直轮询,效率低下
- 用list的阻塞接口 blpop
- 复杂命令可以用json封装一下,然后rpush到list中。取出的时候,再用json解析。
9.BUG
- api接口支持一次执行多条sql语句
- 支持多线程进行阻塞redis读取,进一步降低cpu使用率
- 当select空表时,返回的结果集中不要填充内容
- Update & Insert:effectRows、laseOId、异常的errMsg
- 列类型要正确区分:null和其它数据类型(目前:sql组成的临时表被select时,如果第一行为null,则列类型被设置为null,导致后面行的数据不能正确读取)
10.hiredis阻塞
Redis作为一个高性能的内存数据库,在实际应用中可以很好的解决cache,数据共享等问题。但客户端采用hiredis的时候,需要注意几点:
-
对于block方式的操作:在block操作模式下,每个命令都是单独发向Redis的,且也会等待每个的结果返回。即,通过redisCommand调用,即完成了向Redis发送命令,也等待Redis返回结果。换言之,是一个阻塞的过程。如果在是并发量比较高的情况下,此种方式,会使得客户端效率较差。即便是将Redis部署在本地,通过local进行访问,也会有0.02毫秒级别的等待过程。如果客户端有很多次的访问操作,每次都会阻塞一小段时间,会使得客户端本身的处理速度降下来。为了降低这种开销,可以采用pipeline的方式。也即一次向Redis发送多个命令。将这多个命令发送完毕后,再进行统一处理。当然,这样的代价是客户端的发送缓冲区会变大,接收缓冲区也会变大。但处理的吞吐量会增加。
-
对于non-block方式的操作:在这种模式下,默认的调用redisCommand就会立即返回,相当于block模式下显式的调用AppendCommand。不过在这种模式下,要显示的调用获取回应的replay,然后将reply释放掉,以免造成内存泄漏。
上面说的,都是同步的操作过程。操作过程和客户端的程序是在一个进程里完成的。另外还可以采用异步的方式。
11.编码解析问题
因为横跨游戏层、dbsdk层、redis、python、mysql
,所以命令请求的传输方式使用json格式来传输。但是json不支持直接存储二进制数据。所以用base64算法加密二进制数据,转换为字符串,然后json.dump封装到json中
,在dbsdk、redis、python之间传输。
12.nohup.out文件过大
- 清空文件内容。这个不需要关闭服务,但是如果项目多,nohup.out 文件多 ,不好定位。
echo '' > nohup.out
13.LoadPlayer时,player_oid为0
- 起因:JH502账号无法登陆
- 原因:LoadPlayer时,db返回的player_oid为0
- 怀疑有两种情况:
- redis缓存数据不对。
- 写入到mysql表中的blob数据,已经出错了。
- 先验证3.1,把redis缓存数据手动删除掉。然后登陆账号,发现可以正常登陆。那么说明mysql表中数据时正确的,redis缓存数据出错了。
- redis缓存数据是在python脚本中插入的,且删除缓存以后再登陆,走的是Select操作。那么开始排查Select操作的代码。
- 然后发现在select出结果集rows时,会对row中的
bytes
数据,进行base64.encode()操作。注意这个函数的返回值仍然是bytes
类型。此时再传递到写redis的接口时,接口内部会对row再进行一次base64.encode()。导致blob数据player_game_data被两次base64了。 - 那么在dbsdk中,就无法再解析出正确的数据。