Influxdb數據壓縮


環境: 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

歡迎補充 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM