当前位置: 首页 > 后端技术 > PHP

记一个MongoDB高负载性能优化

时间:2023-03-30 01:01:53 PHP

Last-Modified:2019-06-1311:08:19本文记录某游戏服务器的性能优化。这里涉及到的技术包括:MongoDB(MMAPv1引擎),PHP随着游戏导入量的逐渐增加,单个合集的文档数已经超过400W。经常有玩家反映卡卡,尤其是迁移服务器后(从8核16G到4核8G),卡顿更严重,于是开始排查问题。确认服务器压力,首先使用top命令查看整体情况。此时CPU占用率并不高,%wa比例维持在40%左右。初步判断是磁盘IO过高。使用iotop命令查看进程粒度的io统计,发现MongoDB进程正在全速读取。使用MongoDB内置的mongostat命令发现faults字段持续高达200,也就是说每秒访问失败次数高达200,也就是数据被换出了物理内存并放入SWAP。由于没有设置swap空间,无法通过vmstat命令查看是否正在操作SWAP在mongoshell中执行db.currentOp()来确认当前有大量的操作已经执行了一个很久。至此,问题基本确定:大量查询(无论合理与否)导致MongoDB不断进行磁盘IO操作。由于内存较小(与之前的16G相比),查询到的缓存数据不断被移出内存。开始缩减单个馆藏大小的步骤主要是针对图书馆中几个特别大的馆藏,而这些馆藏中的数据并不重要,容易移动的除外。这里我们以Shop表为例(为每个玩家保存各个店铺的数据)。去掉超过N天未登录玩家的数据后,合集大小从24G减少到3G。通过减小集合大小,不仅可以提高查询效率,同时可以加快日常数据库备份速度。慢日志分析需要打开慢日志profile=1slowms=300逐一确认所有慢日志,分析执行语句问题使用xxx;db.system.profile.find({},{},20)。排序({毫秒:-1});这个时候关键点是确认执行统计字段(execStats)中的阶段是全表扫描(COLLSCAN),这是最大的性能杀手。通过Slowlog分析添加/修改索引发现大部分全表扫描的原因是游戏逻辑需要更新某些集合中满足条件的所有文档,以便定期统计排行榜......对于这些情况,它可以通过添加索引来解决。示例1:玩家等级排行榜//查询语句db.User.find({gm:0},{},100).sort({Lv:-1,Exp:-1});//移除旧索引并添加复合索引db.User.createIndex({Lv:-1,Exp:-1},{background:true});db.User.dropIndex({Lv:-1})生产环境建索引时必须加上{background:true},否则会导致很多阻塞。在删除旧索引之前,记得先建立一个新索引,避免期间出现大量的慢查询。从explain("allPlansExecution")查询分析器可以看出,初始阶段是IXSCAN,即扫描索引。例2:播放器标题处理//查询语句db.User.find({TitleData:{$exists:true}});//添加稀疏索引db.createIndex({TitleData:1},{sparse:true,background:真的});之所以使用稀疏索引是因为大多数玩家没有标题(TitleData字段)。使用稀疏索引时,只会索引存在于该字段中的文档。相比之下,在User集合中,默认的_id_索引大小为138MB,新建立的稀疏索引TitleData_1的大小仅为8KB(最小大小)。修改查询语句。由于项目代码经过多方处理,部分人员经验不足,写代码时没有考虑性能问题。因此,需要修改部分服务器代码。这部分辛苦了,一一修改,属于业务代码优化。示例1:过滤玩家//原始查询语句:发放全服奖励db.User.find({});//修改后:只过滤最近30天的登录,使用已有的索引{LastVisit:-1}db.User.find({LastVisit:{$gt:timestamp30daysago}})例2:公会成员信息//原查询语句:在User集合中搜索指定公会成员db.User.find({GuildId:xx});//修改后:利用Guild集合中已有的GuildMembers成员列表,逐一获取公会成员数据db.Guild.find({Id:xx},{Id:1,GuildMembers:1},1);db.User.find({Id:{$in:[xx,xx,xx]}})timerincreaselock早期服务器数据量小,每个分钟级别的timer可以在1分钟内顺利运行,但是一旦出现慢查询(之前优化了十多分钟),之前的timer没有跑完,下一个定时器又来了,大量慢查询语句堆积在MongoDB中导致整个数据库被拖垮,直接雪崩。这是玩家反映卡住的最直接原因。虽然经过上面的优化后不会再有超过1分钟的查询,但是多次查询累积起来可能会超过1分钟。为了避免定时器脚本堆叠,需要加锁来避免出现问题。具体的加锁方案是:memcachedredis,很简单。避免客户端超时定时器通常用于执行一些耗时的操作。除了上面的锁问题,还有一个也不容忽视:客户端超时。PHP中有些对MongoDB的操作默认是30秒,比如find()操作一旦超过30秒就会抛出“超时异常”,但此时语句仍在MongoDB实例中执行。由于定时任务没有完成,下一个定时器会继续尝试执行同样的操作。.])->超时(-1);更多优化考虑更换存储引擎:将MMAPv1换成WiredTrigger使用集群(或者简单的主从)直接在从库上操作数据导出和数据备份,进一步改造服务端的逻辑代码,将一些慢查询应用到从库上从库(主要没有)