標籤:啟動 scala 理解 操作 null 條件 協議 筆記 記錄檔
在前面的一篇文章《圖形資料庫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": 1234567895 }
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”: 01234343711 }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 text4 );
在上面的樣本中,我們指定了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樹中同一層次的各個結點在磁碟中是連續記錄的。這樣磁碟就可以通過連續讀取來避免在磁碟上的過多尋道,從而大大地提高了運行效率。
Memtable和SSTable
好,剛剛我們已經提到了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來處理各硬體的效能不同,從而在物理硬體層次上形成一種類似《企業級Server Load Balancer簡介》一文所中提到過的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 Neo4J已經不支援@Fetch)。因此如果您發現有什麼描述已經發生了改變,請幫留下評論,以便其它讀者參考。在此感激不盡
開源軟體:NoSql資料庫 - 圖資料庫 Cassandra