天天看點

探秘 Cassandra 資料檔案合并優化前言資料檔案合并(Compaction)寫在最後

前言

Cassandra是一款NoSQL分布式資料庫,采用LSM Tree架構。衆所周知,LSM有兩個重要過程:資料順序刷入磁盤生成資料檔案(SSTable)和 資料檔案合并(Compaction)。今天本文主要說一個Compaction過程中的優化。

資料檔案合并(Compaction)

首先我們要明白Compaction這個過程到底要做什麼事,再來看如何優化。我們先從資料檔案(SSTable)說起。

SSTable: Sorted String Table

這個詞源自Google BigTable論文,是一個不可修改的資料檔案。最初資料來自于使用者寫入,并且緩存在記憶體中。當緩存滿的時候,會将緩存中的資料刷入磁盤,也就生成了SSTable檔案。刷入磁盤的SSTable裡的資料是排好序的。比如,使用者寫入

[1, 0, 2, 4]

這麼4條資料,最後刷入磁盤,這4條資料在SSTable裡的排列是這樣的

[0, 1, 2, 4]

通過寫緩存,可以利用磁盤順序寫性能更好的特點,尤其是機械盤。查詢過程也很簡單,因為資料排好序,隻要二分搜尋即可。

為什麼要做Compaction

随着資料不斷寫入,緩存不斷刷入磁盤生成SSTable的時候,我們會擁有很多SSTable。這時候會有什麼問題呢?可以試想一下下面這個場景:

  • 使用者先寫入

    [1, 3, 8, 2]

    ,刷入磁盤生成SSTable0:

    [1, 2, 3, 8]

  • 使用者再寫入

    [0, 3, 7, 6]

    ,刷入磁盤生成SSTable1:

    [0, 3, 6, 7]

這時候如果要查詢

3

,不得不通路2個檔案。因為2個檔案都包含了

3

,得确定哪個

3

是最新的。更惡心的情況是,使用者繼續寫入

[0, 4, 5, 9]

,雖然沒有

3

,但是因為這個

0~9

這個範圍包含了

3

,你任然需要搜尋這個檔案,确定有沒有你要的資料(這種落空的情況可以通過Bloom filter優化)。

如果SSTable檔案很多,查詢效率就會顯著下降。那麼解決這個問題,就是做Compaction。将SSTable0和SSTable1合并後,我們能得到

[0, 1, 2, 3, 6, 7, 8]

,一個全新的SSTable,後續查詢隻要搜尋這個新的SSTable檔案即可。

這就是Compaction主要的作用,當然Compaction過程中還有一個重要任務是清理過期或者被删除的資料。

簡單抽象一下Compaction過程:多個有序連結清單去重合并

如何合并多個有序連結清單

相信很多同學面試也遇到過這個問題,很直接的想法就是用一個優先隊列解決。Java中是

PriorityQueue

這個類,它的實作就是一個小根堆。代碼也很好實作:

public List<Integer> doCompaction(Iterator<Integer>[] sstables) {

    LinkedList<Integer> result = new LinkedList<>();
    Integer[] topElement = new Integer[sstables.length];

    PriorityQueue<Integer> heap = new PriorityQueue<>(
      (idx1, idx2) -> topElement[idx1] - topElement[idx2]
    );

    // 初始化
    for (int i = 0; i < sstables.length; i++) {
      if (sstables[i].hasNext()) {
        topElement[i] = sstables[i].next();
        heap.add(i);
      }
    }

    while (!heap.isEmpty()) {
      int sstableIdx = heap.poll(); // 最小元素出隊

      // 去重
      if (result.isEmpty() || result.getLast() < topElement[sstableIdx]) {
        result.add(topElement[sstableIdx]);
      } else {
        assert result.getLast() == topElement[sstableIdx];
      }

      if (sstables[sstableIdx].hasNext()) {
        topElement[sstableIdx] = sstables[sstableIdx].next();
        heap.add(sstableIdx); // 新元素入隊
      }
    }

    return result;
  }           

這個複雜度是

O(N·logM)

, N是總共的元素個數,M是有序連結清單數量。這個實作方法要比直接連一起排序快,直接排序是

O(N·logN)

。顯然

N>=M

,是以前者更快。

優化1

上述方法看起來沒有什麼大問題,Cassandra之前一些版本也是這麼實作的。但是仔細分析這個過程,會發現有一些不必要的開銷。

首先每個元素都會經曆

poll

add

兩個過程,這兩個過程的開銷都是

O(logM)

。作為一個通用的PriorityQueue,poll和add之後都要保持堆的結構特性,所有要做下濾(shift down)和上濾(shift up),這兩個操作都是

O(logM)

。poll之後,因為堆頂缺失,會将最後一個元素放到堆頂做一次下濾。add元素則直接放到堆尾,做一次上濾。

是以精确一點複雜度是

O(2N·logM)

。但是因為我們基本每次poll元素後都會再add一個元素,那麼可以直接将新add的元素放入堆頂做一次下濾。這樣就将原來 一次下濾&一次上濾 合成 一次下濾。這樣理論上直接節省一半時間。

優化前過程:

探秘 Cassandra 資料檔案合并優化前言資料檔案合并(Compaction)寫在最後

優化後過程:

探秘 Cassandra 資料檔案合并優化前言資料檔案合并(Compaction)寫在最後

這個在SSTable資料基本不怎麼重疊的情況下,效果更好。因為某個SSTable彈出的資料可能會一直處于堆頂,下濾隻需要比較2次即可。

優化2

下濾調整元素的時候,每一層,需要做2次比較。先比較2個子節點誰最小,再把父節點和最小的比。

如果考慮上面說的那種情況,重疊比較少,某個SSTable一段連續的資料可能會一直最小,會處于堆頂,那麼一直隻需要2次比較就能确定堆頂元素。如果堆頂不是一個二叉,而是一個單鍊,那就隻需要1次比較即可。是以Cassandra裡的堆,是一個很短的有序單鍊 + 一個堆。這種情況還是挺常見的,經過一段時間的壓縮後,尤其對于使用

LeveledCompactionStrategy

的場景來說。

最終堆形狀如下:

探秘 Cassandra 資料檔案合并優化前言資料檔案合并(Compaction)寫在最後

優化3

引入一個

equalParent

變量,表示該元素是否和父節點相等。這樣可以進一步節約一些比較次數,在某些情況下。比較是位元組數組比較,是以還是有一定開銷的,并不像文中抽象的問題那樣,簡單比較整數。

寫在最後

為了營造一個開放的Cassandra技術交流環境,社群建立了微信公衆号和釘釘群。為廣大使用者提供專業的技術分享及問答,定期開展專家技術直播,歡迎大家加入。另阿裡雲為廣大開發者提供雲上Cassandra資源,可用于動手實踐:9.9元可使用三月(限首購)。

直達連結:

https://www.aliyun.com/product/cds
探秘 Cassandra 資料檔案合并優化前言資料檔案合并(Compaction)寫在最後