Pymongo的一个小坑

2020-12-14 09:42:28 浏览数 (1)

//

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小时的时间。

到这里,这个问题就告一段落了,虽然只是一个很小的时间问题,但是稍有不慎,可能就会踩到坑,从而造成不可知的影响。

有帮助的话还希望点下再看哈

0 人点赞