zoukankan      html  css  js  c++  java
  • 腾讯云MongoDB: skip查询内核优化(转载)

    转载自:https://cloud.tencent.com/developer/article/1525137

    背景

    许多用户使用 MongoDB 存储用户的评论数据,并使用 find().skip().limit() 来实现“翻页”功能。

    比如每页有100条评论,如果要跳转到第 10 页,可以通过执行 find({}).skip(900).limit(100)获得结果。

    然而在用户实际的使用过程中,发现性能不尽如人意。特别是skip条数比较大的时候,请求执行时间特别长。

    问题分析

    MongoDB分片集群的架构如下所示。mongos作为接入层,接受客户端请求并路由到1个或者多个分片去执行,然后收集分片的执行结果,并进行过滤排序等聚合操作之后返回给客户端。

    MongoDB分片集群架构

    通过观察机器的资源使用率,我们发现mongod->mongos的网卡流量非常高,大概比mongos返回给客户端的流量要高 1~2 个数量级。如下图所示:

    mongos机器上出入流量对比

    从直观上来看,mongos接收了太多的“无用”数据,然后过滤之后再返回给客户端。


    mongos为什么会接收这么多“无用”数据呢?可以从mongos内核代码层面进行分析。

    mongos在执行客户端的查询请求时,大致会经过下面几步:

    1. 解析请求,通过查找路由表,确定具体去哪个分片或者哪几个分片执行查询请求。
    2. 解析mongos上的查询请求,并标准化成到每个分片mongod的子请求。然后选择一个TaskExecutor给分片发查询子请求,并获得分片执行的初始结果
    3. mongos端通过RouterExecStage对请求进行 sort, skip, limit 等操作,最后将整理好的结果不断传递给客户端。

    其中第 2 步 标准化子请求的流程在 transformQueryForShards 函数中实现,可以参考Github上的代码

    下面对关键代码进行分析:

    // 标准化到每个mongod分片去执行的 查询请求
    StatusWith<std::unique_ptr<QueryRequest>> transformQueryForShards(
        const QueryRequest& qr, bool appendGeoNearDistanceProjection) {
        // If there is a limit, we forward the sum of the limit and the skip.
        // 给mongod的limit = limit+skip, 也就是说:不在mongod上执行skip
        boost::optional<long long> newLimit;
        if (qr.getLimit()) {
            long long newLimitValue;
            if (mongoSignedAddOverflow64(*qr.getLimit(), qr.getSkip().value_or(0), &newLimitValue)) {
                return Status(
                    ErrorCodes::Overflow,
                    str::stream()
                        << "sum of limit and skip cannot be represented as a 64-bit integer, limit: "
                        << *qr.getLimit()
                        << ", skip: "
                        << qr.getSkip().value_or(0));
            }
            newLimit = newLimitValue;
        }
    
        // Similarly, if nToReturn is set, we forward the sum of nToReturn and the skip.
        ...
    
        auto newQR = stdx::make_unique<QueryRequest>(qr);
        newQR->setProj(newProjection);
        newQR->setSkip(boost::none);    // 不在mongod上执行 skip
        newQR->setLimit(newLimit);
        newQR->setNToReturn(newNToReturn);
    
        ...
        return std::move(newQR);
    }

    也就是说mongod会将数据都传给mongos,然后在mongos层执行skip。这种策略在请求需要到多个分片去执行的情景,是完全合理的。

    比如有 2 个分片,

    分片 1 上的数据是: 1, 2, 3, 4,5

    分片 2 上的数据是: 6, 7, 8, 9,10

    如果要执行全表扫描,并过滤最小的5个数字。mongos必须要对 2 个分片上的数据归并排序之后再执行skip。此时把skip交给mongod分片层去做是不合理的,因为在请求的开始阶段,并不能确定每个分片应该skip多少数据。


    上面的代码分析,解释了“无用”数据的合理性和必要性。但是对于某些业务场景,仍然存在很大的优化空间。

    原因在于,查询请求只发送到了某一个特定的分片上执行。比如业务使用文章的TopicId作为shardKey,此时关于这篇文章的评论数据都存在于某一个特定的分片上。

    对于定位到唯一分片的场景,可以在mongod层执行skip+limit操作,并将过滤后的结果返回给mongos;mongos对这种场景不需要执行下一步过滤,而是直接给客户端返回结果。

    这种方案在理论上能够很大程度降低mongos和mongod的压力,并大大缩短请求执行时间。

    解决方案

    基于上面的分析,我们对内核代码进行了优化,整体框架如下所示:

    mongos-skip策略优化

    测试结果

    在测试环境中创建一个分片表,然后准备测试数据,如下:

    for (var i=0;i<10;i++) {db.testcoll.insert({a:1,b:i,c:"someBigString自定义"}); sleep(10);}

    然后发起skip(5000).limit(10) 的查询请求,统计执行时间和资源消耗情况如下:

    版本对比

    请求总数

    并发数

    耗时

    网卡流量

    mongos-CPU(Peak)

    mongod-CPU(Peak)

    原有版本

    200

    5

    6.3s

    120MB/s

    30%

    13%

    优化版本

    200

    5

    0.6s

    <1MB/s

    1.7%

    14%

    CPU消耗的观测方式为top, 网卡消耗的观测方式为sar

    从测试结果来看,优化后的版本速度提升了一个数量级,而且对网卡流量的冲击下降了2个数量级。

    总结

    mongos内核在skip处理流程上存在较大的优化空间,通过区分 去往单一分片 的查询请求,可以明显节省系统资源,提升请求的执行速度。

    目前已经给官方提了 JIRA: SERVER-41329 Improve skip performance in mongos when request is sent to a single shard

    并将代码修改 PR 给了 开源社区:GitHub Commit

    腾讯云MongoDB 目前已经集成了这项优化, 欢迎体验。

  • 相关阅读:
    POJ 2195 Going Home (费用流)
    POJ 1087 A Plug for UNIX (网络流,最大流)
    凸包的直径——旋转卡壳
    凸包--Graham扫描法
    POJ 3167 Layout(差分约束)
    POJ 2187 Beauty Contest(凸包,旋转卡壳)
    HDU 1392 Surround the Trees(凸包)
    HDU 3416 Marriage Match IV(最短路,网络流)
    【USACO4.2】草地排水Drainage Ditches(最大流)
    【模板】网络最大流
  • 原文地址:https://www.cnblogs.com/xibuhaohao/p/13254704.html
Copyright © 2011-2022 走看看