聊聊 MongoDB 時間序列集合
名詞解釋
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>,
...
},
...
}
}
索引
為了保證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: , expireAfterSeconds: ) 也同樣支持在timeseries collection上。
時間字段上支持的索引類型:
- 單字段索引
- 組合索引
- 哈希索引
- 通配符索引
- 稀疏索引
- 多鍵索引
- 帶排序的索引
元數據字段和元數據子字段支持的索引類型:
- 支持所有時間字段上支持的索引類型
- v5.2及以上版本支持2d 索引
- v5.2及以上版本支持2dsphere 索引
- v5.2及以上版本支持 Partial索引
僅在v5.2及以上版本,測量值字段支持的索引類型:
- 單字段索引
- 組合索引
- 2dsphere
- 部分條件索引
`timeseries collections 上不支持的索引類型,包括 唯一索引以及文本索引。
桶目錄
為了保證高效地桶(分組)操作,我們在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的更新)。
粒度
timeseries collection的granularity 選項在集合創建的時候,可以被設置成seconds,minutes或者hours。后期可通過colMod操作來修改這個選項從seconds到minutes或者從minutes到hours,除此之外的轉化修改目前都是不支持的。該參數想要表示在已給定的時序型測量數據之間的粗略的時間間隔,同時也用于調節其他內部參數對分組的影響。
單個bucket被允許的最大時間跨度,是由granularity選項控制,對于seconds,最大的時間跨度被設置成1小時,對于minutes就是24小時,對于hours就是30天。
當通過BucketCatalog開啟新的bucket時,_id里的時間戳就是等同于control.min.的值,該值是從第一個插入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"}}。然后這個轉換后的更新語句就可以像普通的更新操作一樣執行。上面這些轉換流程也適用于刪除操作。
參考文獻
MongoDB Blog: Time Series Data and MongoDB: Part 2 - Schema Design Best Practices
關于作者:黃璜
目前就職于上海DerbySoft,主要從事基礎架構中業務流程設計及研發的工作,平時工作中MongoDB使用的較多。
在提升自己外文的能力的同時,也希望為社區做出微小的貢獻。