技术干货| MongoDB 时间序列集合
名词解释 Glossary
bucket:带有相同的元数据且在一段有限制的间 隔区间内的测量值组。
bucket collection :用于存储时序型集合的底层的分组桶的系统集合。复制、分片和索引都是在桶级别上完成的。
measurement:带有特定时间序列的 K-V 集合。
meta-data:时序序列里很少随时间变化的 K-V 对,同时可以用于识别整个时序序列。
time-series:一段间隔内的一系列测量值。
time-series collection:一种表示可写的非物化的视图的集合类型,它允许存储和查询多个时间序列,每个序列可以有不同的元数据。
MongoDB 在 5.0 中支持了新的timeseries collection
类型的选项,该类型用于存储时序型数据。timeseries collection
提供了一组用于插入和查询测量值的简单接口,同时底层实际的数据是存储在以bucket
形式的集合中。
在创建timeseries collection
时,timeField
字段是最小必备的配置项。metaField
是另一个可选的、可被指定的元数据字段,它是用于在bucket
中对测量值分组的依据。MongoDB 通过提供expireAfterSeconds
字段选项,也支持了对测量值的过期机制。
在mydb
数据库中有个以mytscoll
命名的timeseries collection
,该集合在 MongoDB 内部的catelog
(用于存储集合或视图的信息)里是由一个视图和一个系统集合组成的。
mydb.mytscoll
是个视图,它在 MongoDB 底层是用bucket collection
作为包含特定属性的原始集合实现的:该视图就是通过
aggregation
里的$_internalUnpackBucket
来实现展开bucket
里数据的。该视图是可写的(仅支持插入)。同时每个被插入的文档必须包含时间字段。
在查询视图时,它会隐式地展开底层在
bucket collection
中存储的数据,然后返回原始的非bucket
形式的文档数据。该系统集合的命名空间是
mydb.system.buckets.mytscoll
,它是用来存储实际数据的。每一个在
bucket collection
里的文档,都表示了一组区间间隔的时序型数据。如果在创建
timeseries collection
时,定义了metaField
元数据字段,那么所有在bucket
里的测量值都会有这个通用的元数据字段。除了时间范围,
bucket
还限制了每个文档数据的总条数以及测量值的大小。
Bucket Collection Schema
索引 indexes
为了保证timeseries collection
的查询可以受益于索引扫描而不是全表扫描,timeseries collection
允许索引可以被创建在时间上,元数据上以及元数据的子属性上。从 MongoDB5.2 开始,在timeseries collection
也允许索引被创建在测量值上。用户使用createIndex
命令提供的索引规范被转换为底层buckets collection
的模式。
timeseries collection
与底层的buckets collection
之间的索引映射转换关系细节,你可以参考timeseries_index_schema_conversion_functions.h.在 v5.2 及以上版本的最新支持的索引类型,
timeseries collection
会存储用户原始的索引定义到变换后的索引定义上。当从底层的bucket collection
的索引映射到timeseries collections
的索引时,会返回用户原始的索引定义。
当索引被创建后,可以通过listIndexes
命令或$indexStats
聚合计划来检查。listIndexes
和$indexStats
是作用于timeseries collections
的,执行时,它们会在内部将底层的bucket collection
的索引转化成timeseries
格式的索引,并返回。比如,当我们在元数据字段中定义有mm
的timeseries collection
上执行listIndexes
命令时,底层的bucket collection
的{meta:1}
索引,将会以{mm:1}
格式返回。
dropIndex
和collMod
(hidden: <bool>, expireAfterSeconds: <num>)
也同样支持在timeseries collection
上。
时间字段上支持的索引类型:
元数据字段和元数据子字段支持的索引类型:
支持所有时间字段上支持的索引类型
v5.2 及以上版本支持 2d 索引
v5.2 及以上版本支持 2dsphere 索引
v5.2 及以上版本支持 Partial索引
仅在 v5.2 及以上版本,测量值字段支持的索引类型:
`timeseries collections
上不支持的索引类型,包括 唯一索引以及文本索引。
桶目录 Catalog
为了保证高效地桶(分组)操作,我们在BucketCatalog
里维护了一组开启的桶,你可以在bucket_catalog.h找到。在更高的级别,我们尝试着把并发写程序的写操作分组合并为可以一起提交地批处理,以减少对底层文档的写次数。写程序会插入它的输入批处理里的每一个文档到BucketCatalog
,然后BucketCatalog
会返回一个BucketCatalog::WriteBatch
的处理器。一旦完成上面那些插入操作后,写程序就会检查每个写批处理。如果没有其他的写程序已经对批处理声明提交的权利,那么它会声明权利,并会提交它的批处理。否则,写程序将会稍后再提交处理。当它检查完所有的批处理,写程序将会等待其他的写程序提交每个剩下的批处理。
在内部,BucketCatalog
维护一组对每个bucket
文档的更新操作。当批处理被提交时,它会将这些插入转换到成buckets
的列格式,并确保任何control
字段的更新(例如control.min
和 control.max
)。
当bucket
文档在没有通过BucketCatalog
的情况下被更新时,写程序就需要为有问题的文档或命名空间去调用BucketCatalog::clear
,这样它就可以更新它的内部状态,避免写入任何可能破坏bucket
格式的数据。这通常由 OP 观察者处理,但可能需要通过其他地方去调用。
bucket
既可以通过手动设置选项control.closed
标识来关闭,也可以在许多场景下通过 BucketCatalog
自动关闭。如果BucketCatalog
使用了超出给定的阈值(可通过服务器参数timeseriesIdleBucketExpiryMemoryUsageThreshold
控制)的更多内存,此时它将会开始去关闭空闲的bucket
。如果bucket
是开启的且它没有任何未处于等待中未提交的测量值时,那么它就会被视为空闲的bucket
。在下面这些场下 BucketCatalog
也会关闭bucket
: 如果它拥有超过最大阈值(timeseriesBucketMaxCount
)的测量值数据的数量;如果它拥有过大的数据量大小(timeseriesBucketMaxSize
);又或者一个新的测量值数据是否是会导致bucket
在其最旧的时间戳和最新的时间戳之间跨度比允许的间隔更长的时间(当前硬编码为一小时)。如果传入的测量值在原理上与已经到达给定bucket
的度量不兼容,该bucket
将被关闭,同时可以使用numBucketsClosedDueToSchemaChange
度量进行跟踪。
在第一次提交给定bucket
的写批处理时,就会生成新的完整的文档。后续的批处理提交中,我们只执行更新操作,不再生成新的完整的文档(因此称为‘经典’更新),是直接创建DocDiff
(“delta”或者 v2 的更新)。
粒度 Granularity
timeseries collection
的granularity
选项在集合创建的时候,可以被设置成seconds
,minutes
或者hours
。后期可通过colMod
操作来修改这个选项从seconds
到minutes
或者从minutes
到hours
,除此之外的转化修改目前都是不支持的。该参数想要表示在已给定的时序型测量数据之间的粗略的时间间隔,同时也用于调节其他内部参数对分组的影响。
单个bucket
被允许的最大时间跨度,是由granularity
选项控制,对于seconds
,最大的时间跨度被设置成 1 小时,对于minutes
就是 24 小时,对于hours
就是 30 天。
当通过BucketCatalog
开启新的bucket
时,_id
里的时间戳就是等同于control.min.<time field>
的值,该值是从第一个插入bucket
的测量数据中根据granularity
选项来向下近似舍入而得到的。对于seconds
,它将向下舍入到最接近的分钟,对于minutes
,将向下舍入到最接近的小时,对于hours
,它将向下舍入到最接近的日期。在闰秒和日历中的其他不规则情况下,这种舍入可能并不完美,并且通常通过对自纪元以来的秒数进行基本模运算来完成,假设每分钟 60 秒,每小时 60 分钟,以及每天 24 小时。
更新和删除
timeseries collection
支持符合以下限制的删除语句:
仅支持
metaField
的属性的查询语句支持批量操作
同时更新满足上面同样的条件,另外遵循:
仅支持
metaField
对应的属性值更新操作指定一个带有更新运算符表达式的更新文档(而不是替换文档或者更新的 pipeline 操作)
不支持
upsert:true
操作
这些更新与删除的执行都会被转换成相对应的底层的bucket collection
的更新或删除操作。特别是,对于查询和更新文档,我们会使用真正的字段meta
替换集合的metaField
。(参见 Bucket 集合规范)
例如,对于一个使用 metaField: "tag"
创建的timeseries
集合db.ts
,考虑一个对这个集合的更新操作,其查询语句是{"tag.tag.a": "a"}
,同时更新文档语句是 {$set: {"tag.tag.a": "A"}
,
$rename: {"tag.tag.b": "tag.tag.c"}}
。这个更新操作在 db.system.buckets.ts
上会被转换成,查询语句是{"meta.tag.a": "a"}
,更新语句是 {$set: {"meta.tag.a": "A"}
,
$rename: {"meta.tag.b": "meta.tag.c"}}
。然后这个转换后的更新语句就可以像普通的更新操作一样执行。上面这些转换流程也适用于删除操作。
参考文献 References
MongoDB Blog: Time Series Data and MongoDB: Part 2 - Schema Design Best Practices
关于作者:黄璜
目前就职于上海 DerbySoft,主要从事基础架构中业务流程设计及研发的工作,平时工作中 MongoDB 使用的较多。在提升自己外文的能力的同时,也希望为社区做出微小的贡献。
评论