前言:
生产环境有一个用户反馈,突然有一天find得出的结果集顺序不一致,随协助排查,在此将整个异常的过程复盘,并记录于此;
环境:
mongodb version:4.4
架构:副本集
数据:假设数据在find测试时不会再发生变化
异常现象:
业务通过db.xxx.find({filed:"xxx"}).limit(10) 突然在某天的一个时间点后,返回结果集的顺序前后不一致了;
复盘前的思考:
在find({a:xxx}).limit()时,如果全表扫描,在同一节点上返回的结果集顺序是否一致?
在find({a:xxx}).limit()时,如果全表扫描,在同一副本集不同节点上,返回结果集顺序是否一致?
在find时指定了sort排序,但全表扫描,在不同节点上的结果集顺序是否一致?
在find({a:xxx}).limit()时,如果查询a条件使用了索引,则在同一节点上的结果集顺序是否一致?
在find({a:xxx}).limit()时,如果查询a条件使用了索引,则在不同一节点上的结果集顺序是否一致?
在find({a:xxx,b:xxx}).limit()时,如果查询a条件使用了索引,则在不同一节点上的结果集顺序是否一致?
生产故障原因:
生产故障查询结果集顺序在某一时间点前后不一致,导致此问题的原因是:
业务使用的find({filed:xxx}).limit(10), filed字段上无索引,即全表扫描查询,且未明确指定sort排序规则;
在某时间点mongodb服务端发生了election切主动作
故业务层其实是在新主和旧主上进行的查询,而mongodb存在一些内部机制会导致未指定sort排序时结果集未必准确的情况;
复盘过程:
1.准备测试数据
通过如下脚本方式批量写入测试数据10万条
#!/bin/bash
# 定义要使用的城市数组
cities=("beijing" "xingtai" "tianjin" "shanghai" "guangzhou" "shenzhen" "chengdu" "hangzhou" "wuhan" "nanjing")
# MongoDB连接设置
db_name="testdb4"
collection_name="usertable"
# 初始化插入批次
batch_size=100
batch_data=""
# 执行插入操作
for ((i=1; i<=100000; i++))
do
# 计算当前应该使用的城市
city_index=$(( (i - 1) / 1000 % ${#cities[@]} ))
city=${cities[$city_index]}
# 生成单条记录
record="{num: $i, city: \"$city\"}"
# 拼接到批次中
if [[ -z "$batch_data" ]]; then
batch_data="$record"
else
batch_data="$batch_data, $record"
fi
# 每 batch_size 条执行一次 insertMany
if (( i % batch_size == 0 )); then
mongo_cmd="db.getSiblingDB('${db_name}').getCollection('${collection_name}').insertMany([$batch_data])"
# 执行MongoDB插入命令
echo "$mongo_cmd" | /usr/local/mongodb40/bin/mongo 10.203.xxxx:xxxx/admin -uxxxx -pxxxx --quiet --eval "$mongo_cmd"
# 打印进度
echo "Inserted $i documents..."
# 清空批次数据
batch_data=""
fi
done
# 如果有剩余数据未写入,处理最后一批
if [[ -n "$batch_data" ]]; then
mongo_cmd="db.getSiblingDB('${db_name}').getCollection('${collection_name}').insertMany([$batch_data])"
echo "$mongo_cmd" | /usr/local/mongodb40/bin/mongo 10.203.xxxx:xxxx/admin -uxxxx -pxxxx --quiet --eval "$mongo_cmd"
echo "Inserted the last batch of documents."
fi
echo "Finished inserting 100000 documents."
2.思考问题验证
思考1:在find({a:xxx}).limit()时,如果全表扫描,在同一节点上返回的结果集顺序是否一致?
回答:从测试结果看是一致
7465:PRIMARY> db.tob_tb2.find({city:"tianjin"}).limit(5)
{ "_id" : ObjectId("674ec7d0468ed3da23198c24"), "num" : 2001, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c25"), "num" : 2002, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c26"), "num" : 2003, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c27"), "num" : 2004, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c28"), "num" : 2005, "city" : "tianjin" }
7465:PRIMARY> db.tob_tb2.find({city:"tianjin"}).limit(5)
{ "_id" : ObjectId("674ec7d0468ed3da23198c24"), "num" : 2001, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c25"), "num" : 2002, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c26"), "num" : 2003, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c27"), "num" : 2004, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c28"), "num" : 2005, "city" : "tianjin" }
思考2:在find({a:xxx}).limit()时,如果全表扫描,在同一副本集不同节点上,返回结果集顺序是否一致?
回答:从测试结果上结果集顺序是不一致
Primary节点的查询结果:
7465:PRIMARY> db.tob_tb2.find({city:"tianjin"}).limit(5)
{ "_id" : ObjectId("674ec7d0468ed3da23198c24"), "num" : 2001, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c25"), "num" : 2002, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c26"), "num" : 2003, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c27"), "num" : 2004, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c28"), "num" : 2005, "city" : "tianjin" }
Secondary上的查询结果
7465:SECONDARY> db.tob_tb2.find({city:"tianjin"}).limit(5)
{ "_id" : ObjectId("674ec7d0468ed3da23198c2d"), "num" : 2010, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c2f"), "num" : 2012, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c27"), "num" : 2004, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c2a"), "num" : 2007, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c41"), "num" : 2030, "city" : "tianjin" }
思考3:在find时指定了sort排序,但全表扫描,在不同节点上的结果集顺序是否一致?
回答:这种情况肯定结果集是一致的,因为明确告诉mongodb的sort()函数的结果排序规则
思考4:在find({a:xxx}).limit()时,如果查询a条件使用了索引,则在同一节点上的结果集顺序是否一致?
回答: 结果是一致的,没有发生变化
思考5:在find({a:xxx}).limit()时,如果查询a条件使用了索引,则在不同一节点上的结果集顺序是否一致?
回答:不同节点上的查询结果集是不一致的
--创建索引
db.tob_tb2.createIndex({city:1},{background:true})
Primary节点上查询结果集
7465:PRIMARY> db.tob_tb2.find({city:"tianjin"}).limit(5)
{ "_id" : ObjectId("674ec7d0468ed3da23198c24"), "num" : 2001, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c25"), "num" : 2002, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c26"), "num" : 2003, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c27"), "num" : 2004, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c28"), "num" : 2005, "city" : "tianjin" }
Secondary节点查询结果集
7465:SECONDARY> db.tob_tb2.find({city:"tianjin"}).limit(5)
{ "_id" : ObjectId("674ec7d0468ed3da23198c2d"), "num" : 2010, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c2f"), "num" : 2012, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c27"), "num" : 2004, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c2a"), "num" : 2007, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c41"), "num" : 2030, "city" : "tianjin" }
思考6:在find({a:xxx,b:xxx}).limit()时,如果查询a条件使用了索引,则在不同一节点上的结果集顺序是否一致?
回答:
Primary节点上查询
7465:PRIMARY> db.tob_tb2.find({city:"tianjin",num:{$lte:2100}}).limit(5)
{ "_id" : ObjectId("674ec7d0468ed3da23198c24"), "num" : 2001, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c25"), "num" : 2002, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c26"), "num" : 2003, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c27"), "num" : 2004, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c28"), "num" : 2005, "city" : "tianjin" }
Secondary节点上查询
7465:SECONDARY> db.tob_tb2.find({city:"tianjin",num:{$lte:2100}}).limit(5)
{ "_id" : ObjectId("674ec7d0468ed3da23198c2d"), "num" : 2010, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c2f"), "num" : 2012, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c27"), "num" : 2004, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c2a"), "num" : 2007, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c41"), "num" : 2030, "city" : "tianjin" }
验证数据的严谨性
我们指定sort排序看看主从结果集顺序是否一致,可以看到如下主从在通过主键排序后,数据是一致的,不存在数据缺失导致上述测试结果错误的情况;
Primary节点
7465:PRIMARY> db.tob_tb2.find({city:"tianjin",num:{$lte:2100}}).**sort({_id:1})**.limit(5)
{ "_id" : ObjectId("674ec7d0468ed3da23198c24"), "num" : 2001, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c25"), "num" : 2002, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c26"), "num" : 2003, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c27"), "num" : 2004, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c28"), "num" : 2005, "city" : "tianjin" }
Secondary节点:
7465:SECONDARY> db.tob_tb2.find({city:"tianjin",num:{$lte:2100}}).**sort({_id:1})**.limit(5)
{ "_id" : ObjectId("674ec7d0468ed3da23198c24"), "num" : 2001, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c25"), "num" : 2002, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c26"), "num" : 2003, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c27"), "num" : 2004, "city" : "tianjin" }
{ "_id" : ObjectId("674ec7d0468ed3da23198c28"), "num" : 2005, "city" : "tianjin" }
python驱动
python代码查询返回的是否与上述MongoDB shell下结果一致?
通过python代码分别连接Primary,Secondary节点进行不调用索引的方式查询,结果与上述测试一致
3.原理解析
3.1 natural order
mongodb在数据写入集合后,是按照一定规则将数据存放到storage
存储层面的,称之为natural order
,从官网的文档描述也可以看出来
参考:https://www.mongodb.com/docs/manual/reference/glossary/#term-natural-order
解释:在一个单独实例上执行集合扫描的默认排序顺序是natural order;
在副本集中,natural order是无法保证一致的,并且副本集节点间的natural order可能是不同的;
对于分片集合,natural order没有被定义,但是可以用$natural去强制在每一个分片节点上执行集合扫描;
还有另外一段话:
When executing a find() with no parameters, the database returns objects in forward natural order.
For standard tables, natural order is not particularly useful because, although the order is often close to insertion order, it is not guaranteed to be. However, for Capped Collections, natural order is guaranteed to be the insertion order. This can be very useful.
对于一个find()
方法且没有指定sort排序,MongoDB数据库将以natural order
来返回结果集,对于普通集合来说,natural order
不一定就是插入顺序,无法保证相同;
这个storage层面的natural order可能受到很多因素影响,而导致顺序有变:
副本集Secondary节点同步,因为Secondary节点是根据oplog日志顺序重放写操作;但每一个副本集成员节点在维护本地数据文件时natural order可能不太相同;
compact操作,在集合出现碎片空洞时,执行compact可以重新整理数据,以释放不需要的数据块,此操作后,可能会导致natural order发生变动;
新增Secondary节点并通过内部初始化数据方式拉取数据,这样也可能会导致natural order发生变动;
backup/restore, 通过备份文件恢复出来的实例,natural order也有可能发生变动
再极端一点,如果insert的多,导致数据页分裂,则也有可能导致natural order发生变动;
3.2 如果使用了索引查询呢?
就像上面思考3中测试的,即使使用了索引字段作为查询条件,但是在返回的结果集中,数据limit少量时,也可能时不相同的;
我们知道MongoDB中索引是Btree结构,如果使用了索引,将以找到的数据顺序返回document,但是这个顺序不一定是插入顺序,例如a字段的重复值很多,那么相同a字段值在索引树中,不一定是按照_id主键进行存放 或者说不一定是按照_id被找到的,这就意味着Primary,Secondary节点上在a字段查询条件值相同情况下,每行数据在索引树中的存放顺序也不一定是相同的(因为只需要保证相同值在索引中是连续的即可);
故find()中即使使用率索引查询,但主从节点上的返回结果也不一定是相同的;
3.3 capped collection固定集合
由于固定集合的一些特性(集合固定大小、文档以插入顺序存放、文档不能被显示deleted),所以在natural order在固定集合中是固定顺序的,也是被强制执行的,因此在固定集合中查询数据即使没有指定sort,返回结果集顺序也是一致的;
3.4 什么时候natural order与写入顺序一致呢?
在测试过程中发现,通过for循环一条一条insert()
到MongoDB后,find查询时的结果集主从顺序一致;
但如果使用insertMany()
每次批量插入100条时,find查询时的结果集主从顺序就不一致了;
4、总结
MongoDB默认使用natural order排序,在没有sort显示排序的情况下,find结果是以stored order(即natural order)返回结果集,或者叫被扫描到的顺序返回,有时候我们可以看做natural order 就是插入顺序,但不保证一定是插入顺序,
因为在很多情况下这个stored order可能发生改变,例如主从同步、compact碎片整理、备份恢复等等;
5、建议
在使用MongoDB时如果需要获取到有序的结果集,一定要显示的指定sort()方法,并且在数据量大的情况下,尽量让排序调用上索引
参考
stack overflow博客1:https://stackoverflow.com/questions/33018048/how-does-mongodb-order-their-docs-in-one-collection
stack overflow博客2:https://stackoverflow.com/questions/11599069/how-does-mongodb-sort-records-when-no-sort-order-is-specified
MongoDB官方博客:https://www.mongodb.com/community/forums/t/indexes-not-ordered-correctly/8361/2