Overview
前一篇文章深入了解(jie)Prometheus監控平臺(tai)對prometheus的(de)存(cun)儲結構做(zuo)了一下介紹(shao),在(zai)后續的(de)了解(jie)中(zhong),發現(xian)Prometheus的(de)實現(xian)上實際上參考了Gorilla。
Gorilla 是Facebook內(nei)部(bu)孵化(hua)的一(yi)款內(nei)存(cun)(cun)(cun)時序(xu)數據(ju)庫,用于(yu)存(cun)(cun)(cun)儲Facebook內(nei)部(bu)監控系統(tong)的時序(xu)數據(ju),采用了比較(jiao)激(ji)進的壓縮方(fang)式,節(jie)省大量(liang)的存(cun)(cun)(cun)儲空間(jian),同時基于(yu)內(nei)存(cun)(cun)(cun)的時序(xu)數據(ju)庫,使得(de)查詢效(xiao)率得(de)到(dao)較(jiao)大提高。
Gorilla的具體信息可(ke)以參(can)考Facebook出的官方論(lun)文Gorilla: A Fast, Scalable, In-Memory Time Series Database
這(zhe)篇文(wen)章主(zhu)要想總(zong)結(jie)一下(xia)Gorilla的壓縮算法。
時序數據的組成
對于Gorilla而(er)言,每(mei)一(yi)個(ge)時序數據(ju)由一(yi)個(ge)三元(yuan)組(zu)組(zu)成(這點和(he)prometheus一(yi)致)
- key:
string類型,參考prometheus的series名 - timestamp:時間戳,64bit
- value:
float64類型,時序數據的值
Gorilla集qun由多個shard(實例)構成(cheng),時序(xu)存在哪(na)一個shard上(shang)是對key進行(xing)計算得(de)(de)到,擴容時只(zhi)需要修改分片函數,使(shi)得(de)(de)時序(xu)能(neng)夠分配到新shard上(shang)即(ji)可(ke)。
Gorilla的(de)壓縮算法使得壓縮效率(lv)從沒有壓縮的(de)16bytes/sample壓到了1.37bytes/samples。大概的(de)壓縮流程如下圖(tu)所示。

該壓縮算法對時(shi)間戳和(he)時(shi)序值采用了不(bu)同壓縮方(fang)式,將在下文具(ju)體介紹。
delta of delta -- 時間戳壓縮方式
Gorilla對時間(jian)戳采(cai)用的(de)是delta of delta的(de)壓(ya)縮方式,感覺和信號/圖形處理領域的(de)差分編碼思想上類似(si)。
對于(yu)一個監控(kong)系統(tong)而言(yan),我(wo)們的采樣間(jian)隔總(zong)是(shi)固定的,因此采樣的時間(jian)戳是(shi)很有規律的,比如第(di)0s,第(di)15s,第(di)30s,第(di)45s,只有少量的特殊情況(kuang)會有較小的誤差。
因此,如果我們存(cun)(cun)(cun)儲(chu)的(de)(de)數據是(shi)和(he)前(qian)一(yi)個時(shi)間(jian)的(de)(de)差(cha)值(zhi),那么我們就只需(xu)要存(cun)(cun)(cun)第(di)一(yi)個完整的(de)(de)時(shi)間(jian)戳,后續的(de)(de)時(shi)間(jian)只需(xu)存(cun)(cun)(cun)和(he)前(qian)一(yi)個的(de)(de)差(cha)值(zhi),即15,15,15,15,如果有部分誤(wu)差(cha)的(de)(de)時(shi)間(jian)戳,則(ze)可能存(cun)(cun)(cun)的(de)(de)是(shi)16/14。這樣就可以減少用于存(cun)(cun)(cun)儲(chu)的(de)(de)bit。
再更進一步壓縮,我們可以(yi)存(cun)儲時(shi)(shi)間(jian)(jian)差(cha)(cha)(cha)的差(cha)(cha)(cha)值(zhi),第(di)(di)(di)一個(ge)時(shi)(shi)間(jian)(jian)戳(chuo)是(shi)(shi)0,第(di)(di)(di)二個(ge)時(shi)(shi)間(jian)(jian)戳(chuo)和第(di)(di)(di)一個(ge)時(shi)(shi)間(jian)(jian)戳(chuo)的差(cha)(cha)(cha)值(zhi)是(shi)(shi)15,第(di)(di)(di)三(san)個(ge)時(shi)(shi)間(jian)(jian)戳(chuo)和第(di)(di)(di)二個(ge)時(shi)(shi)間(jian)(jian)的差(cha)(cha)(cha)值(zhi) 減去(qu)第(di)(di)(di)二個(ge)時(shi)(shi)間(jian)(jian)戳(chuo)和第(di)(di)(di)一個(ge)時(shi)(shi)間(jian)(jian)戳(chuo)的差(cha)(cha)(cha)值(zhi) 是(shi)(shi)0,因此我們存(cun)儲的數據就(jiu)是(shi)(shi)0,15,0,0。如果有部分誤差(cha)(cha)(cha)的時(shi)(shi)間(jian)(jian)戳(chuo),那么0可能(neng)就(jiu)是(shi)(shi)1/2/-1/-2。可以(yi)看出,所(suo)需要的bit數大(da)幅地下降。
Gorilla采(cai)用的就是后(hou)面說的這種(zhong)壓(ya)縮方(fang)式(shi)(delta of delta)
每一個時間窗口(2小時)的數據會存放在一個block當中,block header會存放這個時間窗口的開始時間(t−1t_{-1}t−1?),需要64bit
第一個時序數據會存儲數據采樣時間和起始時間的差值(t0t_0t0?),需要14bit,這個(ge)14bit足夠存(cun)2個(ge)小時(shi)的差值了。
然后(hou),對(dui)于后(hou)續的每(mei)一個時間戳,采用下(xia)圖(a)的公式計(ji)算delta of delta的值
- 如果是0,則只需要存0這一個bit
- 如果大小在-63到64區間,則加上前綴10進行存儲,一共存儲2+7個bit
- 如果在-255到256區間,則加上前綴110進行存儲,一共存儲3+9個bit
- 如果在-2047到2048區間,則加上前綴1110進行存儲,一共存儲4+12bit
- 更大的話,則加上前綴1111,再加上實際的差值,一共32bit

對(dui)于(yu)這個區間的選擇,應該是(shi)facebook通(tong)過具(ju)體數據的分析(xi)得到的。
從論文的圖(tu)中可以看到,絕大(da)部(bu)分的時間戳,其(qi)實(shi)用(yong)(yong)1個bit(0)存儲(chu)就夠了,需要(yao)用(yong)(yong)32位(wei)來存儲(chu)的是(shi)極少數。

現在再回頭看(kan)這個圖,就可以知道他的(de)時間是怎么算出(chu)來的(de)了
t2t_2t2?和t1t_1t1?的差值是(shi)60,而前一(yi)個時間(jian)點(dian)的delta是(shi)62,計算(suan)出(chu)來(lai)的D=-2,在-63到64區間(jian),所(suo)以(yi)需要加上10前綴,在用(yong)7bit存儲-2
t3t_3t3?的delta和t2t_2t2?一樣,所以直(zhi)接存0,只用1bit

XOR -- 值壓縮方式
Gorilla的(de)值(zhi)類(lei)型是float64雙精度類(lei)型,對其進(jin)(jin)行壓(ya)縮(suo)的(de)方式則是將(jiang)值(zhi)和前一個值(zhi)進(jin)(jin)行XOR異或

雙精度浮(fu)點數的(de)(de)結構如上圖,1位的(de)(de)符號位,11位的(de)(de)指(zhi)數位,,和52位的(de)(de)小數位
對于監控系(xi)統而(er)言(yan),每隔固定的(de)時間(jian)進行采樣,其(qi)實數據(ju)量并不會(hui)有太大變化,對于平穩(wen)運(yun)行的(de)的(de)系(xi)統,不可能會(hui)cpu突然暴(bao)漲(zhang),內存突然暴(bao)漲(zhang)。所以存儲的(de)值其(qi)實有很多位(wei)是一樣的(de)。
Gorilla利用(yong)這個特性,利用(yong)異或對(dui)數據進行壓縮。


壓縮的思(si)路(lu)大致如下:
-
block的第一(yi)個數不(bu)壓縮存儲
-
后(hou)續(xu)的(de)每個值(zhi)和(he)前一個值(zhi)做XOR異(yi)或
-
如果(guo)結果(guo)為0,則只存(cun)儲一位(wei)0
-
如果結(jie)果不為0,那么會(hui)有兩種情況
- 如果異或的結果和前一個數異或的結果,他們的前導0和后導0數量一樣,那么存儲前綴
10加有效位的數據 - 如果前導0和后導0數量不一致,那么就存前綴
11+ 5位的前導0數量 +6位的有效位數量(實際上就是存了后導0數量) +有效位數據
- 如果異或的結果和前一個數異或的結果,他們的前導0和后導0數量一樣,那么存儲前綴
論文中(zhong)也給出了一些實例

對于絕(jue)大部分的(de)數據,是(shi)(shi)可以共享前導0和后導0的(de),因此需要用于存(cun)儲(chu)的(de)bit是(shi)(shi)較少的(de)。
分布情況
同樣的,再看這張圖,可以知道第二個數據24的XOR結果(0)和前一個時序數據的XOR結果沒有共享前后導0,因此需要存前綴11,然后數據0x001有(you)11個(ge)前導0,有(you)效為長度位1,有(you)效位數據是(shi)1,最后存儲的(de)結(jie)果就如圖所示。

通過上面(mian)的(de)壓(ya)縮(suo)(suo)分(fen)析,其實如果一個block的(de)時間(jian)(jian)窗口越長,壓(ya)縮(suo)(suo)效率會越高(gao)。但同時,解壓(ya)縮(suo)(suo)的(de)成本的(de)會相應提(ti)高(gao),Facebook的(de)論(lun)文給出的(de)一個最(zui)合(he)適的(de)時間(jian)(jian)窗口為2小時,prometheus采用的(de)也(ye)是這個值。

Gorilla內存中的數據結構
Gorilla的實(shi)現用的是(shi)C++,他存儲(chu)的數(shu)據結構(gou)大致如下圖所示

- shardMap是分片的map,ShardMap用一個數組vector來存儲TSmap的指針(unique_ptr),查詢時,通過key計算出時序在哪個分片shard上,然后快速查找到對應的TsMap上。
- TSmap即Timeseries Map,由一個vector和map組成,均作為時序數據的索引使用。vector的元素指向對應時間序列的智能指針(shared_ptr);map以時間序列的名稱作為key(大小寫不敏感但是保留大小寫),指向時間序列的智能指針作為value。vector可以提供較好的范圍查詢能力,map提供較好的點查詢能力。
- TS為真正存儲數據的塊,包含一個打開的block,用于寫入新數據。和多個關閉的塊,存儲舊數據。每個塊存儲2個小時的數據
ps.TsMap對每個(ge)分片(pian)維(wei)護一個(ge)分片(pian)的讀寫鎖(suo)(suo),減小鎖(suo)(suo)的粒度,不(bu)用鎖(suo)(suo)全部數據。
與Gorilla不同,Gorilla 的(de)解碼放(fang)在客戶端(duan),而promethus的(de)解碼在服務端(duan)完成。
高可用
facebook在論文中提(ti)到(dao),Gorilla通過將數(shu)據寫到(dao)兩個不同地區(qu)的(de)集qun中,一個壞了會(hui)切換到(dao)另一個;
Gorrila在(zai)不同的(de)數(shu)據中(zhong)心維護(hu)兩個完全(quan)一樣但不考(kao)慮一致性的(de)實(shi)例級別的(de)數(shu)據副本,以實(shi)現(xian)數(shu)據中(zhong)心故障或分區的(de)情況下的(de)高可(ke)用(yong)。
對于這(zhe)點,prometheus感覺也是采(cai)樣的(de)相似的(de)思路。