前言
? ? ? 本文主要介紹 Cassandra 中數(shù)據(jù)的存儲(chǔ)格式,包括在內(nèi)存中的數(shù)據(jù)和磁盤中數(shù)據(jù)。Cassandra 的寫的性能表現(xiàn)非常好,為什么寫的性能這么好?和它的數(shù)據(jù)結(jié)構(gòu)有沒有關(guān)系,以及和它的寫的機(jī)制又有多大的關(guān)系。同時(shí)也將分析哪些因素會(huì)影響讀的性能 Cassandra 又做了哪些改進(jìn)。
Cassandra 的數(shù)據(jù)存儲(chǔ)結(jié)構(gòu)主要分為三種:
1、CommitLog:主要記錄下客戶端提交過來的數(shù)據(jù)以及操作。這個(gè)數(shù)據(jù)將被持久化到磁盤中,以便數(shù)據(jù)沒有被持久化到磁盤時(shí)可以用來恢復(fù)。
2、Memtable:用戶寫的數(shù)據(jù)在內(nèi)存中的形式,它的對(duì)象結(jié)構(gòu)在后面詳細(xì)介紹。其實(shí)還有另外一種形式是 BinaryMemtable 這個(gè)格式目前 Cassandra 并沒有使用,這里不再介紹了。
3、SSTable:數(shù)據(jù)被持久化到磁盤,這又分為 Data、Index 和 Filter 三種數(shù)據(jù)格式。
CommitLog 數(shù)據(jù)格式
CommitLog 的數(shù)據(jù)只有一種,那就是按照一定格式組成 byte 組數(shù),寫到 IO 緩沖區(qū)中定時(shí)的被刷到磁盤中持久化,在上一篇的配置文件詳解中已經(jīng)有說到 CommitLog 的持久化方式有兩種,一個(gè)是 Periodic 一個(gè)是 Batch,它們的數(shù)據(jù)格式都是一樣的,只是前者是異步的,后者是同步的,數(shù)據(jù)被刷到磁盤的頻繁度不一樣。關(guān)于 CommitLog 的相關(guān)的類結(jié)構(gòu)圖如下:
圖 1. CommitLog 的相關(guān)的類結(jié)構(gòu)圖
它持久化的策略也很簡(jiǎn)單,就是首先將用戶提交的數(shù)據(jù)所在的對(duì)象 RowMutation 序列化成 byte 數(shù)組,然后把這個(gè)對(duì)象和 byte 數(shù)組傳給 LogRecordAdder 對(duì)象,由 LogRecordAdder 對(duì)象調(diào)用 CommitLogSegment 的 write 方法去完成寫操作,這個(gè) write 方法的代碼如下:
清單 1. CommitLogSegment. write
public CommitLogSegment.CommitLogContext write(RowMutation rowMutation,
Object serializedRow){
long currentPosition = -1L;
...
Checksum checkum = new CRC32();
if (serializedRow instanceof DataOutputBuffer){
DataOutputBuffer buffer = (DataOutputBuffer) serializedRow;
logWriter.writeLong(buffer.getLength());
logWriter.write(buffer.getData(), 0, buffer.getLength());
checkum.update(buffer.getData(), 0, buffer.getLength());
}
else{
assert serializedRow instanceof byte[];
byte[] bytes = (byte[]) serializedRow;
logWriter.writeLong(bytes.length);
logWriter.write(bytes);
checkum.update(bytes, 0, bytes.length);
}
logWriter.writeLong(checkum.getValue());
...
}
這個(gè)代碼的主要作用就是如果當(dāng)前這個(gè)根據(jù) columnFamily 的 id 還沒有被序列化過,將會(huì)根據(jù)這個(gè) id 生成一個(gè) CommitLogHeader 對(duì)象,記錄下在當(dāng)前的 CommitLog 文件中的位置,并將這個(gè) header 序列化,覆蓋以前的 header。這個(gè) header 中可能包含多個(gè)沒有被序列化到磁盤中的 RowMutation 對(duì)應(yīng)的 columnFamily 的 id。如果已經(jīng)存在,直接把 RowMutation 對(duì)象的序列化結(jié)果寫到 CommitLog 的文件緩存區(qū)中后面再加一個(gè) CRC32 校驗(yàn)碼。Byte 數(shù)組的格式如下:
圖 2. CommitLog 文件數(shù)組結(jié)構(gòu)
上圖中每個(gè)不同的 columnFamily 的 id 都包含在 header 中,這樣做的目的是更容易的判斷那些數(shù)據(jù)沒有被序列化。
CommitLog 的作用是為恢復(fù)沒有被寫到磁盤中的數(shù)據(jù),那如何根據(jù) CommitLog 文件中存儲(chǔ)的數(shù)據(jù)恢復(fù)呢?這段代碼在 recover 方法中:
清單 2. CommitLog.recover
public static void recover(File[] clogs) throws IOException{
...
final CommitLogHeader clHeader = CommitLogHeader.readCommitLogHeader(reader);
int lowPos = CommitLogHeader.getLowestPosition(clHeader);
if (lowPos == 0) break;
reader.seek(lowPos);
while (!reader.isEOF()){
try{
bytes = new byte[(int) reader.readLong()];
reader.readFully(bytes);
claimedCRC32 = reader.readLong();
}
...
ByteArrayInputStream bufIn = new ByteArrayInputStream(bytes);
Checksum checksum = new CRC32();
checksum.update(bytes, 0, bytes.length);
if (claimedCRC32 != checksum.getValue()){continue;}
final RowMutation rm =
RowMutation.serializer().deserialize(new DataInputStream(bufIn));
}
...
}
這段代碼的思路是:反序列化 CommitLog 文件的 header 為 CommitLogHeader 對(duì)象,尋找 header 對(duì)象中沒有被回寫的最小 RowMutation 位置,然后根據(jù)這個(gè)位置取出這個(gè) RowMutation 對(duì)象的序列化數(shù)據(jù),然后反序列化為 RowMutation 對(duì)象,然后取出 RowMutation 對(duì)象中的數(shù)據(jù)重新保存到 Memtable 中,而不是直接寫到磁盤中。CommitLog 的操作過程可以用下圖來清楚的表示:
圖 3. CommitLog 數(shù)據(jù)格式的變化過程
Memtable 內(nèi)存中數(shù)據(jù)結(jié)構(gòu)
Memtable 內(nèi)存中數(shù)據(jù)結(jié)構(gòu)比較簡(jiǎn)單,一個(gè) ColumnFamily 對(duì)應(yīng)一個(gè)唯一的 Memtable 對(duì)象,所以 Memtable 主要就是維護(hù)一個(gè) ConcurrentSkipListMap《decoratedkey, columnfamily=“” style=“box-sizing: border-box;”》 類型的數(shù)據(jù)結(jié)構(gòu),當(dāng)一個(gè)新的 RowMutation 對(duì)象加進(jìn)來時(shí),Memtable 只要看看這個(gè)結(jié)構(gòu)是否 《decoratedkey, columnfamily=“” style=“box-sizing: border-box;”》集合已經(jīng)存在,沒有的話就加進(jìn)來,有的話取出這個(gè) Key 對(duì)應(yīng)的 ColumnFamily,再把它們的 Column 合并。Memtable 相關(guān)的類結(jié)構(gòu)圖如下:
圖 4. Memtable 相關(guān)的類結(jié)構(gòu)圖
Memtable 中的數(shù)據(jù)會(huì)根據(jù)配置文件中的相應(yīng)配置參數(shù)刷到本地磁盤中。這些參數(shù)在上一篇中已經(jīng)做了詳細(xì)說明。
前面已經(jīng)多處提到了 Cassandra 的寫的性能很好,好的原因就是因?yàn)?Cassandra 寫到數(shù)據(jù)首先被寫到 Memtable 中,而 Memtable 是內(nèi)存中的數(shù)據(jù)結(jié)構(gòu),所以 Cassandra 的寫是寫內(nèi)存的,下圖基本上描述了一個(gè) key/value 數(shù)據(jù)是怎么樣寫到 Cassandra 中的 Memtable 數(shù)據(jù)結(jié)構(gòu)中的。
圖 5. 數(shù)據(jù)被寫到 Memtable
SSTable 數(shù)據(jù)格式
每添加一條數(shù)據(jù)到 Memtable 中,程序都會(huì)檢查一下這個(gè) Memtable 是否已經(jīng)滿足被寫到磁盤的條件,如果條件滿足這個(gè) Memtable 就會(huì)寫到磁盤中。先看一下這個(gè)過程涉及到的類。相關(guān)類圖如圖 6 所示:
圖 6. SSTable 持久化類結(jié)構(gòu)圖
Memtable 的條件滿足后,它會(huì)創(chuàng)建一個(gè) SSTableWriter 對(duì)象,然后取出 Memtable 中所有的 《decoratedkey, columnfamily=“” style=“box-sizing: border-box;”》集合,將 ColumnFamily 對(duì)象的序列化結(jié)構(gòu)寫到 DataOutputBuffer 中。接下去 SSTableWriter 根據(jù) DecoratedKey 和 DataOutputBuffer 分別寫到 Date、Index 和 Filter 三個(gè)文件中。
Data 文件格式如下:
圖 7. SSTable 的 Data 文件結(jié)構(gòu)
Data 文件就是按照上述 byte 數(shù)組來組織文件的,數(shù)據(jù)被寫到 Data 文件中是接著就會(huì)往 Index 文件中寫,Index 中到底寫什么數(shù)據(jù)呢?
其實(shí) Index 文件就是記錄下所有 Key 和這個(gè) Key 對(duì)應(yīng)在 Data 文件中的啟示地址,如圖 8 所示:
圖 8. Index 文件結(jié)構(gòu)
?
Index 文件實(shí)際上就是 Key 的一個(gè)索引文件,目前只對(duì) Key 做索引,對(duì) super column 和 column 都沒有建索引,所以要匹配 column 相對(duì)來說要比 Key 更慢。
Index 文件寫完后接著寫 Filter 文件,F(xiàn)ilter 文件存的內(nèi)容就是 BloomFilter 對(duì)象的序列化結(jié)果。它的文件結(jié)構(gòu)如圖 9 所示:
圖 9. Filter 文件結(jié)構(gòu)
BloomFilter 對(duì)象實(shí)際上對(duì)應(yīng)一個(gè) Hash 算法,這個(gè)算法能夠快速的判斷給定的某個(gè) Key 在不在當(dāng)前這個(gè) SSTable 中,而且每個(gè) SSTable 對(duì)應(yīng)的 BloomFilter 對(duì)象都在內(nèi)存中,F(xiàn)ilter 文件指示 BloomFilter 持久化的一個(gè)副本。三個(gè)文件對(duì)應(yīng)的數(shù)據(jù)格式可以用下圖來清楚的表示:
圖 10. SSTable 數(shù)據(jù)格式轉(zhuǎn)化
這個(gè)三個(gè)文件寫完后,還要做的一件事件就是更新前面提到的 CommitLog 文件,告訴 CommitLog 的 header 所存的當(dāng)前 ColumnFamily 的沒有寫到磁盤的最小位置。
在 Memtable 往磁盤中寫的過程中,這個(gè) Memtable 被放到 memtablesPendingFlush 容器中,以保證在讀時(shí)候它里面存的數(shù)據(jù)能被正確讀到,這個(gè)在后面數(shù)據(jù)讀取時(shí)還會(huì)介紹。
數(shù)據(jù)的寫入
數(shù)據(jù)要寫到 Cassandra 中有兩個(gè)步驟:
1.找到應(yīng)該保存這個(gè)數(shù)據(jù)的節(jié)點(diǎn)
2.往這個(gè)節(jié)點(diǎn)寫數(shù)據(jù)??蛻舳藢懸粭l數(shù)據(jù)必須指定 Keyspace、ColumnFamily、Key、Column Name 和 Value,還可以指定 Timestamp,以及數(shù)據(jù)的安全等級(jí)。
數(shù)據(jù)寫入涉及的主要相關(guān)類如下圖所示:
圖 11. Insert 相關(guān)類圖
大慨的寫入邏輯是這樣的:
CassandraServer 接收到要寫入的數(shù)據(jù)時(shí),首先創(chuàng)建一個(gè) RowMutation 對(duì)象,再創(chuàng)建一個(gè) QueryPath 對(duì)象,這個(gè)對(duì)象中保存了 ColumnFamily、Column Name 或者 Super Column Name。接著把用戶提交的所有數(shù)據(jù)保存在 RowMutation 對(duì)象的 Map《string, columnfamily=“” style=“box-sizing: border-box;”》 結(jié)構(gòu)中。接下去就是根據(jù)提交的 Key 計(jì)算集群中那個(gè)節(jié)點(diǎn)應(yīng)該保存這條數(shù)據(jù)。這個(gè)計(jì)算的規(guī)則是:將 Key 轉(zhuǎn)化成 Token,然后在整個(gè)集群的 Token 環(huán)中根據(jù)二分查找算法找到與給定的 Token 最接近的一個(gè)節(jié)點(diǎn)。如果用戶指定了數(shù)據(jù)要保存多個(gè)備份,那么將會(huì)順序在 Token 環(huán)中返回與備份數(shù)相等的節(jié)點(diǎn)。這是一個(gè)基本的節(jié)點(diǎn)列表,后面 Cassandra 會(huì)判斷這些節(jié)點(diǎn)是否正常工作,如果不正常尋找替換節(jié)點(diǎn)。還有還要檢查是否有節(jié)點(diǎn)正在啟動(dòng),這種節(jié)點(diǎn)也是要在考慮的范圍內(nèi),最終會(huì)形成一個(gè)目標(biāo)節(jié)點(diǎn)列表。最 后把數(shù)據(jù)發(fā)送到這些節(jié)點(diǎn)。
接下去就是將數(shù)據(jù)保存到 Memtable 中和 CommitLog 中,關(guān)于結(jié)果的返回根據(jù)用戶指定的安全等級(jí)不同,可以是異步的,也可以是同步的。如果某個(gè)節(jié)點(diǎn)返回失敗,將會(huì)再次發(fā)送數(shù)據(jù)。下圖是當(dāng) Cassandra 接收到一條數(shù)據(jù)時(shí)到將數(shù)據(jù)寫到 Memtable 中的時(shí)序圖。
圖 12. Insert 操作的時(shí)序圖
評(píng)論
查看更多