MongoDB之初级性能优化
最近在调试Django后台与MongoDB的接口时发现了一个性能上的问题,在解决该问题的时候发现了自己很多知识上的盲点,因此记录下来,供大家参考
文章目录
一、问题发现
1.1 背景描述
所调试的工程项目是一个基于python2的Django+MongoDB的Web项目,主要是从推特上抓取数据存入数据库,经过一些处理之后将处理后的数据存入数据库,并且通过浏览器显示在前端。在对原始推文进行处理时,第一步就是预处理。具体说来就是遍历数据库中所有的推文数据,对每条推文都进行命名实体识别等处理。由于预处理过程较慢,采用了多进程对其进行加速,即一次从数据库中读取若干条推文(如1w条),均分给每个进程进行预处理,并将处理之后的结果回写到数据库
1.2 遇到的问题
在实际操作过程中发现,当预处理程序进行一段时间之后发现,整个预处理模块越跑越慢。
1.3 问题定位
利用time模块对每一个小步骤进行测时,最终发现是每次从数据库中读取数据所花时间越来越多,遂查看源码寻找问题,最终将问题定位到查询数据库的语句,,截取代码片段如下:
i = 0
count = 10000 #每次返回的推文数量
condition = '此处填查询的条件'
whlie(True):
i = i + 1
tweets = database.find(condition).limit(count).skip(i * count)
#忽略预处理程序代码
- 实际上是此处的skip()方法消耗了大量时间
二、问题分析与解决
2.1 关于skip()方法
- skip方法是对查询结果的游标进行处理的一种方法,搭配limit方法可以实现精确截取查询结果中的某一片段的数据。如查询结果是第1到100名学生的成绩,但是我们只关注中等生,即第20-60名的成绩,此时可以用
find().skip(20).limit(40)
控制只返回这40名中等生的成绩,这样就可以过滤掉一些不需要返回的数据,减少传输消耗 - skip方法不适用于与limit方法搭配对数据库进行遍历(如上文1.3中的代码块所示)。这是由skip方法的实现机制决定的,在官方文档中也有描述:
Using cursor.skip():
The cursor.skip() method requires the server to scan from the beginning of the input results set before beginning to return results. As the offset increases, cursor.skip() will become slower
Using Range Queries:
Range queries can use indexes to avoid scanning unwanted documents, typically yielding better performance as the offset grows compared to using cursor.skip() for pagination
skip方法的内部实现机制通俗说来就是“挨个数”。在遍历数据库的应用场景中,如果使用find().skip(100000).limit(10000)
,其实现过程是先将查询结果全部准备好,然后从结果中的第一个文档开始数,数到第100000个文档时,开始提取,根据limit方法的限制从第100001个文档提取10000个文档作为最终结果向客户端返回查询最终结果
随着预处理过的数据量不断增大,花在挨个数不需要的文档上的时间也就越来越多了,这也是实际项目中出现问题的原因
2.2 改进方法
根据官方文档的建议,可以采取如下几种方式进行改进:
- Choose a field such as _id which generally changes in a consistent
direction over time and has a unique index to prevent duplicate
values - Query for documents whose field is less than the start value using the $lt and cursor.sort() operators, and
- Store the last-seen field value for the next query.
概括起来,就是控制查询条件来实现遍历数据库这类操作,常采用的查询条件可以是_id
或者是其他带有自增性的键进行定位
这里采用了基于_id
的精确查询来作为改进,但是在使用的时候也有限制,具体可见下节关于ObjectId的描述
三、知识扩展
3.1 关于ObjectId
- ObjectId使用12字节存储空间。前四个字节是以秒为单位的时间戳,然后是三个字节的所在主机标识符,通常是机器主机名的hash值,再之后是两个字节的PID,最后三个字节是一个自动递增的计数器
- 前9字节保证了同一秒钟不同机器不同进程产生的ObjectId是唯一的,后3 字节就是一个自动增加的计数器,确保相同进程同一秒产生的ObjectId 也是不一样的
- ObjectID提供了全局唯一性但是只提供了秒级的自增性,之所以不提供严格意义的自增性是因为自增性与分布式相违背。在高并发时无法做到全局自增性
- 默认情况下ObjectId是由客户端生成的,并不是不设置就由服务端生成的
- ObjectId是可被排序的,也可以被比较大小的
3.2 实践建议
- 当存入数据的数据量较大时,建议采用同一进程的多线程而不建议使用多进程。因为MongoDB会根据进程的PID在没有指定
_id
的时候使用ObjectId作为_id
- 同一进程多个线程存储数据时会尽可能的保证
_id
的自增性