记一次mongodb脚本的调优历程

        先说下事情的背景,公司的某个业务系统使用的是mongodb数据库,app首页是大概十几个统计维度的一个报表,可以按照时间筛选。用户组织的层级关系有点 类似集团-大区-小区-经销商这种,上一级可以点进去看下一级的数据,最后一级是经销商的数据。当前单表数据大概60w,考虑到后期的业务增长,以及app对于接口响应要求相对高一些,决定采用定时脚本的方式每晚统计历史数据保持到一张统计表,app只能查看截至前一天的数据。

        设计思路定下来之后,下面就开始写mongodb脚本了:

//统计信息数据,有10个统计维度,这里就写10个
var statInfo = [{
    "collectionName":"collection1",
    "condition":{},
    "countKey":"countKey1"
}]

//统计字段有两个:1个日期(将日期转换为年月日的格式),1个是用户里面最底的层级(这个例子里指companyId)
var statFunc = function(info){
    db.getCollection(info.collectionName).aggregate([
        {"$match":info.condition},
        {"$project": {"companyId": 1, "date": {"$dateToString": {"date": "$createTime", "format": "%Y-%m-%d", "timezone": "Asia/Shanghai"}}}},
        {"$group": {"_id": {"companyId": "$companyId",
                            "date": "$date"}, "count": {"$sum": 1}}}
    ]).forEach(x =>{
        
//将统计结果一一遍历更新到统计表,依据查询条件,没有就插入,有就更新
db.getCollection("company_daily_report").update({"companyId":x._id.companyId,"date":x._id.date},{"$set":{[info.countKey]:x.count}},true,false)
    })  
}

//遍历统计各个维度
statInfo.forEach(info => statFunc(info))

        上面这个例子是通常的mongodb脚本形式,由于之前写过很多,包括维护前面同事留下的脚本。这种形式统计的话,时间会很长:按照统计内容的不同,少则几十分钟,多则好几小时,开发调试阶段时,非常耽误工作。于是决定放弃这种写法,准备尝试用python实现下,看能不能有所改善。

import pymongo

url = 'xxxxxxxxxxxxxxxxx'

client = pymongo.MongoClient(url)

db = client.xxx

# 统计信息
statInfo = [{"collectionName":"collection1","condition":{},"countKey":"countKey1"}]

# 统计函数
def statFun(info):
    pipeline = [
        {"$match": info.condition},
        {"$project": {"companyId": 1, "date": {"$dateToString": {"date": "${}".format(
            dateKey), "format": "%Y-%m-%d", "timezone": "Asia/Shanghai"}}}},
        {"$group": {"_id": {"companyId": "$companyId",
                            "date": "$date"}, "count": {"$sum": 1}}}
    ]
    result = db.get_collection(info.collectionName).aggregate(pipeline)
    for item in result:
        filter = {"companyId": item["_id"]
              ["companyId"], "date": item["_id"]["date"]}
        update = {"$set": {info.countKey: item["count"]}}
        db.get_collection("company_daily_report").find_one_and_update(
            filter, update, upsert=True)

# 遍历统计信息进行统计
for info in statInfo:
    statFun(info)
    

         上面这种形式,只是js脚本的python翻译而已,显然不会有太大性能提升,尝试执行了一次我就放弃了,十多个统计维度,十几分钟了第一个维度都还没有执行完。其实考虑用python写的一个主要原因是想用下python的多线程。下面加入多线程改善下代码:

import pymongo
import concurrent.futures

url = 'xxxxxxxxxxxxxxxxx'

client = pymongo.MongoClient(url)

db = client.xxx

# 统计信息
statInfo = [{"collectionName":"collection1","condition":{},"countKey":"countKey1"}]

def updateCompanyDailyStat(item, countKey):
    filter = {"companyId": item["_id"]
              ["companyId"], "date": item["_id"]["date"]}
    update = {"$set": {countKey: item["count"]}}
    db.get_collection("company_daily_stat").find_one_and_update(
        filter, update, upsert=True)

# 统计函数
def statFun(info):
    pipeline = [
        {"$match": info.condition},
        {"$project": {"companyId": 1, "date": {"$dateToString": {"date": "${}".format(
            dateKey), "format": "%Y-%m-%d", "timezone": "Asia/Shanghai"}}}},
        {"$group": {"_id": {"companyId": "$companyId",
                            "date": "$date"}, "count": {"$sum": 1}}}
    ]
    result = db.get_collection(info.collectionName).aggregate(pipeline)
    # 加入多线程进行结果保持
    with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
       futures = [executor.submit(updateCompanyDailyStat,item, info.countKey) for item in result]
       results = [f.result() for f in concurrent.futures.as_completed(futures)]
       print("info.countKey:", len(results))

# 遍历统计信息进行统计
for info in statInfo:
    statFun(info)
    

        引入多线程后效果明显,十多个统计维度执行下来,合计耗费25分钟左右,这可是个人以及公司前人们都没有达到的速度,效果很满意,目标达成!

        可上线没几天,问题来了,原因是pc端有个类似的报表,当时由于各种因素,做的是实时统计,看最近一段时间范围的数据的话,响应时间在1-2秒,勉强可以接受。但是问题不在这里,问题在有几个统计维度,历史数据是可能会少量变化的,于是有人就拿pc和app开始对数了,怎么两边数据不一致?解释一通理解了,但是还是得要求两边数据一致,结果就是app也得做成实时统计。好了,前面工作白费。。。不情愿归不情愿,工作还得做!

        最直接的肯定是把app的对应接口重写一遍,改成实时统计,不过这个工作耗时相对长一点,短期内工作排期有点安排不开。于是想着能不能再把脚本优化下,缩短下执行时间,从每天执行一次,改成每天执行多次。

        很多时候,从0做到90很简单,从90做到100却很难。

        下面的优化,直接写答案吧,艰难过程不表,耗时大半天时间。

        1、把多线程的最大线程数从20改成了100,执行时间从25分钟降到了 20分钟左右;

        2、在统计表创建索引{"company":1,"date":-1},一般我们可能是在有数据后才创建索引,但是这里如果先创建索引的话,会提升upsert的速度;

        3、把管道里面的$project省略掉,日期转换表达式写在$group里面,把一个聚合查询从3.6s优化到了2.6s左右,节约时间1s:

import pymongo
import concurrent.futures

url = 'xxxxxxxxxxxxxxxxx'

client = pymongo.MongoClient(url)

db = client.xxx

# 统计信息
statInfo = [{"collectionName":"collection1","condition":{},"countKey":"countKey1"}]

def updateCompanyDailyStat(item, countKey):
    filter = {"companyId": item["_id"]
              ["companyId"], "date": item["_id"]["date"]}
    update = {"$set": {countKey: item["count"]}}
    db.get_collection("company_daily_stat").find_one_and_update(
        filter, update, upsert=True)

# 统计函数
def statFun(info):
    pipeline = [
        {"$match": info.condition},
        {"$group": {"_id": {"companyId": "$companyId",
                            "date": {"$dateToString": {"date": "$createTime", "format": "%Y-%m-%d", "timezone": "Asia/Shanghai"}}}, "count": {"$sum": 1}}},
    ]
    result = db.get_collection(info.collectionName).aggregate(pipeline)
    # 加入多线程进行结果保存
    with concurrent.futures.ThreadPoolExecutor(max_workers=100) as executor:
       futures = [executor.submit(updateCompanyDailyStat,item, info.countKey) for item in result]
       results = [f.result() for f in concurrent.futures.as_completed(futures)]
       print("info.countKey:", len(results))

# 遍历统计信息进行统计
for info in statInfo:
    statFun(info)
    

        4、其实整个脚本耗时占比最大的不是聚合查询,是将查询结果遍历更新到统计表,下面使用了内置函数$merge,省掉了遍历,整个脚本耗时从20分钟降到了10秒,服务器资源占用率也极大降低,执行前需要创建一个唯一索引{"company":1,"date":-1},否则会报错:

import pymongo
import concurrent.futures

url = 'xxxxxxxxxxxxxxxxx'

client = pymongo.MongoClient(url)

db = client.xxx

# 统计信息
statInfo = [{"collectionName":"collection1","condition":{},"countKey":"countKey1"}]

# 统计函数
def statFun(info):
    pipeline = [
        {"$match": info.condition},
        {"$group": {"_id": {"companyId": "$companyId",
                            "date": {"$dateToString": {"date": "$createTime", "format": "%Y-%m-%d", "timezone": "Asia/Shanghai"}}}, "count": {"$sum": 1}}},
        {"$project": {"_id": 0, "companyId": "$_id.companyId",
                      "date": "$_id.date", info.countKey: "$count"}},
        {'$merge': {
            "into": "company_daily_report",
            "on": ["companyId", "date"],
            "whenMatched": "merge",
            "whenNotMatched": "insert"
        }}
    ]
    return db.get_collection(info.collectionName).aggregate(pipeline)

# 加入多线程进行统计
with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
    futures = [executor.submit(statFun,info) for info in statInfo]
    results = [f.result() for f in concurrent.futures.as_completed(futures)]
    print("size:", len(results))
    

        到这里,本次脚本调优就算结束了。虽然后面还是需要编写实时统计相关的接口,不过本次调优的历程及相关优化方法,为以后的开发积累了宝贵经验。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值