環境: CentOS6.5_x64
InfluxDB版本:1.1.0
數據壓縮可以參考:
https://docs.influxdata.com/influxdb/v1.1/concepts/storage_engine/#compression
influxdb根據不同的數據類型會采用不同的壓縮算法。
- int
首先使用ZigZag算法進行編碼,如果編碼后的值小於 (1 << 60 ) - 1,使用simple8b算法;
如果大於該值,不壓縮;
- timestamp
時間戳為獨立的數據類型,並且具有一定的規律可循,在InfluxDB中, 針對時間戳先執行排序操作后使用差分編碼算法進行編碼,然后再根據編碼結果采用不同的算法。
解釋如下:
1、根據輸入的原始數組arrValues計算出差值數組deltaValues;
2、如果差值數組的所有值相同,使用RLE編碼算法;
3、如果差值數組的所有值不同,並且差值數組的最大值大於(1 << 60)- 1,使用Raw編碼算法;
4、如果差值數組的所有值不同,並且差值數組的最大值不大於(1 << 60)- 1,使用Packed編碼;
- float
使用 Facebook Gorilla paper提供的浮點數壓縮算法
- bool
只有1位數據,采用簡單的位數據打包策略
- string
采用snappy算法
壓縮算法介紹
ZigZag算法
ZigZag這個算法使用的基礎就是認為在大多數情況下,我們使用的數字都是不大的數字。 其原理是將標志位后移至末尾,並去掉編碼中多余的0,從而達到壓縮效果。
算法描述
編碼過程
其編碼過程如下:
1)獲取int64類型輸入X;
2)對X執行左移1位的操作,得到X1;
3)對X執行右移63位的操作,得到X2;
4)對X1和X2執行異或運算,得到ZigZag編碼結果;
從編碼過程可以看出,該算法的原理是將標志位后移至末尾,如果是負數則保留符號位移過來的1,非負數直接為0(異或操作),去掉編碼中多余的前導0,則可以使用更少的字節來存儲數據,從而達到壓縮效果。
比如int64類型的數字1,其標志位為0,用二進制表示時前面會有63個0,最后一位才是1,執行位移操作后,X1為2,X2為0,執行異或操作后的值為2,前面有62個0, 去掉前面多余的0,僅用最后8位數表示,則編碼后的數據為: 00000010 。
標志位后移主要是為了處理負數,比如int64類型的數字 -1 ,其標志位為1,用二進制表示時兩端各有一個1,中間有62個0,執行位移操作后,X1為0xfffffffffffffffe,X2為0xffffffffffffffff,執行異或操作后的值為1,前面有62個0,去掉前面多余的0,僅用最后8位數表示,則編碼后的數據為: 00000001 。
如果用原來的64位int傳輸顯然很浪費帶寬,可以使用8位的int傳輸,則帶寬為原來的 1/8 ,針對小數據壓縮效果很明顯。
小整數對應的ZigZag碼字短,大整數對應的ZigZag碼字長。在特定的場景下,比如,要傳輸的整數為大整數居多,ZigZag編碼的壓縮效率就不理想了。
解碼過程
該算法的解碼過程如下:
1)獲取ZigZag編碼結果V;
2)對V執行右移1位的操作,得到結果V1;
3)將V與1相與,得到中間值,將中間值左移63位,然后右移63位,得到結果V2;
4)對V1和V2執行異或操作,得到結果X;
算法實現
ZigZag編碼實現(go語言代碼):
// ZigZagEncode converts a int64 to a uint64 by zig zagging negative and positive values // across even and odd numbers. Eg. [0,-1,1,-2] becomes [0, 1, 2, 3] func ZigZagEncode(x int64) uint64 { return uint64(uint64(x<<1) ^ uint64((int64(x) >> 63))) } // ZigZagDecode converts a previously zigzag encoded uint64 back to a int64 func ZigZagDecode(v uint64) int64 { return int64((v >> 1) ^ uint64((int64(v&1)<<63)>>63)) }
其它
示例代碼:
package main import ( "fmt" ) func ZigZagEncode(x int64) uint64 { return uint64(uint64(x<<1) ^ uint64((int64(x) >> 63))) } func ZigZagDecode(v uint64) int64 { return int64((v >> 1) ^ uint64((int64(v&1)<<63)>>63)) } func main() { var arr []int64 arr = append(arr,-1) arr = append(arr,0) arr = append(arr,1) fmt.Printf("original \t encode \t decode \t\n") for _,a := range arr { a1 := ZigZagEncode(a) a2 := ZigZagDecode(a1) fmt.Printf("%d \t\t %d \t\t %d\n",a,a1,a2) } }
運行效果如下:
[root@localhost test]# go run zigzagTest1.go original encode decode -1 1 -1 0 0 0 1 2 1 [root@localhost test]#
simple8b算法
Simple8b算法是64位算法,實現將多個整型數據(在 0 和 1<<60 - 1 之間)壓縮到一個64位的存儲結構中。
其中前4位為選擇器,后面60位用於存儲數據,數據使用下表進行編碼:
┌──────────────┬─────────────────────────────────────────────────────────────┐ │ Selector │ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15│ ├──────────────┼─────────────────────────────────────────────────────────────┤ │ Bits │ 0 0 1 2 3 4 5 6 7 8 10 12 15 20 30 60│ ├──────────────┼─────────────────────────────────────────────────────────────┤ │ N │ 240 120 60 30 20 15 12 10 8 7 6 5 4 3 2 1│ ├──────────────┼─────────────────────────────────────────────────────────────┤ │ Wasted Bits│ 60 60 0 0 0 0 12 0 4 4 0 0 0 0 0 0│ └──────────────┴─────────────────────────────────────────────────────────────┘
壓縮過程描述
壓縮流程如下:
1)selector 從 0 到 15 ,依次檢查是否滿足壓縮條件;
2)如果可以被壓縮,則使用對應規則執行壓縮過程;
3)記錄已壓縮數據數組的下標,並產生新的未壓縮數據數組;
4)執行步驟1)直至未壓縮數組為空;
下面舉例說明下該算法的大致流程及壓縮效果。
1、數組中存儲的數字相同
比如有如下數組(30個3):
[3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3]
該數組中的最大數據為3,可以使用2位二進制表示,則查表可得,Selector等於3,每2個bit存儲一個數據,可以存儲30個數據。
前4位數據為: 0011
后面存儲了30個3,則后面60位數據為:111111111111111111111111111111111111111111111111111111111111
兩部分數據合並在一起表示:0011111111111111111111111111111111111111111111111111111111111111
使用16進制進行表示: 0x3fffffffffffffff
因此,30個3使用該算法壓縮后可表示為: 0x3fffffffffffffff
如果上面的30個3都使用int64進行存儲,該算法的壓縮后占用空間為原來的 3.3%( (1 * 8) / (30.0 * 8)= 0.033);
如果上面的30個3都使用int32進行存儲,該算法的壓縮后占用空間為原來的 6.7%( (1 * 8) / (30.0 * 4)= 0.067);
如果上面的30個3都使用int8(即一個Byte)進行存儲,該算法的壓縮后占用空間為原來的 26.7%( 8 / 30.0 = 0.267);
2、數組中存儲的數字不同
上面的數據是比較理想的情況,如果有如下數組:
[0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29]
可以將數據分成3組分別進行壓縮。
1)前15個數據中([0 1 2 3 4 5 6 7 8 9 10 11 12 13 14])的最大值為14(0x0E), 可以使用4位bit進行存儲,編碼規則選擇5,則這15個數據可存儲為: 0x50123456789abcde , 如果逆序存放,則表示為: 0x5edcba9876543210
事實上,Simple8b算法中使用逆序存放數據(go語言):
// pack15 packs 15 values from in using 3 bits each func pack15(src []uint64) uint64 { return 5<<60 | src[0] | src[1]<<4 | src[2]<<8 | src[3]<<12 | src[4]<<16 | src[5]<<20 | src[6]<<24 | src[7]<<28 | src[8]<<32 | src[9]<<36 | src[10]<<40 | src[11]<<44 | src[12]<<48 | src[13]<<52 | src[14]<<56 }
2)緊挨着的12個數字([15 16 17 18 19 20 21 22 23 24 25 26])的最大值為26(0x1A), 可以使用5位bit進行存儲,編碼器選擇6,則這12個數據可存儲為: 0x6d6717b56939460f
可用以下代碼進行驗證(Python代碼):
def pack12(src) : ret = 6<<60 for i in range(12): ret = ret | (src[i] <<(i*5)) return ret arr = range(15,27) print arr,len(arr) ret = pack12(arr) print ret,'0x%08x' % ret
3)后面3個數字([ 27 28 29 ])的最大值為29,但只有3個數字,編碼規則選擇13, 則這3個數據可存儲為: 0xd0001d0001c0001b
可用以下代碼進行驗證(Python代碼):
def pack3(src) : ret = 13<<60 for i in range(3): ret = ret | (src[i] <<(i*20)) return ret arr = range(27,30) print arr,len(arr) ret = pack3(arr) print ret,'0x%08x' % ret
如果上面的30個數據都使用int64進行存儲,該算法的壓縮后占用空間為原來的 10%( (3 * 8) / (30.0 * 8)= 0.1);
如果上面的30個數據都使用int32進行存儲,該算法的壓縮后占用空間為原來的 20%( 3 * 8 / (30.0 * 4) = 0.2);
如果上面的30個數據都使用int8(即一個Byte)進行存儲,該算法的壓縮后占用空間為原來的 80%( 3 * 8 / 30.0 = 0.8);
由上面兩個例子可以看出,該算法針對使用int64和int32存儲數據的場景壓縮效果是比較明顯的,如果存儲數據的范圍波動比較大,需要使用64位或32位的int進行存儲,但大部分數據的絕對值比較小(比如可以使用一個字節存儲),則使用該算法的壓縮效果比較明顯。
解壓縮過程描述
解壓縮流程如下:
1)首先獲取壓縮數據V的前4個bit作為Selector的值;
2)如果Selector的值大於或等於16,直接出錯返回;
3)如果Selector的值小於16,執行解碼操作:根據不同的Selector值選取不同的解碼規則進行解碼操作。
下面舉例說明下該算法的大致流程。
1、數組中存儲的數字相同
比如V為 : 0x3fffffffffffffff
則Selector為3(Selector = V >> 60),查表可知每2個bit存儲一個數據,則解碼過程如下(python示例代碼):
def unpack30(V,refDst): for i in range(30): dst[i] = (V >> (i*2)) & 3 dst=[0]*30 V = 0x3fffffffffffffff unpack30(V,dst) print dst
2、數組中存儲的數字不同
比如V為 : 0x5edcba9876543210
則Selector為5(Selector = V >> 60),查表可知每4個bit存儲一個數據,則解碼過程如下(python示例代碼):
def unpack15(V,refDst): for i in range(15): dst[i] = (V >> (i*4)) & 15 dst = [0]*15 V = 0x5edcba9876543210 unpack15(V,dst) print dst
其它
示例代碼如下(go語言):
package main import ( "fmt" "github.com/jwilder/encoding/simple8b" ) func testEncode(in []uint64) { enc := simple8b.NewEncoder() for _,e := range in { enc.Write(e) } fmt.Println("data in : ",in) encoded, err := enc.Bytes() if err != nil { fmt.Println("error occur!") } fmt.Println("encoded(arr) : ",encoded) fmt.Printf("len(encoded) : %d bytes\r\n",len(encoded)) fmt.Printf("encoded(hex) : ") for _,ele := range encoded { fmt.Printf("%x ",ele) } fmt.Println("") fmt.Printf("decode : ") dec := simple8b.NewDecoder(encoded) i := 0 for dec.Next() { if i >= len(in) { fmt.Printf("Decoded too many values: got %v, exp %v", i, len(in)) } decTmp := dec.Read() if decTmp != in[i] { fmt.Printf("Decoded[%d] != %v, got %v", i, in[i], dec.Read()) }else{ fmt.Printf("%d ",decTmp) } i += 1 } fmt.Println("") fmt.Println("--------------------------") } func main(){ N := 30 in := make([]uint64, N) for i:=0;i < N;i++ { in[i]=3 } testEncode(in) for i := 0 ; i < N ; i++ { in[i] = uint64(i) } testEncode(in) }
運行效果如下:
[root@localhost test]# ./simp8bTest1 data in : [3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3] encoded(arr) : [63 255 255 255 255 255 255 255] len(encoded) : 8 bytes encoded(hex) : 3f ff ff ff ff ff ff ff decode : 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -------------------------- data in : [0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29] encoded(arr) : [94 220 186 152 118 84 50 16 109 103 23 181 105 57 70 15 208 0 29 0 1 192 0 27] len(encoded) : 24 bytes encoded(hex) : 5e dc ba 98 76 54 32 10 6d 67 17 b5 69 39 46 f d0 0 1d 0 1 c0 0 1b decode : 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 -------------------------- [root@localhost test]#
時間戳類型相關編碼算法
RLE編碼算法描述
使用該算法的前提是差值數組的所有數值都相同。使用該算法進行編碼時,其存儲結構如下:
解釋如下:
EncodeType : 記錄編碼類型,占4個bit
Divisor :記錄除數的log10值,占4個bit
Timestamp : 記錄第一個時間戳的值
DeltaValue : 記錄第一個差值
N : 重復次數
該算法的核心思想是記錄數據的重復次數,其存儲結構的第一個字節的高4位用於記錄該存儲結構使用了RLE編碼,后4位記錄除數的log10值。 由於差值數組是相對原始數組的第一個數據計算的,所以原始數組的第一個值(第一個時間戳)必須記錄,即上述結構中的Timestamp字段。 差值數組的所有值都相同,所以可以在存儲結構中可以記錄第一個差值和重復次數,即上述結構中的DeltaValue字段和N字段。
Raw編碼算法描述
使用該算法的前提是差值數組的最大值大於(1 << 60)- 1。使用該算法進行編碼時,其存儲結構如下:
解釋如下:
EncodeType :編碼類型,和其它結構兼容,第一個字節的前4個bit用於記錄編碼類型;
RawData : 原始數組的數據;
該算法數據沒有壓縮,反而增加了一個字節。 為了和其它結構兼容,第一個字節的前4個bit用於記錄當前存儲的數據使用的是Raw編碼類型。
Packed編碼算法描述
使用該算法的前提是在差值數組的所有數值均不同,並且差值數組中數據的最大值不大於(1 << 60)- 1 。使用該算法進行編碼時,其存儲結構如下:
解釋如下:
EncodeType :記錄編碼類型,占4個bit;
Divisor :記錄除數的log10值,站4個bit;
Timestamp :記錄第一個時間戳的值;
Simple8bData :差值數組使用Simple8b算法編碼后的結果;
該算法首先使用差值編碼對原始數據進行編碼,將編碼后的值除於最大共同除數Divisor(10的倍數或1), 使差分數組的值盡量縮小。然后將差值數組使用Simple8b算法進行編碼,進一步提高壓縮效果。
浮點數XOR算法描述
第一個值不壓縮, 后面的值是跟第一個值XOR的結果來的,如果結果相同,僅存儲一個0, 如果結果不同,存儲XOR后的結果。
算法描述
該算法是結合遵循IEEE754標准的浮點數存儲格式的數據特征設計的特定算法。
數據編碼過程如下:
1、第一個值不壓縮(記錄為v0);
2、計算后續值v與第一個值v0的異或值vDelta;
3、如果vDelta為0(即:v與v0的值相同),接下來的一個bit存儲一個0(占用一個bit);
4、如果vDelta不為0(即:v與v0的值不相同),接下來的一個bit存儲一個1(占用一個bit),然后根據vDelta的值分以下兩種情況進行處理:
如果重置前導值或尾數存儲空間更優化,則按如下流程處理:
1)接下來的一個bit寫入1;
2)接下來的 5 個 bit 寫入vDelta值(二進制表示)中前導0的個數leading;
3)接下來的 6 個 bit 寫入vDelta值(二進制表示)中有效位大小sigbits;
4)將vDelta值(二進制表示)右移去掉后面多余的0(長度前面有效數字已經標記過)得到vDelta2,寫入vDelata2的值(僅有效長度);
如果重置前導值或尾數存儲空間沒有達到更優效果,則之前使用之前的參數,按如下流程處理:
1)接下來的一個bit寫入0;
2)將vDelta值(二進制表示)右移去掉后面多余的0(長度前面有效數字已經標記過)得到vDelta2,寫入vDelata2的值(僅有效長度);
存儲示例1
比如有以下數組(30個12):
[12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12]
存儲結果(12.0的二進制表示方式后面跟29個bit的0,數據補齊后用16進制表示) : 0x402800000000000000000000
共12個字節,則壓縮后的數據為原來的: (12 * 1.0) / (30 * 8.0) = 0.05 = 5%
該算法的解碼過程與編碼過程剛好相反,這里暫不描述。
參考資料:
http://www.vldb.org/pvldb/vol8/p1816-teller.pdf
snappy算法
以下是Google幾年前發布的一組測試數據(《HBase: The Definitive Guide》):
Algorithm % remaining Encoding Decoding GZIP 13.4% 21 MB/s 118 MB/s LZO 20.5% 135 MB/s 410 MB/s Zippy/Snappy 22.2% 172 MB/s 409 MB/s
其中:
1)GZIP的壓縮率最高,但是它是CPU密集型的,對CPU的消耗比其他算法要多,壓縮和解壓速度也慢;
2)LZO的壓縮率居中,比GZIP要低一些,但是壓縮和解壓速度明顯要比GZIP快很多,其中解壓速度快的更多;
3)Zippy/Snappy的壓縮率最低,而壓縮和解壓速度要稍微比LZO要快一些。
好,就這些了,希望對你有幫助。
本文github地址:
https://github.com/mike-zhang/mikeBlogEssays/blob/master/2017/20170423_Influxdb數據壓縮描述.rst
歡迎補充