mmzb 游戏事故分析
最近一次线上更新,老项目挂了,遍地哀嚎,日活跃掉了好多,心痛。。。
这次维护时,SA 为了缩减硬件资源,做了一次数据库迁移。给到开发手上的 player db,只有一些索引数据,不带有任一玩家数据。玩家上线的时候,skynet 自动从 redis-persist 服务中拉取(redis-persist 是一个基于旁路监听的 redis 落地程序,相对独立,参见 之前的博文 )。这也是 RP 服务的第一次高强度使用。按之前的策略,每次玩家下线都会在 RP 服务中存档,三个月不上线的玩家,会自动从 redis 数据库中移除,以节约内存。当流失玩家再度上线时,skynet 先从 redis 中直接查询,无法加载到玩家的话,再度从 RP 服务中拉取,可以简单的将 RP 服务视作 redis 的一个二级缓存。玩家下线后,数据继续在 redis 中保留三个月。所以,设计之初,RP 服务就是作为一个低频服务存在的,没有考虑到大量加载玩家的情况。
这次开服前,SA 有同步到这个情况,我们以为只是玩家上线登录会慢一点而已,结果换来的却是惨痛的教训。上线后,因为 redis 里面空空如也,而各种排行榜和群雄逐鹿玩法,需要拉取玩家的基本信息,诸如名字、uid、等级、公会、性别等等基础数据。这是依赖于一个 profile 服务实现的。而 profile 加载玩家信息的接口,又跟玩家登录所用的接口不一样,没有实现从 RP 服务中加载。这是当时 RP 服务上线时,特地干掉相应接口的,因为当时已经考虑到 profile 服务会是一个高频访问,容易压垮 RP 服务。在策划的再三要求下,开发迫不得已把这个接口加回去,免得玩家信息空白。结果,上线一个小时左右,skynet 就因为占用内存太大,直接挂掉了。
事后分析,应该是 profile 服务太慢了,需要一个个从磁盘上加载玩家数据,虽然自身有缓存,但是压上来的请求数量太多,请求也没有做去重(比如同一个 uid,只对 RP 服务发出一次请求)。返回结果时比较缓慢,玩家就直接退出重新登录,之前查询到的数据作为临时表,已经没有接收者了(原有的 session 已经消失),于是变为 GC 的压力,最后把系统压垮了。再次启动上线后,关闭群雄逐鹿玩法,希望能减少 profile 服务的压力,结果轮到战斗搜索出问题了,线上搜索玩家进行攻击,依赖于 redis 里的数据,由于 redis 里玩家太少(只有刚刚登录上的),无法搜索到有价值的对手。开发这边就通过运营导出最近玩家数据,将热玩家数据重新导入 redis 里,解决问题。
这次事故里,profile 服务因为单点故障,备受诟病。但是,从一个高度依赖 redis 内存数据库的服务器里,转到主要依赖磁盘的数据库上,这个改动就不是一个小改动。不止 profile 服务,还有其他服务也依赖于 redis 数据库(比如战斗搜索),正因为 redis 的访问代价低,代码里的 cache 和预加载就可以少写一点。如果相关玩法的假设是,数据都从磁盘加载,排行榜和群雄逐鹿玩法就可以先单独从磁盘上加载好自己要用的玩家数据,不需要依赖 profile 服务了。相反,SA 部署新的 redis 实例的时候,将那部分热门玩家先加载到内存中,让 skynet 直接在 redis 就可以取到数据,不需借道 RP 服务,也可以解决问题。往后这数据加热,还是应该自动化完成,只是先前数据都在内存,不需要加热,就没这个问题。
退一步说,假设各个玩法依然从 profile 里加载基本信息,如果 profile 服务可以过滤重复请求,就可以释放掉 RP 服务的压力。这时候,压力就集中在 profile 服务的消息队列上了,由于磁盘加载速度慢,依然会有大量的请求积压。云风曾经提过 一个 skynet.call 的超时处理方案 ,如果这个方案再改动一下,发现某服务大量积压请求时,自动随机抛弃新请求,profile 服务就可以得到解放了。