資安

【譯】深入淺出 Cassandra 的刪除和墓碑

【原文鏈接】About Deletes and Tombstones in Cassandra

從Apache Cassandra這樣的系統中刪除分佈式和複製式的數據,要比在關係型數據庫中刪除數據要棘手得多。當我們考慮到Cassandra將其數據存儲在磁盤上的不可改變的文件中時,刪除的過程就變得更加有趣。在這樣的系統中,為了記錄發生了刪除的事實,需要寫入一個叫做 "墓碑"(tombstone)的特殊值,用於標識之前的值已經被刪除。儘管這可能看起來很不尋常甚至有悖直覺(特別是當你意識到刪除實際上會佔用磁盤空間時),我們將用這篇博文來解釋實際發生的事情,同時附上例子,你可以自行試驗加深理解。

Cassandra:可用性 + 一致性

在我們深入瞭解細節之前,我們應該先退後一步進行回顧,看看Cassandra作為一個分佈式系統是如何工作的,尤其是在可用性和一致性的背景下。這對於正確理解分佈式刪除以及其對應的潛在問題將會有所幫助。

可用性。為了確保可用性,Cassandra會複製數據。具體來說,根據複製因子(RF),每個數據的多個副本存儲在不同的節點上。RF定義了每個數據中心每個 keyspace 要保存的副本數量。根據配置,每個副本也可以由不同的機架保存,只要有足夠的機架可用,並且配置對應 snitch 和 topology 策略。採用這樣的方法,當任何節點或機架發生故障時,仍然可以從其他副本中讀取數據。

一致性。為了確保讀取的數據具有很強的一致性,我們必須遵守以下規則。

CL.READ  = 用於讀取的一致性級別(CL)。基本上是指Cassandra認為讀取成功而必須確認的節點數量。
CL.WRITE = 用於寫入的一致性級別(CL)
RF       = 複製因子

CL.READ + CL.WRITE > RF

這樣我們就可以確保從至少一個寫入數據的節點中讀取。

用例:讓我們考慮以下常見的配置

RF       = 3
CL.READ  = QUORUM = RF/2 + 1 = 2
CL.WRITE = QUORUM = RF/2 + 1 = 2

CL.READ + CL.WRITE > RF --> 4 > 3

通過這個配置,避免了單點故障(SPOF),從而收穫了高可用性。我們可以承受損失一個節點,因為我們確信任何讀取請求都會至少在一個節點上獲取寫入的數據,然後應用 Last Write Wins (LWW) 算法來選擇哪個節點持有這次讀的正確數據。
在這裡插入圖片描述理解了上述配置和行為,下面來看一些執行刪除的例子。

分佈式刪除的問題

從上一節的內容瞭解到這種配置之下應該是強一致的。讓我們暫時忘掉墓碑(tombstone),考慮一下 Cassandra 沒有使用墓碑刪除數據的情況。讓我們考慮一個成功的刪除,在一個節點上失敗了(三個節點中,RF=3)。這個刪除仍然會被認為是成功的(兩個節點承認了刪除,使用CL.QUORUM)。涉及該節點的下一次讀取將是一個模稜兩可的讀取,因為沒有辦法確定什麼是正確的:返回一個空響應還是返回數據?Cassandra一定會認為返回數據是正確的做法,所以刪除經常會導致數據重新出現,被稱為 "殭屍 "或 "幽靈",它們的行為將是不可預測的。
在這裡插入圖片描述
注意:這個問題即使通過墓碑(tombstone)的方式也並沒有完全解決,而是按照以下方式解決:作為 Cassandra 操作者,我們必須對任何執行刪除的集群至少每 gc_grace_seconds 運行一次全面修復。參見下面的 "墓碑刪除 "部分。

從 Cassandra 單節點的角度看刪除問題

如前所述,墓碑解決了使用不可變文件(sstable)存儲數據的系統中刪除數據的問題。

Cassandra 的特點之一是它使用了一個日誌結構的合併樹(LSM樹),而大多數 RDBMS 使用的是 B 樹。理解這一點的最好方法是記住 Cassandra 總是將寫的數據追加更新,讀的時候負責將一行的數據碎片合併在一起,挑選每個列的最新版本返回。

LSM樹的另一個屬性是數據寫在不可變的文件中(在Cassandra中稱為SSTables)。正如最初討論的那樣,那麼很明顯,通過這樣的系統,刪除只能通過一種特殊的寫來完成。讀取將獲取墓碑,而不考慮墓碑時間戳之前的任何數據。

墓碑

在Cassandra的上下文中,墓碑是與標準數據一起存儲的特定數據。刪除只不過是插入一個墓碑。當Cassandra讀取數據時,它將從 memtable 和SSTables 中合併所有請求行的碎片。然後,它應用 Last Write Wins(LWW)算法來選擇什麼是正確的數據,不管它是標準值還是墓碑。

舉個例子。

讓我們考慮以下例子,在一個有3個節點的Cassandra 3.7集群上(使用CCM)。

CREATE KEYSPACE tlp_lab WITH replication = {'class': 'NetworkTopologyStrategy', 'datacenter1' : 3};
CREATE TABLE tlp_lab.tombstones (fruit text, date text, crates set<int>, PRIMARY KEY (fruit, date));

並添加一些數據

INSERT INTO tlp_lab.tombstones (fruit, date, crates) VALUES ('apple', '20160616', {1,2,3,4,5});
INSERT INTO tlp_lab.tombstones (fruit, date, crates) VALUES ('apple', '20160617', {1,2,3});
INSERT INTO tlp_lab.tombstones (fruit, date, crates) VALUES ('pickles', '20160616', {6,7,8}) USING TTL 2592000;

這是剛才存儲的數據。

alain$ echo "SELECT * FROM tlp_lab.tombstones LIMIT 100;" | cqlsh

fruit   | date     | crates
---------+----------+-----------------
apple | 20160616 | {1, 2, 3, 4, 5}
apple | 20160617 |       {1, 2, 3}
pickles | 20160616 |       {6, 7, 8}

現在我們需要手動刷新數據(即在磁盤上寫一個新的SSTable,釋放內存),因為內存不像磁盤上的SSTable,其內容是支持變更的,所以內存中的墓碑,更準確的說是memtable中的墓碑,會覆蓋memtable中現有的任何值,這一行為與應用到磁盤的 sstable 上時存在區別。

nodetool -p 7100 flush

我們現在可以看到磁盤上的數據。

alain$ ll /Users/alain/.ccm/Cassa-3.7/node1/data/tlp_lab/tombstones-c379952033d311e6aa4261d6a7221ccb/
total 72
drwxr-xr-x  11 alain  staff   374 Jun 16 20:53 .
drwxr-xr-x   3 alain  staff   102 Jun 16 20:25 ..
drwxr-xr-x   2 alain  staff    68 Jun 16 17:05 backups
-rw-r--r--   1 alain  staff    43 Jun 16 20:53 mb-5-big-CompressionInfo.db
-rw-r--r--   1 alain  staff   127 Jun 16 20:53 mb-5-big-Data.db
-rw-r--r--   1 alain  staff    10 Jun 16 20:53 mb-5-big-Digest.crc32
-rw-r--r--   1 alain  staff    16 Jun 16 20:53 mb-5-big-Filter.db
-rw-r--r--   1 alain  staff    20 Jun 16 20:53 mb-5-big-Index.db
-rw-r--r--   1 alain  staff  4740 Jun 16 20:53 mb-5-big-Statistics.db
-rw-r--r--   1 alain  staff    61 Jun 16 20:53 mb-5-big-Summary.db
-rw-r--r--   1 alain  staff    92 Jun 16 20:53 mb-5-big-TOC.txt

我們可以使用SSTabledump工具將 sstable 中的內容以可讀的方式呈現

alain$ SSTabledump /Users/alain/.ccm/Cassa-3.7/node1/data/tlp_lab/tombstones-c379952033d311e6aa4261d6a7221ccb/mb-5-big-Data.db
[
  {
    "partition" : {
      "key" : [ "apple" ],
      "position" : 0
    },
    "rows" : [
      {
        "type" : "row",
        "position" : 19,
        "clustering" : [ "20160616" ],
        "liveness_info" : { "tstamp" : "2016-06-16T18:52:41.900451Z" },
        "cells" : [
          { "name" : "crates", "deletion_info" : { "marked_deleted" : "2016-06-16T18:52:41.900450Z", "local_delete_time" : "2016-06-16T18:52:41Z" } },
          { "name" : "crates", "path" : [ "1" ], "value" : "" },
          { "name" : "crates", "path" : [ "2" ], "value" : "" },
          { "name" : "crates", "path" : [ "3" ], "value" : "" },
          { "name" : "crates", "path" : [ "4" ], "value" : "" },
          { "name" : "crates", "path" : [ "5" ], "value" : "" }
        ]
      },
      {
        "type" : "row",
        "position" : 66,
        "clustering" : [ "20160617" ],
        "liveness_info" : { "tstamp" : "2016-06-16T18:52:41.902093Z" },
        "cells" : [
          { "name" : "crates", "deletion_info" : { "marked_deleted" : "2016-06-16T18:52:41.902092Z", "local_delete_time" : "2016-06-16T18:52:41Z" } },
          { "name" : "crates", "path" : [ "1" ], "value" : "" },
          { "name" : "crates", "path" : [ "2" ], "value" : "" },
          { "name" : "crates", "path" : [ "3" ], "value" : "" }
        ]
      }
    ]
  },
  {
    "partition" : {
      "key" : [ "pickles" ],
      "position" : 104
    },
    "rows" : [
      {
        "type" : "row",
        "position" : 125,
        "clustering" : [ "20160616" ],
        "liveness_info" : { "tstamp" : "2016-06-16T18:52:41.903751Z", "ttl" : 2592000, "expires_at" : "2016-07-16T18:52:41Z", "expired" : false },
        "cells" : [
          { "name" : "crates", "deletion_info" : { "marked_deleted" : "2016-06-16T18:52:41.903750Z", "local_delete_time" : "2016-06-16T18:52:41Z" } },
          { "name" : "crates", "path" : [ "6" ], "value" : "" },
          { "name" : "crates", "path" : [ "7" ], "value" : "" },
          { "name" : "crates", "path" : [ "8" ], "value" : "" }
        ]
      }
    ]
  }
]

現在磁盤上存儲了兩個分區(3行,2行共享同一個分區)。現在讓我們考慮不同類型的刪除。

Cell 刪除

在Cassandra存儲引擎中,來自特定行的一列稱為 "Cell"。

從行中刪除一個單元格

DELETE crates FROM tlp_lab.tombstones WHERE fruit='apple' AND date ='20160617';

在對應的行中,crates 列顯示為 null。

alain$ echo "SELECT * FROM tlp_lab.tombstones LIMIT 100;" | cqlsh

fruit   | date     | crates
---------+----------+-----------------
apple | 20160616 | {1, 2, 3, 4, 5}
apple | 20160617 |            null
pickles | 20160616 |       {6, 7, 8}

(3 rows)

刷新後我們在磁盤上多了一個SSTable,mb-6-big。

alain$ ll /Users/alain/.ccm/Cassa-3.7/node1/data/tlp_lab/tombstones-c379952033d311e6aa4261d6a7221ccb/
total 144
drwxr-xr-x  19 alain  staff   646 Jun 16 21:12 .
drwxr-xr-x   3 alain  staff   102 Jun 16 20:25 ..
drwxr-xr-x   2 alain  staff    68 Jun 16 17:05 backups
-rw-r--r--   1 alain  staff    43 Jun 16 20:53 mb-5-big-CompressionInfo.db
-rw-r--r--   1 alain  staff   127 Jun 16 20:53 mb-5-big-Data.db
-rw-r--r--   1 alain  staff    10 Jun 16 20:53 mb-5-big-Digest.crc32
-rw-r--r--   1 alain  staff    16 Jun 16 20:53 mb-5-big-Filter.db
-rw-r--r--   1 alain  staff    20 Jun 16 20:53 mb-5-big-Index.db
-rw-r--r--   1 alain  staff  4740 Jun 16 20:53 mb-5-big-Statistics.db
-rw-r--r--   1 alain  staff    61 Jun 16 20:53 mb-5-big-Summary.db
-rw-r--r--   1 alain  staff    92 Jun 16 20:53 mb-5-big-TOC.txt
-rw-r--r--   1 alain  staff    43 Jun 16 21:12 mb-6-big-CompressionInfo.db
-rw-r--r--   1 alain  staff    43 Jun 16 21:12 mb-6-big-Data.db
-rw-r--r--   1 alain  staff    10 Jun 16 21:12 mb-6-big-Digest.crc32
-rw-r--r--   1 alain  staff    16 Jun 16 21:12 mb-6-big-Filter.db
-rw-r--r--   1 alain  staff     9 Jun 16 21:12 mb-6-big-Index.db
-rw-r--r--   1 alain  staff  4701 Jun 16 21:12 mb-6-big-Statistics.db
-rw-r--r--   1 alain  staff    59 Jun 16 21:12 mb-6-big-Summary.db
-rw-r--r--   1 alain  staff    92 Jun 16 21:12 mb-6-big-TOC.txt

而這裡是mb-6-big的內容。

alain$ SSTabledump /Users/alain/.ccm/Cassa-3.7/node1/data/tlp_lab/tombstones-c379952033d311e6aa4261d6a7221ccb/mb-6-big-Data.db
[
  {
    "partition" : {
      "key" : [ "apple" ],
      "position" : 0
    },
    "rows" : [
      {
        "type" : "row",
        "position" : 19,
        "clustering" : [ "20160617" ],
        "cells" : [
          { "name" : "crates", "deletion_info" : { "marked_deleted" : "2016-06-16T19:10:53.267240Z", "local_delete_time" : "2016-06-16T19:10:53Z" } }
        ]
      }
    ]
  }
]

看看這個墓碑刪除的 cell 和插入的行 cell 相比有多相似。partition、row 和cell 都還在,只是在列的層面上沒有liveness_info了。deletion_info 字段也相應地更新了。這就是一個 Cell 墓碑。

Row 刪除

從分區中刪除一條記錄

DELETE FROM tlp_lab.tombstones WHERE fruit='apple' AND date ='20160617';

刪除後,該行不再顯示

alain$ echo "SELECT * FROM tlp_lab.tombstones LIMIT 100;" | cqlsh

 fruit   | date     | crates
---------+----------+-----------------
   apple | 20160616 | {1, 2, 3, 4, 5}
 pickles | 20160616 |       {6, 7, 8}

(2 rows)

刷新後,磁盤上多了一個SSTable,'mb-7-big',它的樣子如下。

alain$ SSTabledump /Users/alain/.ccm/Cassa-3.7/node1/data/tlp_lab/tombstones-c379952033d311e6aa4261d6a7221ccb/mb-7-big-Data.db
[
  {
    "partition" : {
      "key" : [ "apple" ],
      "position" : 0
    },
    "rows" : [
      {
        "type" : "row",
        "position" : 19,
        "clustering" : [ "20160617" ],
        "deletion_info" : { "marked_deleted" : "2016-06-16T19:31:41.142454Z", "local_delete_time" : "2016-06-16T19:31:41Z" },
        "cells" : [ ]
      }
    ]
  }
]

可以看到 cell 的值是一個空數組。row 墓碑是指沒有liveness_info和沒有cell的行。deletion_info 字段存在於行級別

Range 刪除

從單個分區中刪除一個範圍(即許多行)。

DELETE FROM tlp_lab.tombstones WHERE fruit='apple' AND date > '20160615';

apple 分區不再返回數據,因為它已經沒有行了。除非我們有比20160616小的數據

echo "SELECT * FROM tlp_lab.tombstones LIMIT 100;" | cqlsh

fruit   | date     | crates
---------+----------+-----------
pickles | 20160616 | {6, 7, 8}

(1 rows)

刷新後,磁盤上多了一個SSTable,'mb-8-big',內容如下。

alain$ SSTabledump /Users/alain/.ccm/Cassa-3.7/node1/data/tlp_lab/tombstones-c379952033d311e6aa4261d6a7221ccb/mb-8-big-Data.db
[
  {
    "partition" : {
      "key" : [ "apple" ],
      "position" : 0
    },
    "rows" : [
      {
        "type" : "range_tombstone_bound",
        "start" : {
          "type" : "exclusive",
          "clustering" : [ "20160615" ],
          "deletion_info" : { "marked_deleted" : "2016-06-16T19:53:21.133300Z", "local_delete_time" : "2016-06-16T19:53:21Z" }
        }
      },
      {
        "type" : "range_tombstone_bound",
        "end" : {
          "type" : "inclusive",
          "deletion_info" : { "marked_deleted" : "2016-06-16T19:53:21.133300Z", "local_delete_time" : "2016-06-16T19:53:21Z" }
        }
      }
    ]
  }
]

我們可以看到,我們現在有一個新的特殊插入,它的類型字段 type 不是 row,而是range_tombstone_bound。並且伴隨 start 和 end 字段:clustering key 從20160615排除到無窮大。那些帶有range_tombstone_bound類型的條目按照預期嵌套在 apple 對應的分區中。所以從磁盤空間的角度來看,刪除整個範圍是相當高效的,我們並不是每個單元格寫一個信息,只是存儲對應的刪除邊界。

Partition 刪除

刪除整個分區

DELETE FROM tlp_lab.tombstones WHERE fruit='pickles';

在刪除後,分區和所有嵌套的行都不再顯示,正如預期的那樣。該表現在是空的。

alain$ echo "SELECT * FROM tlp_lab.tombstones LIMIT 100;" | cqlsh

 fruit | date | crates
-------+------+--------

(0 rows)

刷新後磁盤上多了一個SSTable,mb-9-big,內容如下。

alain$ SSTabledump /Users/alain/.ccm/Cassa-3.7/node1/data/tlp_lab/tombstones-c379952033d311e6aa4261d6a7221ccb/mb-9-big-Data.db
[
  {
    "partition" : {
      "key" : [ "pickles" ],
      "position" : 0,
      "deletion_info" : { "marked_deleted" : "2016-06-17T09:38:52.550841Z", "local_delete_time" : "2016-06-17T09:38:52Z" }
    }
  }
]

所以,我們又插入了一個特定的標記。Partition 類型的墓碑是指插入的分區有deletion_info,沒有行。

注意:當使用集合類型時,每次使用整個集合進行復制操作的時候,range墓碑都會由INSERT和UPDATE操作生成,而並非更新原有集合。插入一個新集合,而不是追加或只更新原集合中的元素,也會導致先插入一個 range 墓碑 + 插入集合的新值。這樣隱藏式進行的DELETE操作,往往導致一些奇怪的問題。

仔細看一下第一個SSTabledump的輸出,就在上面的數據插入之後,在任何刪除之前,你會發現一個墓碑已經存在了。

"cells" : [
  { "name" : "crates", "deletion_info" : { "marked_deleted" : "2016-06-16T18:52:41.900450Z", "local_delete_time" : "2016-06-16T18:52:41Z" } },
  { "name" : "crates", "path" : [ "1" ], "value" : "" },
  { "name" : "crates", "path" : [ "2" ], "value" : "" },
  { "name" : "crates", "path" : [ "3" ], "value" : "" },
  { "name" : "crates", "path" : [ "4" ], "value" : "" },
  { "name" : "crates", "path" : [ "5" ], "value" : "" }
]

從郵件列表中,我發現James Ravn用 list 舉例討論了這個話題,但對所有的集合類型都適用,這裡不再展開更多的細節,我只是想指出這一點,因為乍看之下比較意外,見:http://www.jsravn.com/2015/05/13/cassandra-tombstones-collections.html#lists

墓碑可能產生的問題

好了,現在我們明白了為什麼我們要使用墓碑,並且對墓碑的內容有了大致的瞭解,讓我們看看墓碑可能引起的潛在問題,以及我們可以採取哪些措施來緩解這些問題。

第一件顯而易見的事情是,我們不僅沒有刪除數據,而是存儲了更多的數據。在某些時候,我們需要刪除這些墓碑,以釋放一些磁盤空間,並限制不必要的數據讀取量,改善延遲和資源利用率。正如我們很快就會看到的那樣,這是通過 Compaction 的過程來實現的。

Compactions

當我們讀取一條特定的行時,需要查閱的SSTables越多,讀取速度就越慢。因此,為了保持較低的讀取延遲,有必要通過 Compaction 的過程來合併 sstable 文件。同時因為我們要繼續儘可能地釋放磁盤空間,所以這一過程也包含刪除符合條件的墓碑。

Compaction 的工作方式是合併來自多個SSTables的行片段,在條件滿足的情況下刪除墓碑。這些條件部分是在創建表的時候指定的,從而可以調整,比如gc_grace_seconds,部分條件由於Cassandra內部實現的原因,是硬編碼的,以確保數據的持久性和一致性。確保所有數據片段所在的 sstable 都參與當前的 compaction 中(通常被稱為 "重疊SSTables")是必要的,以避免不一致,因為一旦墓碑被驅逐,這些數據就會重新出現,形成這種 "殭屍 "數據。

考慮到上面的例子。在所有的刪除和刷新之後,表文件夾的樣子是這樣的。

alain$ ll /Users/alain/.ccm/Cassa-3.7/node1/data/tlp_lab/tombstones-c379952033d311e6aa4261d6a7221ccb/
total 360
drwxr-xr-x  43 alain  staff  1462 Jun 17 11:39 .
drwxr-xr-x   3 alain  staff   102 Jun 16 20:25 ..
drwxr-xr-x   2 alain  staff    68 Jun 16 17:05 backups
-rw-r--r--   1 alain  staff    43 Jun 17 11:13 mb-10-big-CompressionInfo.db
-rw-r--r--   1 alain  staff    43 Jun 17 11:13 mb-10-big-Data.db
-rw-r--r--   1 alain  staff    10 Jun 17 11:13 mb-10-big-Digest.crc32
-rw-r--r--   1 alain  staff    16 Jun 17 11:13 mb-10-big-Filter.db
-rw-r--r--   1 alain  staff     9 Jun 17 11:13 mb-10-big-Index.db
-rw-r--r--   1 alain  staff  4701 Jun 17 11:13 mb-10-big-Statistics.db
-rw-r--r--   1 alain  staff    59 Jun 17 11:13 mb-10-big-Summary.db
-rw-r--r--   1 alain  staff    92 Jun 17 11:13 mb-10-big-TOC.txt
-rw-r--r--   1 alain  staff    43 Jun 17 11:33 mb-11-big-CompressionInfo.db
-rw-r--r--   1 alain  staff    53 Jun 17 11:33 mb-11-big-Data.db
-rw-r--r--   1 alain  staff     9 Jun 17 11:33 mb-11-big-Digest.crc32
-rw-r--r--   1 alain  staff    16 Jun 17 11:33 mb-11-big-Filter.db
-rw-r--r--   1 alain  staff     9 Jun 17 11:33 mb-11-big-Index.db
-rw-r--r--   1 alain  staff  4611 Jun 17 11:33 mb-11-big-Statistics.db
-rw-r--r--   1 alain  staff    59 Jun 17 11:33 mb-11-big-Summary.db
-rw-r--r--   1 alain  staff    92 Jun 17 11:33 mb-11-big-TOC.txt
-rw-r--r--   1 alain  staff    43 Jun 17 11:33 mb-12-big-CompressionInfo.db
-rw-r--r--   1 alain  staff    42 Jun 17 11:33 mb-12-big-Data.db
-rw-r--r--   1 alain  staff    10 Jun 17 11:33 mb-12-big-Digest.crc32
-rw-r--r--   1 alain  staff    16 Jun 17 11:33 mb-12-big-Filter.db
-rw-r--r--   1 alain  staff     9 Jun 17 11:33 mb-12-big-Index.db
-rw-r--r--   1 alain  staff  4611 Jun 17 11:33 mb-12-big-Statistics.db
-rw-r--r--   1 alain  staff    59 Jun 17 11:33 mb-12-big-Summary.db
-rw-r--r--   1 alain  staff    92 Jun 17 11:33 mb-12-big-TOC.txt
-rw-r--r--   1 alain  staff    43 Jun 17 11:39 mb-13-big-CompressionInfo.db
-rw-r--r--   1 alain  staff    32 Jun 17 11:39 mb-13-big-Data.db
-rw-r--r--   1 alain  staff     9 Jun 17 11:39 mb-13-big-Digest.crc32
-rw-r--r--   1 alain  staff    16 Jun 17 11:39 mb-13-big-Filter.db
-rw-r--r--   1 alain  staff    11 Jun 17 11:39 mb-13-big-Index.db
-rw-r--r--   1 alain  staff  4591 Jun 17 11:39 mb-13-big-Statistics.db
-rw-r--r--   1 alain  staff    65 Jun 17 11:39 mb-13-big-Summary.db
-rw-r--r--   1 alain  staff    92 Jun 17 11:39 mb-13-big-TOC.txt
-rw-r--r--   1 alain  staff    43 Jun 17 11:12 mb-9-big-CompressionInfo.db
-rw-r--r--   1 alain  staff   127 Jun 17 11:12 mb-9-big-Data.db
-rw-r--r--   1 alain  staff    10 Jun 17 11:12 mb-9-big-Digest.crc32
-rw-r--r--   1 alain  staff    16 Jun 17 11:12 mb-9-big-Filter.db
-rw-r--r--   1 alain  staff    20 Jun 17 11:12 mb-9-big-Index.db
-rw-r--r--   1 alain  staff  4740 Jun 17 11:12 mb-9-big-Statistics.db
-rw-r--r--   1 alain  staff    61 Jun 17 11:12 mb-9-big-Summary.db
-rw-r--r--   1 alain  staff    92 Jun 17 11:12 mb-9-big-TOC.txt

你的SSTable很可能與上面的例子不一致,但是插入和刪除的內容是完全一樣的。我們可以看到這個表實際上是空的。存儲在磁盤上的文件只包含墓碑和他們專門刪除的條目。從讀取的角度來看,沒有任何結果返回。

echo "SELECT * FROM tlp_lab.tombstones LIMIT 100;" | cqlsh

fruit | date | crates
-------+------+--------

(0 rows)

此時,讓我們觸發一個 major compaction 來合併所有的SSTables。當 compaction 是自動觸發的時候,通常會運行得更好。禁用自動 compaction 手動觸發 major compaction 很少是一個好主意。這裡是為了教學目的而做的。

nodetool -p 7100 compact

現在,所有的SSTable已經合併成一個SSTable

alain$ ll /Users/alain/.ccm/Cassa-3.7/node1/data/tlp_lab/tombstones-c379952033d311e6aa4261d6a7221ccb/
total 72
drwxr-xr-x  11 alain  staff   374 Jun 17 14:50 .
drwxr-xr-x   3 alain  staff   102 Jun 16 20:25 ..
drwxr-xr-x   2 alain  staff    68 Jun 16 17:05 backups
-rw-r--r--   1 alain  staff    51 Jun 17 14:50 mb-14-big-CompressionInfo.db
-rw-r--r--   1 alain  staff   105 Jun 17 14:50 mb-14-big-Data.db
-rw-r--r--   1 alain  staff    10 Jun 17 14:50 mb-14-big-Digest.crc32
-rw-r--r--   1 alain  staff    16 Jun 17 14:50 mb-14-big-Filter.db
-rw-r--r--   1 alain  staff    20 Jun 17 14:50 mb-14-big-Index.db
-rw-r--r--   1 alain  staff  4737 Jun 17 14:50 mb-14-big-Statistics.db
-rw-r--r--   1 alain  staff    61 Jun 17 14:50 mb-14-big-Summary.db
-rw-r--r--   1 alain  staff    92 Jun 17 14:50 mb-14-big-TOC.txt

這裡是SSTable的內容,包含所有的墓碑,合併到同一個結構中,在同一個文件中。

alain$ SSTabledump /Users/alain/.ccm/Cassa-3.7/node1/data/tlp_lab/tombstones-c379952033d311e6aa4261d6a7221ccb/mb-14-big-Data.db
[
  {
    "partition" : {
      "key" : [ "apple" ],
      "position" : 0
    },
    "rows" : [
      {
        "type" : "range_tombstone_bound",
        "start" : {
          "type" : "exclusive",
          "clustering" : [ "20160615" ],
          "deletion_info" : { "marked_deleted" : "2016-06-17T09:14:11.697040Z", "local_delete_time" : "2016-06-17T09:14:11Z" }
        }
      },
      {
        "type" : "row",
        "position" : 40,
        "clustering" : [ "20160617" ],
        "deletion_info" : { "marked_deleted" : "2016-06-17T09:33:56.367859Z", "local_delete_time" : "2016-06-17T09:33:56Z" },
        "cells" : [ ]
      },
      {
        "type" : "range_tombstone_bound",
        "end" : {
          "type" : "inclusive",
          "deletion_info" : { "marked_deleted" : "2016-06-17T09:14:11.697040Z", "local_delete_time" : "2016-06-17T09:14:11Z" }
        }
      }
    ]
  },
  {
    "partition" : {
      "key" : [ "pickles" ],
      "position" : 73,
      "deletion_info" : { "marked_deleted" : "2016-06-17T09:38:52.550841Z", "local_delete_time" : "2016-06-17T09:38:52Z" }
    }
  }
]

請注意,此時被刪除的數據在 compaction 過程中被直接刪除。然而,正如前面所討論的那樣,我們仍然會在磁盤上存儲一個墓碑標記,因為我們需要保留刪除本身的記錄,以便有效地將刪除操作傳達給集群的其他成員。我們不需要保留實際值,因為為了保持一致性,不需要這樣做。

墓碑標記清除

Cassandra會在compaction觸發時,只有在數據所屬表上定義的local_delete_time + gc_grace_seconds之後,才會完全清理這些墓碑標記。請記住,所有的節點都應該在gc_grace_seconds內被修復,以確保墓碑標記的正確分佈,並防止被刪除的數據再次出現,如上所述。

這個gc_grace_seconds參數是數據被刪除後,墓碑在磁盤上保留的最小時間。我們需要確保所有的副本也收到了刪除,並且有墓碑存儲,避免出現一些殭屍數據的問題。我們唯一的辦法就是全面修復。在gc_grace_seconds之後,墓碑最終會被驅逐,如果有節點錯過了墓碑,我們就會出現上面所說的情況,數據會重新出現。TTL不受此影響,因為沒有一個節點可以擁有數據而錯過相關的TTL,它是原子的,是同一個記錄。任何擁有數據的節點也會知道數據什麼時候要被刪除。

另外,為了從磁盤中刪除數據和墓碑,Cassandra代碼還需要遵循其他安全規則。我們需要一行或一個分區的所有碎片都在同一個 Compaction 中,墓碑才能被刪除。比如一個 Compaction 處理文件1到4,如果一些數據在文件5上,墓碑不會被驅逐,因為我們仍然需要它將SSTable 5上的數據標記為被刪除,否則SSTable 5上的數據會回來(殭屍)。

這些條件有時會使刪除墓碑成為一件非常複雜的事情。它常常給Cassandra用戶帶來很多麻煩。墓碑不被移除可能意味著使用了大量的磁盤空間,讀取速度較慢,維修工作較多,GC壓力較大,需要更多的資源等等。當你的一張表的大部分SSTables的墓碑比例很高的時候(90%的數據都是墓碑),就很難讀到一個合適的值,一個相關的數據,存儲成本就會比較高。這類問題甚至會導致磁盤空間的耗盡。

很多用法都會導致數據刪除(TTL或deletes),這是我們作為Cassandra運維人員需要關注和處理的。

最後一次回到我們的例子。幾天後我重啟了節點(幾天>10天,默認的gc_grace_seconds)。Cassandra打開了我們在mb-14-big上面建立的壓縮SSTable,它馬上就被壓縮了。

MacBook-Pro:tombstones alain$ grep 'mb-14-big' /Users/alain/.ccm/Cassa-3.7/node1/logs/system.log

DEBUG [SSTableBatchOpen:1] 2016-06-28 15:56:17,947 SSTableReader.java:482 - Opening /Users/alain/.ccm/Cassa-3.7/node1/data/tlp_lab/tombstones-c379952033d311e6aa4261d6a7221ccb/mb-14-big (0.103KiB)
DEBUG [CompactionExecutor:2] 2016-06-28 15:56:18,525 CompactionTask.java:150 - Compacting (166f61c0-3d38-11e6-bfe3-e9e451310a18) [/Users/alain/.ccm/Cassa-3.7/node1/data/tlp_lab/tombstones-c379952033d311e6aa4261d6a7221ccb/mb-14-big-Data.db:level=0, ]

此時由於gc_grace_seconds已經過了,墓碑是符合清理條件的。所以所有的墓碑都被刪除了,由於這個表裡已經沒有數據了,所以數據文件夾現在終於空了。

MacBook-Pro:tombstones alain$ ll /Users/alain/.ccm/Cassa-3.7/node1/data/tlp_lab/tombstones-c379952033d311e6aa4261d6a7221ccb/
total 0
drwxr-xr-x  3 alain  staff  102 Jun 28 15:56 .
drwxr-xr-x  3 alain  staff  102 Jun 16 20:25 ..
MacBook-Pro:tombstones alain$

如果墓碑在所有的副本上都能正確複製,我們就會有一個完全一致的刪除,不會再出現數據。這時我們還可以多釋放一些磁盤空間,讓我們更容易讀取其他值,即使我的例子對於演示這個目的有點傻,因為現在表已經完全空了。

監控墓碑佔比和過期

由於Cassandra設計的原因,在刪除數據或使用TTL時出現墓碑是正常的,然而這是我們需要控制的。

要想知道一個表的墓碑率,或者想知道任何SSTable上墓碑清理時間(即刪除時間戳)的估計分佈,可以使用SSTablemetadata。

alain$ SSTablemetadata /Users/alain/.ccm/Cassa-3.7/node1/data/tlp_lab/tombstones-c379952033d311e6aa4261d6a7221ccb/mb-14-big-Data.db

--
Estimated droppable tombstones: 2.0
--
Estimated tombstone drop times:
1466154851:         2
1466156036:         1
1466156332:         1
--

上面展示的比例可能是錯誤的,因為在這個mb-14-big文件中,我有0個有用的行,只有墓碑的特定狀態,但它通常是一個關於墓碑的SSTable狀態的好指標。

另外,Cassandra通過JMX為所有表暴露了這個名為TombstoneScannedHistogram的監控指標。scope=tombstones 這樣就是指定表:tombstones

org.apache.cassandra.metrics:type=Table,keyspace=tlp_lab,scope=tombstones,name=TombstoneScannedHistogram

這是值得納入進監控工具的一個指標,如Graphite / Grafana,Datadog,New Relic等。

下面是我上面的例子在 Compaction 清理墓碑之前的jconsole輸出。

單文件 compaction

在Jonathan Ellis在CASSANDRA-3442中報告了以下內容後,並在Cassandra 1.2中引入了單文件SSTable compaction。

在 sized tired compaction 的情況下,往往出現不經常執行 compaction 的大型SSTable文件。在這種情況下,如果混入了過期的數據,我們可能會浪費很多空間。

如上所述,compaction 負責墓碑清理工作。在某些情況下,compaction 無法做好墓碑的清理工作。不僅上述提到的 STCS 策略,其實目前所有的 compaction 策略都是如此。有些SSTables的 compaction 頻率可能很低,或者有重疊的SSTables的時間很長。這就是為什麼,現在所有的 compaction 策略都帶有一套參數來幫助墓碑清理。

tombstone_threshold。這個參數的行為和Jonathan Ellis 在 2011 年所提交的 ticket 的描述的一模一樣。

如果我們在SSTable元數據中保留一個TTL EstimatedHistogram,我們就可以對過期數據超過20%的SSTable進行單SSTable壓實。

所以當認為可清理的墓石比例高於X(X=0.2,默認為20%)時,這個選項會觸發一次單文件 compaction。因為計算出來的墓石比例沒有考慮gc_grace_seconds所以往往實際能清理的墓碑會小於估計值。

tombstone_compaction_interval.這個選項在CASSANDRA-4781中被引入,當墓碑比例高到足以觸發單文件 compaction 時,可能由於重疊 sstable 的因素導致墓碑實際上無法被清理,進而引起無限循環,這個選項就是為了解決這個問題引入的。我們必須確保刪除所有的數據碎片以避免殭屍。在這種情況下,一些SSTable上會持續不斷的進行 compaction。因為單文件的 compaction 觸發條件僅僅基於墓碑率,而墓碑率是一個估計值。這個選項可以調整2個單文件 compaction 之間的最小間隔,默認是1天。

unchecked_tombstone_compaction。由Paulo Motta在CASSANDRA -6563中引入。這裡是他對單SSTable歷史的介紹,以及他引入這個參數的原因,非常有趣,我也無法解釋得更好。

但是要注意 trade-off:把這個選項設置為true,只要墓碑比(估計)高於0.2(20%的數據是墓碑,墓碑_閾值默認),就會每天觸發單文件 compaction(默認間隔:tombstone_compaction_interval),即使實際上沒有一個墓碑是可以清理的。這將是最壞的情況。

所以,trade-off 是消耗更多的資源,期望能更好的完成墓碑清理。

建議:在一些數據中心遇到墓碑清理麻煩的時候,儘快給這個選項一個嘗試應該是值得的。我有一些使用這個選項的成功經驗,沒有真正的壞經驗,而是有一些情況下,這個改變沒有真正的影響。我甚至在一些磁盤使用率達到100%的節點上將這個選項設置為true並且手動 compaction 正確的SSTables後,觀測到磁盤使用率下降到一個合理的水位

要改變這些參數中的任何一個,先對你想要改的表使用 describe 命令,然後填寫完整的 compaction 參數以避免任何問題。要改變tlp_lab鍵空間中表墓碑的 compaction 選項,我會這樣做。

MacBook-Pro:~ alain$ echo "DESCRIBE TABLE tlp_lab.tombstones;" | cqlsh
CREATE TABLE tlp_lab.tombstones (
fruit text,
date text,
crates set<int>,
PRIMARY KEY (fruit, date)
) WITH CLUSTERING ORDER BY (date ASC)
AND bloom_filter_fp_chance = 0.01
AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'}
AND comment = ''
AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'}
AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'}
AND crc_check_chance = 1.0
AND dclocal_read_repair_chance = 0.1
AND default_time_to_live = 0
AND gc_grace_seconds = 864000
AND max_index_interval = 2048
AND memtable_flush_period_in_ms = 0
AND min_index_interval = 128
AND read_repair_chance = 0.0
AND speculative_retry = '99PERCENTILE';

然後,我複製 compaction 參數,並修改表格如下。

echo "ALTER TABLE tlp_lab.tombstones WITH compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4', 'unchecked_tombstone_compaction': 'true', 'tombstone_threshold': '0.1'};" | cqlsh

或者把以下內容寫入腳本文件中執行。

ALTER TABLE tlp_lab.tombstones WITH compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4', 'unchecked_tombstone_compaction': 'true', 'tombstone_threshold': '0.1'};

cqlsh -f myfile.cql

也可以使用-e選項

cqlsh -e "ALTER TABLE tlp_lab.tombstones WITH compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4', 'unchecked_tombstone_compaction': 'true', 'tombstone_threshold': '0.1'};"

注意:我使用其中一個選項,而不是進入cqlsh控制檯,因為它更容易和 pipe 相結合。在選擇數據或描述表時,它更有意義。想象一下,你需要檢查你所有表的read_repair_chance情況

MacBook-Pro:~ alain$ echo "DESCRIBE TABLE tlp_lab.tombstones;" | cqlsh | grep -e TABLE -e read_repair_chance
    CREATE TABLE tlp_lab.tombstones (
        AND dclocal_read_repair_chance = 0.1
        AND read_repair_chance = 0.0
    CREATE TABLE tlp_lab.foo (
        AND dclocal_read_repair_chance = 0.0
        AND read_repair_chance = 0.1
    CREATE TABLE tlp_lab.bar (
        AND dclocal_read_repair_chance = 0.0
        AND read_repair_chance = 0.0

手動清理墓碑

有時候一些SSTables包含95%的墓碑,但由於 compaction 參數設置、SSTables重疊,或者僅僅是單SSTable compaction 的優先級低於常規 compaction,從而一直未能執行 compaction 操作。要知道,我們可以手動強制Cassandra運行用戶定義的 compaction,這一點很重要。要做到這一點,我們需要能夠通過JMX發送命令。我這裡會考慮使用jmxterm。你可以選擇自己喜歡的工具。

下載 jmxterm。

wget http://sourceforge.net/projects/cyclops-group/files/jmxterm/1.0-alpha-4/jmxterm-1.0-alpha-4-uber.jar

然後運行JMX命令,比如強制 compaction

echo "run -b org.apache.cassandra.db:type=CompactionManager forceUserDefinedCompaction myks-mytable-marker-sstablenumber-Data.db" | java -jar jmxterm-1.0-alpha-4-uber.jar -l localhost:7199

在某些情況下,我使用了基於這種命令的腳本,結合sstablemetadata工具給出的墓碑比例來搜索最差的文件,並將它們壓縮,效果非常成功。

總結

針對分佈式系統執行刪除一直是一個棘手的操作,特別是當試圖同時兼顧可用性、一致性和持久性時。

在像Apache Cassandra這樣的分佈式系統中,使用墓碑是一種執行刪除的明智方式,但它也有一些注意事項。我們需要用一種非直觀的方式來思考,因為在刪除的時候,添加這塊數據並不是一件自然的事情。然後瞭解墓碑的生命週期,這也不是小事。然而,當我們瞭解了墓碑的行為,並使用相應的工具來幫助我們解決墓碑問題時,墓碑也就變得容易理解了。

由於Cassandra是一個快速發展的系統,這裡有一些正在進行的關於墓碑的提議,你可能有興趣瞭解一下。

進行中提議:。

CASSANDRA-7019:改進墓碑 compaction(主要是解決SSTable墓碑重疊的問題,通過讓單SSTable compaction 實際運行在多個智能選擇的SSTables上)。

CASSANDRA-8527:凡是我們統計墓碑的地方都要考慮 range 類型的墓碑。看起來 range 類型的墓碑可能在代碼的多個部分沒有被很好地考慮。CASSANDRA-11166 和 CASSANDRA-9617 也指出了這個問題。

入群邀約
為了營造一個開放的 Cassandra 技術交流環境,社區建立了微信群公眾號和釘釘群,為廣大用戶提供專業的技術分享及問答,定期開展專家技術直播,歡迎大家加入。

在這裡插入圖片描述
雲 Cassandra
阿里雲提供商業化Cassandra使用,支持中國站和國際站:
https://www.aliyun.com/product/cds

Leave a Reply

Your email address will not be published. Required fields are marked *