時(shí)序數(shù)據(jù)庫(kù)技術(shù)體系-時(shí)序數(shù)據(jù)存儲(chǔ)模型設(shè)計(jì)
時(shí)序數(shù)據(jù)庫(kù)技術(shù)體系中一個(gè)非常重要的技術(shù)點(diǎn)是時(shí)序數(shù)據(jù)模型設(shè)計(jì),不同的時(shí)序系統(tǒng)有不同的設(shè)計(jì)模式,不同的設(shè)計(jì)模式對(duì)時(shí)序數(shù)據(jù)的讀寫性能、數(shù)據(jù)壓縮效率等各個(gè)方面都有不同程度的影響。這篇文章筆者將會(huì)分別針對(duì)OpenTSDB、Druid、InfluxDB以及Beringei這四個(gè)時(shí)序系統(tǒng)中的時(shí)序數(shù)據(jù)模型設(shè)計(jì)進(jìn)行介紹。
在詳細(xì)介紹時(shí)序數(shù)據(jù)模型之前,還是有必要簡(jiǎn)單回顧一下時(shí)序數(shù)據(jù)的幾個(gè)基本概念,如下圖所示:
上圖是一個(gè)典型的時(shí)序數(shù)據(jù)示意圖,由圖中可以看出,時(shí)序數(shù)據(jù)由兩個(gè)維度坐標(biāo)來表示,橫坐標(biāo)表示時(shí)間軸,隨著時(shí)間的不斷流逝,數(shù)據(jù)也會(huì)源源不斷地吐出來;和橫坐標(biāo)不同,縱坐標(biāo)由兩種元素構(gòu)成,分別是數(shù)據(jù)源和metric,數(shù)據(jù)源由一系列的標(biāo)簽(tag,也稱為維度)唯一表示,圖中數(shù)據(jù)源是一個(gè)廣告數(shù)據(jù)源,這個(gè)數(shù)據(jù)源由publisher、advertiser、gender以及country四個(gè)維度值唯一表示,metric表示待收集的數(shù)據(jù)源指標(biāo)。一個(gè)數(shù)據(jù)源通常會(huì)采集很多指標(biāo)(metric),上圖中廣告數(shù)據(jù)源就采集了impressions、clicks以及revenue這三種指標(biāo),分別表示廣告瀏覽量、廣告點(diǎn)擊率以及廣告收入。
看到這里,相信大家對(duì)時(shí)序數(shù)據(jù)已經(jīng)有了一個(gè)初步的了解,可以簡(jiǎn)單的概括為:一個(gè)時(shí)序數(shù)據(jù)點(diǎn)(point)由datasource(tags)+metric+timestamp這三部分唯一確定。然而,這只是邏輯上的概念理解,那具體的時(shí)序數(shù)據(jù)庫(kù)到底是如何將這樣一系列時(shí)序數(shù)據(jù)點(diǎn)進(jìn)行存儲(chǔ)的呢?下文筆者針對(duì)OpenTSDB、Druid、InfluxDB以及Beringei四種系統(tǒng)進(jìn)行介紹。
OpenTSDB(HBase)時(shí)序數(shù)據(jù)存儲(chǔ)模型
OpenTSDB基于HBase存儲(chǔ)時(shí)序數(shù)據(jù),在HBase層面設(shè)計(jì)RowKey規(guī)則為: metric+timestamp+datasource(tags) 。HBase是一個(gè)KV數(shù)據(jù)庫(kù),一個(gè)時(shí)序數(shù)據(jù)(point)如果以KV的形式表示,那么其中的V必然是point的具體數(shù)值,而K就自然而然是唯一確定point數(shù)值的datasource+metric+timestamp。 這種規(guī)律不僅適用于HBase,還適用于其他KV數(shù)據(jù)庫(kù),比如Kudu。
既然HBase中K是由datasource、metric以及timestamp三者構(gòu)成,現(xiàn)在我們可以簡(jiǎn)單認(rèn)為rowkey就為這三者的組合,那問題來了:這三者的組合順序是怎么樣的呢?
首先來看哪個(gè)應(yīng)該排在首位。因?yàn)镠Base中一張表的數(shù)據(jù)組織方式是按照rowkey的字典序順序排列的,為了將同一種指標(biāo)的所有數(shù)據(jù)集中放在一起,HBase將將metric放在了rowkey的最前面。假如將timestamp放在最前面,同一時(shí)刻的數(shù)據(jù)必然會(huì)寫入同一個(gè)數(shù)據(jù)分片,無(wú)法起到散列的效果;而如果將datasource(即tags)放在最前面的話,這里有個(gè)更大的問題,就是datasource本身由多個(gè)標(biāo)簽組成,如果用戶指定其中部分標(biāo)簽查找,而且不是前綴標(biāo)簽的話,在HBase里面將會(huì)變成大范圍的掃描過濾查詢,查詢效率非常之低。舉個(gè)上面的例子,如果將datasource放在最前面,那rowkey就可以表示為publisher=ultrarimfast.com&advertiser:google.com&gender:Male&country:USA_impressions_20110101000000,此時(shí)用戶想查找20110101000000這個(gè)時(shí)間點(diǎn)所有發(fā)布在USA的所有廣告的瀏覽量,即只根據(jù)country=USA這樣一個(gè)維度信息查找指定時(shí)間點(diǎn)的某個(gè)指標(biāo),而且這個(gè)維度不是前綴維度,就會(huì)掃描大量的記錄進(jìn)行過濾。
確定了metric放在最前面之后,再來看看接下來應(yīng)該將datasource放在中間呢還是應(yīng)該將timestamp放在中間?將metric放在前面已經(jīng)可以解決請(qǐng)求均勻分布(散列)的要求,因此HBase將timestamp放在中間,將datasource放在最后。試想,如果將datasource放在中間,也會(huì)遇到上文中說到的后綴維度查找的問題。
因此,OpenTSDB中rowkey的設(shè)計(jì)為:metric+timestamp+datasource,好了,那HBase就可以只設(shè)置一個(gè)columnfamily和一個(gè)column。那問題來了,OpenTSDB的這種設(shè)計(jì)有什么問題?在了解設(shè)計(jì)問題之前需要簡(jiǎn)單看看HBase在文件中存儲(chǔ)KV的方式,即一系列時(shí)序數(shù)據(jù)在文件、內(nèi)存中的存儲(chǔ)方式,如下圖所示:
上圖是HBase中一個(gè)存儲(chǔ)KeyValue(KV)數(shù)據(jù)的數(shù)據(jù)塊結(jié)構(gòu),一個(gè)數(shù)據(jù)塊由多個(gè)KeyValue數(shù)據(jù)組成,在我們的事例中KeyValue就是一個(gè)時(shí)序數(shù)據(jù)點(diǎn)(point)。其中Value結(jié)構(gòu)很簡(jiǎn)單,就是一個(gè)數(shù)值。而Key就比較復(fù)雜了,由rowkey+columnfamily+column+timestamp+keytype組成,其中rowkey等于metric+timestamp+datasource。
- 問題一:存在很多無(wú)用的字段。 一個(gè)KeyValue中只有rowkey是有用的,其他字段諸如columnfamily、column、timestamp以及keytype從理論上來講都沒有任何實(shí)際意義,但在HBase的存儲(chǔ)體系里都必須存在,因而耗費(fèi)了很大的存儲(chǔ)成本。
- 問題二:數(shù)據(jù)源和采集指標(biāo)冗余。 KeyValue中rowkey等于metric+timestamp+datasource,試想同一個(gè)數(shù)據(jù)源的同一個(gè)采集指標(biāo),隨著時(shí)間的流逝不斷吐出采集數(shù)據(jù),這些數(shù)據(jù)理論上共用同一個(gè)數(shù)據(jù)源(datasource)和采集指標(biāo)(metric),但在HBase的這套存儲(chǔ)體系下,共用是無(wú)法體現(xiàn)的,因此存在大量的數(shù)據(jù)冗余,主要是數(shù)據(jù)源冗余以及采集指標(biāo)冗余。
- 問題三:無(wú)法有效的壓縮。 HBase提供了塊級(jí)別的壓縮算法-snappy、gzip等,這些通用壓縮算法并沒有針對(duì)時(shí)序數(shù)據(jù)進(jìn)行設(shè)置,壓縮效率比較低。HBase同樣提供了一些編碼算法,比如FastDiff等等,可以起到一定的壓縮效果,但是效果并不佳。效果不佳的主要原因是HBase沒有數(shù)據(jù)類型的概念,沒有schema的概念,不能針對(duì)特定數(shù)據(jù)類型進(jìn)行特定編碼,只能選擇通用的編碼,效果可想而知。
- 問題四:不能完全保證多維查詢能力。 HBase本身沒有schema,目前沒有實(shí)現(xiàn)倒排索引機(jī)制,所有查詢必須指定metric、timestamp以及完整的tags或者前綴tags進(jìn)行查詢,對(duì)于后綴維度查詢也勉為其難。
雖說有這樣那樣的問題,但是OpenTSDB還是針對(duì)存儲(chǔ)模型做了兩個(gè)方面的優(yōu)化:
- 優(yōu)化一:timestamp并不是想象中細(xì)粒度到秒級(jí)或毫秒級(jí),而是精確到小時(shí)級(jí)別,然后將小時(shí)中每一秒設(shè)置到列上。 這樣一行就會(huì)有3600列,每一列表示一小時(shí)的一秒。這樣設(shè)置據(jù)說可以有效的取出一小時(shí)整的數(shù)據(jù)。
- 優(yōu)化二:所有metrics以及所有標(biāo)簽信息(tags)都使用了全局編碼將標(biāo)簽值編碼成更短的bit,減少rowkey的存儲(chǔ)數(shù)據(jù)量。 上文分析HBase這種存儲(chǔ)方式的弊端是說道會(huì)存在大量的數(shù)據(jù)源(tags)冗余以及指標(biāo)(metric)冗余,有冗余是吧,那我就搞個(gè)編碼,將string編碼成bit,盡最大努力減少冗余。雖說這樣的全局編碼可以有效降低數(shù)據(jù)的存儲(chǔ)量,但是因?yàn)槿志幋a字典需要存儲(chǔ)在內(nèi)存中,因此在很多時(shí)候(海量標(biāo)簽值),字典所需內(nèi)存都會(huì)非常之大。
上述兩個(gè)優(yōu)化可以參考OpenTSDB這張經(jīng)典的示意圖:
Druid時(shí)序數(shù)據(jù)存儲(chǔ)模型設(shè)計(jì)
和HBase和Kudu這類KV數(shù)據(jù)庫(kù)不同,Druid是另一種玩法。Druid是一個(gè)不折不扣的列式存儲(chǔ)系統(tǒng),沒有HBase的主鍵。上述時(shí)序數(shù)據(jù)在Druid中表示是下面這個(gè)樣子的:
Druid是一個(gè)列式數(shù)據(jù)庫(kù),所以每一列都會(huì)獨(dú)立存儲(chǔ),比如Timestamp列會(huì)存儲(chǔ)在一起形成一個(gè)文件,publish列會(huì)存儲(chǔ)在一起形成一個(gè)文件,以此類推。細(xì)心的童鞋就會(huì)說了,這樣存儲(chǔ),依然會(huì)有數(shù)據(jù)源(tags)大量冗余的問題。針對(duì)冗余這個(gè)問題,Druid和HBase的處理方式一樣,都是采用編碼字典對(duì)標(biāo)簽值進(jìn)行編碼,將string類型的標(biāo)簽值編碼成int值。但和HBase不一樣的是,Druid編碼是局部編碼,Druid和HBase都采用LSM結(jié)構(gòu),數(shù)據(jù)先寫入內(nèi)存再flush到數(shù)據(jù)文件,Druid編碼是文件級(jí)別的,局部編碼可以有效減小對(duì)內(nèi)存的巨大壓力。除此之外,Druid的這種列式存儲(chǔ)模式還有如下好處:
數(shù)據(jù)存儲(chǔ)壓縮率高。每列獨(dú)立存儲(chǔ),可以針對(duì)每列進(jìn)行壓縮,而且可以為每列設(shè)置對(duì)應(yīng)的壓縮策略,比如時(shí)間列、int、fload、double、string都可以分別進(jìn)行壓縮,壓縮效果更好。
支持多維查找。Druid為datasource的每個(gè)列分別設(shè)置了Bitmap索引,利用Bitmap索引可以有效實(shí)現(xiàn)多維查找,比如用戶想查找20110101T00:00:00這個(gè)時(shí)間點(diǎn)所有發(fā)布在USA的所有廣告的瀏覽量,可以根據(jù)country=USA在Bitmap索引中找到要找的行號(hào),再根據(jù)行號(hào)定位待查的metrics。
然而,這樣的存儲(chǔ)模型也有一些問題:
- 數(shù)據(jù)依然存在冗余。 和OpenTSDB一樣,tags存在大量的冗余。
- 指定數(shù)據(jù)源的范圍查找并沒有OpenTSDB高效。 這是因?yàn)镈ruid會(huì)將數(shù)據(jù)源拆開成多個(gè)標(biāo)簽,每個(gè)標(biāo)簽都走Bitmap索引,再最后使用與操作找到滿足條件的行號(hào),這個(gè)過程需要一定的開銷。而OpenTSDB中直接可以根據(jù)數(shù)據(jù)源拼成rowkey,查找走B+樹索引,效率必然會(huì)更高。
InfluxDB時(shí)序數(shù)據(jù)存儲(chǔ)模型設(shè)計(jì)
相比OpenTSDB以及Druid,可能很多童鞋對(duì)InfluxDB并不特別熟悉,然而在時(shí)序數(shù)據(jù)庫(kù)排行榜單上InfluxDB卻是遙遙領(lǐng)先。InfluxDB是一款專業(yè)的時(shí)序數(shù)據(jù)庫(kù),只存儲(chǔ)時(shí)序數(shù)據(jù),因此在數(shù)據(jù)模型的存儲(chǔ)上可以針對(duì)時(shí)序數(shù)據(jù)做非常多的優(yōu)化工作。
為了保證寫入的高效,InfluxDB也采用LSM結(jié)構(gòu),數(shù)據(jù)先寫入內(nèi)存,當(dāng)內(nèi)存容量達(dá)到一定閾值之后flush到文件。InfluxDB在時(shí)序數(shù)據(jù)模型設(shè)計(jì)方面提出了一個(gè)非常重要的概念:seriesKey,seriesKey實(shí)際上就是datasource(tags)+metric,時(shí)序數(shù)據(jù)寫入內(nèi)存之后按照seriesKey進(jìn)行組織:
內(nèi)存中實(shí)際上就是一個(gè)Map:>,Map中一個(gè)SeriesKey對(duì)應(yīng)一個(gè)List,List中存儲(chǔ)時(shí)間線數(shù)據(jù)。數(shù)據(jù)進(jìn)來之后根據(jù)datasource(tags)+metric拼成SeriesKey,再將Timestamp|Value組合值寫入時(shí)間線數(shù)據(jù)List中。內(nèi)存中的數(shù)據(jù)flush的文件后,同樣會(huì)將同一個(gè)SeriesKey中的時(shí)間線數(shù)據(jù)寫入同一個(gè)Block塊內(nèi),即一個(gè)Block塊內(nèi)的數(shù)據(jù)都屬于同一個(gè)數(shù)據(jù)源下的一個(gè)metric。
這種設(shè)計(jì)我們認(rèn)為是將時(shí)間序列數(shù)據(jù)按照時(shí)間線挑了出來。先來看看這樣設(shè)計(jì)的好處:
- 好處一:同一數(shù)據(jù)源的tags不再冗余存儲(chǔ)。一個(gè)Block內(nèi)的數(shù)據(jù)都共用一個(gè)SeriesKey,只需要將這個(gè)SeriesKey寫入這個(gè)Block的Trailer部分就可以。 大大降低了時(shí)序數(shù)據(jù)的存儲(chǔ)量。
- 好處二:時(shí)間序列和value可以在同一個(gè)Block內(nèi)分開獨(dú)立存儲(chǔ),獨(dú)立存儲(chǔ)就可以對(duì)時(shí)間列以及數(shù)值列分別進(jìn)行壓縮。InfluxDB對(duì)時(shí)間列的存儲(chǔ)借鑒了Beringei的壓縮方式,使用delta-delta壓縮方式極大的提高了壓縮效率。而對(duì)Value的壓縮可以針對(duì)不同的數(shù)據(jù)類型采用相同的壓縮效率。
- 好處三:對(duì)于給定數(shù)據(jù)源以及時(shí)間范圍的數(shù)據(jù)查找,可以非常高效的進(jìn)行查找。這一點(diǎn)和OpenTSDB一樣。
細(xì)心的同學(xué)可能會(huì)問了,將datasource(tags)和metric拼成SeriesKey,不是也不能實(shí)現(xiàn)多維查找。確實(shí)是這樣,不過InfluxDB內(nèi)部實(shí)現(xiàn)了倒排索引機(jī)制,即實(shí)現(xiàn)了tag到SeriesKey的映射關(guān)系,如果用戶想根據(jù)某個(gè)tag查找的話,首先根據(jù)tag在倒排索引中找到對(duì)應(yīng)的SeriesKey,再根據(jù)SeriesKey定位具體的時(shí)間線數(shù)據(jù)。 InfluxDB的這種存儲(chǔ)引擎稱為TSM,全稱為Timestamp-Structure Merge Tree,基本原理類似于LSM。后期筆者將會(huì)對(duì)InfluxDB的數(shù)據(jù)寫入、文件格式、倒排索引以及數(shù)據(jù)讀取進(jìn)行專題介紹。
Beringei時(shí)序數(shù)據(jù)存儲(chǔ)模型設(shè)計(jì)
Beringei是今年Facebook開源的一個(gè)時(shí)序數(shù)據(jù)庫(kù)系統(tǒng)。InfluxDB時(shí)序數(shù)據(jù)模型設(shè)計(jì)很好地將時(shí)間序列按照數(shù)據(jù)源以及metric挑選了出來,解決了維度列值冗余存儲(chǔ),時(shí)間列不能有效壓縮的問題。但I(xiàn)nfluxDB沒有很好的解決寫入緩存壓縮的問題:InfluxDB在寫入內(nèi)存的時(shí)候并沒有壓縮,而是在數(shù)據(jù)寫入文件的時(shí)候進(jìn)行對(duì)應(yīng)壓縮。我們知道時(shí)序數(shù)據(jù)最大的特點(diǎn)之一是最近寫入的數(shù)據(jù)最熱,將最近寫入的數(shù)據(jù)全部放在內(nèi)存可以極大提升讀取效率。Beringei很好的解決了這個(gè)問題,流式壓縮意味著數(shù)據(jù)寫入內(nèi)存之后就進(jìn)行壓縮,這樣會(huì)使得內(nèi)存中可以緩存更多的時(shí)序數(shù)據(jù),這樣對(duì)于最近數(shù)據(jù)的查詢會(huì)有很大的幫助。
Beringei的時(shí)序數(shù)據(jù)模型設(shè)計(jì)與InfluxDB基本一致,也是提出類似于SeriesKey的概念,將時(shí)間線挑了出來。但和InfluxDB有兩個(gè)比較大的區(qū)別:
- 文件組織形式不同。Beringei的文件存儲(chǔ)形式按照時(shí)間窗口組織,比如最近5分鐘的數(shù)據(jù)全部寫入同一個(gè)文件,這個(gè)文件分為很多block,每個(gè)block中的所有時(shí)序數(shù)據(jù)共用一個(gè)SeriesKey。Beringei文件沒有索引,InfluxDB有索引。
- Beringei目前沒有倒排索引機(jī)制,因此對(duì)于多維查詢并不高效。
后續(xù)筆者也會(huì)針對(duì)Beringei的數(shù)據(jù)寫入、流式壓縮、文件格式等進(jìn)行介紹。在筆者看來,如果將Beringei和InfluxDB有效結(jié)合起來,就能夠?qū)r(shí)序數(shù)據(jù)高效存儲(chǔ)在內(nèi)存,另外數(shù)據(jù)按照維度進(jìn)行組織,可以非常高效的提高數(shù)據(jù)在文件的存儲(chǔ)效率以及查詢效率,最后結(jié)合InfluxDB的倒排索引功能可以有效提高多維查詢能力。
本文是時(shí)序數(shù)據(jù)庫(kù)技術(shù)體系的第一篇文章,筆者主要結(jié)合OpenTSDB、Druid、InfluxDB以及Beringei這四種時(shí)序數(shù)據(jù)庫(kù)分別對(duì)時(shí)序數(shù)據(jù)這種數(shù)據(jù)形式的存儲(chǔ)模型進(jìn)行了介紹。每種數(shù)據(jù)庫(kù)都有自己的一套存儲(chǔ)方式,而每種存儲(chǔ)方式都有各自的一些優(yōu)勢(shì)以及缺陷,正是這些優(yōu)劣式直接決定了相應(yīng)時(shí)序數(shù)據(jù)庫(kù)的壓縮性能、讀寫性能。