zoukankan      html  css  js  c++  java
  • mongodb3.6 query plan机制变更导致慢查询问题排查

    我们在升级mongodb3.6之后,线上数据库存在大量慢查询,经过分析explain结果发现是query plan阶段耗时过长,于是我先研究了下mongodb3.6的query plan。

    query plan机制

    现有索引:

    {
        "key" : {
            "c1" : 1.0
        },
        "name" "c1_1",
        "ns" "test.test"
    },
    {
        "key" : {
            "c2" : 1.0
        },
        "name" "c2_1",
        "ns" "test.test"
    }

    首次执行查询语句:

    db.test.find({c1: 22, c2: 33});

    mongodb会检测到所有可能使用的索引,很明显我们可以使用c1_1和c2_1这两个索引,但是只能使用其中一个,这就涉及到选择使用哪个索引的问题。

    mongodb有一个算法进行选择(query plan):

    在c1_1和c2_1这两个索引B-tree(假设叫treeA、treeB)上进行扫描,对于treeA,只扫描c1=22的节点,对于treeB,只扫描c2=33的节点,两棵树的扫描过程同时进行,互不干扰。

    对于每个索引,每次扫描得到一个document,判断这个document是否同时满足c1==22&&c2==33,如果满足就在当前索引的计数+1(对应db.datas.getPlanCache().getPlansByQuery的advanced值)。

    扫描停止条件:

    1、其中任意一棵树扫描完毕

    2、其中任意一棵树advaned值达到101

    3、扫描次数达到 max(10000, collectionSize * 0.3)

    正常情况下,query plan会在101次扫描内结束,但是可能出现扫描collectionSize * 0.3次的情况,这种情况下一般是索引设计的有问题,比如有下面这样的数据:

    idc1c2
    1 1 1
    2 1 1
    3 1 1
    4 1 1
    ............
    499999 1 1

    500000

    2 2
    500001 2 2
    .............
    1000000 2 2

    前一半数据是{c1:1, c2:1},另一半数据是{ c1:2, c2:2 },执行queryPlan时,会扫描 100 0000 * 0.3 = 30 0000次,在实际查询阶段也要扫描50 0000次。plan生成好之后会缓存plan结果,下次查询不需要再执行query plan。

     综上来看,query plan算法的设计还是非常合理的。

    这个问题暂时搁置,只好又退回到mongodb3.4版本,有一次升级我拿到了具体的慢查询语句,最终找到了原因,先整理如下:

    问题描述

    mongodb线上环境从3.4升级到3.6之后,发现部分聚合语句的query plan耗时过长。

    对比分析3.6和3.4的query plan源码,分析了query plan的机制,并没有发现什么问题。

    Diagram of query planner logic

    源码分析

    第二次升级拿到了具体的具体的聚合语句,该语句$and包含12个$or子条件。通过分析mongodb3.6的源代码,发现该语句在“Evaluate Candiate Plans”阶段,每一个候选plan都扫描了61937个索引树节点,而一共有13个候选,所以一共扫描了61937*13次索引树节点,总共大约花费12s。每一个候选plan扫描61937次其实是比较正常的,而有13个候选是不正常的。又对比了3.4版本,发现3.4版本只有一个候选plan。到这里原因就比较明确了,问题出在"Generate Candidate Plans"阶段而不是之前一直怀疑的"Evaluate Candidate Plans"阶段。

    Generate Candidate Plans:根据查询语句预测可能用到哪些索引,并且预测不同的 index bounds。

    Evaluate Candidate Plans:对候选plans进行正式评估,会扫描索引树,产生IO。

    于是开始分析“Generate Candidate Plans”阶段的代码,看看到底哪里和3.4版本有大的差别:

    这一阶段会首先执行findRelevantIndices函数,该函数执行过程如下:

        1、静态扫描查询语句,得到相关列名,比如 fields[] = ["appId", "entryId", "state", "_widget_123456789"],日志中显示的是"predicate over field"

        2、执行QueryPlannerIXSelect::findRelevantIndices函数,遍历当前collection的所有索引,{ appId:1, entryId:1, state:1 } 等等,对于每一个索引,查找fields是否包含这个索引的第一列。比如对于索引 { appId:1, entryId:1, state:1 },在fields数组中查找"appId",能够找到就把这个索引加入到RelevantIndices数组;又比如对于 { expireTime: 1 },在fields数组中查找"expireTime",找不到,就舍弃;再比如对于(假设存在这个索引)  { entryId: 1, appId: 1  },在fields数组中查找"entryId",能够找到就把这个索引加入到RelevantIndices数组

    然后确认3.4和3.6版本findRelevantIndices的执行结果是一样的。

    接下来RelevantIndices会进入class PlanEnumerator类进行处理,发现这一步的执行结果在3.6和3.4中是不同的,进一步分析代码,果不其然在index_tag.cpp中改动了大量代码,加入了很多新的函数。

    3.6新特性

    下面根据具体语句来说明,先看下面的查询语句:

     

    find({a: 1, $or: [{b: 2, c: 2}, {b: 3, c: 3}]}) 

    假设有索引 {a: 1, c: 1} 和 {b: 1, c: 1},在3.6版本之前,会生成两个候选plan:

    1. 扫描索引 { a: 1, c: 1 },且扫描边界为{a: [[1, 1]], c: [["MinKey", "MaxKey"]]}
    2. 扫描索引{ b: 1, c: 1 }(针对OR条件)

    因为有$or条件的pushdown机制,条件 a: 1 会被pushdown到 $or 的所有子分支,即等价于  $or: [ { a: 1, b: 2, c: 2 }, { a: 1, b: 3, c: 3 } ]。

    3.6版本的变化是,它会认为在扫描{ a: 1, c: 1 }索引的时候,可以有两种不同的边界,一种是{a: [[1, 1]], c: [[2, 2]]},另一种是{a: [[1, 1]], c: [[3, 3]]},而不仅仅是{a: [[1, 1]], c: [["MinKey", "MaxKey"]]}。这两个边界组合生成了一个候选plan。

    3.6版本的这种机制的改变在某些时候确实起到了优化作用,扫描的索引树总节点数量变少了,减少了IO次数。

    线上问题分析总结

    下面我们来分析线上更新遇到的问题,查询语句形如:

        a: 1,
        $and: [
            { $or: [{b: 2, c: 2}, {b: 3, c: 3}] },
            { $or: [{b: 2, c: 2}, {b: 3, c: 3}] },
            { $or: [{b: 2, c: 2}, {b: 3, c: 3}] }
            ...
        ]
    });

    在3.4版本中,会认为每个$and子条件都只需要走 { b: 1, c: 1 } 这一个索引就可以了,因为index bounds都相同,即所有的$or仅需要生成一个候选plan即可。但是在3.6版本中,根据上面的分析,每个$or条件会生成不同的边界组合,所以不同的$or就不能共用一个plan了,每个$or都需要生成一个plan,这些plans都拥有不同的index bounds,而如果查询语句里有非索引字段的过滤条件,就会导致每个plans都进行全表扫描

  • 相关阅读:
    MySQL5.7修改字符集
    MySQL-day1数据库的安装与介绍
    简述Python中的break和continue的区别
    Python实现用户交互,显示省市县三级联动的选择
    Mac升级Node.js和npm到最新版本指令
    vue+Typescript初级入门
    js-md5加密
    create-react-app 工程,如何修改react端口号?
    chrome安装react-devtools开发工具插件
    mac下更新node版本
  • 原文地址:https://www.cnblogs.com/lastone/p/10534004.html
Copyright © 2011-2022 走看看