资讯专栏INFORMATION COLUMN

记一次MongoDB高负载的性能优化

huhud / 2581人阅读

摘要:年月日本文是关于记录某次游戏服务端的性能优化此处涉及的技术包括引擎随着游戏导入人数逐渐增加单个集合的文档数已经超过经常有玩家反馈说卡特别是在服务器迁移后从核降到核卡顿更严重了遂开始排查问题确认服务器压力首先使用命令查看总体情况此时占用不高

Last-Modified: 2019年6月13日11:08:19

本文是关于记录某次游戏服务端的性能优化, 此处涉及的技术包括: MongoDB(MMAPv1引擎), PHP

随着游戏导入人数逐渐增加, 单个集合的文档数已经超过400W, 经常有玩家反馈说卡, 特别是在服务器迁移后(从8核16G降到4核8G), 卡顿更严重了, 遂开始排查问题.

确认服务器压力

首先使用top 命令查看总体情况, 此时cpu占用不高, %wa比例维持在40%左右, 初步判断是磁盘IO过高

使用iotop命令以进程粒度来查看io统计, 发现MongoDB进程全速在读操作.

使用MongoDB自带的mongostat 命令, 发现 faults字段持续高达200以上, 这意味着每秒访问失败数高达200, 即数据被交换出物理内存, 放到SWAP

由于未设置交换空间, 因此无法通过 vmstat 命令查看是否正在操作SWAP

在mongo shell中执行 db.currentOp() 确认当前存在大量执行超久的操作

到了此时基本确定问题所在了: 大量的查询(先不管是否合理)导致MongoDB不断进行磁盘IO操作, 由于内存较小(相较之前的16G)导致查询过的缓存数据不断被移出内存.

开始处理 减小单个集合大小

这一步骤主要是针对库中几个特别大的集合, 且这些集合中的数据不重要且易移除.

此处以Shop表为例(保存每个玩家各种商店的数据), 在移除超过N天未登录玩家数据后, 集合大小从24G降为3G

通过减小集合大小, 不仅可以提高查询效率, 同时可以加快每天的数据库备份速度.

慢日志分析

需要打开慢日志

profile=1
slowms=300

逐条确认所有慢日志, 分析执行语句问题

use xxx;
db.system.profile.find({}, {}, 20).sort({millis:-1});

此时的重点在于确认执行统计字段(execStats)中 阶段(stage)是全表扫描(COLLSCAN)的, 这是最大的性能杀手.

增加/修改索引

通过慢日志分析, 发现大部分全表扫描的原因在于:

排行榜定期统计

游戏逻辑需要对某些集合中符合条件的所有文档 update

针对这几种情况, 可以通过增加索引来解决.

举例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:true});

之所以使用稀疏索引, 是因为大部分玩家是不具有称号(TitleData字段), 使用稀疏索引时只会索引存在该字段的文档, 通过对比, User集合中, 默认的 _id_ 索引大小138MB, 刚建立的稀疏索引TitleData_1大小仅为8KB(最小大小).

修改查询语句

由于项目代码经过多手, 部分人员经验不足, 代码编写时未考虑到性能问题.

因此需要改造部分服务端代码, 这部分就是苦力活了, 逐个去修改, 属于业务代码优化.

举例1: 筛选玩家

// 原查询语句: 发放全服奖励
db.User.find({});

// 修改后: 筛选仅最近30天登陆, 利用现有索引 {LastVisit:-1}
db.User.find({LastVisit:{$gt: 30天前的时间戳}})

举例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]}})
定时器增加锁

早期服务器数据量较小时, 每个分钟级定时器都能顺利在1分钟内跑完, 但一旦出现慢查询(未优化之前出现过十几分钟的), 上一个定时器未跑完, 下一个定时器又来了, 大量的慢查询语句堆在MongoDB中导致整个数据库被拖垮, 直接雪崩. 这是玩家反馈卡顿的最直接原因.

尽管经过上面优化后不会出现一个查询1分钟以上这种情况, 但是多个查询累加起来, 也有可能超过1分钟.

为了避免定时器脚本堆叠, 因此需要加个锁, 避免出现问题.

具体的加锁方案有:

memcached

redis

很简单.

避免客户端超时

定时器通常是用于执行一些耗时操作, 除了上面的锁问题外, 还有一个不可忽视的: 客户端超时.

PHP中对MongoDB的一些操作, 默认是30秒, 比如 find() 操作一旦超过30秒会抛出 "超时异常", 然而此时该语句还在MongoDB实例中执行.

由于定时任务未完成, 下一个定时器来的时候还是会继续尝试进行同样的操作..

解决方案很简单, 以php代码为例

$mongo->selectCollection("xx")->find([...])->timeout(-1);
更多的优化考虑

更换存储引擎: 将 MMAPv1 替换为 WiredTrigger

使用集群(或简单的主从), 将数据导出及数据备份等直接从库上操作, 更进一步是改造服务端逻辑代码, 将部分慢查询应用到从库中(主要不要)

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/31697.html

相关文章

  • 一次MongoDB负载性能优化

    摘要:年月日本文是关于记录某次游戏服务端的性能优化此处涉及的技术包括引擎随着游戏导入人数逐渐增加单个集合的文档数已经超过经常有玩家反馈说卡特别是在服务器迁移后从核降到核卡顿更严重了遂开始排查问题确认服务器压力首先使用命令查看总体情况此时占用不高 Last-Modified: 2019年6月13日11:08:19 本文是关于记录某次游戏服务端的性能优化, 此处涉及的技术包括: MongoDB...

    vibiu 评论0 收藏0
  • 一次PHP并发性能调优实战 -- 性能提升104%

    摘要:这是多处理器系统中,调度器用来分散任务到不同的机制,通常也被称为处理器间中断,。文章编写计划 待完成: 详细介绍用到的各个工具 作者: 万千钧(祝星) 适合阅读人群 文中的调优思路无论是php, java, 还是其他任何语言都是用. 如果你有php使用经验, 那肯定就更好了 业务背景 框架及相应环境 laravel5.7, mysql5.7, redis5, nginx1.15 cento...

    番茄西红柿 评论0 收藏0
  • 一次PHP并发性能调优实战 -- 性能提升104%

    摘要:这是多处理器系统中,调度器用来分散任务到不同的机制,通常也被称为处理器间中断,。文章编写计划 待完成: 详细介绍用到的各个工具 作者: 万千钧(祝星) 适合阅读人群 文中的调优思路无论是php, java, 还是其他任何语言都是用. 如果你有php使用经验, 那肯定就更好了 业务背景 框架及相应环境 laravel5.7, mysql5.7, redis5, nginx1.15 cento...

    xeblog 评论0 收藏0
  • 2017年3月份前端资源分享

    平日学习接触过的网站积累,以每月的形式发布。2017年以前看这个网址:http://www.kancloud.cn/jsfron... 03月份前端资源分享 1. Javascript 175453545 Redux compose and middleware 源码分析 深入 Promise(二)——进击的 Promise Effective JavaScript leeheys blog -...

    ermaoL 评论0 收藏0

发表评论

0条评论

huhud

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<