時(shí)序數(shù)據(jù)基礎(chǔ)
時(shí)序數(shù)據(jù)特點(diǎn)
時(shí)序數(shù)據(jù)TimeSeries是一連串隨時(shí)間推移而發(fā)生變化的相關(guān)事件旷痕。
以下圖的 CPU 監(jiān)控?cái)?shù)據(jù)為例脐帝,同個(gè) IP 的相關(guān)監(jiān)控?cái)?shù)據(jù)組成了一條時(shí)序數(shù)據(jù)滔悉,不相關(guān)數(shù)據(jù)則分布在不同的時(shí)間序列上向叉。
常見(jiàn)時(shí)序數(shù)據(jù)有:
監(jiān)控日志:機(jī)器的 CPU 負(fù)載變化
用戶行為:用戶在電商網(wǎng)站上的訪問(wèn)記錄
金融行情:股票的日內(nèi)成交記錄
這類(lèi)數(shù)據(jù)具有以下特點(diǎn):
必然帶有時(shí)間戳又跛,可能存在時(shí)效性
數(shù)據(jù)量巨大伪窖,并且生成速度極快
更關(guān)注數(shù)據(jù)變化的趨勢(shì)逸寓,而非數(shù)據(jù)本身
關(guān)系型數(shù)據(jù)庫(kù)的不足
當(dāng)面對(duì)時(shí)序數(shù)據(jù)時(shí),傳統(tǒng)的關(guān)系型數(shù)據(jù)庫(kù)就顯得有些力不從心覆山。
關(guān)系型數(shù)據(jù)庫(kù)模型時(shí)序數(shù)據(jù)庫(kù)需求
數(shù)據(jù)按主鍵索引組織竹伸、存儲(chǔ)數(shù)據(jù)按時(shí)間戳進(jìn)行組織、存儲(chǔ)簇宽,便于按時(shí)間維度查詢
數(shù)據(jù)持久化后永久存在數(shù)據(jù)具有的生命周期勋篓,定期清理過(guò)期數(shù)據(jù)
支持復(fù)雜的 OLTP 功能(點(diǎn)查、改魏割、刪)支持的 OLAP 操作(基于時(shí)間窗口)
并發(fā)修改加鎖譬嚣,提供事務(wù)保證Last?Write?Win 解決寫(xiě)沖突,無(wú)需事務(wù)
兩者之間的存在的沖突:
如果主鍵設(shè)計(jì)的不好见妒,時(shí)序數(shù)據(jù)的順序插入可能變?yōu)殡S機(jī)寫(xiě)入孤荣,影響寫(xiě)入性能
傳統(tǒng)關(guān)系型數(shù)據(jù)為提高數(shù)據(jù)生命周期管理功能,需要定時(shí)執(zhí)行清理任務(wù)须揣,避免磁盤(pán)空間耗盡
關(guān)系型數(shù)據(jù)庫(kù)對(duì)事務(wù)的支持盐股,對(duì)于時(shí)序數(shù)據(jù)來(lái)說(shuō)略顯多余,還會(huì)影響寫(xiě)入能力
除此之外耻卡,關(guān)系型數(shù)據(jù)庫(kù)還存在以下天然短板:
需要通過(guò)分表分庫(kù)sharding實(shí)現(xiàn)橫向擴(kuò)展
分庫(kù)分表引入額外復(fù)雜度疯汁,需要維護(hù)映射關(guān)系。 此時(shí) SQL 語(yǔ)言的查詢優(yōu)勢(shì)不復(fù)存在卵酪,多數(shù)查詢都會(huì)退化為 KV 查找
寫(xiě)時(shí)模式schema on write靈活度不足
關(guān)系數(shù)據(jù)庫(kù)新增字段屬于 DDL 操作幌蚊,會(huì)導(dǎo)致鎖表鎖庫(kù)。 頻繁的庫(kù)表變更影響系統(tǒng)穩(wěn)定溃卡,無(wú)法適應(yīng)快速的業(yè)務(wù)變更溢豆。
Redis 的不足
存儲(chǔ)時(shí)序數(shù)據(jù)庫(kù)的另一個(gè)挑戰(zhàn)就是其夸張的數(shù)據(jù)生成速度。以用戶行為數(shù)據(jù)為例瘸羡,如果一個(gè)接口的QPS是1萬(wàn)漩仙。就意味著一秒鐘內(nèi)會(huì)生成1萬(wàn)條用戶行為記錄。假設(shè)這樣的接口有100個(gè),那么每秒鐘生成的記錄數(shù)可達(dá)100萬(wàn)队他。
一種解決方式是將消息積壓至 Kafka 這類(lèi)中間件卷仑,然后進(jìn)行異步處理。但對(duì)于某些業(yè)務(wù)場(chǎng)景來(lái)說(shuō)麸折,這一處理方式則顯得不合時(shí)宜锡凝。以股票成交數(shù)據(jù)為例,為了保障行情的時(shí)效性垢啼,無(wú)法采用異步批處理的方式實(shí)現(xiàn)窜锯。為了實(shí)現(xiàn)極高的寫(xiě)入吞吐量,通常會(huì)考慮使用 Redis 實(shí)現(xiàn)這一功能芭析。
然而這一方案也存在以下問(wèn)題:
redis 不適合存儲(chǔ)大 Key衬浑,刪除 Key 的內(nèi)存釋放操作可能導(dǎo)致長(zhǎng)時(shí)間的阻塞
假設(shè)數(shù)據(jù)以 list 的形式存儲(chǔ),執(zhí)行 lrange 命令的時(shí)間復(fù)雜度為O(S+N)放刨,訪問(wèn)性能較差
內(nèi)存空間有限,無(wú)法存儲(chǔ)大量的數(shù)據(jù)
時(shí)序數(shù)據(jù)庫(kù)
時(shí)序數(shù)據(jù)庫(kù)是一類(lèi)專(zhuān)門(mén)用于存儲(chǔ)時(shí)序數(shù)據(jù)的數(shù)據(jù)管理系統(tǒng)尸饺,這類(lèi)數(shù)據(jù)庫(kù)的設(shè)計(jì)思想大致可以總結(jié)為下面幾條:
使用特殊設(shè)計(jì)的外存索引來(lái)組織數(shù)據(jù)
強(qiáng)制使用 timestamp 作為唯一的主鍵
不檢查寫(xiě)沖突进统,避免加鎖,提高寫(xiě)入性能
對(duì)按時(shí)間順序?qū)懭脒M(jìn)行優(yōu)化浪听,提高寫(xiě)入性能
不支持細(xì)粒度的數(shù)據(jù)刪除功能螟碎,提高查詢寫(xiě)入性能
犧牲強(qiáng)一致性來(lái)提高系統(tǒng)的查詢吞吐量,提高查詢性能
提供基于時(shí)間窗口的 OLAP 操作迹栓,放棄關(guān)聯(lián)查詢等高級(jí)功能
通過(guò)無(wú)模式schemaless設(shè)計(jì)使系統(tǒng)更易于水平擴(kuò)展
時(shí)序數(shù)據(jù)模型
類(lèi)似于關(guān)系型數(shù)據(jù)庫(kù)掉分,時(shí)序數(shù)據(jù)庫(kù)也有自己的數(shù)據(jù)模型,并且兩者直接存在不少相似之處:
關(guān)系模型時(shí)序模型含義
tablemetric / measurement表 → 指標(biāo)(時(shí)間序列集合)
columnvalue / field列 → 值(無(wú)索引列)
indextag索引 → 標(biāo)簽(索引列)
rowpoint記錄行 → 數(shù)據(jù)點(diǎn)(時(shí)間序列中某個(gè)時(shí)刻的數(shù)據(jù))
primary keytimestamp行主鍵 → 點(diǎn)時(shí)間戳(時(shí)間序列內(nèi)唯一標(biāo)識(shí))
其中 tag 的概念較為重要:
tag 是一個(gè)字符串類(lèi)型的鍵值對(duì)
tag 并不是時(shí)序數(shù)據(jù)克伊,而是元數(shù)據(jù)
tag 的唯一組合確定一個(gè)時(shí)間序列
tag 可以方便實(shí)現(xiàn)粗粒度的聚合
tag 決定了索引的組織形式
tag 組合的數(shù)量數(shù)量不宜過(guò)多
常見(jiàn)的時(shí)序數(shù)據(jù)庫(kù)
時(shí)序數(shù)據(jù)庫(kù)排行榜中介紹了不少的時(shí)序數(shù)據(jù)庫(kù)酥郭,其中比較具有代表性的有以下兩款。
OpenTSDB
OpenTSDB 是一種基于 HBase 來(lái)構(gòu)建的分布式愿吹、可擴(kuò)展的時(shí)間序列數(shù)據(jù)庫(kù)不从。OpenTSDB 被廣泛應(yīng)用于存儲(chǔ)、索引和服務(wù)從大規(guī)模計(jì)算機(jī)系統(tǒng)(網(wǎng)絡(luò)設(shè)備犁跪、操作系統(tǒng)椿息、應(yīng)用程序)采集來(lái)的監(jiān)控指標(biāo)數(shù)據(jù),并且使這些數(shù)據(jù)易于訪問(wèn)和可視化坷衍。
OpenTSDB 由時(shí)間序列守護(hù)程序 (TSD) 以及一組命令行實(shí)用程序組成寝优。每個(gè) TSD 都是獨(dú)立的。 沒(méi)有主節(jié)點(diǎn)枫耳,沒(méi)有共享狀態(tài)乏矾。
優(yōu)點(diǎn):
TSD 是無(wú)狀態(tài)的,所有狀態(tài)數(shù)據(jù)保存在 HBase 中,天然支持水平擴(kuò)展
缺點(diǎn):
Hadoop 全家桶運(yùn)維難度較大妻熊,需要專(zhuān)人負(fù)責(zé)
新版本的 OpenTSDB 底層支持 Cassandra
存儲(chǔ)模型過(guò)于簡(jiǎn)化
單值模型且只能存儲(chǔ)數(shù)值類(lèi)型數(shù)據(jù)
單個(gè) metric 最多支持 8 個(gè) tag key
雖然利用了 HBase 作為底層存儲(chǔ)夸浅,但是沒(méi)有提供對(duì) MapReduce 的支持。
可以通過(guò) Spark 實(shí)現(xiàn)復(fù)雜的處理邏輯
InfluxDB
時(shí)序數(shù)據(jù)庫(kù) InfluxDB 是一款專(zhuān)門(mén)處理高寫(xiě)入和查詢負(fù)載的時(shí)序數(shù)據(jù)庫(kù)扔役,基于 InfluxDB 能夠快速構(gòu)建具有海量時(shí)序數(shù)據(jù)處理能力的分析和監(jiān)控軟件帆喇。
該項(xiàng)目的發(fā)起者是 influxdata 公司,該公司提供了一套用于處理時(shí)序數(shù)據(jù)的完整解決方案亿胸,InfluxDB 是該解決方案中的核心產(chǎn)品坯钦。
優(yōu)點(diǎn):
開(kāi)箱即用,運(yùn)維簡(jiǎn)單
多值存儲(chǔ)模型侈玄、豐富的數(shù)據(jù)類(lèi)型
提供了類(lèi) SQL 的查詢語(yǔ)言
獨(dú)創(chuàng) TSM 索引
缺點(diǎn):
開(kāi)源版本不支持集群部署婉刀,對(duì)于大規(guī)模應(yīng)用來(lái)說(shuō),使用前需要慎重考慮
深入InfluxDB
數(shù)據(jù)模型
InfluxDB 的數(shù)據(jù)模型已經(jīng)很接近傳統(tǒng)的關(guān)系模型:
database命名空間序仙,相互隔離
retention policy保存策略突颊,定義數(shù)據(jù)生命周期
bucketdatabase + retention policy
measurement相關(guān)時(shí)間序列集合
tag標(biāo)簽索引,索引列
field時(shí)序數(shù)據(jù)潘悼,無(wú)索引列
point數(shù)據(jù)點(diǎn)律秃,時(shí)間序列中某個(gè)時(shí)刻的數(shù)據(jù)
time時(shí)間戳,時(shí)間序列內(nèi)唯一標(biāo)識(shí)
保留策略retention policy?用于管理數(shù)據(jù)生命周期治唤,其中包含了:
持續(xù)時(shí)間duration:指定了數(shù)據(jù)保留時(shí)間棒动,過(guò)期的數(shù)據(jù)將自動(dòng)從數(shù)據(jù)庫(kù)中刪除
副本個(gè)數(shù)replication factor:指定了集群模式下,需要維護(hù)數(shù)據(jù)副本的個(gè)數(shù)(僅在集群模式下有效)
分片粒度hard duration):指定了 shard group 的時(shí)間跨度(影響數(shù)據(jù)分片大斜鎏怼)
保留策略與 database 存在以下關(guān)系:
一個(gè) database 可以有多個(gè) RP船惨,每個(gè) RP 只屬于一個(gè) database?1:N
創(chuàng)建 point 時(shí)可以指定 RP,一個(gè) measurement 可以有不同的 RP?N:N
這意味著:
同個(gè) measurement 可能存在兩個(gè)有著完全相同的 time 的 point缕陕。為了解決數(shù)據(jù)重復(fù)的問(wèn)題粱锐,InfluxDB 2 引入了一個(gè) bucket 的概念,用于避免這一情況扛邑。
Series
時(shí)間序列?Series?在 InfluxDB 中也是個(gè)核心概念:
series keymeasurement + tag + field key
series時(shí)間序列卜范,serial key 相同的數(shù)據(jù)集合
series cardinality序列基數(shù),series key 的數(shù)量
為了唯一標(biāo)識(shí)一個(gè)時(shí)間序列鹿榜,InfluxDB 引入了?Serieskey?的概念:
每個(gè)數(shù)據(jù)點(diǎn)都有?Serieskey海雪,Serieskey?相同的數(shù)據(jù)點(diǎn)屬于同個(gè)時(shí)間序列,會(huì)存儲(chǔ)在同一個(gè)文件中舱殿,方便后續(xù)查詢
Serieskey?的數(shù)量稱(chēng)為序列基數(shù)?series cardinality:
序列基數(shù)是一個(gè)重要的性能指標(biāo)奥裸,InfluxDB 會(huì)為每個(gè)?Serieskey?在內(nèi)存中維護(hù)一個(gè)索引記錄,因此序列基數(shù)能夠直觀反映當(dāng)前數(shù)據(jù)的內(nèi)存壓力
上圖的 series cardinality 為 4沪袭,其中包含以下 series key:
measurement + tags + field
census, location=1, scientist=langstroth, butterflier
census, location=1, scientist=langstroth, honeybees
census, location=1, scientist=perpetual, butterflier
census, location=1, scientist=perpetual, honeybees
注意:即便兩條記錄的 measurement湾宙、time樟氢、tag、field 完全一致侠鳄,但只要使用的是不同的 RP埠啃,那么它們就屬于不同的 series,會(huì)被存儲(chǔ)在不同的 bucket 中伟恶。
查詢語(yǔ)言
InfluxDB 提供了兩種查詢語(yǔ)言:
InfluxQL:類(lèi) SQL 的聲明式查詢語(yǔ)言碴开,同時(shí)具有 DDL 與 DML 功能
Flux:函數(shù)式查詢語(yǔ)言,僅支持 DML 功能博秫,能支持復(fù)雜的查詢條件潦牛,但不支持增刪改操作
下面通過(guò)一些實(shí)際操作來(lái)熟悉一下 InfluxQL:
# 創(chuàng)建數(shù)據(jù)庫(kù)CREATEDATABASE"sample_data"USEsample_data# 插入樣例數(shù)據(jù),格式參考:https://docs.influxdata.com/influxdb/v1.8/write_protocols/line_protocol_tutorialINSERTcensus,location=1,scientist=langstroth butterflies=12i,honeybees=23i1439827200000000000INSERTcensus,location=1,scientist=perpetua butterflies=1i,honeybees=30i1439827200000000000INSERTcensus,location=1,scientist=langstroth butterflies=11i,honeybees=28i1439827560000000000INSERTcensus,location=1,scientist=perpetua butterflies=3i,honeybees=28i1439827560000000000INSERTcensus,location=2,scientist=langstroth butterflies=2i,honeybees=11i1439848440000000000INSERTcensus,location=2,scientist=langstroth butterflies=1i,honeybees=10i1439848800000000000INSERTcensus,location=2,scientist=perpetua butterflies=8i,honeybees=23i1439849160000000000INSERTcensus,location=2,scientist=perpetua butterflies=7i,honeybees=22i1439849520000000000# 顯示數(shù)據(jù)庫(kù)中的表# measurement 無(wú)需預(yù)先定義挡育,由 InfluxDB 動(dòng)態(tài)創(chuàng)建SHOWMEASUREMENTS# 顯示數(shù)據(jù)庫(kù)中的 field keySHOWFIELDKEYS# 顯示數(shù)據(jù)庫(kù)中的 tag keySHOWTAGKEYS# 顯示數(shù)據(jù)庫(kù)中的 tag valueSHOWTAGVALUESWITHKEY= scientist# 查詢所有數(shù)據(jù)SELECT*FROMcensus;# 對(duì) location = 1 的數(shù)據(jù)求和SELECTSUM(butterflies)ASbutterflies,SUM(honeybees)AShoneybeesFROMcensusWHERElocation ='1';# 刪除 location = 1 的數(shù)據(jù)DELETEFROMcensusWHERElocation ='1';SELECT*FROMcensus;# 更新特定數(shù)據(jù)SELECT*FROMcensus;INSERTcensus,location=2,scientist=perpetua butterflies=10i,honeybees=50i1439849520000000000SELECT*FROMcensus;# 更新數(shù)據(jù)時(shí)要保證數(shù)據(jù)類(lèi)型一致巴碗,否則會(huì)報(bào)錯(cuò)INSERTcensus,location=2,scientist=perpetua butterflies=abc,honeybees=efg1439849520000000000# 刪除數(shù)據(jù)庫(kù)DROPDATABASEsample_data;
Flux 無(wú)命令行支持,只能通過(guò) http 接口請(qǐng)求即寒。有興趣可以參考下面的腳本橡淆,動(dòng)手嘗試一下:
curl -XPOST 127.0.0.1:8086/api/v2/query -sS \
-H 'Accept:application/csv' \
-H 'Content-type:application/vnd.flux' \
-H 'Authorization: Token root:123456' \
-d '
from(bucket:"sample_data")
|> range(start:time(v: 1439827200000), stop:time(v: 143984952000))
|> filter(fn:(r) => r._measurement == "census" and r.location == "1" and (r._field == "honeybees" or r._field == "butterflies"))
|> limit(n: 100)'
整體架構(gòu)
在了解完查詢語(yǔ)言之后,接下來(lái)看看 InfluxDB 的整體架構(gòu):
上圖將 InfluxDB 分為了 4 層母赵,上面 database 與 RP 這兩層之前已經(jīng)介紹過(guò)明垢,我們重點(diǎn)關(guān)注下面兩層:
shard存儲(chǔ)的時(shí)序數(shù)據(jù)的磁盤(pán)文件
shard groupshard 容器,責(zé)管理數(shù)據(jù)的生命周期市咽,清除過(guò)期的數(shù)據(jù)
shard durationshard duration
由于時(shí)序數(shù)據(jù)的數(shù)據(jù)量通常十分巨大,因此 InfluxDB 的設(shè)計(jì)中引入了分片的策略抵蚊。并且同時(shí)采用了兩種分片策略:
shard group 層采用了基于時(shí)間的分片策略施绎,方便實(shí)現(xiàn)按照時(shí)間條件范圍查詢
shard 層則是基于 hashmod 進(jìn)行分片,避免出現(xiàn)寫(xiě)熱點(diǎn)產(chǎn)生性能瓶頸
每個(gè) shard 由 WAL贞绳、Cache谷醉、TSM文件 3 部分組成:
整個(gè)數(shù)據(jù)的寫(xiě)入流程簡(jiǎn)化為 3 個(gè)步驟:
先寫(xiě)入 WAL
然后寫(xiě)入 Cache
最終持久化為 TSM File
WAL
預(yù)寫(xiě)日志W(wǎng)rite-Ahead-Log是一種常見(jiàn)的提高數(shù)據(jù)庫(kù)優(yōu)化手段,能夠在保證數(shù)據(jù)安全的同時(shí)冈闭,提升系統(tǒng)的寫(xiě)入性能俱尼。
InfluxDB WAL 由一組定長(zhǎng)的 segement 文件構(gòu)成,每個(gè)文件大小約為 10MB萎攒。這些 segment 文件只允許追加遇八,不允許修改。
Cache
Cache 是 WAL 的一個(gè)內(nèi)存快照耍休,保證 WAL 中的數(shù)據(jù)對(duì)用戶實(shí)時(shí)可見(jiàn)刃永。
當(dāng) Cache 空閑或者過(guò)滿時(shí),對(duì)應(yīng)的 WAL 將被壓縮并轉(zhuǎn)換為 TSM羊精,最終釋放內(nèi)存空間斯够。
每次重啟時(shí)會(huì)根據(jù) WAL 重新構(gòu)造 Cache。
TSM File
TSM 是一組存儲(chǔ)在磁盤(pán)上的外存索引文件,細(xì)節(jié)將在后續(xù)進(jìn)行介紹读规。
它們之間的關(guān)系可以簡(jiǎn)單描述為:
Cache = WAL
Cache + TSM = 完整的數(shù)據(jù)
存儲(chǔ)引擎發(fā)展史
在討論 TSM 之前抓督,線回顧一下 InfluxDB 存儲(chǔ)引擎的發(fā)展歷程:
LSM tree 時(shí)代 (0.8.x)
引擎:LevelDB、RocksDB束亏、HyperLevelDB铃在、LMDB
優(yōu)點(diǎn):極高的寫(xiě)入吞吐量,且支持?jǐn)?shù)據(jù)壓縮
缺點(diǎn):
懶刪除機(jī)制導(dǎo)致刪除操作耗時(shí)枪汪,且過(guò)期數(shù)據(jù)無(wú)法及時(shí)清理
按照時(shí)間維度分庫(kù)可以規(guī)避上述問(wèn)題涌穆,但是又會(huì)導(dǎo)致新問(wèn)題:
單個(gè)進(jìn)程打開(kāi)過(guò)多的文件導(dǎo)致句柄耗盡
過(guò)多的 WAL 可能會(huì)令順序追加退化為隨機(jī)寫(xiě)入
B+Tree 時(shí)代 (0.9.x)
引擎:BoltDB
優(yōu)點(diǎn):?jiǎn)挝募P停€(wěn)定性更好雀久,Go 實(shí)現(xiàn)更易嵌入
缺點(diǎn):
文件較大時(shí)宿稀,寫(xiě)放大效應(yīng)會(huì)導(dǎo)致 IOPS 會(huì)極劇上升
通過(guò)在 Bolt 前嵌入自研的 WAL 模塊緩解了這一問(wèn)題:
合并多個(gè)相鄰的寫(xiě)入操作,減少 fsync
將隨機(jī)寫(xiě)入變?yōu)轫樞蜃芳永蛋疲瑴p少寫(xiě)放大
TSM-Tree 時(shí)代 (0.9.5)
引擎:Time Structured Merge Tree
特點(diǎn):
整體實(shí)現(xiàn)借鑒 LSM tree 架構(gòu)祝沸,能有效規(guī)避寫(xiě)放大
更少的數(shù)據(jù)庫(kù)文件,避免順序?qū)懲嘶癁殡S機(jī)寫(xiě)越庇,不再出現(xiàn)文件句柄耗盡的情況
針對(duì)時(shí)序數(shù)據(jù)特性罩锐,采用了更具針對(duì)性的數(shù)據(jù)壓縮算法
關(guān)于 LSM-Tree 與 B-Tree 的寫(xiě)放大分析,可以參考這篇文章:https://www.cnblogs.com/buttercup/p/12991585.html
TSM 解析
數(shù)據(jù)組織
TSM 是一個(gè)列存引擎columnar storage卤唉,內(nèi)部按照 SeriesKey 對(duì)時(shí)序數(shù)據(jù)進(jìn)行組織:
每個(gè) SeriesKey 對(duì)應(yīng)一個(gè)數(shù)組涩惑,里面存儲(chǔ)著 time,value 構(gòu)成的時(shí)間點(diǎn)數(shù)據(jù)
同個(gè) SeriesKey 的數(shù)據(jù)存儲(chǔ)在一起,不同的 SeriesKey 的數(shù)據(jù)分開(kāi)存儲(chǔ)
列存引擎的優(yōu)勢(shì)
高效處理動(dòng)態(tài) schema 與稀疏數(shù)據(jù)
新增列時(shí)桑驱,對(duì)現(xiàn)有的數(shù)據(jù)無(wú)影響竭恬。并且由于不同列相互分離,可以直接忽略 null 值熬的,不需要耗費(fèi)空間存儲(chǔ)標(biāo)記
同類(lèi)型的數(shù)據(jù)能夠進(jìn)行高效的壓縮
同個(gè)列的數(shù)據(jù)必然具有相同的數(shù)據(jù)類(lèi)型痊硕,可以采取不同的壓縮手段進(jìn)行優(yōu)化。
查詢時(shí)能減少不必要的 I/O
查詢時(shí)能夠指定要返回的數(shù)據(jù)列押框,可以按需遍歷用戶指定的列岔绸,對(duì)于 OLAP 操作更友好。
列存引擎的劣勢(shì)
存儲(chǔ)稠密數(shù)據(jù)需要付出額外代價(jià)
當(dāng)多個(gè)列具有相同的時(shí)間戳?xí)r橡伞,timestamp 會(huì)被重復(fù)存儲(chǔ)盒揉。
數(shù)據(jù)變更操作需要更多的 I/O
列分開(kāi)存儲(chǔ)后,查兑徘、改酌住、刪操作可能要同時(shí)修改多個(gè)文件残炮。
無(wú)法提供原子性操作呛每,事務(wù)實(shí)現(xiàn)困難
無(wú)法實(shí)現(xiàn)高效的悲觀鎖,如果使用場(chǎng)景中需要用到事務(wù)翘县,建議使用行存引擎。
文件格式
TSM File 是一組列存格式的只讀文件谴分,每個(gè)文件對(duì)應(yīng)一組特定的 SeriesKey锈麸。
每個(gè)文件由 4 部分構(gòu)成:
Header:幻數(shù) + 版本號(hào)
DataBlock:時(shí)序數(shù)據(jù)塊
IndexBlock:時(shí)序數(shù)據(jù)索引
Footer:索引塊指針
Block 與 Series 的對(duì)應(yīng)關(guān)系:
每個(gè) IndexBlock 只屬于一個(gè) Series
每個(gè) DataBlock 只保存一個(gè) Field 的數(shù)據(jù)
DataBlock 的結(jié)構(gòu)較為簡(jiǎn)單,其中存儲(chǔ)了壓縮過(guò)的時(shí)序數(shù)據(jù):
DataBlock
Type數(shù)據(jù)類(lèi)型
Length時(shí)間戳數(shù)據(jù)長(zhǎng)度
Timestamps時(shí)間戳列表(壓縮后)
Values的時(shí)序數(shù)據(jù)列表(壓縮后)
IndexBlock 則較為復(fù)雜牺蹄,每個(gè) IndexBlock 由一個(gè) Meta 和多個(gè) Entry 構(gòu)成:
IndexBlock
Index Meta索引信息
Index Entry索引記錄列表
IndexMeta
Series Key索引所屬的 Series
Index Entry Count索引記錄數(shù)量
IndexEntry
Min Time數(shù)據(jù)開(kāi)始時(shí)間
Max Time數(shù)據(jù)結(jié)束時(shí)間
Offset數(shù)據(jù)塊的起始位置
Size數(shù)據(jù)塊的長(zhǎng)度
Meta 中存儲(chǔ)了 IndexBlock 對(duì)應(yīng)的 SeriesKey 以及對(duì)應(yīng)的 Entry 數(shù)量忘伞。
每個(gè) Entry 對(duì)應(yīng)一個(gè) DataBlock,描述了這個(gè) DataBlock 對(duì)應(yīng)的時(shí)間區(qū)間沙兰,以及實(shí)際的存儲(chǔ)地址氓奈。
當(dāng)需要查找 TSM 中的數(shù)據(jù)時(shí),只需要將 IndexBlock 加載到內(nèi)存中鼎天。就可以定位到相應(yīng)的數(shù)據(jù)舀奶,提高查詢效率。
壓縮算法
InfluxDB 中的數(shù)據(jù)類(lèi)型可以分為五種?timestamp斋射,float,?int,?bool,?string育勺。為了達(dá)到最優(yōu)的壓縮效果,InfluxDB 針對(duì)不同類(lèi)型的數(shù)據(jù)罗岖,使用了不同的壓縮算法涧至。不過(guò)這些壓縮算法的原理都大同小異:使用變長(zhǎng)編碼來(lái)保存數(shù)據(jù)有效位,避免存儲(chǔ)無(wú)效的 0 bit桑包。
timestamp
時(shí)序數(shù)據(jù)都是按照時(shí)間順序進(jìn)行排序的南蓬,因此首先會(huì)使用?delta-delta?編碼精簡(jiǎn)數(shù)據(jù):
相鄰的兩個(gè)時(shí)間戳相減,減少了數(shù)據(jù)的有效位長(zhǎng)度哑了,有利于后續(xù)的壓縮
若時(shí)間按固定區(qū)間分布赘方,優(yōu)先使用游程編碼run-length encoding進(jìn)行壓縮:
如果時(shí)間戳間隔固定,則使用兩個(gè) 64bit 數(shù)據(jù)可以編碼264個(gè)時(shí)間戳
若編碼后所有值均小于260垒手,則使用simple8b編碼,將多個(gè)值打包進(jìn)單個(gè) 64bit 整數(shù)中:
simple8b 將 64 位整數(shù)分為兩部分:
selector(4bit)?用于指定剩余 60bit 中存儲(chǔ)的整數(shù)的個(gè)數(shù)與有效位長(zhǎng)度
payload(60bit)?則是用于存儲(chǔ)多個(gè)定長(zhǎng)的整數(shù)
根據(jù)一個(gè)查找表倒信,將數(shù)據(jù)模式匹配到最優(yōu)的 selector科贬,然后將多個(gè)數(shù)據(jù)編碼至 payload
如果無(wú)法不滿足以上壓縮條件,則直接存儲(chǔ)原始數(shù)據(jù)鳖悠。
float
Facebook 工程師通過(guò)觀察時(shí)序數(shù)據(jù)榜掌,發(fā)現(xiàn)相鄰時(shí)序數(shù)據(jù)進(jìn)行異或操作后,僅有中間一小部分發(fā)生了變化乘综。
根據(jù)這個(gè)規(guī)律憎账,發(fā)明了一個(gè)簡(jiǎn)單高效的浮點(diǎn)數(shù)壓縮算法:先異或求值,然后存儲(chǔ)中間的有效數(shù)據(jù)卡辰。
通過(guò)這一算法胞皱,他們將浮點(diǎn)數(shù)據(jù)的平均存儲(chǔ)空間壓縮至 1.37 字節(jié)邪意。
算法過(guò)程可以參考這篇論文,或者直接參考下面的實(shí)現(xiàn):
@Data@Accessors(fluent = true)@ToStringstaticclassBlock{intleadingZero;inttailingZero;intblockSize;longvalue;booleanvalueOf(inti){? ? ? ? Validate.isTrue(i < blockSize);return((value >>> (blockSize-1-i)) &0x1) >0;? ? }booleanfallInSameBlock(Block block){returnblock !=null&& block.leadingZero == leadingZero && block.tailingZero == tailingZero;? ? }}staticBlockcalcBlock(doublex,doubley){longa = Double.doubleToRawLongBits(x);longb = Double.doubleToRawLongBits(y);longxor = a ^ b;? ? Block block =newBlock().? ? ? ? ? ? leadingZero(Long.numberOfLeadingZeros(xor)).? ? ? ? ? ? tailingZero(Long.numberOfTrailingZeros(xor));returnblock.value(xor >>> block.tailingZero()).? ? ? ? ? blockSize(block.value() ==0?0:64- block.leadingZero() - block.tailingZero());}staticPair> encode(double[] values) {intoffset =0;? ? BitSet buffer =newBitSet();booleanctrlBit;doubleprevious = values[0];? ? Block prevBlock =null;for(intn=1; n0; i--) {? ? ? ? ? ? ? ? ? ? buffer.set(offset++, ((leadingZero >> (i -1)) &0x1) >0);? ? ? ? ? ? ? ? }for(inti =6; i >0; i--) {? ? ? ? ? ? ? ? ? ? buffer.set(offset++, ((blockSize >> (i -1)) &0x1) >0);? ? ? ? ? ? ? ? }? ? ? ? ? ? }for(inti =0; i < block.blockSize(); i++) {? ? ? ? ? ? ? ? buffer.set(offset++, block.valueOf(i));? ? ? ? ? ? }? ? ? ? }? ? ? ? previous = values[n];? ? ? ? prevBlock = block;? ? }returnPair.of(Double.doubleToLongBits(values[0]), Pair.of(offset, buffer.toByteArray()));}staticListdecode(Pair> data){longprevious = data.getLeft();intdataLen = data.getRight().getKey();? ? BitSet buffer = BitSet.valueOf(data.getRight().getValue());? ? List values =newArrayList<>();? ? values.add(Double.longBitsToDouble(previous));intoffset =0;? ? Block blockMeta =null;while(offset < dataLen) {if(! buffer.get(offset++)) {? ? ? ? ? ? values.add(0d);? ? ? ? }else{booleanctrlBit = buffer.get(offset++);if(ctrlBit) {intleadingZero =0;intblockSize =0;for(inti =0; i <5; i++) {? ? ? ? ? ? ? ? ? ? leadingZero = (leadingZero <<1) | (buffer.get(offset++) ?0x1:0x0);? ? ? ? ? ? ? ? }for(inti =0; i <6; i++) {? ? ? ? ? ? ? ? ? ? blockSize = (blockSize <<1) | (buffer.get(offset++) ?0x1:0x0);? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? blockMeta =newBlock().leadingZero(leadingZero).blockSize(blockSize).? ? ? ? ? ? ? ? ? ? ? ? tailingZero(64- leadingZero - blockSize);? ? ? ? ? ? }? ? ? ? ? ? Validate.notNull(blockMeta);longvalue =0;for(inti =0; i < blockMeta.blockSize(); i++) {? ? ? ? ? ? ? ? value = (value <<1) | (buffer.get(offset++) ?0x1:0x0);? ? ? ? ? ? }? ? ? ? ? ? previous ^= (value << blockMeta.tailingZero());? ? ? ? ? ? values.add(Double.longBitsToDouble(previous));? ? ? ? }? ? }? ? Validate.isTrue(offset == dataLen);returnvalues;}publicstaticvoidmain(String[] args){double[] values =newdouble[]{15.5,14.0625,3.25,8.625,13.1,0,25.5};? ? Pair> data = encode(values);? ? System.out.println(data.getRight().getKey());// 編碼后的數(shù)據(jù)長(zhǎng)度反砌,單位 bitsSystem.out.println(decode(data));// 解碼后的數(shù)據(jù)}
int
對(duì)于整形數(shù)據(jù)雾鬼,首先會(huì)使用 ZigZag 編碼精簡(jiǎn)數(shù)據(jù)。
然后嘗試使用 RLE 或 simple8b 對(duì)精簡(jiǎn)后的數(shù)據(jù)進(jìn)行壓縮宴树。
如果無(wú)法不滿足壓縮條件策菜,則存儲(chǔ)原始數(shù)據(jù)。
bool
直接使用 Bitmap 對(duì)數(shù)據(jù)進(jìn)行編碼酒贬。
string
將多個(gè)字符串拼接在一起又憨,然后使用 Snappy 進(jìn)行壓縮
參考資料
InfluxDB
In-memory indexing and the Time-Structured Merge Tree (TSM)
時(shí)序數(shù)據(jù)庫(kù)技術(shù)體系 – 初識(shí)InfluxDB(推薦)
阿里云InfluxDB? Raft HybridStorage實(shí)現(xiàn)方案
壓縮編碼
Time-series compression algorithms, explained