MongoDB爱好者
垂直技术交流平台

技术干货| 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

{
    _id: <Object ID with time component equal to control.min.<time field>>,
    control: {
        // <Some statistics on the measurements such min/max values of data fields> 
       version: 1,  // Version of bucket schema. Currently fixed at 1 since this is the 
                    // first iteration of time-series collections.
        min: {
            <time field>: <time of first measurement in this bucket, rounded down based on granularity>,
            <field0>: <minimum value of 'field0' across all measurements>,
            <field1>: <maximum value of 'field1' across all measurements>, 
           ...
        },
        max: {
            <time field>: <time of last measurement in this bucket>,
            <field0>: <maximum value of 'field0' across all measurements>,
            <field1>: <maximum value of 'field1' across all measurements>,
            ...
        },
        closed: <bool> // Optional, signals the database that this document will not receive any
                       // additional measurements.
    },
    meta: <meta-data field (if specified at creation) value common to all measurements in this bucket>,
    data: {
        <time field>: {
            '0', <time of first measurement>,
            '1', <time of second measurement>,
            ...
            '<n-1>': <time of n-th measurement>,
        },
        <field0>: {
            '0', <value of 'field0' in first measurement>,
            '1', <value of 'field0' in first measurement>,
            ...
        },
        <field1>: {
            '0', <value of 'field1' in first measurement>,
            '1', <value of 'field1' in first measurement>,
            ...
        },
        ...
    }
}


索引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格式的索引,并返回。比如,当我们在元数据字段中定义有mmtimeseries 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 collectiongranularity 选项在集合创建的时候,可以被设置成secondsminutes或者hours。后期可通过colMod操作来修改这个选项从secondsminutes或者从minuteshours,除此之外的转化修改目前都是不支持的。该参数想要表示在已给定的时序型测量数据之间的粗略的时间间隔,同时也用于调节其他内部参数对分组的影响。

单个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使用的较多。
在提升自己外文的能力的同时,也希望为社区做出微小的贡献。

社区招募

为了让社区组委会成员和志愿者朋友们灵活参与,同时我们为想要深度参与社区建设的伙伴们开设了“招募通道”,如果您想要在社区里面结交志同道合的技术伙伴,想要通过在社区沉淀有价值的干货内容,想要一个展示自己的舞台,提升自身的技术影响力,即刻加入社区贡献队伍~ 点击链接提交申请:
http://mongoingmongoing.mikecrm.com/CPDCj1B
赞(0)
未经允许不得转载:MongoDB中文社区 » 技术干货| MongoDB时间序列集合

评论 抢沙发

评论前必须登录!