數(shù)據(jù)的讀取
Cassandra 的寫(xiě)的性能要好于讀的性能,為何寫(xiě)的性能要比讀好很多呢?原因是,Cassandra 的設(shè)計(jì)原則就是充分讓寫(xiě)的速度更快、更方便而犧牲了讀的性能。事實(shí)也的確如此,僅僅看 Cassandra 的數(shù)據(jù)的存儲(chǔ)形式就能發(fā)現(xiàn),首先是寫(xiě)到 Memtable 中,然后將 Memtable 中數(shù)據(jù)刷到磁盤(pán)中,而且都是順序保存的不檢查數(shù)據(jù)的唯一性,而且是只寫(xiě)不刪(刪除規(guī)則在后面介紹),最后才將順序結(jié)構(gòu)的多個(gè) SSTable 文件合并。這每一步難道不是讓 Cassandra 寫(xiě)的更快。這個(gè)設(shè)計(jì)想想對(duì)讀會(huì)有什么影響。首先,數(shù)據(jù)結(jié)構(gòu)的復(fù)雜性,Memtable 中和 SSTable 中數(shù)據(jù)結(jié)構(gòu)肯定不同,但是返回給用戶的肯定是一樣的,這必然會(huì)要轉(zhuǎn)化。其次,數(shù)據(jù)在多個(gè)文件中,要找的數(shù)據(jù)可能在 Memtable 中,也可能在某個(gè) SSTable 中,如果有 10 個(gè) SSTable,那么就要在到 10 個(gè) SSTable 中每個(gè)找一遍,雖然使用了 BloomFilter 算法可以很快判斷到底哪個(gè) SSTable 中含有指定的 key。還有可能在 Memtable 到 SSTable 的轉(zhuǎn)化過(guò)程中,這也是要檢查一遍的,也就是數(shù)據(jù)有可能存在什么地方,就要到哪里去找一遍。還有找出來(lái)的數(shù)據(jù)可能是已經(jīng)被刪除的,但也沒(méi)辦法還是要取。
下面是讀取數(shù)據(jù)的相關(guān)類(lèi)圖:
圖 13. 讀取相關(guān)類(lèi)圖
根據(jù)上面的類(lèi)圖讀取的邏輯是,CassandraServer 創(chuàng)建 ReadCommand 對(duì)象,這個(gè)對(duì)象保存了用戶要獲取記錄的所有必須指定的條件。然后交給 weakReadLocalCallable 這個(gè)線程去到 ColumnFamilyStore 對(duì)象中去搜索數(shù)據(jù),包括 Memtable 和 SSTable。將找到的數(shù)據(jù)組裝成 Row 返回,這樣一個(gè)查詢(xún)過(guò)程就結(jié)束了。這個(gè)查詢(xún)邏輯可以用下面的時(shí)序圖來(lái)表示:
圖 14. 查詢(xún)數(shù)據(jù)時(shí)序圖
在上圖中還一個(gè)地方要說(shuō)明的是,取得 key 對(duì)應(yīng)的 ColumnFamily 要至少在三個(gè)地方查詢(xún),第一個(gè)就是 Memtable 中,第二個(gè)是 MemtablesPendingFlush,這個(gè)是將 Memtable 轉(zhuǎn)化為 SSTable 之前的一個(gè)臨時(shí) Memtable。第三個(gè)是 SSTable。在 SSTable 中查詢(xún)最為復(fù)雜,它首先將要查詢(xún)的 key 與每個(gè) SSTable 所對(duì)應(yīng)的 Filter 做比較,這個(gè) Filter 保存了所有這個(gè) SSTable 文件中含有的所有 key 的 Hash 值,這個(gè) Hsah 算法能快速判斷指定的 key 在不在這個(gè) SSTable 中,這個(gè) Filter 的值在全部保存在內(nèi)存中,這樣能快速判斷要查詢(xún)的 key 在那個(gè) SSTable 中。接下去就要在 SSTable 所對(duì)應(yīng)的 Index 中查詢(xún) key 所對(duì)應(yīng)的位置,從前面的 Index 文件的存儲(chǔ)結(jié)構(gòu)知道,Index 中保存了具體數(shù)據(jù)在 Data 文件中的 Offset。,拿到這個(gè) Offset 后就可以直接到 Data 文件中取出相應(yīng)的長(zhǎng)度的字節(jié)數(shù)據(jù),反序列化就可以達(dá)到目標(biāo)的 ColumnFamily。由于 Cassandra 的存儲(chǔ)方式,同一個(gè) key 所對(duì)應(yīng)的值可能存在于多個(gè) SSTable 中,所以直到查找完所有的 SSTable 文件后再與前面的兩個(gè) Memtable 查找出來(lái)的結(jié)果合并,最終才是要查詢(xún)的值。
另外,前面所描述的是最壞的情況,也就是查詢(xún)?cè)谕耆珱](méi)有緩存的情況下,當(dāng)然 Cassandra 在對(duì)查詢(xún)操作也提供了多級(jí)緩存。第一級(jí)直接針對(duì)查詢(xún)結(jié)果做緩存,這個(gè)緩存的設(shè)置的配置項(xiàng)是 Keyspace 下面的 RowsCached。查詢(xún)的時(shí)候首先會(huì)在這個(gè) Cache 中找。第二級(jí) Cache 對(duì)應(yīng) SSTable 的 Index 文件,它可以直接緩存要查詢(xún) key 所對(duì)應(yīng)的索引。這個(gè)配置項(xiàng)同樣在 Keyspace 下面的 KeysCached 中,如果這個(gè) Cache 能命中,將會(huì)省去 Index 文件的一次 IO 查詢(xún)。最后一級(jí) Cache 是做磁盤(pán)文件與內(nèi)存文件的 mmap,這種方式可以提高磁盤(pán) IO 的操作效率,鑒于索引大小的限制,如果 Data 文件太大只能在 64 位機(jī)器上使用這個(gè)技術(shù)。
數(shù)據(jù)的刪除
從前面的數(shù)據(jù)寫(xiě)入規(guī)則可以想象,Cassandra 要想刪除數(shù)據(jù)是一件麻煩的事,為何這樣說(shuō)?理由如下:
1.數(shù)據(jù)有多處 同時(shí)還可能在多個(gè)節(jié)點(diǎn)都有保存。
2.數(shù)據(jù)的結(jié)構(gòu)有多種 數(shù)據(jù)會(huì)寫(xiě)在 CommitLog 中、Memtable 中、SSTable 中,它們的數(shù)據(jù)結(jié)構(gòu)都不一樣。
4.數(shù)據(jù)時(shí)效性不一致 由于是集群,所以數(shù)據(jù)在節(jié)點(diǎn)之間傳輸必然有延時(shí)。
6.除了這三點(diǎn)之外還有其它一些難點(diǎn)如 SSTable 持久化數(shù)據(jù)是順序存儲(chǔ)的,如果刪除中間一段,那數(shù)據(jù)有如何移動(dòng),這些問(wèn)題都非常棘手,如果設(shè)計(jì)不合理,性能將會(huì)非常之差。
本部分將討論 Cassandra 是如何解決這些問(wèn)題的。
CassandraServer 中刪除數(shù)據(jù)的接口只有一個(gè) remove,下面是 remove 方法的源碼:
清單 3. CassandraServer.remove
public void remove(String table, String key, ColumnPath column_path,
long timestamp, ConsistencyLevel consistency_level){
checkLoginDone();
ThriftValidation.validateKey(key);
ThriftValidation.validateColumnPathOrParent(table, column_path);
RowMutation rm = new RowMutation(table, key);
rm.delete(new QueryPath(column_path), timestamp);
doInsert(consistency_level, rm);
}
仔細(xì)和 insert 方法比較,發(fā)現(xiàn)只有一行不同:insert 方法調(diào)用的是 rm.add 而這里是 rm.delete。那么這個(gè) rm.delete 又做了什么事情呢?下面是 delete 方法的源碼:
清單 4. RowMutation. Delete
public void delete(QueryPath path, long timestamp){
....
if (columnFamily == null)
columnFamily = ColumnFamily.create(table_, cfName);
if (path.superColumnName == null && path.columnName == null){
columnFamily.delete(localDeleteTime, timestamp);
}else if (path.columnName == null){
SuperColumn sc = new SuperColumn(path.superColumnName,
DatabaseDescriptor.getSubComparator(table_, cfName));
sc.markForDeleteAt(localDeleteTime, timestamp);
columnFamily.addColumn(sc);
}else{
ByteBuffer bytes = ByteBuffer.allocate(4);
bytes.putInt(localDeleteTime);
columnFamily.addColumn(path, bytes.array(), timestamp, true);
}
}
這段代碼的主要邏輯就是,如果是刪除指定 Key 下的某個(gè) Column,那么將這個(gè) Key 所對(duì)應(yīng)的 Column 的 vlaue 設(shè)置為當(dāng)前系統(tǒng)時(shí)間,并將 Column 的 isMarkedForDelete 屬性設(shè)置為 TRUE,如果是要?jiǎng)h除這個(gè) Key 下的所有 Column 則設(shè)置這個(gè) ColumnFamily 的刪除時(shí)間期限屬性。然后將這個(gè)新增的一條數(shù)據(jù)按照 Insert 方法執(zhí)行下去。
這個(gè)思路現(xiàn)在已經(jīng)很明顯了,它就是通過(guò)設(shè)置同一個(gè) Key 下對(duì)應(yīng)不同的數(shù)據(jù)來(lái)更新已經(jīng)在 ConcurrentSkipListMap 集合中存在的數(shù)據(jù)。這種方法的確很好,它能夠達(dá)到如下目的:
1.簡(jiǎn)化了數(shù)據(jù)的操作邏輯。將添加、修改和刪除邏輯都統(tǒng)一起來(lái)。
2.解決了前面提到的三個(gè)難點(diǎn)。因?yàn)樗褪前凑諗?shù)據(jù)產(chǎn)生的方式,來(lái)修改數(shù)據(jù)。有點(diǎn)以其人之道還治其人之身的意思。
4.但是這仍然有兩個(gè)問(wèn)題:這個(gè)只是修改了指定的數(shù)據(jù),它并沒(méi)有刪除這條數(shù)據(jù);還有就是 SSTable 是根據(jù) Memtable 中的數(shù)據(jù)保存的,很可能會(huì)出現(xiàn)不同的 SSTable 中保存相同的數(shù)據(jù),這個(gè)又怎么解決?的確如此,Cassandra 并沒(méi)有刪除你要?jiǎng)h除的數(shù)據(jù),Cassandra 只是在你查詢(xún)數(shù)據(jù)返回之前,過(guò)濾掉 isMarkedForDelete 為 TRUE 的記錄。它能夠保證你刪除的數(shù)據(jù)你不能再查到,至于什么時(shí)候真正刪除,你就不需要關(guān)心了。Cassandra 刪除數(shù)據(jù)的過(guò)程很復(fù)雜,真正刪除數(shù)據(jù)是在 SSTable 被壓縮的過(guò)程中,SSTable 壓縮的目的就是把同一個(gè) Key 下對(duì)應(yīng)的數(shù)據(jù)都統(tǒng)一到一個(gè) SSTable 文件中,這樣就解決了同一條數(shù)據(jù)在多處的問(wèn)題。壓縮的過(guò)程中 Cassandra 會(huì)根據(jù)判斷規(guī)則判定哪些數(shù)據(jù)應(yīng)該被刪除。
SSTable 的壓縮
數(shù)據(jù)的壓縮實(shí)際上是數(shù)據(jù)寫(xiě)入 Cassandra 的一個(gè)延伸,前面描述的數(shù)據(jù)寫(xiě)入和數(shù)據(jù)的讀取都有一些限制,如:在寫(xiě)的過(guò)程中,數(shù)據(jù)會(huì)不停的將一定大小的 Memtable 刷到磁盤(pán)中,這樣不停的刷,勢(shì)必會(huì)產(chǎn)生很多的同樣大小的 SSTable 文件,不可能這樣無(wú)限下去。同樣在讀的過(guò)程中,如果太多的 SSTable 文件必然會(huì)影響讀的效率,SSTable 越多就會(huì)越影響查詢(xún)。還有一個(gè) Key 對(duì)應(yīng)的 Column 分散在多個(gè) SSTable 同樣也會(huì)是問(wèn)題。還有我們知道 Cassandra 的刪除同樣也是一個(gè)寫(xiě)操作,同樣要處理這些無(wú)效的數(shù)據(jù)。
鑒于以上問(wèn)題,必然要對(duì) SSTable 文件進(jìn)行合并,合并的最終目的就是要將一個(gè) Key 對(duì)應(yīng)的所有 value 合并在一起。該組合的組合、該修改的修改,該刪除的刪除。然后將這個(gè) Key 所對(duì)應(yīng)的數(shù)據(jù)寫(xiě)在 SSTable 所對(duì)應(yīng)的 Data 文件的一段連續(xù)的空間上。
何時(shí)壓縮 SSTable 文件由 Cassandra 來(lái)控制,理想的 SSTable 文件個(gè)數(shù)在 4~32 個(gè)。當(dāng)新增一個(gè) SSTable 文件后 Cassandra 會(huì)計(jì)算當(dāng)期的平均 SSTable 文件的大小當(dāng)新增的 SSTable 大小在平均 SSTable 大小的 0.5~1.5 倍時(shí) Cassandra 就會(huì)調(diào)用壓縮程序壓縮 SSTable 文件,導(dǎo)致的結(jié)果就是重新建立 Key 的索引。這個(gè)過(guò)程可以用下圖描述:
圖 15 數(shù)據(jù)壓縮
評(píng)論