本周主要工作是解决 dbd 的多线程 bug,可是失败了。

为了重现这个多线程的 bug,准备模拟多用户登录的情景,当多个用户需要保存数据的时候,就会跑到多线程序列化数据的部分,应该可以重现这个 bug。于是,从周一到周三,花了三天时间,用 python+gevent 写了一个模拟客户端,实现了客户端连接服务器和登录的流程。其实这个测试工具,在上一个项目里已经写了一点了,基本数据类型的解包比如整型,字符串解包已经有了。但是,我翻看代码才发现,当时没有写打包协议的实现写的过程比较流畅,用了 mixin 的方式将 rpc 协议独立出来一个 RpcMixin 类,这个类最后会被 SimulateUser 类继承,而 SimualteUser 则负责提供连接好的 socket,以及相应的 greenlet 协程、登录用户名。唯一比较严重的 bug,是启动时并发多个 greenlet 协程的时候,用了 range 而非 xrange,又手残多填了几个 0,结果 CPU 和内存都耗在了产生 range(1000000000) 这个巨大无比的整数列表上了。

解决完以上的问题,周四到周六就开始正式的压测过程。压测出来的结果比较诡异,主要情况是多玩家登录后,再断开连接,而 gamed(游戏逻辑进程)和 dbd(游戏数据储存进程)依然保持着高水位内存。有一次还显示 2000 多个在线玩家,但模拟客户端已经全部断开连接了。这个有可能是进程最大 fd 连接数引起的,错误处理没做好,fd 最大值设成 200000 后就没重现过这个问题了。

维持高水位内存,目前猜测是两种可能。一是内部实现了内存池之类的设施,一经申请后再不释放,因为上到高水位后,断掉模拟客户端,再重新连接进行压测,内存也不再增长,只保留在高水位。二是有内存泄露,因为高水位后断开模拟客户端,过一晚上之后内存还有轻微增长。翻看了一下引擎的文档,里面有一点前辈留下的关于内存泄露排查的记录。里面提到的主要是脚本层的内存泄露排查,提供了一些 simul_efun 去 dump 出所有内存里的脚本 object,各个 object 的引用数和大小。排查了一遍 object,只有一屏幕不到的类型,很快确定没有问题。单纯客户端连接登录,不做任何操作,脚本层对象的大小才 600+MB,这时候已经差不多 11000 的模拟用户登录了。然后再排查了一下基本类型,主要是字符串,mapping,array 这三种。数据也比较正常,无法拼凑出 gamed 1G 多的内存占用。

EDIT:gamed 引擎里的确有 pool 的概念,一经申请再不释放。在玩家管理里,用到了 user_stack,断线释放的玩家结构数据块都会放在这里。