目录

  • 背景
  • 初步分析
  • 索引seeks的原因
  • 优化思路
  • 小结

声明:本文同步发表于 MongoDB 中文社区,传送门:
http://www.mongoing.com/archives/27310

背景

最近线上的一个工单分析服务一直不大稳定,监控平台时不时发出数据库操作超时的告警。
运维兄弟沟通后,发现在每天凌晨1点都会出现若干次的业务操作失败,而数据库监控上并没有发现明显的异常。
在该分析服务的日志中发现了某个数据库操作产生了 SocketTimeoutException

开发同学一开始希望通过调整 MongoDB Java Driver 的超时参数来规避这个问题。
但经过详细分析之后,这样是无法根治问题的,而且超时配置应该如何调整也难以评估。

下面是关于对这个问题的分析、调优的过程。

初步分析

从出错的信息上看,是数据库的操作响应超时了,此时客户端配置的 SocketReadTimeout 为 60s。
那么,是什么操作会导致数据库 60s 还没能返回呢?

业务操作

左边的数据库是一个工单数据表(t_work_order),其中记录了每张工单的信息,包括工单编号(oid)、最后修改时间(lastModifiedTime)
分析服务是Java实现的一个应用程序,在每天凌晨1:00 会拉取出前一天修改的工单信息(要求按工单号排序)进行处理。
由于工单表非常大(千万级),所以在处理时会采用分页的做法(每次取1000条),使用按工单号翻页的方式:

  • 第一次拉取
db.t_work_order.find({"lastModifiedTime":{$gt: new Date("2019-04-09T09:44:57.106Z"),$lt: new Date("2019-04-09T10:44:57.106Z")}, "oid": {$exists: true}}).sort({"oid":1}).limit(1000)
  • 第二次拉取,以第一次拉取的最后一条记录的工单号作为起点
db.t_work_order.find({"lastModifiedTime":{$gt: new Date("2019-04-09T09:44:57.106Z"),$lt: new Date("2019-04-09T10:44:57.106Z")}, "oid": {$exists: true, $gt: "VXZ190"}}).sort({"oid":1}).limit(1000)

..

根据这样的查询,开发人员给数据表使用的索引如下:

db.t_work_order.ensureIndexes({"oid" : 1,"lastModifiedTime" : -1
})

尽管该索引与查询字段基本是匹配的,但在实际执行时却表现出很低的效率:
第一次拉取时间非常的长,经常超过60s导致报错,而后面的拉取时间则会快一些

为了精确的模拟该场景,我们在测试环境中预置了小部分数据,对拉取记录的SQL执行Explain:

db.t_work_order.find({"lastModifiedTime":{$gt: new Date("2019-04-09T09:44:57.106Z"),$lt: new Date("2019-04-09T10:44:57.106Z")}"oid": {$exists: true}}).sort({"oid":1}).limit(1000).explain("executionStats")

输出结果如下

"nReturned" : 1000,
"executionTimeMillis" : 589,
"totalKeysExamined" : 136661,
"totalDocsExamined" : 1000,..."indexBounds" : {"oid" : [ "[MinKey, MaxKey]"],"lastModifiedTime" : [ "(new Date(1554806697106), new Date(1554803097106))"]
},
"keysExamined" : 136661,
"seeks" : 135662,

在执行过程中发现,检索1000条记录,居然需要扫描 13.6 W条索引项!
其中,几乎所有的开销都花费在了 一个seeks操作上了。

索引seeks的原因

官方文档对于 seeks 的解释如下:
The number of times that we had to seek the index cursor to a new position in order to complete the index scan.

翻译过来就是:
seeks 是指为了完成索引扫描(stage),执行器必须将游标定位到新位置的次数。

我们都知道 MongoDB 的索引是B+树的实现(3.x以上),对于连续的叶子节点扫描来说是非常快的(只需要一次寻址),那么seeks操作太多则表示整个扫描过程中出现了大量的寻址(跳过非目标节点)。
而且,这个seeks指标是在3.4版本支持的,因此可以推测该操作对性能是存在影响的。

为了探究 seeks 是怎么产生的,我们对查询语句尝试做了一些变更:

去掉 exists 条件

exists 条件的存在是因为历史问题(一些旧记录并不包含工单号的字段),为了检查exists查询是否为关键问题,修改如下:

db.t_work_order.find({"lastModifiedTime":{$gt: new Date("2019-04-09T09:44:57.106Z"),$lt: new Date("2019-04-09T10:44:57.106Z")}}).sort({"oid":1}).limit(1000).explain("executionStats")

执行后的结果为:

"nReturned" : 1000,
"executionTimeMillis" : 1533,
"totalKeysExamined" : 272322,
"totalDocsExamined" : 272322,..."inputStage" : {"stage" : "FETCH","filter" : {"$and" : [ {"lastModifiedTime" : {"$lt" : ISODate("2019-04-09T10:44:57.106Z")}}, {"lastModifiedTime" : {"$gt" : ISODate("2019-04-09T09:44:57.106Z")}}]
}, ..."indexBounds" : {"oid" : [ "[MinKey, MaxKey]"],"lastModifiedTime" : [ "[MaxKey, MinKey]"]
},
"keysExamined" : 272322,
"seeks" : 1,

这里发现,去掉 exists 之后,seeks 变成了1次,但整个查询扫描了 27.2W 条索引项! 刚好是去掉之前的2倍。
seeks 变为1次说明已经使用了叶节点顺序扫描的方式,然而由于扫描范围非常大,为了找到目标记录,会执行顺序扫描并过滤大量不符合条件的记录
在 FETCH 阶段出现了 filter可说明这一点。与此同时,我们检查了数据表的特征:同一个工单号是存在两条记录的!于是可以说明:

  • 在存在exists查询条件时,执行器会选择按工单号进行seeks跳跃式检索,如下图:

  • 在不存在exists条件的情况下,执行器选择了叶节点顺序扫描的方式,如下图:

gt 条件和反序

除了第一次查询之外,我们对后续的分页查询也进行了分析,如下:

db.t_work_order.find({"lastModifiedTime":{$gt: new Date("2019-04-09T09:44:57.106Z"),$lt: new Date("2019-04-09T10:44:57.106Z")}, "oid": {$exists: true, $gt: "VXZ190"}}).sort({"oid":1}).limit(1000).explain("executionStats")

上面的语句中,主要是增加了$gt: "VXZ190"这一个条件,执行过程如下:

"nReturned" : 1000,
"executionTimeMillis" : 6,
"totalKeysExamined" : 1004,
"totalDocsExamined" : 1000,..."indexBounds" : {"oid" : [ "(\"VXZ190\", {})"],"lastModifiedTime" : [ "(new Date(1554806697106), new Date(1554803097106))"]
},
"keysExamined" : 1004,
"seeks" : 5,

可以发现,seeks的数量非常少,而且检索过程只扫描了1004条记录,效率是很高的。
那么,是不是意味着在后面的数据中,满足查询的条件的记录非常密集呢?

为了验证这一点,我们将一开始第一次分页的查询做一下调整,改为按工单号降序的方式(从后往前扫描):

db.t_work_order.find({"lastModifiedTime":{$gt: new Date("2019-04-09T09:44:57.106Z"),$lt: new Date("2019-04-09T10:44:57.106Z")}, "oid": {$exists: true}}).sort({"oid":-1}).limit(1000).explain("executionStats")

新的"反序查询语句"的执行过程如下:

"nReturned" : 1000,
"executionTimeMillis" : 6,
"totalKeysExamined" : 1001,
"totalDocsExamined" : 1000,..."direction" : "backward",
"indexBounds" : {"oid" : [ "[MaxKey, MinKey]"],"lastModifiedTime" : [ "(new Date(1554803097106), new Date(1554806697106))"]
},
"keysExamined" : 1001,
"seeks" : 2,

可以看到,执行的效率更高了,几乎不需要什么 seeks 操作!
经过一番确认后,我们获知了在所有数据的分布中,工单号越大的记录其更新时间值也越大,基本上我们想查询的目标数据都集中在尾端

于是就会出现一开始提到的,第一次查询非常慢甚至超时,而后面的查询就快了。

上面提到的两个查询执行路线如图所示:

  • 加入$gt 条件,从中间开始检索

  • 反序,从后面开始检索

优化思路

通过分析,我们知道了问题的症结在于索引的扫描范围过大,那么如何优化,以避免扫描大量记录呢?
从现有的索引及条件来看,由于同时存在gt、exists以及叶子节点的时间范围限定,不可避免的会产生seeks操作,
而且查询的性能是不稳定的,跟数据分布、具体查询条件都有很大的关系
于是一开始所提到的仅仅是增加 socketTimeout 的阈值可能只是治标不治本,一旦数据的索引值分布变化或者数据量持续增大,可能会发生更严重的事情。

回到一开始的需求场景,定时器要求读取每天更新的工单(按工单号排序),再进行分批处理
那么,按照化零为整的思路,新增一个lastModifiedDay字段,这个存储的就是lastModifiedTime对应的日期值(低位取整),这样在同一天内更新的工单记录都有同样的值。

建立组合索引 {lastModifiedDay:1, oid:1},相应的查询条件改为:

{"lastModifiedDay": new Date("2019-04-09 00:00:00.000"),"oid": {$gt: "VXZ190"}
}
-- limit 1000

执行结果如下:

"nReturned" : 1000,
"executionTimeMillis" : 6,
"totalKeysExamined" : 1000,
"totalDocsExamined" : 1000,..."indexBounds" : {"lastModifiedDay" : [ "(new Date(1554803000000), new Date(1554803000000))"],"oid" : [ "(\"VXZ190\", {})"]
},
"keysExamined" : 1000,
"seeks" : 1,

这样优化之后,每次查询最多只扫描1000条记录,查询速度是非常快的!

小结

本质上,这就是一种空间换时间的方法,即通过存储一个额外的索引字段来加速查询,通过增加少量的存储开销提升了整体的效能。
在对于许多问题进行优化时,经常是需要从应用场景触发,适当的转换思路。
比如在本文的问题中,是不是一定要增加字段呢?如果业务上可以接受不按工单号排序进行读取,那么仅使用更新时间字段进行分页拉取也是可以达到效果的,具体还是要由业务场景来定。

作者: 华为云合作专家美码师(zale)

MongoDB 谨防索引seek的效率问题【华为云技术分享】相关推荐

  1. 【华为云技术分享】“技术-经济范式”视角下的开源软件演进剖析-part 3

    4. 微观层面 4.1 个体动机 在开源软件发展之初, 商业组织的投入很少甚至没有, 完全是靠Richard Stallman 或者 linus Torvalds 这样的个人在努力推动开源软件艰难前行 ...

  2. 【华为云技术分享】三大前端技术(React,Vue,Angular)探密(下)

    [华为云技术分享]三大前端技术(React,Vue,Angular)探密(上) [Angular] Angular(通常被称为 "Angular 2+"或 "Angula ...

  3. 【华为云技术分享】“技术-经济范式”视角下的开源软件演进剖析-part 1

    前言 以互联网为代表的信息技术的迅猛发展对整个经济体系产生了巨大的影响.信息技术的发展一方面使知识的积累和传播更加迅速,知识爆炸性的增长:另一方面,使信息的获取变得越来越容易,信息交流的强度逐渐增加, ...

  4. 【华为云技术分享】直播回顾丨激发数据裂变新动能,HDC.Cloud云数据库前沿技术解读

    3月24日14:00-17:00,HDC.Cloud开发者沙龙系列云数据库专场直播线上开启,此次华为云数据库通过三场直播从NoSQL数据库新技术.数据库迁移.行业解决方案等方面对云端数据库进行深度解读 ...

  5. 【华为云技术分享】云图说|人工智能新科技—文字识别服务

    在日常生产和生活中,我们往往要处理大量的文字.报表和文本.为了减轻人们的劳动,提高工作效率,华为云文字识别服务应用而生.您可以调用服务提供的文字识别API接口,将我们日常中大量的证件.票据.表格识别成 ...

  6. 【华为云技术分享】MongoDB经典故障系列三:副本集延迟太高怎么办?

    MongoDB副本集延迟太高,数据读取时间过长怎么办?不要慌,菊长教您一个小妙招:在集合创建的时候,就建立好索引,然后按照索引去寻找您所需要的数据.如果觉得比较麻烦,华为云文档数据库服务DDS了解一下 ...

  7. 【华为云技术分享】从自建MongoDB聊聊云数据库MongoDB的蓬勃张力

    在很长一段时间内,企业为了自身发展大多选择自建数据库,而随着企业的发展壮大和数据量的猛增,自建数据库越来越不能满足企业对数据库提出的高要求,为了更好地管理和使用海量数据,越来越多企业选择把云下数据库迁 ...

  8. 【华为云技术分享】HBase与AI/用户画像/推荐系统的结合:CloudTable标签索引特性介绍

    标签数据已经成为越来越普遍的一类数据,以用户画像场景最为典型,如在电商场景中,这类数据可被应用于精准营销推荐.常见的用户画像标签数据举例如下: 基础信息:如性别,职业,收入,房产,车辆等. 购买能力: ...

  9. 【华为云技术分享】MongoDB经典故障系列五:sharding集群执行sh.stopBalancer()命令被卡住怎么办?

    [摘要] MongoDB sharding集群执行sh.stopBalancer()命令时被卡住怎么办?别慌,华为云数据库来给您支招,收下这份方案指南,让您分分钟远离被自建MongoDB数据库支配的恐 ...

最新文章

  1. jquery查找ul属性不是hide,jQuery的ul显示/隐藏功能
  2. 【转】MySQL日期函数与日期转换格式化函数大全
  3. 花体字转换器微信小程序源码支持多种花样字体不同风格
  4. Linux-3.10-x86_64 内核配置选项简介
  5. 分布式事务处理【TX-LCN】
  6. 常用物流快递单号自动识别api接口-快递鸟对接
  7. matplotlib——画布分辨率和尺寸
  8. 计算机里的le是什么符号,在python中传递le或ge符号
  9. 哲理故事与管理之道 10 -你还在崇拜交付速度吗
  10. Android仿今日头条的开源项目
  11. 如何打破微信不能群发群消息的局限
  12. 【k8s的持久化存储】PV、PVC、StorageClass讲解
  13. Macbook pro2019 13.3寸 开机问题 APPLE客服协助解决,体验感超棒
  14. loadrunner之获取登陆接口中的token值及 LoadRunner数据更新与更新方式
  15. jQuery Chosen 使用
  16. Hadoop基础-Idea打包详解之手动添加依赖(SequenceFile的压缩编解码器案例)
  17. 用java分析班级成绩
  18. spss分析方法-两个关联样本检验(转载)
  19. 【C++初阶】:动态管理
  20. 【云周刊】第134期:阿里云发布ECS企业级产品家族 19款实例族涵盖173个应用场景

热门文章

  1. 2017甘肃省计算机二级考试,甘肃省2017年计算机二级考试网上报名须知及流程
  2. mysql repos_mysql yum源安装
  3. 计算机英语初级考试时间,2020年考研考试时间安排及考试科目_考研网
  4. 电脑如何进入bios模式_如何进入BIOS设置U盘启动盘
  5. html的title设置,动态设置html的title
  6. java的向下转型_终于搞明白向下转型的作用了,还不懂的进来看下.
  7. log日志java web_Javaweb项目中使用Log4j记录日志
  8. Linux学习笔记(一)——简介
  9. 前端写分页(用了自己同事写的插件)
  10. 如何查看linux命令源代码(转自网络)