1. 程式人生 > >開源軟件:NoSql數據庫 - 圖數據庫 Cassandra

開源軟件:NoSql數據庫 - 圖數據庫 Cassandra

避免 更多 創建 負載平衡 這就是 重復 9.png tar 客戶

轉載自原文地址:http://www.cnblogs.com/loveis715/p/5299495.html

Cassandra簡介

  在前面的一篇文章《圖形數據庫Neo4J簡介》中,我們介紹了一種非常流行的圖形數據庫Neo4J的使用方法。而在本文中,我們將對另外一種類型的NoSQL數據庫——Cassandra進行簡單地介紹。

  接觸Cassandra的原因與接觸Neo4J的原因相同:我們的產品需要能夠記錄一系列關系型數據庫所無法快速處理的大量數據。Cassandra,以及後面將要介紹的MongoDB,都是我們在技術選型過程中的一個備選方案。雖然說最後我們並沒有選擇Cassandra,但是在整個技術選型過程中所接觸到的一系列內部機制,思考方式等都是非常有趣的。而且在整個選型過程中也借鑒了CAM(Cloud Availability Manager)組在實際使用過程中所得到的一些經驗。因此我在這裏把自己的筆記總結成一篇文章,分享出來。

技術選型

  技術選型常常是一個非常嚴謹的過程。由於一個項目通常是由數十位甚至上百位開發人員協同開發的,因此一個精準的技術選型常常能夠大幅提高整個項目的開發效率。在嘗試為某一類需求設計解決方案時,我們常常會有很多種可以選擇的技術。為了能夠精準地選擇一個適合於這些需求的技術,我們就需要考慮一系列有關學習曲線,開發,維護等眾多方面的因素。這些因素主要包括:

  • 該技術所提供的功能是否能夠完整地解決問題。
  • 該技術的擴展性如何。是否允許用戶添加自定義組成來滿足特殊的需求。
  • 該技術是否有豐富完整的文檔,並且能夠以免費甚至付費的形式得到專業的支持。
  • 該技術是否有很多人使用,尤其是一些大型企業在使用,並存在著成功的案例。

  在該過程中,我們會逐漸篩選市面上所能找到的各種技術,並最終確定適合我們需求的那一種。

  針對我們剛剛所提到的需求——記錄並處理系統自動生成的大量數據,我們在技術選型的初始階段會有很多種選擇:Key-Value數據庫,如Redis,Document-based數據庫,如MongoDB,Column-based數據庫,如Cassandra等。而且在實現特定功能時,我們常常可以通過以上所列的任何一種數據庫來搭建一個解決方案。可以說,如何在這三種數據庫之間選擇常常是NoSQL數據庫初學者所最為頭疼的問題。導致這種現象的一個原因就是,Key-Value,Document-based以及Column-based實際上是對NoSQL數據庫的一種較為泛泛的分類。不同的數據庫提供商所提供的NoSQL數據庫常常具有略為不同的實現方式,並提供了不同的功能集合,進而會導致這些數據庫類型之間的邊界並不是那麽清晰。

  恰如其名所示,Key-Value數據庫會以鍵值對的方式來對數據進行存儲。其內部常常通過哈希表這種結構來記錄數據。在使用時,用戶只需要通過Key來讀取或寫入相應的數據即可。因此其在對單條數據進行CRUD操作時速度非常快。而其缺陷也一樣明顯:我們只能通過鍵來訪問數據。除此之外,數據庫並不知道有關數據的其它信息。因此如果我們需要根據特定模式對數據進行篩選,那麽Key-Value數據庫的運行效率將非常低下,這是因為此時Key-Value數據庫常常需要掃描所有存在於Key-Value數據庫中的數據。

  因此在一個服務中,Key-Value數據庫常常用來作為服務端緩存使用,以記錄一系列經由較為耗時的復雜計算所得到的計算結果。最著名的就是Redis。當然,為Memcached添加了持久化功能的MemcacheDB也是一種Key-Value數據庫。

  Document-based數據庫和Key-Value數據庫之間的不同主要在於,其所存儲的數據將不再是一些字符串,而是具有特定格式的文檔,如XML或JSON等。這些文檔可以記錄一系列鍵值對,數組,甚至是內嵌的文檔。如:

技術分享
 1 {
 2     Name: "Jefferson",
 3     Children: [{
 4         Name:"Hillary",
 5         Age: 14
 6     }, {
 7         Name:"Todd",
 8         Age: 12
 9     }],
10     Age: 45,
11     Address: {
12         number: 1234,
13         street: "Fake road",
14         City: "Fake City",
15         state: "NY",
16         Country: "USA"
17     }
18 }
技術分享

  有些讀者可能會有疑問,我們同樣也可以通過Key-Value數據庫來存儲JSON或XML格式的數據,不是麽?答案就是Document-based數據庫常常會支持索引。我們剛剛提到過,Key-Value數據庫在執行數據的查找及篩選時效率非常差。而在索引的幫助下,Document-based數據庫則能夠很好地支持這些操作了。有些Document-based數據庫甚至允許執行像關系型數據庫那樣的JOIN操作。而且相較於關系型數據庫,Document-based數據庫也將Key-Value數據庫的靈活性得以保留。

  而Column-based數據庫則與前面兩種數據庫非常不同。我們知道,一個關系型數據庫中所記錄的數據常常是按照行來組織的。每一行中包含了表示不同意義的多個列,並被順序地記錄在持久化文件中。我們知道,關系型數據庫中的一個常見操作就是對具有特定特征的數據進行篩選及操作,而且該操作常常是通過WHERE子句來完成的:

1 SELECT * FROM customers WHERE country=‘Mexico‘;

  在一個傳統的關系型數據庫中,該語句所操作的表可能如下所示:

技術分享

  而在該表所對應的數據庫文件中,每一行中的各個數值將被順序記錄,從而形成了如下圖所示的數據文件:

技術分享

  因此在執行上面的SQL語句時,關系型數據庫並不能連續操作文件中所記錄的數據:

技術分享

  這大大降低了關系型數據庫的性能:為了運行該SQL語句,關系型數據庫需要讀取每一行中的id域和name域。這將導致關系型數據庫所要讀取的數據量顯著增加,也需要在訪問所需數據時執行一系列偏移量計算。況且上面所舉的例子僅僅是一個最簡單的表。如果表中包含了幾十列,那麽數據讀取量將增大幾十倍,偏移量計算也會變得更為復雜。

  那麽我們應該如何解決這個問題呢?答案就是將一列中的數據連續地存在一起:

技術分享

  而這就是Column-based數據庫的核心思想:按照列來在數據文件中記錄數據,以獲得更好的請求及遍歷效率。這裏有兩點需要註意:首先,Column-based數據庫並不表示會將所有的數據按列進行組織,也沒有那個必要。對某些需要執行請求的數據進行按列存儲即可。另外一點則是,Cassandra對Query的支持實際上是與其所使用的數據模型關聯在一起的。也就是說,對Query的支持很有限。我們馬上就會在下面的章節中對該限制進行介紹。

  至此為止,您應該能夠根據各種數據庫所具有的特性來為您的需求選擇一個合適的NoSQL數據庫了。

Cassandra初體驗

  OK,在簡單地介紹了Key-Value,Document-based以及Column-based三種不同類型的NoSQL數據庫之後,我們就要開始嘗試著使用Cassandra了。鑒於我個人在使用一系列NoSQL數據庫時常常遇到它們的版本更新缺乏API後向兼容性這一情況,我在這裏直接使用了Datastax Java Driver的樣例。這樣讀者也能從該頁面中查閱針對最新版本客戶端的示例代碼。

  一段最簡單的讀取一條記錄的Java代碼如下所示:

技術分享
Cluster cluster = null;
try {
    // 創建連接到Cassandra的客戶端
    cluster = Cluster.builder()
            .addContactPoint("127.0.0.1")
            .build();
    // 創建用戶會話
    Session session = cluster.connect();

    // 執行CQL語句
    ResultSet rs = session.execute("select release_version from system.local");
    // 從返回結果中取出第一條結果
    Row row = rs.one();
    System.out.println(row.getString("release_version"));
} finally {
    // 調用cluster變量的close()函數並關閉所有與之關聯的鏈接
    if (cluster != null) {
        cluster.close();
    }
}
技術分享

  看起來很簡單,是麽?其實在客戶端的幫助下,操作Cassandra實際上並不是非常困難的一件事。反過來,如何為Cassandra所記錄的數據設計模型才是最需要讀者仔細考慮的。與大家所最為熟悉的關系型數據庫建模方式不同,Cassandra中的數據模型設計需要是Join-less的。簡單地說,那就是由於這些數據分布在Cassandra的不同結點上,因此這些數據的Join操作並不能被高效地執行。

  那麽我們應該如何為這些數據定義模型呢?首先我們要了解Cassandra所支持的基本數據模型。這些基本數據模型有:Column,Super Column,Column Family以及Keyspace。下面我們就對它們進行簡單地介紹。

  Column是Cassandra所支持的最基礎的數據模型。該模型中可以包含一系列鍵值對:

1 {
2     "name": "Auther Name",
3     "value": "Sam",
4     "timestamp": 123456789
5 }

  Super Column則包含了一系列Column。在一個Super Column中的屬性可以是一個Column的集合:

技術分享
1 {
2     "name": "Cassandra Introduction",
3     "value": {
4         "auther": { "name": "Auther Name", "value": "Sam", "timestamp": 123456789},
5         "publisher": { "name": "Publisher", "value": "China Press", "timestamp": 234567890}
6     }
7 }
技術分享

  這裏需要註意的是,Cassandra文檔已經不再建議過多的使用Super Column,而原因卻沒有直接說明。據說這和Super Column常常需要在數據訪問時執行反序列化相關。一個最為常見的證據就是,網絡上常常會有一些開發人員在Super Column中添加了過多的數據,並進而導致和這些Super Column相關的請求運行緩慢。當然這只是猜測。只不過既然官方文檔都已經開始對Super Column持謹慎意見,那麽我們也需要在日常使用過程中盡量避免使用Super Column。

  而一個Column Family則是一系列Column的集合。在該集合中,每個Column都會有一個與之相關聯的鍵:

技術分享
 1 Authers = {
 2     “1332”: {
 3         "name": "Auther Name",
 4         "value": "Sam",
 5         "timestamp": 123456789
 6     },
 7     “1452”: {
 8         “name”: “Auther Name”,
 9         “value”: “Lucy”,
10         “timestamp”: 012343437
11     }
12 }
技術分享

  上面的Column Family示例中所包含的是一系列Column。除此之外,Column Family還可以包含一系列Super Column(請謹慎使用)。

  最後,Keyspace則是一系列Column Family的集合。

  發現了麽?上面沒有任何一種方法能夠通過一個Column(Super Column)引用另一個Column(Super Column),而只能通過Super Column包含其它Column的方式來完成這種信息的包含。這與我們在關系數據庫設計過程中通過外鍵與其它記錄相關聯的使用方法非常不同。還記得之前我們通過外鍵來創建數據關聯這一方法的名稱麽?對的,Normalization。該方法可以通過外鍵所指示的關聯關系有效地消除在關系型數據庫中的冗余數據。而在Cassandra中,我們要使用的方法就是Denormalization,也即是允許可以接受的一定程度的數據冗余。也就是說,這些關聯的數據將直接記錄在當前數據類型之中。

  在使用Cassandra時,哪些不該抽象為Cassandra數據模型,而哪些數據應該有一個獨立的抽象呢?這一切決定於我們的應用所常常執行的讀取及寫入請求。想想我們為什麽使用Cassandra,或者說Cassandra相較於關系型數據庫的優勢:快速地執行在海量數據上的讀取或寫入請求。如果我們僅僅根據所操作的事物抽象數據模型,而不去理會Cassandra在這些模型之上的執行效率,甚至導致這些數據模型無法支持相應的業務邏輯,那麽我們對Cassandra的使用也就沒有實際的意義了。因此一個較為正確的做法就是:首先根據應用的需求來定義一個抽象概念,並開始針對該抽象概念以及應用的業務邏輯設計在該抽象概念上運行的請求。接下來,軟件開發人員就可以根據這些請求來決定如何為這些抽象概念設計模型了。

  在抽象設計模型時,我們常常需要面對另外一個問題,那就是如何指定各Column Family所使用的各種鍵。在Cassandra相關的各類文檔中,我們常常會遇到以下一系列關鍵的名詞:Partition Key,Clustering Key,Primary Key以及Composite Key。那麽它們指的都是什麽呢?

  Primary Key實際上是一個非常通用的概念。在Cassandra中,其表示用來從Cassandra中取得數據的一個或多個列:

1 create table sample (
2     key text PRIMARY KEY,
3     data text
4 );

  在上面的示例中,我們指定了key域作為sample的PRIMARY KEY。而在需要的情況下,一個Primary Key也可以由多個列共同組成:

1 create table sample {
2     key_one text,
3     key_two text,
4     data text,
5     PRIMARY KEY(key_one, key_two)
6 };

  在上面的示例中,我們所創建的Primary Key就是一個由兩個列key_one和key_two組成的Composite Key。其中該Composite Key的第一個組成被稱為是Partition Key,而後面的各組成則被稱為是Clustering Key。Partition Key用來決定Cassandra會使用集群中的哪個結點來記錄該數據,每個Partition Key對應著一個特定的Partition。而Clustering Key則用來在Partition內部排序。如果一個Primary Key只包含一個域,那麽其將只擁有Partition Key而沒有Clustering Key。

  Partition Key和Clustering Key同樣也可以由多個列組成:

技術分享
1 create table sample {
2     key_primary_one text,
3     key_primary_two text,
4     key_cluster_one text,
5     key_cluster_two text,
6     data text,
7     PRIMARY KEY((key_primary_one, key_primary_two), key_cluster_one, key_cluster_two)
8 };
技術分享

  而在一個CQL語句中,WHERE等子句所標示的條件只能使用在Primary Key中所使用的列。您需要根據您的數據分布決定到底哪些應該是Partition Key,哪些應該作為Clustering Key,以對其中的數據進行排序。

  一個好的Partition Key設計常常會大幅提高程序的運行性能。首先,由於Partition Key用來控制哪個結點記錄數據,因此Partition Key可以決定是否數據能夠較為均勻地分布在Cassandra的各個結點上,以充分利用這些結點。同時在Partition Key的幫助下,您的讀請求應盡量使用較少數量的結點。這是因為在執行讀請求時,Cassandra需要協調處理從各個結點中所得到的數據集。因此在響應一個讀操作時,較少的結點能夠提供較高的性能。因此在模型設計中,如何根據所需要運行的各個請求指定模型的Partition Key是整個設計過程中的一個關鍵。一個取值均勻分布的,卻常常在請求中作為輸入條件的域,常常是一個可以考慮的Partition Key。

  除此之外,我們也應該好好地考慮如何設置模型的Clustering Key。由於Clustering Key可以用來在Partition內部排序,因此其對於包含範圍篩選的各種請求的支持較好。

Cassandra內部機制

  在本節中,我們將對Cassandra的一系列內部機制進行簡單地介紹。這些內部機制很多都是業界所常用的解決方案。因此在了解了Cassandra是如何使用它們的之後,您就可以非常容易地理解其它類庫對這些機制的使用,甚至在您自己的項目中借鑒及使用它們。

  這些常見的內部機制有:Log-Structured Merge-Tree,Consistent Hash,Virtual Node等。

Log-Structured Merge-Tree

  最有意思的一個數據結構莫過於Log-Structured Merge-Tree。Cassandra內部使用類似的結構來提高服務實例的運行效率。那它是如何工作的呢?

  簡單地說,一個Log-Structured Merge-Tree主要由兩個樹形結構的數據組成:存在於內存中的C0,以及主要存在於磁盤中的C1

技術分享

  在添加一個新的結點時,Log-Structured Merge-Tree會首先在日誌文件中添加一條有關該結點插入的記錄,然後再將該結點插入到樹C0中。添加到日誌文件中的記錄主要是基於數據恢復的考慮。畢竟C0樹處於內存中,非常容易受到系統宕機等因素的影響。而在讀取數據時,Log-Structured Merge-Tree會首先嘗試從C0樹中查找數據,然後再在C1樹中查找。

  在C0樹滿足一定條件之後,如其所占用的內存過大,那麽它所包含的數據將被遷移到C1中。在Log-Structured Merge-Tree這個數據結構中,該操作被稱為是rolling merge。其會把C0樹中的一系列記錄歸並到C1樹中。歸並的結果將會寫入到新的連續的磁盤空間。

技術分享

幾乎是論文中的原圖

  就單個樹來看,C1和我們所熟悉的B樹或者B+樹有點像,是不?

  不知道您註意到沒有。上面的介紹突出了一個詞:連續的。這是因為C1樹中同一層次的各個結點在磁盤中是連續記錄的。這樣磁盤就可以通過連續讀取來避免在磁盤上的過多尋道,從而大大地提高了運行效率。

MemtableSSTable

  好,剛剛我們已經提到了Cassandra內部使用和Log-Structured Merge-Tree類似的數據結構。那麽在本節中,我們就將對Cassandra的一些主要數據結構及操作流程進行介紹。可以說,如果您大致理解了上一節對Log-Structured Merge-Tree的講解,那麽理解這些數據結構也將是非常容易的事情。

  在Cassandra中有三個非常重要的數據結構:記錄在內存中的Memtable,以及保存在磁盤中的Commit Log和SSTable。Memtable在內存中記錄著最近所做的修改,而SSTable則在磁盤上記錄著Cassandra所承載的絕大部分數據。在SSTable內部記錄著一系列根據鍵排列的一系列鍵值對。通常情況下,一個Cassandra表會對應著一個Memtable和多個SSTable。除此之外,為了提高對數據進行搜索和訪問的速度,Cassandra還允許軟件開發人員在特定的列上創建索引。

  鑒於數據可能存儲於Memtable,也可能已經被持久化到SSTable中,因此Cassandra在讀取數據時需要合並從Memtable和SSTable所取得的數據。同時為了提高運行速度,減少不必要的對SSTable的訪問,Cassandra提供了一種被稱為是Bloom Filter的組成:每個SSTable都有一個Bloom Filter,以用來判斷與其關聯的SSTable是否包含當前查詢所請求的一條或多條數據。如果是,Cassandra將嘗試從該SSTable中取出數據;如果不是,Cassandra則會忽略該SSTable,以減少不必要的磁盤訪問。

  在經由Bloom Filter判斷出與其關聯的SSTable包含了請求所需要的數據之後,Cassandra就會開始嘗試從該SSTable中取出數據了。首先,Cassandra會檢查Partition Key Cache是否緩存了所要求數據的索引項Index Entry。如果存在,那麽Cassandra會直接從Compression Offset Map中查詢該數據所在的地址,並從該地址取回所需要的數據;如果Partition Key Cache並沒有緩存該Index Entry,那麽Cassandra首先會從Partition Summary中找到Index Entry所在的大致位置,並進而從該位置開始搜索Partition Index,以找到該數據的Index Entry。在找到Index Entry之後,Cassandra就可以從Compression Offset Map找到相應的條目,並根據條目中所記錄的數據的位移取得所需要的數據:

技術分享

較文檔中原圖略作調整

  發現了麽?實際上SSTable中所記錄的數據仍然是順序記錄的各個域,但是不同的是,它的查找首先經由了Partition Key Cache以及Compression Offset Map等一系列組成。這些組成僅僅包含了一系列對應關系,也就是相當於連續地記錄了請求所需要的數據,進而提高了數據搜索的運行速度,不是麽?

  Cassandra的寫入流程也與Log-Structured Merge-Tree的寫入流程非常類似:Log-Structured Merge-Tree中的日誌對應著Commit Log,C0樹對應著Memtable,而C1樹則對應著SSTable的集合。在寫入時,Cassandra會首先將數據寫入到Memtable中,同時在Commit Log的末尾添加該寫入所對應的記錄。這樣在機器斷電等異常情況下,Cassandra仍能通過Commit Log來恢復Memtable中的數據。

  在持續地寫入數據後,Memtable的大小將逐漸增長。在其大小到達某個閾值時,Cassandra的數據遷移流程就將被觸發。該流程一方面會將Memtable中的數據添加到相應的SSTable的末尾,另一方面則會將Commit Log中的寫入記錄移除。

  這也就會造成一個容易讓讀者困惑的問題:如果是將新的數據寫入到SSTable的末尾,那麽數據遷移的過程該如何執行對數據的更新?答案就是:在需要對數據進行更新時,Cassandra會在SSTable的末尾添加一條具有當前時間戳的記錄,以使得其能夠標明自身為最新的記錄。而原有的在SSTable中的記錄隨即宣告失效。

  這會導致一個問題,那就是對數據的大量更新會導致SSTable所占用的磁盤空間迅速增長,而且其中所記錄的數據很多都已經是過期數據。因此在一段時間之後,磁盤的空間利用率會大幅下降。此時我們就需要通過壓縮SSTable的方式釋放這些過期數據所占用的空間:

技術分享

  現在有一個問題,那就是我們可以根據重復數據的時間戳來判斷哪條是最新的數據,但是我們應該如何處理數據的刪除呢?在Cassandra中,對數據的刪除是通過一個被稱為tombstone的組成來完成的。如果一條數據被添加了一個tombstone,那麽其在下次壓縮時就被認為是一條已經被刪除的數據,從而不會添加到壓縮後的SSTable中。

  在壓縮過程中,原有的SSTable和新的SSTable同時存在於磁盤上。這些原有的SSTable用來完成對數據讀取的支持。一旦新的SSTable創建完畢,那麽老的SSTable就將被刪除。

  在這裏我們要提幾點在日常使用Cassandra的過程中需要註意的問題。首先是,由於通過Commit Log來重建Memtable是一個較為耗時的過程,因此我們在需要重建Memtable的一系列操作前需要嘗試手動觸發歸並邏輯,以將該結點上Memtable中的數據持久化到SSTable中。最常見的一種需要重建Memtable的操作就是重新啟動Cassandra所在的結點。

  另一個需要註意的地方是,不要過度地使用索引。雖然說索引可以大幅地增加數據的讀取速度,但是我們同樣需要在數據寫入時對其進行維護,造成一定的性能損耗。在這點上,Cassandra和傳統的關系型數據庫沒有太大區別。

Cassandra集群

  當然,使用單一的數據庫實例來運行Cassandra並不是一個好的選擇。單一的服務器可能導致服務集群產生單點失效的問題,也無法充分利用Cassandra的橫向擴展能力。因此從本節開始,我們就將對Cassandra集群以及集群中所使用的各種機制進行簡單地講解。

  在一個Cassandra集群中常常包含著以下一系列組成:結點(Node),數據中心(Data Center)以及集群(Cluster)。結點是Cassandra集群中用來存儲數據的最基礎結構;數據中心則是處於同一地理區域的一系列結點的集合;而集群則常常由多個處於不同區域的數據中心所組成:

技術分享

  上圖所展示的Cassandra集群由三個數據中心組成。這三個數據中心中的兩個處於同一區域內,而另一個數據中心則處於另一個區域中。可以說,兩個數據中心處於同一區域的情況並不多見,但是Cassandra的官方文檔也沒有否定這種集群搭建方式。每個數據中心則包含了一系列結點,以用來存儲Cassandra集群所要承載的數據。

  有了集群,我們就需要使用一系列機制來完成集群之間的相互協作,並考慮集群所需要的一系列非功能性需求了:結點的狀態維護,數據分發,擴展性(Scalability),高可用性,災難恢復等。

  對結點的狀態進行探測是高可用性的第一步,也是在結點間分發數據的基礎。Cassandra使用了一種被稱為是Gossip的點對點通訊方案,以在Cassandra集群中的各個結點之間共享及傳遞各個結點的狀態。只有這樣,Cassandra才能知道到底哪些結點可以有效地保存數據,進而將對數據的操作分發給各結點。

  在保存數據的過程中,Cassandra會使用一個被稱為是Partitioner的組成來決定數據到底要分發到哪些結點上。而另一個和數據存儲相關的組成就是Snitch。其會提供根據集群中所有結點的性能來決定如何對數據進行讀寫。

  這些組成內部也使用了一系列業界所常用的方法。例如Cassandra內部通過VNode來處理各硬件的性能不同,從而在物理硬件層次上形成一種類似《企業級負載平衡簡介》一文所中提到過的Weighted Round Robin的解決方案。再比如其內部使用了Consistent Hash,我們也在《Memcached簡介》一文中給出過介紹。

  好了,簡介完成。在下面幾節中,我們就將對Cassandra所使用的這些機制進行介紹。

Gossip

  首先就是Gossip。其是用來在Cassandra集群中的各個結點之間傳輸結點狀態的協議。它每秒都將運行一次,並將當前Cassandra結點的狀態以及其所知的其它結點的狀態與至多三個其它結點交換。通過這種方法,Cassandra的有效結點能很快地了解當前集群中其它結點的狀態。同時這些狀態信息還包含一個時間戳,以允許Gossip判斷到底哪個狀態是更新的狀態。

  除了在集群中的各個結點之間交換各結點的狀態之外,Gossip還需要能夠應對對集群進行操作的一系列動作。這些操作包括結點的添加,移除,重新加入等。為了能夠更好地處理這些情況,Gossip提出了一個叫做Seed Node的概念。其用來為各個新加入的結點提供一個啟動Gossip交換的入口。在加入到Cassandra集群之後,新結點就可以首先嘗試著跟其所記錄的一系列Seed Node交換狀態。這一方面可以得到Cassandra集群中其它結點的信息,進而允許其與這些結點進行通訊,又可以將自己加入的信息通過這些Seed Node傳遞出去。由於一個結點所得到的結點狀態信息常常被記錄在磁盤等持久化組成中,因此在重新啟動之後,其仍然可以通過這些持久化後的結點信息進行通訊,以重新加入Gossip交換。而在一個結點失效的情況下,其它結點將會定時地向該結點發送探測消息,以嘗試與其恢復連接。但是這會為我們永久地移除一個結點帶來麻煩:其它Cassandra結點總覺得該結點將在某一時刻重新加入集群,因此一直向該結點發送探測信息。此時我們就需要使用Cassandra所提供的結點工具了。

  那麽Gossip是如何判斷是否某個結點失效了呢?如果在交換過程中,參與交換的另一方很久不回答,那麽當前結點就會將目標結點標示為失效,並進而通過Gossip協議將該狀態傳遞出去。由於Cassandra集群的拓撲結構可能非常復雜,如跨區域等,因此其用來判斷一個結點是否失效的標準並不是在多長時間之內沒有響應就判定為失效。畢竟這會導致很大的問題:兩個在同一個Lab中的結點進行狀態交換會非常快,而跨區域的交換則會比較慢。如果我們設置的時間較短,那麽跨區域的狀態交換常常會被誤報為失效;如果我們所設置的時間較長,那麽Gossip對結點失效的探測靈敏度將降低。為了避免這種情況,Gossip使用的是一種根據以往結點間交換歷史等眾多因素綜合起來的決策邏輯。這樣對於兩個距離較遠的結點,其將擁有較大的時間窗,從而不會產生誤報。而對於兩個距離較近的結點,Gossip將使用較小的時間窗,從而提高探測的靈敏度。

Consistent Hash

  接下來我們要講的是Consistent Hash。在通常的哈希算法中常常包含著桶這個概念。每次哈希計算都是在決定特定數據需要存儲在哪個桶中。而如果桶的數量發生了變化,那麽之前的哈希計算結果都將失效。而Consistent Hash則很好地解決了該問題。

  那Consistent Hash是如何工作的呢?首先請考慮一個圓,在該圓上分布了多個點,以表示整數0到1023。這些整數平均分布在整個圓上:

技術分享

  在上圖中,我們突出地顯示了將圓六等分的六個藍點,表示用來記錄數據的六個結點。這六個結點將各自負責一個範圍。例如512這個藍點所對應的結點就將記錄從哈希值為512到681這個區間的數據。在Cassandra以及其它的一些領域中,這個圓被稱為是一個Ring。接下來我們就對當前需要存儲的數據執行哈希計算,並得到該數據所對應的哈希值。例如一段數據的哈希值為900,那麽它就位於853和1024之間:

技術分享

  因此該數據將被藍點853所對應的結點記錄。這樣一旦其它結點失效,該數據所在的結點也不會發生變化:

技術分享

  那每段數據的哈希值到底是如何計算出來的呢?答案是Partitioner。其輸入為數據的Partition Key。而其計算結果在Ring上的位置就決定了到底是由哪些結點來完成對數據的保存。

Virtual Node

  上面我們介紹了Consistent Hash的運行原理。但是這裏還有一個問題,那就是失效的那個結點上的數據該怎麽辦?我們就無法訪問了麽?這取決於我們對Cassandra集群數據復制方面的設置。通常情況下,我們都會啟用該功能,從而使得多個結點同時記錄一份數據的拷貝。那麽在其中一個結點失效的情況下,其它結點仍然可以用來讀取該數據。

  這裏要處理的一個情況就是,各個物理結點所具有的容量並不相同。簡單地說,如果一個結點所能提供的服務能力遠小於其它結點,那麽為其分配相同的負載將使得它不堪重負。為了處理這種情況,Cassandra提供了一種被稱為VNode的解決方案。在該解決方案中,每個物理結點將根據其實際容量被劃分為一系列具有相同容量的VNode。每個VNode則用來負責Ring上的一段數據。例如對於剛剛所展示的具有六個結點的Ring,各個VNode和物理機之間的關系則可能如下所示:

技術分享

  在使用VNode時,我們常常需要註意的一點就是Replication Factor的設置。從其所表示的意義來講,Cassandra中的Replication Factor和其它常見數據庫中所使用的Replication Factor沒有什麽不同:其所具有的數值用來表示記錄在Cassandra中的數據有多少份拷貝。例如在其被設置為1的情況下,Cassandra將只會保存一份數據。如果其被設置為2,那麽Cassandra將多保存一份這些數據的拷貝。

  在決定Cassandra集群所需要使用的Replication Factor時,我們需要考慮以下一系列因素:

  • 物理機的數量。試想一下,如果我們將Replication Factor設置為超過物理機的數量,那麽必然會有物理機保存了同一份數據的兩部分拷貝。這實際上沒有太大的作用:一旦該物理機出現異常,那就會一次損失多份數據。因此就高可用性這一點來說,Replication Factor的數值超過物理機的數量時,多出的這些數據拷貝意義並不大。
  • 物理機的異構性。物理機的異構性常常也會影響您所設Replication Factor的效果。舉一個極端的例子。如果說我們有一個Cassandra集群而且其由五臺物理機組成。其中一臺物理機的容量是其它物理機的4倍。那麽將Replication Factor設置為3時將會出現具有較大容量的物理機上存儲了同樣的數據這種問題。其並不比設置為2好多少。

  因此在決定一個Cassandra集群的Replication Factor時,我們要仔細地根據集群中物理機的數量和容量設置一個合適的數值。否則其只會導致更多的無用的數據拷貝。

註:這篇文章寫於15年8月。鑒於NoSQL數據庫發展非常快,而且常常具有一系列影響後向兼容性的更改(如Spring Data [email protected])。因此如果您發現有什麽描述已經發生了改變,請幫留下評論,以便其它讀者參考。在此感激不盡

開源軟件:NoSql數據庫 - 圖數據庫 Cassandra