壓縮爲王-阿里第五屆中間件複賽總結

1.前言

翻了一下公衆號已經快兩個月沒有認真的寫一篇文章了,這段時間主要是再忙阿里中間件的複賽,再加上前段時間團隊旅遊,因此才拖到如今開始寫複賽的總結。首先先貼下成績吧:面試

首先說一下本次的題目吧,複賽的題目背景是rocketmq,實現一個進程內消息持久化存儲引擎,要求包含如下功能:算法

- 發送消息功能
- 根據必定的條件作查詢或聚合計算,包括
  A. 查詢必定時間窗口內的消息
  B. 對必定時間窗口內的消息屬性某個字段求平均
複製代碼

例子:t表示時間,時間窗口[1000, 1002]表示: t>=1000 & t<=1002 (這裏的t和實際時間戳沒有任何關係, 只是一個模擬時間範圍)數據庫

對接口層而言,消息包括兩個字段,一個是業務字段a,一個是時間戳,以及一個byte數組消息體。實際存儲格式用戶本身定義,只要能實現對應的讀寫接口就好。數組

發送消息以下(忽略消息體):緩存

消息1,消息屬性{"a":1,"t":1001}
消息2,消息屬性{"a":2,"t":1002}
消息3,消息屬性{"a":3,"t":1003}
複製代碼

查詢以下:bash

示例1-
    輸入:時間窗口[1001,9999],對a求平均
    輸出:2, 即:(1+2+3)/3=2
示例2-
    輸入:時間窗口[1002,9999],求符合的消息
    輸出:{"a":1,"t":1002},{"a":3,"t":1003}
示例3-
    輸入:時間窗口[1000,9999]&(a>=2),對a求平均
    輸出:2 (去除小數位)
複製代碼

題目來講總體是比較簡單的,基本就是輸入20億數據而後根據不一樣的要求查詢出不一樣的數據。成績的分數分爲3個階段: 發送階段、查詢聚合消息階段、查詢平均數階段:app

  • 發送階段:假設發送消息條數爲N1,全部消息發送完畢的時間爲T1;發送線程多個,消息屬性爲: a(隨機整數), t(輸入時間戳模擬值,和實際時間戳沒有關係, 線程內升序).消息總大小爲50字節,消息條數在20億條左右,總數據在100G左右框架

  • 查詢聚合消息階段:有屢次查詢,消息總數爲N2,全部查詢時間爲T2; 返回以t和a爲條件的消息, 返回消息按照t升序排列oop

  • 查詢平均數階段: 有屢次查詢,消息總數爲N3,全部查詢時間爲T3; 返回以t和a爲條件對a求平均的值學習

若查詢結果都正確,則最終成績爲N1/T1 + N2/T2 + N3/T3

再講具體的作法以前,仍是先說下本次比賽比較曲折的經歷。本次比賽分爲幾個階段:

  • 首先是樸素階段,最開始的時候你們都按照普通的思路(數據都存儲在文件),分數都在3-5萬之間。可是第一名那個時候查詢平均數階段是15萬,總體是16萬,你們都在想究竟是用了什麼騷套路竟然求平均數能有這麼高分,因而就有了比賽的第二階段。
  • 在第二階段中,你們基本都是統一的套路壓縮A和T至內存,再經過一些緩存塊速過濾或者累加求出平均值,這個階段最高分有人達到260萬。在這個階段中基本就淪爲了平均數計算大賽。
  • 主辦方一看這個趨勢不行,因而清空了榜單,更新評測的數據集,讓A更加隨機,沒法壓縮,讓三個階段的分儘可能均勻,再這個階段中基本都在前3名,正當覺得能夠順順利利的結束比賽的時候,又有大佬將第三階段拉昇的很高,就這樣來到了第四階段。
  • 第四階段,也就是比賽的最後一週,主辦方又清空了榜單,其實原本也是沒有這一週的,可是主辦方硬生生的延遲了一週,最後這一週你們都或多或少找到了一些第三階段快速計算的方法,雖然大致的思路有一些可是,因爲團隊旅遊最後這一週並無去實現這個思路,最後名次是28名。

2.數據壓縮

最初的想法原本覺得此次比賽是一次考驗文件I/O的一個比賽,可是最後發現實際上是一個平均值大賽,其核心思想就是 壓縮+緩存,緩存這一次後續有不少東西最後沒有落地這裏就很差講述,因此這一次主要講的就是數據壓縮,因此標題也叫這個。

咱們細細的研讀賽題,發現message由三個部分組成,一個是總體基本有序的t,一個是沒有規律的a,還有一個34字節的body。通常的作法是將這三個部分都存入文件,而後創建稀疏索引,再將咱們的結果查詢出來。

可是這樣明顯效率很低,全部的查詢操做都須要走I/O,有沒有什麼辦法將咱們的查詢儘量的再內存中去完成呢?固然有這個就須要咱們的壓縮上場了。

2.1 什麼是壓縮

壓縮是一種經過特定的算法來減少計算機文件大小的機制,壓縮其實也只是利用文件數據的規律來達到其目的,好比一個字符串「張張張張張張張張張張三」,這裏有十一個字,那麼其實咱們能夠縮寫成「十張一三」,咱們利用「十張」代替了10個張,「一三」代替的了一個三,最後用四個字就表示了咱們的是十一個字可是若是字符串是「趙錢孫李」,那麼其自己是沒有規律可循,若是使用壓縮的策略,反而會增長的文件的大小。

2.2 數字壓縮

在咱們的比賽賽題中,a和body都是沒有規律的若是咱們選擇對其壓縮那麼成本比較高,咱們的t再題目中給出的是基本有序,基本有序也能夠解讀成每一個數字之間都會有規律,因此咱們能夠對其進行數字壓縮。咱們知道不少經常使用的壓縮,好比zip,rar等等,可是這些壓縮算法並非針對咱們的數字來進行壓縮的,若是將其直接使用到咱們的場景中反而效率更低。

對於有規律的數字來講,有不少比較不錯的開源的壓縮數字的算法,下面一一給你們介紹。

3. 壓縮算法

3.1 zigzag

zigzag壓縮算法應用比較普遍,再Avro和Thrift中都有其身影,在thrift中數據傳輸的時候用作數字的壓縮,以減小數據的傳輸量。

爲了很好的說明zigzag的思想,咱們能夠先看看咱們整數1,若是咱們是int類型,再咱們計算機中表示爲:

00000000_00000000_00000000_00000001
複製代碼

能夠看見咱們有不少0都是無用的那麼咱們能夠經過一些策略將31個前導0進行壓縮,若是是-1 怎麼辦呢,因爲咱們負數的編碼在計算機中須要用補碼錶示,以下:

11111111_11111111_11111111_11111111
複製代碼

咱們補碼的第一位是符號位,若是是負數那麼確定就會爲1,這個1天然而然就阻礙了咱們的壓縮前導0,咱們將1進行放到咱們編碼的最後,其餘數字進行左移一位,總體來講就是循環左移一位,那麼就會有下面的結果:

11111111_11111111_11111111_11111111
複製代碼

移完以後發現怎麼和咱們以前的如出一轍,那也依然不能壓縮,因此zigzag又進行了一個優化,符號位不變,其餘位置進行取反,其餘邏輯和上面同樣,那麼就變成了:

00000000_00000000_00000000_00000001
複製代碼

能夠看見又充滿了前導0,可是仔細一看這個不就是1的二進制碼?你-1變成了這個,1怎麼辦呢?一樣的咱們的正數也須要進行一樣的移位邏輯,正數的符號位是0,可是數據位不須要進行取反,因而就有1的編碼爲:

00000000_00000000_00000000_00000010
複製代碼

這樣無論是正數仍是負數均可以進行壓縮前導0,那麼前導0該如何壓縮,咱們前面舉例過字符串壓縮,咱們可使用輔助信息用來表示,若是咱們的數字是Int,那麼不會超過32位,而咱們能夠額外用5位來表示咱們數據實際的長度,上面的正數1,實際就兩位有效長度那麼其實就是10,咱們就能夠用下面的數據表示:

00010_10
複製代碼

咱們在解碼的時候先讀取5位數字,這裏是2,表明咱們還有2位數據,最後讀取剩下的2位10,而後再進行循環右移獲得咱們最後的結果。

在實際的zigzag中採起的是另一種壓縮方式,將32位數字拆成7bit表示,不夠的就補0:

0000000_0000000_0000000_0000000_0000010
複製代碼

這樣咱們就會有5個字節,目前他們都只有7個Bit,還差一個標記位,咱們對全部字節中有有效數據的都補充一個0,最後一個就補充1。 因此結果就是

10000010
複製代碼

zigzag比較適用於數字較小,也就是前導0不少的狀況,可是咱們賽題t數字都比較大都是long型,如何進行處理呢?別忘記了咱們有個規律是基本有序,那就證實咱們的兩個連續的t之間相差較小,咱們能夠算出連續t之間的差值,而後將差值經過這種方式進行壓縮,差值的壓縮這裏比較重要後續基本和其有關係。

3.2 vint,vlong壓縮

對於hadoop或者lucene比較熟悉的同窗有可能對其有一點了解,其本質也是可變長的壓縮,基本和zigzag對正數的處理一致,因爲vint不須要考慮負數因此就不須要移動符號位。其壓縮原理: 一個字節的8個bit,後7個bit表示實際的值,第一個bit表示後面是否還有其餘的字節。 將266寫成二進制形式00000000_00000000_00000001_00001010,採用vint表示 00000010_10001010; 這樣就節約了兩個字節,vint適合都是正數的作法,固然不少狀況下咱們的數字都是正數,能夠根據實際場景進行選擇。

3.3 delta-of-delta

Facebook開源的beringei時序數據庫。beringei用來解決內部監控數據存儲和查詢需求的數據庫,特色是讀寫速度快。咱們本次的賽題基本其實也是相似時序數據庫,因此beringei 的讀寫速度快的思想原理就能夠很好的借鑑到咱們本次賽題上。

要知道beringei的速度爲什麼快,那麼就不得不說他的壓縮算法delta-of-delta,什麼是delta-of-delta呢?咱們在上面介紹過delta算法,即便咱們採用壓縮,若是咱們數據比較大,那麼其壓縮率確定不會特別理想,因此這裏就能夠利用咱們delta進行優化,

t delta
1100 0
1160 60
1121 61
1178 57

如上面表格所述,咱們能夠將原本的數據利用delta進行壓縮,也就是將咱們的數據直接變成,0,60,61,57便可,通常來講這種優化基本就知足了,可是咱們能夠進一步優化,咱們存儲delta和delta之間的差值,將會進一步的進行壓縮,

t delta delta-of-delta
1100 0 0
1160 60 60
1121 61 1
1178 57 -4

經過這種方式咱們將61,57,只有1和-4就能夠進行壓縮表達,這樣咱們的壓縮率會進一步的提升。 再本次比賽中咱們選取的就是這個壓縮算法,能夠將20億的timestamp,從15G壓縮到300M-400M左右,再加上一些輔助索引用於二分查找總體使用800M不到就能夠將timestamp壓縮到內存,而且查詢速度也足夠快。

3.4 XOR + DFCM

beringei時序數據庫中對time的壓縮採用delta-of-dleta,對於數值的壓縮採用另一種方法:異或(XOR),對於數值來講基原本說都是無規律的,因此沒有采用差值進行壓縮,而是使用異或:

value 16進制 異或結果
15.5 0x402F000000000000 0
14.0625 0x402C200000000000 0x0003200000000000
3.25 0x400A000000000000 0x0026200000000000
8.625 0x4021400000000000 0x002b400000000000

能夠看見經過異或後,咱們也能獲得一個像delta同樣稍微較小的值,可是這個值看起來依然很大,壓縮率感受並不會很高,彆着急,咱們以前介紹過壓縮前導0,經過上面的數據咱們能夠發現後導0的個數看起來更多,這裏咱們能夠將前導0和後導0一塊兒進行壓縮。在DFCM中被分爲三部分:

  • Leading Zeros: 就是XOR後非零數值前面零的個數
  • Trailing Zeros: 就是XOR後非零數值後面零的個數
  • Meaningful Bits: 中間非零的個數

劃分出部分以後,編碼規則以下:

  • 第一個數據不壓縮用於還原
  • 而後判斷控制位:
  1. 若是XOR是0,存儲1bit-0
  2. 若是XOR不是0,控制位第一個bit設置1,第二個bit以及隨後的數據按照如下方式計算:
  3. meaningful bits落在了前一個XOR的meaningful bits區域內,控制位的第二個bit爲1,接下來是XOR數值.
  4. 不然控制位的第二個bit爲0 .而且接下來存放: 5 bits: Leading bits 個數;6 bits: Meaningful bits 個數;隨後放置數值
value 異或壓縮結果 異或結果
15.5 不壓縮 0
14.0625 13bits(頭部控制位)+5bits(實際數值) 0x0003200000000000
3.25 13bits(頭部控制位)+10bits(實際數值) 0x0026200000000000
8.625 2bits(頭部控制位)+10bits(實際數值) 0x002b400000000000

經過這種作法比gzip,rar等在速度上都要快挺多。

可是其在本次賽題中效果不大,故最後沒有選取它。

3.5 snappy

snappy本質上不是專門的數字壓縮算法,咱們可使用其對咱們的message進行壓縮,snappy在Google不少場景中都有使用,好比BigTable,MapReduce,rpc等等。

Snappy比gzip,zip等一些壓縮算法擁有更快的壓縮/解壓速度,可是其壓縮大小沒有其餘算法小。再複賽中最開始是使用了Snappy進行壓縮message,寫入速度一度達到了8500,壓縮率在40%左右,可是最後出題人更新了message的數據致使Snappy沒法對咱們的message進行壓縮。說實話出題人有點狗,我以前找他詢問過是否能夠壓縮message而且是通用算法,這我的過了幾天就直接把message變成了沒法壓縮的數據,再實際場景中snappy應該是能夠對數據進行壓縮的,他這麼一改反而不符合咱們實際場景。

3.6 RoaringBitmap

RoaringBitmap是我最開始調研的壓縮算法之一,雖然他不能在咱們的場景中使用,可是其若是在正確的場景中使用威力也是巨大的,不少牛逼的框架都在使用:Spark,Hive,Tez等等。

你們在刷面試題的時候其實都接觸過Bitmap,經過bit來記錄數據,好比你的數據全是int類型的那麼只須要記錄使用2的32次方個bit記錄就行,最大能夠節約32倍內存,可是bitmap有一個問題是若是你的數據很小,可是因爲你使用了2的32次方個bit,那麼就會有大量的bit被浪費,因此就有了RoaringBitmap的出現。

Roaringbitmap是一種超常規的壓縮BitMap。它的速度比未壓縮的BitMap快上百倍,其核心思想就是將bit爲0的進行壓縮,具體的細節能夠網上自行搜索,這裏不進行詳解。

Roraingbitmap不只僅是有壓縮大小的特色,對於多個Roraingbitmap求並集和交集也有很快的速度,roaringBitmap能夠用作倒排索引,若是咱們一張很大的用戶表,若是其中有個用戶性別,咱們知道若是對用戶性別這種區分度不高的字段創建索引是不推薦的,由於特別浪費,可是若是使用Roraingbitmap咱們就能夠作這件事,咱們創建兩個Roraingbitmap,一個用來保存性別男,一個用來保存性別女,Roraingbitmap中數據爲用戶id,咱們就能夠對其創建索引,若是這個表中還有其餘字段也是區分度不高,那麼一樣的能夠創建Roraingbitmap。若是咱們須要對這些區分度不高的字段查詢的時候,就能夠經過Roraingbitmap的並集和交集進行查詢。

4. 總結

雖然本次的成績不是很理想,可是在比賽中學習到了不少的壓縮技巧,感受這個是本次比賽最大的收穫,在這裏分享給你們,但願之後你們能在適當的場景中對其進行使用。另一個感悟是作這種比賽,變化太大,動不動就改數據從新搞評測,因此下次有機會再來的時候不要太急於作,差很少在截止前2周作一作就差很少了。本次比賽還要感謝:美團的惠偉兄,Kirtio,以及普架,kk,老王,他們都對我在比賽中有很大的幫助。

若是你們以爲這篇文章對你有幫助,你的關注和轉發是對我最大的支持,O(∩_∩)O:

相關文章
相關標籤/搜索