//
Pymongo的一个小坑
//
这两天遇到一个线上的问题,是关于Pymongo处理时间方面的,这里记录一下,希望对大家有帮助。
故事的场景是这样的:线上业务反馈MongoDB的集合中总是存在过期的数据,他已经在某个时间字段上设置了TTL索引,但是TTL的时间过期之后,文档没有删除,依旧包含旧文档。
看到这个问题,解决的思路肯定是沿着TTL索引这个方向去查找的,我们先来看看TTL索引是个什么东西?
TTL索引简介
TTL索引是与时间生命周期相关的索引,该索引指定文档中时间字段,并为它设计一个时间值,如果文档的时间超过了这个值,则文档会被自动删除。它通常用来设置哪些不重要的文档,例如,一个文件系统的log文档、巡检信息文档等。
它包含4个主要的特点:
1、TTL索引不可以是符合索引,否则不知道该以哪个时间为准。
2、如果TTL创建的字段不是时间字段,那么该文档不会被删除
3、TTL索引每60s运行一次,移除过期的文档
4、如果时间类型的字段已经被设置为其他索引,则无法通过设置TTL索引来删除此文档。
为了加深理解,我们来看下面的测试:
首先我们使用下面的语句来创建一个集合timetest,这个集合包含2个字段,一个是text字段,另外一个是createtime时间字段:
代码语言:javascript复制插入时间字段:
db.timetest.insert({"text":"timetest","createtime":new Fate()})
我们给集合中插入数据,插入数据的过程中,使用new Date函数插入默认当前时间的数据。插入完成之后的结果如下:
代码语言:javascript复制>
> db.timetest.insert({"text":"timetest","createtime":new Date()})
WriteResult({ "nInserted" : 1 })
> db.timetest.insert({"text":"timetest","createtime":new Date()})
WriteResult({ "nInserted" : 1 })
> db.timetest.insert({"text":"timetest","createtime":new Date()})
WriteResult({ "nInserted" : 1 })
> db.timetest.insert({"text":"timetest","createtime":new Date()})
WriteResult({ "nInserted" : 1 })
> db.timetest.insert({"text":"timetest","createtime":new Date()})
WriteResult({ "nInserted" : 1 })
> db.timetest.insert({"text":"timetest","createtime":new Date()})
WriteResult({ "nInserted" : 1 })
> db.timetest.insert({"text":"timetest","createtime":new Date()})
WriteResult({ "nInserted" : 1 })
> db.timetest.insert({"text":"timetest","createtime":new Date()})
WriteResult({ "nInserted" : 1 })
> db.timetest.insert({"text":"timetest","createtime":new Date()})
WriteResult({ "nInserted" : 1 })
> db.timetest.insert({"text":"timetest","createtime":new Date()})
WriteResult({ "nInserted" : 1 })
> db.timetest.insert({"text":"timetest","createtime":new Date()})
WriteResult({ "nInserted" : 1 })
> db.timetest.insert({"text":"timetest","createtime":new Date()})
WriteResult({ "nInserted" : 1 })
> db.timetest.insert({"text":"timetest","createtime":new Date()})
WriteResult({ "nInserted" : 1 })
> db.timetest.find()
{ "_id" : ObjectId("5fcb2a6cc08d6a7b799f457a"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:36:28.894Z") }
{ "_id" : ObjectId("5fcb2a81c08d6a7b799f457b"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:36:49.845Z") }
{ "_id" : ObjectId("5fcb2a83c08d6a7b799f457c"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:36:51.460Z") }
{ "_id" : ObjectId("5fcb2a85c08d6a7b799f457d"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:36:53.165Z") }
{ "_id" : ObjectId("5fcb2a86c08d6a7b799f457e"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:36:54.647Z") }
{ "_id" : ObjectId("5fcb2a88c08d6a7b799f457f"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:36:56.117Z") }
{ "_id" : ObjectId("5fcb2a89c08d6a7b799f4580"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:36:57.605Z") }
{ "_id" : ObjectId("5fcb2a8bc08d6a7b799f4581"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:36:59.095Z") }
{ "_id" : ObjectId("5fcb2a8cc08d6a7b799f4582"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:37:00.615Z") }
{ "_id" : ObjectId("5fcb2a8ec08d6a7b799f4583"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:37:02.038Z") }
{ "_id" : ObjectId("5fcb2a8fc08d6a7b799f4584"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:37:03.471Z") }
{ "_id" : ObjectId("5fcb2a90c08d6a7b799f4585"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:37:04.909Z") }
{ "_id" : ObjectId("5fcb2a92c08d6a7b799f4586"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:37:06.383Z") }
{ "_id" : ObjectId("5fcb2a93c08d6a7b799f4587"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:37:07.817Z") }
>
可以看到,数据已经插入到集合当中了。
紧接着,我们使用createIndex来设置createtime这个时间字段为TTL索引,然后过期时间是60s,如下:
代码语言:javascript复制>
> db.timetest.createIndex({"createtime":-1},{expireAfterSeconds:60})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}
>
>
> db.timetest.getIndexes()
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_",
"ns" : "yeyz.timetest"
},
{
"v" : 2,
"key" : {
"createtime" : -1
},
"name" : "createtime_-1",
"ns" : "yeyz.timetest",
"expireAfterSeconds" : 60
}
]
>
可以看到,插入索引成功后,timetest这个集合中有2个索引了。
此时我们进行find操作,看看这些记录会不会自动删除:
代码语言:javascript复制> db.timetest.find()
{ "_id" : ObjectId("5fcb2a6cc08d6a7b799f457a"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:36:28.894Z") }
{ "_id" : ObjectId("5fcb2a81c08d6a7b799f457b"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:36:49.845Z") }
{ "_id" : ObjectId("5fcb2a83c08d6a7b799f457c"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:36:51.460Z") }
{ "_id" : ObjectId("5fcb2a85c08d6a7b799f457d"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:36:53.165Z") }
{ "_id" : ObjectId("5fcb2a86c08d6a7b799f457e"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:36:54.647Z") }
{ "_id" : ObjectId("5fcb2a88c08d6a7b799f457f"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:36:56.117Z") }
{ "_id" : ObjectId("5fcb2a89c08d6a7b799f4580"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:36:57.605Z") }
{ "_id" : ObjectId("5fcb2a8bc08d6a7b799f4581"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:36:59.095Z") }
{ "_id" : ObjectId("5fcb2a8cc08d6a7b799f4582"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:37:00.615Z") }
{ "_id" : ObjectId("5fcb2a8ec08d6a7b799f4583"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:37:02.038Z") }
{ "_id" : ObjectId("5fcb2a8fc08d6a7b799f4584"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:37:03.471Z") }
{ "_id" : ObjectId("5fcb2a90c08d6a7b799f4585"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:37:04.909Z") }
{ "_id" : ObjectId("5fcb2a92c08d6a7b799f4586"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:37:06.383Z") }
{ "_id" : ObjectId("5fcb2a93c08d6a7b799f4587"), "text" : "timetest", "createtime" : ISODate("2020-12-05T06:37:07.817Z") }
> db.timetest.find()
我们发现,第一次查看的时候,所有的记录还是存在于集合timetest当中的,但是第二次查询的时候,集合中的记录就不复存在了。
上面的实验过程,说明了2个问题:
1、说明TTL索引生效了,已经自动帮助我们删除了过期的文档,
2、TTL索引创建完成之后,不是立即生效的,需要有一定的时间间隔,然后才会按照索引的这个时间字段进行删除操作。
有了这个实验,想必对于TTL索引,大家已经有一个直观的认知了。
案例分析
为了方便阅读,我把之前案例场景搬迁过来:
故事的场景是这样的:线上业务反馈MongoDB的集合中总是存在过期的数据,他已经在某个时间字段上设置了TTL索引,但是TTL的时间过期之后,文档没有删除,依旧包含旧文档。
这个问题看起来似乎变的很玄乎,设置了生效时间,结果没有删除掉,根据刚才我们了解的TTL特点,它可能是以下几个问题:
1、这个字段上还有其他的索引
2、索引刚刚创建好,需要等待一段时间才可以看到被删除后的结果
3、数据记录的时间错误了。
我们一一来看,经过查看业务表当前的时间字段没有其他的索引,因此排除场景1;经过了解,这个索引已经创建了好几天,时间肯定大于60s了,一定是触发了删除操作的,排除场景2;所以只可能是场景3了。我们的重点问题是查看场景3到底是哪儿的时间出现问题了。
验证结果
首先我们看MongoDB里面的时间:
代码语言:javascript复制> db.timetest.insert({"text":"timetest","createtime":new Date()})
WriteResult({ "nInserted" : 1 })
> db.timetest.find()
{ "_id" : ObjectId("5fcb31b9c08d6a7b799f4588"), "text" : "timetest", "createtime" : ISODate("2020-12-05T07:07:37.749Z") }
正常插入当前时间new Date,发现时间变成了:
2020-12-05T07:07:37.749Z
但是当前写公众号文章的真正时间是:
2020-12-05 15:07:37
关于这个时间转换,做过MySQL运维的人肯定比较熟悉,看ERROR log的时候,经常会遇到"T"的时间。
其实带"T"的时间是格林尼治时间,它在数值上等于UTC时间,北京时间和UTC时间之间相差8小时,准确的来讲:
北京时间=UTC时间 8小时=格林尼治时间 8小时
所以,根据上面createtime的结果,不难看出来,MongoDB的默认时间是格林尼治时间。
到这里,可以判定可能是MongoDB的默认时间和我们普遍认知的时间有偏差导致的问题,于是再次追问业务看到的时间点是多少,结果得到了以下4个时间信息。
1、发现错误的真正时间:2020-12-04 11:30:00 (大概)
2、MongoDB数据库中存储的时间:
2020-12-04T11:30:00.000Z
3、TTL索引设置的过期时间:3600s
4、业务方认为没删除的文档在数据库中存储的时间:
2020-12-04T02:29:00.00Z
到这里,我明白了时间误差所在的地方:
1、业务方误认为上面第4个时间2020-12-04T02:29:00.00Z代表今天的凌晨两点,其实不然,它是格林尼治表示法,代表的是早晨10:29:00这个时间。
2、MongoDB数据库中存储的时间是错误的,因为2020-12-04T11:30:00.000Z代表的是晚上19:30:00,而不是发现错误的时间11:30,也就是说,业务方在中午11:30这个时间,写入到数据库中的时间是:2020-12-04T11:30:00.000Z,这明显是写入错误了。
看到这里,继续追问业务方写入MongoDB的方法是什么,得知是pymongo之后,我查询了pymongo的官方文档,看到了下面的内容:
简单说,就是从pymongo驱动将当前时间写入到mongodb数据库的时候,时间字段需要使用python的utcnow()函数来写入,而不能使用now()函数。具体细节大家可以进入官方文档进行查看:
https://pymongo.readthedocs.io/en/stable/examples/datetimes.html?highlight=date
到这里,我们需要进一步测试验证自己的猜想,于是我自己用pymongo写了一个小脚本来写入数据,给yeyz的timetest表里面插入一条数据,看看数据的结果是多少:
代码语言:javascript复制# encoding=utf-8
import pymongo
import time
from datetime import datetime, timedelta
conn = pymongo.MongoClient("mongodb://127.0.0.1:27017/")
# 指定已经授权的DB
db = conn['admin']
db.authenticate("root", "123456")
#
# # 获取数据库下的表
db1 = conn['yeyz']
my_table = db1["timetest"]
curtime = datetime.now()
print curtime
mydict = {"name": "timetest", "createtime": curtime}
my_table.insert_one(mydict)
当前时间是15:39,来看写入的时间:
代码语言:javascript复制> db.timetest.find()
{ "_id" : ObjectId("5fcb3a244049990699a0974f"), "name" : "now", "createtime" : ISODate("2020-12-05T15:43:32.966Z") }
{ "_id" : ObjectId("5fcb3a244049990699a09750"), "name" : "utcnow", "createtime" : ISODate("2020-12-05T07:43:32.966Z") }
可以看到,如果我们使用now()写入的时间变成了:
ISODate("2020-12-05T15:43:32.966Z")
它代表今天晚上的23:43分,明显与实际不符。
而使用utcnow()写入的时间变成了:
ISODate("2020-12-05T07:43:32.966Z")
这个时间是正确的,跟我们实际期望相符。
此时如果createtime字段上设置了3600s的TTL索引,那么文档的时间最多可以保存到2020-12-05T06:43:32.966Z左右,但是不一定刚好1个小时。
说明只有使用utcnow()的时候,pymongo才能写入正确的时间字段,否则,时间字段的转换就会出错,写入时间将是单签时间未来8小时的时间。
到这里,这个问题就告一段落了,虽然只是一个很小的时间问题,但是稍有不慎,可能就会踩到坑,从而造成不可知的影响。
有帮助的话还希望点下再看哈