Cassandra簡(jiǎn)介
在前面的一篇文章《圖形數(shù)據(jù)庫(kù)Neo4J簡(jiǎn)介》中,我們介紹了一種非常流行的圖形數(shù)據(jù)庫(kù)Neo4J的使用方法。而在本文中,我們將對(duì)另外一種類型的NoSQL數(shù)據(jù)庫(kù)——Cassandra進(jìn)行簡(jiǎn)單地介紹。
接觸Cassandra的原因與接觸Neo4J的原因相同:我們的產(chǎn)品需要能夠記錄一系列關(guān)系型數(shù)據(jù)庫(kù)所無(wú)法快速處理的大量數(shù)據(jù)。Cassandra,以及后面將要介紹的MongoDB,都是我們?cè)诩夹g(shù)選型過(guò)程中的一個(gè)備選方案。雖然說(shuō)最后我們并沒(méi)有選擇Cassandra,但是在整個(gè)技術(shù)選型過(guò)程中所接觸到的一系列內(nèi)部機(jī)制,思考方式等都是非常有趣的。而且在整個(gè)選型過(guò)程中也借鑒了CAM(Cloud Availability Manager)組在實(shí)際使用過(guò)程中所得到的一些經(jīng)驗(yàn)。因此我在這里把自己的筆記總結(jié)成一篇文章,分享出來(lái)。
技術(shù)選型
技術(shù)選型常常是一個(gè)非常嚴(yán)謹(jǐn)?shù)倪^(guò)程。由于一個(gè)項(xiàng)目通常是由數(shù)十位甚至上百位開發(fā)人員協(xié)同開發(fā)的,因此一個(gè)精準(zhǔn)的技術(shù)選型常常能夠大幅提高整個(gè)項(xiàng)目的開發(fā)效率。在嘗試為某一類需求設(shè)計(jì)解決方案時(shí),我們常常會(huì)有很多種可以選擇的技術(shù)。為了能夠精準(zhǔn)地選擇一個(gè)適合于這些需求的技術(shù),我們就需要考慮一系列有關(guān)學(xué)習(xí)曲線,開發(fā),維護(hù)等眾多方面的因素。這些因素主要包括:
- 該技術(shù)所提供的功能是否能夠完整地解決問(wèn)題。
- 該技術(shù)的擴(kuò)展性如何。是否允許用戶添加自定義組成來(lái)滿足特殊的需求。
- 該技術(shù)是否有豐富完整的文檔,并且能夠以免費(fèi)甚至付費(fèi)的形式得到專業(yè)的支持。
- 該技術(shù)是否有很多人使用,尤其是一些大型企業(yè)在使用,并存在著成功的案例。
在該過(guò)程中,我們會(huì)逐漸篩選市面上所能找到的各種技術(shù),并最終確定適合我們需求的那一種。
針對(duì)我們剛剛所提到的需求——記錄并處理系統(tǒng)自動(dòng)生成的大量數(shù)據(jù),我們?cè)诩夹g(shù)選型的初始階段會(huì)有很多種選擇:Key-Value數(shù)據(jù)庫(kù),如Redis,Document-based數(shù)據(jù)庫(kù),如MongoDB,Column-based數(shù)據(jù)庫(kù),如Cassandra等。而且在實(shí)現(xiàn)特定功能時(shí),我們常常可以通過(guò)以上所列的任何一種數(shù)據(jù)庫(kù)來(lái)搭建一個(gè)解決方案??梢哉f(shuō),如何在這三種數(shù)據(jù)庫(kù)之間選擇常常是NoSQL數(shù)據(jù)庫(kù)初學(xué)者所最為頭疼的問(wèn)題。導(dǎo)致這種現(xiàn)象的一個(gè)原因就是,Key-Value,Document-based以及Column-based實(shí)際上是對(duì)NoSQL數(shù)據(jù)庫(kù)的一種較為泛泛的分類。不同的數(shù)據(jù)庫(kù)提供商所提供的NoSQL數(shù)據(jù)庫(kù)常常具有略為不同的實(shí)現(xiàn)方式,并提供了不同的功能集合,進(jìn)而會(huì)導(dǎo)致這些數(shù)據(jù)庫(kù)類型之間的邊界并不是那么清晰。
恰如其名所示,Key-Value數(shù)據(jù)庫(kù)會(huì)以鍵值對(duì)的方式來(lái)對(duì)數(shù)據(jù)進(jìn)行存儲(chǔ)。其內(nèi)部常常通過(guò)哈希表這種結(jié)構(gòu)來(lái)記錄數(shù)據(jù)。在使用時(shí),用戶只需要通過(guò)Key來(lái)讀取或?qū)懭胂鄳?yīng)的數(shù)據(jù)即可。因此其在對(duì)單條數(shù)據(jù)進(jìn)行CRUD操作時(shí)速度非常快。而其缺陷也一樣明顯:我們只能通過(guò)鍵來(lái)訪問(wèn)數(shù)據(jù)。除此之外,數(shù)據(jù)庫(kù)并不知道有關(guān)數(shù)據(jù)的其它信息。因此如果我們需要根據(jù)特定模式對(duì)數(shù)據(jù)進(jìn)行篩選,那么Key-Value數(shù)據(jù)庫(kù)的運(yùn)行效率將非常低下,這是因?yàn)榇藭r(shí)Key-Value數(shù)據(jù)庫(kù)常常需要掃描所有存在于Key-Value數(shù)據(jù)庫(kù)中的數(shù)據(jù)。
因此在一個(gè)服務(wù)中,Key-Value數(shù)據(jù)庫(kù)常常用來(lái)作為服務(wù)端緩存使用,以記錄一系列經(jīng)由較為耗時(shí)的復(fù)雜計(jì)算所得到的計(jì)算結(jié)果。最著名的就是Redis。當(dāng)然,為Memcached添加了持久化功能的MemcacheDB也是一種Key-Value數(shù)據(jù)庫(kù)。
Document-based數(shù)據(jù)庫(kù)和Key-Value數(shù)據(jù)庫(kù)之間的不同主要在于,其所存儲(chǔ)的數(shù)據(jù)將不再是一些字符串,而是具有特定格式的文檔,如XML或JSON等。這些文檔可以記錄一系列鍵值對(duì),數(shù)組,甚至是內(nèi)嵌的文檔。如:
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 }
有些讀者可能會(huì)有疑問(wèn),我們同樣也可以通過(guò)Key-Value數(shù)據(jù)庫(kù)來(lái)存儲(chǔ)JSON或XML格式的數(shù)據(jù),不是么?答案就是Document-based數(shù)據(jù)庫(kù)常常會(huì)支持索引。我們剛剛提到過(guò),Key-Value數(shù)據(jù)庫(kù)在執(zhí)行數(shù)據(jù)的查找及篩選時(shí)效率非常差。而在索引的幫助下,Document-based數(shù)據(jù)庫(kù)則能夠很好地支持這些操作了。有些Document-based數(shù)據(jù)庫(kù)甚至允許執(zhí)行像關(guān)系型數(shù)據(jù)庫(kù)那樣的JOIN操作。而且相較于關(guān)系型數(shù)據(jù)庫(kù),Document-based數(shù)據(jù)庫(kù)也將Key-Value數(shù)據(jù)庫(kù)的靈活性得以保留。
而Column-based數(shù)據(jù)庫(kù)則與前面兩種數(shù)據(jù)庫(kù)非常不同。我們知道,一個(gè)關(guān)系型數(shù)據(jù)庫(kù)中所記錄的數(shù)據(jù)常常是按照行來(lái)組織的。每一行中包含了表示不同意義的多個(gè)列,并被順序地記錄在持久化文件中。我們知道,關(guān)系型數(shù)據(jù)庫(kù)中的一個(gè)常見操作就是對(duì)具有特定特征的數(shù)據(jù)進(jìn)行篩選及操作,而且該操作常常是通過(guò)WHERE子句來(lái)完成的:
1 SELECT * FROM customers WHERE country='Mexico';
在一個(gè)傳統(tǒng)的關(guān)系型數(shù)據(jù)庫(kù)中,該語(yǔ)句所操作的表可能如下所示:

而在該表所對(duì)應(yīng)的數(shù)據(jù)庫(kù)文件中,每一行中的各個(gè)數(shù)值將被順序記錄,從而形成了如下圖所示的數(shù)據(jù)文件:

因此在執(zhí)行上面的SQL語(yǔ)句時(shí),關(guān)系型數(shù)據(jù)庫(kù)并不能連續(xù)操作文件中所記錄的數(shù)據(jù):

這大大降低了關(guān)系型數(shù)據(jù)庫(kù)的性能:為了運(yùn)行該SQL語(yǔ)句,關(guān)系型數(shù)據(jù)庫(kù)需要讀取每一行中的id域和name域。這將導(dǎo)致關(guān)系型數(shù)據(jù)庫(kù)所要讀取的數(shù)據(jù)量顯著增加,也需要在訪問(wèn)所需數(shù)據(jù)時(shí)執(zhí)行一系列偏移量計(jì)算。況且上面所舉的例子僅僅是一個(gè)最簡(jiǎn)單的表。如果表中包含了幾十列,那么數(shù)據(jù)讀取量將增大幾十倍,偏移量計(jì)算也會(huì)變得更為復(fù)雜。
那么我們應(yīng)該如何解決這個(gè)問(wèn)題呢?答案就是將一列中的數(shù)據(jù)連續(xù)地存在一起:

而這就是Column-based數(shù)據(jù)庫(kù)的核心思想:按照列來(lái)在數(shù)據(jù)文件中記錄數(shù)據(jù),以獲得更好的請(qǐng)求及遍歷效率。這里有兩點(diǎn)需要注意:首先,Column-based數(shù)據(jù)庫(kù)并不表示會(huì)將所有的數(shù)據(jù)按列進(jìn)行組織,也沒(méi)有那個(gè)必要。對(duì)某些需要執(zhí)行請(qǐng)求的數(shù)據(jù)進(jìn)行按列存儲(chǔ)即可。另外一點(diǎn)則是,Cassandra對(duì)Query的支持實(shí)際上是與其所使用的數(shù)據(jù)模型關(guān)聯(lián)在一起的。也就是說(shuō),對(duì)Query的支持很有限。我們馬上就會(huì)在下面的章節(jié)中對(duì)該限制進(jìn)行介紹。
至此為止,您應(yīng)該能夠根據(jù)各種數(shù)據(jù)庫(kù)所具有的特性來(lái)為您的需求選擇一個(gè)合適的NoSQL數(shù)據(jù)庫(kù)了。
Cassandra初體驗(yàn)
OK,在簡(jiǎn)單地介紹了Key-Value,Document-based以及Column-based三種不同類型的NoSQL數(shù)據(jù)庫(kù)之后,我們就要開始嘗試著使用Cassandra了。鑒于我個(gè)人在使用一系列NoSQL數(shù)據(jù)庫(kù)時(shí)常常遇到它們的版本更新缺乏API后向兼容性這一情況,我在這里直接使用了Datastax Java Driver的樣例。這樣讀者也能從該頁(yè)面中查閱針對(duì)最新版本客戶端的示例代碼。
一段最簡(jiǎn)單的讀取一條記錄的Java代碼如下所示:
Cluster cluster = null; try { // 創(chuàng)建連接到Cassandra的客戶端 cluster = Cluster.builder() .addContactPoint("127.0.0.1") .build(); // 創(chuàng)建用戶會(huì)話 Session session = cluster.connect(); // 執(zhí)行CQL語(yǔ)句 ResultSet rs = session.execute("select release_version from system.local"); // 從返回結(jié)果中取出第一條結(jié)果 Row row = rs.one(); System.out.println(row.getString("release_version")); } finally { // 調(diào)用cluster變量的close()函數(shù)并關(guān)閉所有與之關(guān)聯(lián)的鏈接 if (cluster != null) { cluster.close(); } }
看起來(lái)很簡(jiǎn)單,是么?其實(shí)在客戶端的幫助下,操作Cassandra實(shí)際上并不是非常困難的一件事。反過(guò)來(lái),如何為Cassandra所記錄的數(shù)據(jù)設(shè)計(jì)模型才是最需要讀者仔細(xì)考慮的。與大家所最為熟悉的關(guān)系型數(shù)據(jù)庫(kù)建模方式不同,Cassandra中的數(shù)據(jù)模型設(shè)計(jì)需要是Join-less的。簡(jiǎn)單地說(shuō),那就是由于這些數(shù)據(jù)分布在Cassandra的不同結(jié)點(diǎn)上,因此這些數(shù)據(jù)的Join操作并不能被高效地執(zhí)行。
那么我們應(yīng)該如何為這些數(shù)據(jù)定義模型呢?首先我們要了解Cassandra所支持的基本數(shù)據(jù)模型。這些基本數(shù)據(jù)模型有:Column,Super Column,Column Family以及Keyspace。下面我們就對(duì)它們進(jìn)行簡(jiǎn)單地介紹。
Column是Cassandra所支持的最基礎(chǔ)的數(shù)據(jù)模型。該模型中可以包含一系列鍵值對(duì):
1 { 2 "name": "Auther Name", 3 "value": "Sam", 4 "timestamp": 123456789 5 }
Super Column則包含了一系列Column。在一個(gè)Super Column中的屬性可以是一個(gè)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文檔已經(jīng)不再建議過(guò)多的使用Super Column,而原因卻沒(méi)有直接說(shuō)明。據(jù)說(shuō)這和Super Column常常需要在數(shù)據(jù)訪問(wèn)時(shí)執(zhí)行反序列化相關(guān)。一個(gè)最為常見的證據(jù)就是,網(wǎng)絡(luò)上常常會(huì)有一些開發(fā)人員在Super Column中添加了過(guò)多的數(shù)據(jù),并進(jìn)而導(dǎo)致和這些Super Column相關(guān)的請(qǐng)求運(yùn)行緩慢。當(dāng)然這只是猜測(cè)。只不過(guò)既然官方文檔都已經(jīng)開始對(duì)Super Column持謹(jǐn)慎意見,那么我們也需要在日常使用過(guò)程中盡量避免使用Super Column。
而一個(gè)Column Family則是一系列Column的集合。在該集合中,每個(gè)Column都會(huì)有一個(gè)與之相關(guān)聯(lián)的鍵:
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(請(qǐng)謹(jǐn)慎使用)。
最后,Keyspace則是一系列Column Family的集合。
發(fā)現(xiàn)了么?上面沒(méi)有任何一種方法能夠通過(guò)一個(gè)Column(Super Column)引用另一個(gè)Column(Super Column),而只能通過(guò)Super Column包含其它Column的方式來(lái)完成這種信息的包含。這與我們?cè)陉P(guān)系數(shù)據(jù)庫(kù)設(shè)計(jì)過(guò)程中通過(guò)外鍵與其它記錄相關(guān)聯(lián)的使用方法非常不同。還記得之前我們通過(guò)外鍵來(lái)創(chuàng)建數(shù)據(jù)關(guān)聯(lián)這一方法的名稱么?對(duì)的,Normalization。該方法可以通過(guò)外鍵所指示的關(guān)聯(lián)關(guān)系有效地消除在關(guān)系型數(shù)據(jù)庫(kù)中的冗余數(shù)據(jù)。而在Cassandra中,我們要使用的方法就是Denormalization,也即是允許可以接受的一定程度的數(shù)據(jù)冗余。也就是說(shuō),這些關(guān)聯(lián)的數(shù)據(jù)將直接記錄在當(dāng)前數(shù)據(jù)類型之中。
在使用Cassandra時(shí),哪些不該抽象為Cassandra數(shù)據(jù)模型,而哪些數(shù)據(jù)應(yīng)該有一個(gè)獨(dú)立的抽象呢?這一切決定于我們的應(yīng)用所常常執(zhí)行的讀取及寫入請(qǐng)求。想想我們?yōu)槭裁词褂肅assandra,或者說(shuō)Cassandra相較于關(guān)系型數(shù)據(jù)庫(kù)的優(yōu)勢(shì):快速地執(zhí)行在海量數(shù)據(jù)上的讀取或?qū)懭胝?qǐng)求。如果我們僅僅根據(jù)所操作的事物抽象數(shù)據(jù)模型,而不去理會(huì)Cassandra在這些模型之上的執(zhí)行效率,甚至導(dǎo)致這些數(shù)據(jù)模型無(wú)法支持相應(yīng)的業(yè)務(wù)邏輯,那么我們對(duì)Cassandra的使用也就沒(méi)有實(shí)際的意義了。因此一個(gè)較為正確的做法就是:首先根據(jù)應(yīng)用的需求來(lái)定義一個(gè)抽象概念,并開始針對(duì)該抽象概念以及應(yīng)用的業(yè)務(wù)邏輯設(shè)計(jì)在該抽象概念上運(yùn)行的請(qǐng)求。接下來(lái),軟件開發(fā)人員就可以根據(jù)這些請(qǐng)求來(lái)決定如何為這些抽象概念設(shè)計(jì)模型了。
在抽象設(shè)計(jì)模型時(shí),我們常常需要面對(duì)另外一個(gè)問(wèn)題,那就是如何指定各Column Family所使用的各種鍵。在Cassandra相關(guān)的各類文檔中,我們常常會(huì)遇到以下一系列關(guān)鍵的名詞:Partition Key,Clustering Key,Primary Key以及Composite Key。那么它們指的都是什么呢?
Primary Key實(shí)際上是一個(gè)非常通用的概念。在Cassandra中,其表示用來(lái)從Cassandra中取得數(shù)據(jù)的一個(gè)或多個(gè)列:
1 create table sample ( 2 key text PRIMARY KEY, 3 data text 4 );
在上面的示例中,我們指定了key域作為sample的PRIMARY KEY。而在需要的情況下,一個(gè)Primary Key也可以由多個(gè)列共同組成:
1 create table sample { 2 key_one text, 3 key_two text, 4 data text, 5 PRIMARY KEY(key_one, key_two) 6 };
在上面的示例中,我們所創(chuàng)建的Primary Key就是一個(gè)由兩個(gè)列key_one和key_two組成的Composite Key。其中該Composite Key的第一個(gè)組成被稱為是Partition Key,而后面的各組成則被稱為是Clustering Key。Partition Key用來(lái)決定Cassandra會(huì)使用集群中的哪個(gè)結(jié)點(diǎn)來(lái)記錄該數(shù)據(jù),每個(gè)Partition Key對(duì)應(yīng)著一個(gè)特定的Partition。而Clustering Key則用來(lái)在Partition內(nèi)部排序。如果一個(gè)Primary Key只包含一個(gè)域,那么其將只擁有Partition Key而沒(méi)有Clustering Key。
Partition Key和Clustering Key同樣也可以由多個(gè)列組成:
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 };
而在一個(gè)CQL語(yǔ)句中,WHERE等子句所標(biāo)示的條件只能使用在Primary Key中所使用的列。您需要根據(jù)您的數(shù)據(jù)分布決定到底哪些應(yīng)該是Partition Key,哪些應(yīng)該作為Clustering Key,以對(duì)其中的數(shù)據(jù)進(jìn)行排序。
一個(gè)好的Partition Key設(shè)計(jì)常常會(huì)大幅提高程序的運(yùn)行性能。首先,由于Partition Key用來(lái)控制哪個(gè)結(jié)點(diǎn)記錄數(shù)據(jù),因此Partition Key可以決定是否數(shù)據(jù)能夠較為均勻地分布在Cassandra的各個(gè)結(jié)點(diǎn)上,以充分利用這些結(jié)點(diǎn)。同時(shí)在Partition Key的幫助下,您的讀請(qǐng)求應(yīng)盡量使用較少數(shù)量的結(jié)點(diǎn)。這是因?yàn)樵趫?zhí)行讀請(qǐng)求時(shí),Cassandra需要協(xié)調(diào)處理從各個(gè)結(jié)點(diǎn)中所得到的數(shù)據(jù)集。因此在響應(yīng)一個(gè)讀操作時(shí),較少的結(jié)點(diǎn)能夠提供較高的性能。因此在模型設(shè)計(jì)中,如何根據(jù)所需要運(yùn)行的各個(gè)請(qǐng)求指定模型的Partition Key是整個(gè)設(shè)計(jì)過(guò)程中的一個(gè)關(guān)鍵。一個(gè)取值均勻分布的,卻常常在請(qǐng)求中作為輸入條件的域,常常是一個(gè)可以考慮的Partition Key。
除此之外,我們也應(yīng)該好好地考慮如何設(shè)置模型的Clustering Key。由于Clustering Key可以用來(lái)在Partition內(nèi)部排序,因此其對(duì)于包含范圍篩選的各種請(qǐng)求的支持較好。
Cassandra內(nèi)部機(jī)制
在本節(jié)中,我們將對(duì)Cassandra的一系列內(nèi)部機(jī)制進(jìn)行簡(jiǎn)單地介紹。這些內(nèi)部機(jī)制很多都是業(yè)界所常用的解決方案。因此在了解了Cassandra是如何使用它們的之后,您就可以非常容易地理解其它類庫(kù)對(duì)這些機(jī)制的使用,甚至在您自己的項(xiàng)目中借鑒及使用它們。
這些常見的內(nèi)部機(jī)制有:Log-Structured Merge-Tree,Consistent Hash,Virtual Node等。
Log-Structured Merge-Tree
最有意思的一個(gè)數(shù)據(jù)結(jié)構(gòu)莫過(guò)于Log-Structured Merge-Tree。Cassandra內(nèi)部使用類似的結(jié)構(gòu)來(lái)提高服務(wù)實(shí)例的運(yùn)行效率。那它是如何工作的呢?
簡(jiǎn)單地說(shuō),一個(gè)Log-Structured Merge-Tree主要由兩個(gè)樹形結(jié)構(gòu)的數(shù)據(jù)組成:存在于內(nèi)存中的C0,以及主要存在于磁盤中的C1:

在添加一個(gè)新的結(jié)點(diǎn)時(shí),Log-Structured Merge-Tree會(huì)首先在日志文件中添加一條有關(guān)該結(jié)點(diǎn)插入的記錄,然后再將該結(jié)點(diǎn)插入到樹C0中。添加到日志文件中的記錄主要是基于數(shù)據(jù)恢復(fù)的考慮。畢竟C0樹處于內(nèi)存中,非常容易受到系統(tǒng)宕機(jī)等因素的影響。而在讀取數(shù)據(jù)時(shí),Log-Structured Merge-Tree會(huì)首先嘗試從C0樹中查找數(shù)據(jù),然后再在C1樹中查找。
在C0樹滿足一定條件之后,如其所占用的內(nèi)存過(guò)大,那么它所包含的數(shù)據(jù)將被遷移到C1中。在Log-Structured Merge-Tree這個(gè)數(shù)據(jù)結(jié)構(gòu)中,該操作被稱為是rolling merge。其會(huì)把C0樹中的一系列記錄歸并到C1樹中。歸并的結(jié)果將會(huì)寫入到新的連續(xù)的磁盤空間。

幾乎是論文中的原圖
就單個(gè)樹來(lái)看,C1和我們所熟悉的B樹或者B+樹有點(diǎn)像,是不?
不知道您注意到?jīng)]有。上面的介紹突出了一個(gè)詞:連續(xù)的。這是因?yàn)镃1樹中同一層次的各個(gè)結(jié)點(diǎn)在磁盤中是連續(xù)記錄的。這樣磁盤就可以通過(guò)連續(xù)讀取來(lái)避免在磁盤上的過(guò)多尋道,從而大大地提高了運(yùn)行效率。
Memtable和SSTable
好,剛剛我們已經(jīng)提到了Cassandra內(nèi)部使用和Log-Structured Merge-Tree類似的數(shù)據(jù)結(jié)構(gòu)。那么在本節(jié)中,我們就將對(duì)Cassandra的一些主要數(shù)據(jù)結(jié)構(gòu)及操作流程進(jìn)行介紹。可以說(shuō),如果您大致理解了上一節(jié)對(duì)Log-Structured Merge-Tree的講解,那么理解這些數(shù)據(jù)結(jié)構(gòu)也將是非常容易的事情。
在Cassandra中有三個(gè)非常重要的數(shù)據(jù)結(jié)構(gòu):記錄在內(nèi)存中的Memtable,以及保存在磁盤中的Commit Log和SSTable。Memtable在內(nèi)存中記錄著最近所做的修改,而SSTable則在磁盤上記錄著Cassandra所承載的絕大部分?jǐn)?shù)據(jù)。在SSTable內(nèi)部記錄著一系列根據(jù)鍵排列的一系列鍵值對(duì)。通常情況下,一個(gè)Cassandra表會(huì)對(duì)應(yīng)著一個(gè)Memtable和多個(gè)SSTable。除此之外,為了提高對(duì)數(shù)據(jù)進(jìn)行搜索和訪問(wèn)的速度,Cassandra還允許軟件開發(fā)人員在特定的列上創(chuàng)建索引。
鑒于數(shù)據(jù)可能存儲(chǔ)于Memtable,也可能已經(jīng)被持久化到SSTable中,因此Cassandra在讀取數(shù)據(jù)時(shí)需要合并從Memtable和SSTable所取得的數(shù)據(jù)。同時(shí)為了提高運(yùn)行速度,減少不必要的對(duì)SSTable的訪問(wèn),Cassandra提供了一種被稱為是Bloom Filter的組成:每個(gè)SSTable都有一個(gè)Bloom Filter,以用來(lái)判斷與其關(guān)聯(lián)的SSTable是否包含當(dāng)前查詢所請(qǐng)求的一條或多條數(shù)據(jù)。如果是,Cassandra將嘗試從該SSTable中取出數(shù)據(jù);如果不是,Cassandra則會(huì)忽略該SSTable,以減少不必要的磁盤訪問(wèn)。
在經(jīng)由Bloom Filter判斷出與其關(guān)聯(lián)的SSTable包含了請(qǐng)求所需要的數(shù)據(jù)之后,Cassandra就會(huì)開始嘗試從該SSTable中取出數(shù)據(jù)了。首先,Cassandra會(huì)檢查Partition Key Cache是否緩存了所要求數(shù)據(jù)的索引項(xiàng)Index Entry。如果存在,那么Cassandra會(huì)直接從Compression Offset Map中查詢?cè)摂?shù)據(jù)所在的地址,并從該地址取回所需要的數(shù)據(jù);如果Partition Key Cache并沒(méi)有緩存該Index Entry,那么Cassandra首先會(huì)從Partition Summary中找到Index Entry所在的大致位置,并進(jìn)而從該位置開始搜索Partition Index,以找到該數(shù)據(jù)的Index Entry。在找到Index Entry之后,Cassandra就可以從Compression Offset Map找到相應(yīng)的條目,并根據(jù)條目中所記錄的數(shù)據(jù)的位移取得所需要的數(shù)據(jù):

較文檔中原圖略作調(diào)整
發(fā)現(xiàn)了么?實(shí)際上SSTable中所記錄的數(shù)據(jù)仍然是順序記錄的各個(gè)域,但是不同的是,它的查找首先經(jīng)由了Partition Key Cache以及Compression Offset Map等一系列組成。這些組成僅僅包含了一系列對(duì)應(yīng)關(guān)系,也就是相當(dāng)于連續(xù)地記錄了請(qǐng)求所需要的數(shù)據(jù),進(jìn)而提高了數(shù)據(jù)搜索的運(yùn)行速度,不是么?
Cassandra的寫入流程也與Log-Structured Merge-Tree的寫入流程非常類似:Log-Structured Merge-Tree中的日志對(duì)應(yīng)著Commit Log,C0樹對(duì)應(yīng)著Memtable,而C1樹則對(duì)應(yīng)著SSTable的集合。在寫入時(shí),Cassandra會(huì)首先將數(shù)據(jù)寫入到Memtable中,同時(shí)在Commit Log的末尾添加該寫入所對(duì)應(yīng)的記錄。這樣在機(jī)器斷電等異常情況下,Cassandra仍能通過(guò)Commit Log來(lái)恢復(fù)Memtable中的數(shù)據(jù)。
在持續(xù)地寫入數(shù)據(jù)后,Memtable的大小將逐漸增長(zhǎng)。在其大小到達(dá)某個(gè)閾值時(shí),Cassandra的數(shù)據(jù)遷移流程就將被觸發(fā)。該流程一方面會(huì)將Memtable中的數(shù)據(jù)添加到相應(yīng)的SSTable的末尾,另一方面則會(huì)將Commit Log中的寫入記錄移除。
這也就會(huì)造成一個(gè)容易讓讀者困惑的問(wèn)題:如果是將新的數(shù)據(jù)寫入到SSTable的末尾,那么數(shù)據(jù)遷移的過(guò)程該如何執(zhí)行對(duì)數(shù)據(jù)的更新?答案就是:在需要對(duì)數(shù)據(jù)進(jìn)行更新時(shí),Cassandra會(huì)在SSTable的末尾添加一條具有當(dāng)前時(shí)間戳的記錄,以使得其能夠標(biāo)明自身為最新的記錄。而原有的在SSTable中的記錄隨即宣告失效。
這會(huì)導(dǎo)致一個(gè)問(wèn)題,那就是對(duì)數(shù)據(jù)的大量更新會(huì)導(dǎo)致SSTable所占用的磁盤空間迅速增長(zhǎng),而且其中所記錄的數(shù)據(jù)很多都已經(jīng)是過(guò)期數(shù)據(jù)。因此在一段時(shí)間之后,磁盤的空間利用率會(huì)大幅下降。此時(shí)我們就需要通過(guò)壓縮SSTable的方式釋放這些過(guò)期數(shù)據(jù)所占用的空間:

現(xiàn)在有一個(gè)問(wèn)題,那就是我們可以根據(jù)重復(fù)數(shù)據(jù)的時(shí)間戳來(lái)判斷哪條是最新的數(shù)據(jù),但是我們應(yīng)該如何處理數(shù)據(jù)的刪除呢?在Cassandra中,對(duì)數(shù)據(jù)的刪除是通過(guò)一個(gè)被稱為tombstone的組成來(lái)完成的。如果一條數(shù)據(jù)被添加了一個(gè)tombstone,那么其在下次壓縮時(shí)就被認(rèn)為是一條已經(jīng)被刪除的數(shù)據(jù),從而不會(huì)添加到壓縮后的SSTable中。
在壓縮過(guò)程中,原有的SSTable和新的SSTable同時(shí)存在于磁盤上。這些原有的SSTable用來(lái)完成對(duì)數(shù)據(jù)讀取的支持。一旦新的SSTable創(chuàng)建完畢,那么老的SSTable就將被刪除。
在這里我們要提幾點(diǎn)在日常使用Cassandra的過(guò)程中需要注意的問(wèn)題。首先是,由于通過(guò)Commit Log來(lái)重建Memtable是一個(gè)較為耗時(shí)的過(guò)程,因此我們?cè)谛枰亟∕emtable的一系列操作前需要嘗試手動(dòng)觸發(fā)歸并邏輯,以將該結(jié)點(diǎn)上Memtable中的數(shù)據(jù)持久化到SSTable中。最常見的一種需要重建Memtable的操作就是重新啟動(dòng)Cassandra所在的結(jié)點(diǎn)。
另一個(gè)需要注意的地方是,不要過(guò)度地使用索引。雖然說(shuō)索引可以大幅地增加數(shù)據(jù)的讀取速度,但是我們同樣需要在數(shù)據(jù)寫入時(shí)對(duì)其進(jìn)行維護(hù),造成一定的性能損耗。在這點(diǎn)上,Cassandra和傳統(tǒng)的關(guān)系型數(shù)據(jù)庫(kù)沒(méi)有太大區(qū)別。
Cassandra集群
當(dāng)然,使用單一的數(shù)據(jù)庫(kù)實(shí)例來(lái)運(yùn)行Cassandra并不是一個(gè)好的選擇。單一的服務(wù)器可能導(dǎo)致服務(wù)集群產(chǎn)生單點(diǎn)失效的問(wèn)題,也無(wú)法充分利用Cassandra的橫向擴(kuò)展能力。因此從本節(jié)開始,我們就將對(duì)Cassandra集群以及集群中所使用的各種機(jī)制進(jìn)行簡(jiǎn)單地講解。
在一個(gè)Cassandra集群中常常包含著以下一系列組成:結(jié)點(diǎn)(Node),數(shù)據(jù)中心(Data Center)以及集群(Cluster)。結(jié)點(diǎn)是Cassandra集群中用來(lái)存儲(chǔ)數(shù)據(jù)的最基礎(chǔ)結(jié)構(gòu);數(shù)據(jù)中心則是處于同一地理區(qū)域的一系列結(jié)點(diǎn)的集合;而集群則常常由多個(gè)處于不同區(qū)域的數(shù)據(jù)中心所組成:

上圖所展示的Cassandra集群由三個(gè)數(shù)據(jù)中心組成。這三個(gè)數(shù)據(jù)中心中的兩個(gè)處于同一區(qū)域內(nèi),而另一個(gè)數(shù)據(jù)中心則處于另一個(gè)區(qū)域中??梢哉f(shuō),兩個(gè)數(shù)據(jù)中心處于同一區(qū)域的情況并不多見,但是Cassandra的官方文檔也沒(méi)有否定這種集群搭建方式。每個(gè)數(shù)據(jù)中心則包含了一系列結(jié)點(diǎn),以用來(lái)存儲(chǔ)Cassandra集群所要承載的數(shù)據(jù)。
有了集群,我們就需要使用一系列機(jī)制來(lái)完成集群之間的相互協(xié)作,并考慮集群所需要的一系列非功能性需求了:結(jié)點(diǎn)的狀態(tài)維護(hù),數(shù)據(jù)分發(fā),擴(kuò)展性(Scalability),高可用性,災(zāi)難恢復(fù)等。
對(duì)結(jié)點(diǎn)的狀態(tài)進(jìn)行探測(cè)是高可用性的第一步,也是在結(jié)點(diǎn)間分發(fā)數(shù)據(jù)的基礎(chǔ)。Cassandra使用了一種被稱為是Gossip的點(diǎn)對(duì)點(diǎn)通訊方案,以在Cassandra集群中的各個(gè)結(jié)點(diǎn)之間共享及傳遞各個(gè)結(jié)點(diǎn)的狀態(tài)。只有這樣,Cassandra才能知道到底哪些結(jié)點(diǎn)可以有效地保存數(shù)據(jù),進(jìn)而將對(duì)數(shù)據(jù)的操作分發(fā)給各結(jié)點(diǎn)。
在保存數(shù)據(jù)的過(guò)程中,Cassandra會(huì)使用一個(gè)被稱為是Partitioner的組成來(lái)決定數(shù)據(jù)到底要分發(fā)到哪些結(jié)點(diǎn)上。而另一個(gè)和數(shù)據(jù)存儲(chǔ)相關(guān)的組成就是Snitch。其會(huì)提供根據(jù)集群中所有結(jié)點(diǎn)的性能來(lái)決定如何對(duì)數(shù)據(jù)進(jìn)行讀寫。
這些組成內(nèi)部也使用了一系列業(yè)界所常用的方法。例如Cassandra內(nèi)部通過(guò)VNode來(lái)處理各硬件的性能不同,從而在物理硬件層次上形成一種類似《企業(yè)級(jí)負(fù)載平衡簡(jiǎn)介》一文所中提到過(guò)的Weighted Round Robin的解決方案。再比如其內(nèi)部使用了Consistent Hash,我們也在《Memcached簡(jiǎn)介》一文中給出過(guò)介紹。
好了,簡(jiǎn)介完成。在下面幾節(jié)中,我們就將對(duì)Cassandra所使用的這些機(jī)制進(jìn)行介紹。
Gossip
首先就是Gossip。其是用來(lái)在Cassandra集群中的各個(gè)結(jié)點(diǎn)之間傳輸結(jié)點(diǎn)狀態(tài)的協(xié)議。它每秒都將運(yùn)行一次,并將當(dāng)前Cassandra結(jié)點(diǎn)的狀態(tài)以及其所知的其它結(jié)點(diǎn)的狀態(tài)與至多三個(gè)其它結(jié)點(diǎn)交換。通過(guò)這種方法,Cassandra的有效結(jié)點(diǎn)能很快地了解當(dāng)前集群中其它結(jié)點(diǎn)的狀態(tài)。同時(shí)這些狀態(tài)信息還包含一個(gè)時(shí)間戳,以允許Gossip判斷到底哪個(gè)狀態(tài)是更新的狀態(tài)。
除了在集群中的各個(gè)結(jié)點(diǎn)之間交換各結(jié)點(diǎn)的狀態(tài)之外,Gossip還需要能夠應(yīng)對(duì)對(duì)集群進(jìn)行操作的一系列動(dòng)作。這些操作包括結(jié)點(diǎn)的添加,移除,重新加入等。為了能夠更好地處理這些情況,Gossip提出了一個(gè)叫做Seed Node的概念。其用來(lái)為各個(gè)新加入的結(jié)點(diǎn)提供一個(gè)啟動(dòng)Gossip交換的入口。在加入到Cassandra集群之后,新結(jié)點(diǎn)就可以首先嘗試著跟其所記錄的一系列Seed Node交換狀態(tài)。這一方面可以得到Cassandra集群中其它結(jié)點(diǎn)的信息,進(jìn)而允許其與這些結(jié)點(diǎn)進(jìn)行通訊,又可以將自己加入的信息通過(guò)這些Seed Node傳遞出去。由于一個(gè)結(jié)點(diǎn)所得到的結(jié)點(diǎn)狀態(tài)信息常常被記錄在磁盤等持久化組成中,因此在重新啟動(dòng)之后,其仍然可以通過(guò)這些持久化后的結(jié)點(diǎn)信息進(jìn)行通訊,以重新加入Gossip交換。而在一個(gè)結(jié)點(diǎn)失效的情況下,其它結(jié)點(diǎn)將會(huì)定時(shí)地向該結(jié)點(diǎn)發(fā)送探測(cè)消息,以嘗試與其恢復(fù)連接。但是這會(huì)為我們永久地移除一個(gè)結(jié)點(diǎn)帶來(lái)麻煩:其它Cassandra結(jié)點(diǎn)總覺(jué)得該結(jié)點(diǎn)將在某一時(shí)刻重新加入集群,因此一直向該結(jié)點(diǎn)發(fā)送探測(cè)信息。此時(shí)我們就需要使用Cassandra所提供的結(jié)點(diǎn)工具了。
那么Gossip是如何判斷是否某個(gè)結(jié)點(diǎn)失效了呢?如果在交換過(guò)程中,參與交換的另一方很久不回答,那么當(dāng)前結(jié)點(diǎn)就會(huì)將目標(biāo)結(jié)點(diǎn)標(biāo)示為失效,并進(jìn)而通過(guò)Gossip協(xié)議將該狀態(tài)傳遞出去。由于Cassandra集群的拓?fù)浣Y(jié)構(gòu)可能非常復(fù)雜,如跨區(qū)域等,因此其用來(lái)判斷一個(gè)結(jié)點(diǎn)是否失效的標(biāo)準(zhǔn)并不是在多長(zhǎng)時(shí)間之內(nèi)沒(méi)有響應(yīng)就判定為失效。畢竟這會(huì)導(dǎo)致很大的問(wèn)題:兩個(gè)在同一個(gè)Lab中的結(jié)點(diǎn)進(jìn)行狀態(tài)交換會(huì)非???,而跨區(qū)域的交換則會(huì)比較慢。如果我們?cè)O(shè)置的時(shí)間較短,那么跨區(qū)域的狀態(tài)交換常常會(huì)被誤報(bào)為失效;如果我們所設(shè)置的時(shí)間較長(zhǎng),那么Gossip對(duì)結(jié)點(diǎn)失效的探測(cè)靈敏度將降低。為了避免這種情況,Gossip使用的是一種根據(jù)以往結(jié)點(diǎn)間交換歷史等眾多因素綜合起來(lái)的決策邏輯。這樣對(duì)于兩個(gè)距離較遠(yuǎn)的結(jié)點(diǎn),其將擁有較大的時(shí)間窗,從而不會(huì)產(chǎn)生誤報(bào)。而對(duì)于兩個(gè)距離較近的結(jié)點(diǎn),Gossip將使用較小的時(shí)間窗,從而提高探測(cè)的靈敏度。
Consistent Hash
接下來(lái)我們要講的是Consistent Hash。在通常的哈希算法中常常包含著桶這個(gè)概念。每次哈希計(jì)算都是在決定特定數(shù)據(jù)需要存儲(chǔ)在哪個(gè)桶中。而如果桶的數(shù)量發(fā)生了變化,那么之前的哈希計(jì)算結(jié)果都將失效。而Consistent Hash則很好地解決了該問(wèn)題。
那Consistent Hash是如何工作的呢?首先請(qǐng)考慮一個(gè)圓,在該圓上分布了多個(gè)點(diǎn),以表示整數(shù)0到1023。這些整數(shù)平均分布在整個(gè)圓上:

在上圖中,我們突出地顯示了將圓六等分的六個(gè)藍(lán)點(diǎn),表示用來(lái)記錄數(shù)據(jù)的六個(gè)結(jié)點(diǎn)。這六個(gè)結(jié)點(diǎn)將各自負(fù)責(zé)一個(gè)范圍。例如512這個(gè)藍(lán)點(diǎn)所對(duì)應(yīng)的結(jié)點(diǎn)就將記錄從哈希值為512到681這個(gè)區(qū)間的數(shù)據(jù)。在Cassandra以及其它的一些領(lǐng)域中,這個(gè)圓被稱為是一個(gè)Ring。接下來(lái)我們就對(duì)當(dāng)前需要存儲(chǔ)的數(shù)據(jù)執(zhí)行哈希計(jì)算,并得到該數(shù)據(jù)所對(duì)應(yīng)的哈希值。例如一段數(shù)據(jù)的哈希值為900,那么它就位于853和1024之間:

因此該數(shù)據(jù)將被藍(lán)點(diǎn)853所對(duì)應(yīng)的結(jié)點(diǎn)記錄。這樣一旦其它結(jié)點(diǎn)失效,該數(shù)據(jù)所在的結(jié)點(diǎn)也不會(huì)發(fā)生變化:

那每段數(shù)據(jù)的哈希值到底是如何計(jì)算出來(lái)的呢?答案是Partitioner。其輸入為數(shù)據(jù)的Partition Key。而其計(jì)算結(jié)果在Ring上的位置就決定了到底是由哪些結(jié)點(diǎn)來(lái)完成對(duì)數(shù)據(jù)的保存。
Virtual Node
上面我們介紹了Consistent Hash的運(yùn)行原理。但是這里還有一個(gè)問(wèn)題,那就是失效的那個(gè)結(jié)點(diǎn)上的數(shù)據(jù)該怎么辦?我們就無(wú)法訪問(wèn)了么?這取決于我們對(duì)Cassandra集群數(shù)據(jù)復(fù)制方面的設(shè)置。通常情況下,我們都會(huì)啟用該功能,從而使得多個(gè)結(jié)點(diǎn)同時(shí)記錄一份數(shù)據(jù)的拷貝。那么在其中一個(gè)結(jié)點(diǎn)失效的情況下,其它結(jié)點(diǎn)仍然可以用來(lái)讀取該數(shù)據(jù)。
這里要處理的一個(gè)情況就是,各個(gè)物理結(jié)點(diǎn)所具有的容量并不相同。簡(jiǎn)單地說(shuō),如果一個(gè)結(jié)點(diǎn)所能提供的服務(wù)能力遠(yuǎn)小于其它結(jié)點(diǎn),那么為其分配相同的負(fù)載將使得它不堪重負(fù)。為了處理這種情況,Cassandra提供了一種被稱為VNode的解決方案。在該解決方案中,每個(gè)物理結(jié)點(diǎn)將根據(jù)其實(shí)際容量被劃分為一系列具有相同容量的VNode。每個(gè)VNode則用來(lái)負(fù)責(zé)Ring上的一段數(shù)據(jù)。例如對(duì)于剛剛所展示的具有六個(gè)結(jié)點(diǎn)的Ring,各個(gè)VNode和物理機(jī)之間的關(guān)系則可能如下所示:

在使用VNode時(shí),我們常常需要注意的一點(diǎn)就是Replication Factor的設(shè)置。從其所表示的意義來(lái)講,Cassandra中的Replication Factor和其它常見數(shù)據(jù)庫(kù)中所使用的Replication Factor沒(méi)有什么不同:其所具有的數(shù)值用來(lái)表示記錄在Cassandra中的數(shù)據(jù)有多少份拷貝。例如在其被設(shè)置為1的情況下,Cassandra將只會(huì)保存一份數(shù)據(jù)。如果其被設(shè)置為2,那么Cassandra將多保存一份這些數(shù)據(jù)的拷貝。
在決定Cassandra集群所需要使用的Replication Factor時(shí),我們需要考慮以下一系列因素:
- 物理機(jī)的數(shù)量。試想一下,如果我們將Replication Factor設(shè)置為超過(guò)物理機(jī)的數(shù)量,那么必然會(huì)有物理機(jī)保存了同一份數(shù)據(jù)的兩部分拷貝。這實(shí)際上沒(méi)有太大的作用:一旦該物理機(jī)出現(xiàn)異常,那就會(huì)一次損失多份數(shù)據(jù)。因此就高可用性這一點(diǎn)來(lái)說(shuō),Replication Factor的數(shù)值超過(guò)物理機(jī)的數(shù)量時(shí),多出的這些數(shù)據(jù)拷貝意義并不大。
- 物理機(jī)的異構(gòu)性。物理機(jī)的異構(gòu)性常常也會(huì)影響您所設(shè)Replication Factor的效果。舉一個(gè)極端的例子。如果說(shuō)我們有一個(gè)Cassandra集群而且其由五臺(tái)物理機(jī)組成。其中一臺(tái)物理機(jī)的容量是其它物理機(jī)的4倍。那么將Replication Factor設(shè)置為3時(shí)將會(huì)出現(xiàn)具有較大容量的物理機(jī)上存儲(chǔ)了同樣的數(shù)據(jù)這種問(wèn)題。其并不比設(shè)置為2好多少。
因此在決定一個(gè)Cassandra集群的Replication Factor時(shí),我們要仔細(xì)地根據(jù)集群中物理機(jī)的數(shù)量和容量設(shè)置一個(gè)合適的數(shù)值。否則其只會(huì)導(dǎo)致更多的無(wú)用的數(shù)據(jù)拷貝。
注:這篇文章寫于15年8月。鑒于NoSQL數(shù)據(jù)庫(kù)發(fā)展非??欤页3>哂幸幌盗杏绊懞笙蚣嫒菪缘母模ㄈ鏢pring Data Neo4J已經(jīng)不支持@Fetch)。因此如果您發(fā)現(xiàn)有什么描述已經(jīng)發(fā)生了改變,請(qǐng)幫留下評(píng)論,以便其它讀者參考。在此感激不盡
轉(zhuǎn)載請(qǐng)注明原文地址并標(biāo)明轉(zhuǎn)載:http://www.rzrgm.cn/loveis715/p/5299495.html
商業(yè)轉(zhuǎn)載請(qǐng)事先與我聯(lián)系:silverfox715@sina.com
公眾號(hào)一定幫忙別標(biāo)成原創(chuàng),因?yàn)閰f(xié)調(diào)起來(lái)太麻煩了。。。

浙公網(wǎng)安備 33010602011771號(hào)