打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
cassandra数据库存储结构
userphoto

2019.01.10

关注

前言


      本文主要介绍 Cassandra 中数据的存储格式,包括在内存中的数据和磁盘中数据。Cassandra 的写的性能表现非常好,为什么写的性能这么好?和它的数据结构有没有关系,以及和它的写的机制又有多大的关系。同时也将分析哪些因素会影响读的性能 Cassandra 又做了哪些改进。

Cassandra 的数据存储结构主要分为三种:

1、CommitLog:主要记录下客户端提交过来的数据以及操作。这个数据将被持久化到磁盘中,以便数据没有被持久化到磁盘时可以用来恢复。

2、Memtable:用户写的数据在内存中的形式,它的对象结构在后面详细介绍。其实还有另外一种形式是 BinaryMemtable 这个格式目前 Cassandra 并没有使用,这里不再介绍了。

3、SSTable:数据被持久化到磁盘,这又分为 Data、Index 和 Filter 三种数据格式。

CommitLog 数据格式

CommitLog 的数据只有一种,那就是按照一定格式组成 byte 组数,写到 IO 缓冲区中定时的被刷到磁盘中持久化,在上一篇的配置文件详解中已经有说到 CommitLog 的持久化方式有两种,一个是 Periodic 一个是 Batch,它们的数据格式都是一样的,只是前者是异步的,后者是同步的,数据被刷到磁盘的频繁度不一样。关于 CommitLog 的相关的类结构图如下:

图 1. CommitLog 的相关的类结构图

它持久化的策略也很简单,就是首先将用户提交的数据所在的对象 RowMutaTIon 序列化成 byte 数组,然后把这个对象和 byte 数组传给 LogRecordAdder 对象,由 LogRecordAdder 对象调用 CommitLogSegment 的 write 方法去完成写操作,这个 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());

...

}

这个代码的主要作用就是如果当前这个根据 columnFamily 的 id 还没有被序列化过,将会根据这个 id 生成一个 CommitLogHeader 对象,记录下在当前的 CommitLog 文件中的位置,并将这个 header 序列化,覆盖以前的 header。这个 header 中可能包含多个没有被序列化到磁盘中的 RowMutation 对应的 columnFamily 的 id。如果已经存在,直接把 RowMutation 对象的序列化结果写到 CommitLog 的文件缓存区中后面再加一个 CRC32 校验码。Byte 数组的格式如下:

图 2. CommitLog 文件数组结构

上图中每个不同的 columnFamily 的 id 都包含在 header 中,这样做的目的是更容易的判断那些数据没有被序列化。

CommitLog 的作用是为恢复没有被写到磁盘中的数据,那如何根据 CommitLog 文件中存储的数据恢复呢?这段代码在 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 对象,寻找 header 对象中没有被回写的最小 RowMutation 位置,然后根据这个位置取出这个 RowMutation 对象的序列化数据,然后反序列化为 RowMutation 对象,然后取出 RowMutation 对象中的数据重新保存到 Memtable 中,而不是直接写到磁盘中。CommitLog 的操作过程可以用下图来清楚的表示:

图 3. CommitLog 数据格式的变化过程

Memtable 内存中数据结构

Memtable 内存中数据结构比较简单,一个 ColumnFamily 对应一个唯一的 Memtable 对象,所以 Memtable 主要就是维护一个 ConcurrentSkipListMap《decoratedkey, columnfamily=“” style=“box-sizing: border-box;”》 类型的数据结构,当一个新的 RowMutation 对象加进来时,Memtable 只要看看这个结构是否 《decoratedkey, columnfamily=“” style=“box-sizing: border-box;”》集合已经存在,没有的话就加进来,有的话取出这个 Key 对应的 ColumnFamily,再把它们的 Column 合并。Memtable 相关的类结构图如下:

图 4. Memtable 相关的类结构图

Memtable 中的数据会根据配置文件中的相应配置参数刷到本地磁盘中。这些参数在上一篇中已经做了详细说明。

前面已经多处提到了 Cassandra 的写的性能很好,好的原因就是因为 Cassandra 写到数据首先被写到 Memtable 中,而 Memtable 是内存中的数据结构,所以 Cassandra 的写是写内存的,下图基本上描述了一个 key/value 数据是怎么样写到 Cassandra 中的 Memtable 数据结构中的。

图 5. 数据被写到 Memtable

SSTable 数据格式

每添加一条数据到 Memtable 中,程序都会检查一下这个 Memtable 是否已经满足被写到磁盘的条件,如果条件满足这个 Memtable 就会写到磁盘中。先看一下这个过程涉及到的类。相关类图如图 6 所示:

图 6. SSTable 持久化类结构图

Memtable 的条件满足后,它会创建一个 SSTableWriter 对象,然后取出 Memtable 中所有的 《decoratedkey, columnfamily=“” style=“box-sizing: border-box;”》集合,将 ColumnFamily 对象的序列化结构写到 DataOutputBuffer 中。接下去 SSTableWriter 根据 DecoratedKey 和 DataOutputBuffer 分别写到 Date、Index 和 Filter 三个文件中。

Data 文件格式如下:

图 7. SSTable 的 Data 文件结构

Data 文件就是按照上述 byte 数组来组织文件的,数据被写到 Data 文件中是接着就会往 Index 文件中写,Index 中到底写什么数据呢?

其实 Index 文件就是记录下所有 Key 和这个 Key 对应在 Data 文件中的启示地址,如图 8 所示:

图 8. Index 文件结构


 

Index 文件实际上就是 Key 的一个索引文件,目前只对 Key 做索引,对 super column 和 column 都没有建索引,所以要匹配 column 相对来说要比 Key 更慢。

Index 文件写完后接着写 Filter 文件,Filter 文件存的内容就是 BloomFilter 对象的序列化结果。它的文件结构如图 9 所示:

图 9. Filter 文件结构

BloomFilter 对象实际上对应一个 Hash 算法,这个算法能够快速的判断给定的某个 Key 在不在当前这个 SSTable 中,而且每个 SSTable 对应的 BloomFilter 对象都在内存中,Filter 文件指示 BloomFilter 持久化的一个副本。三个文件对应的数据格式可以用下图来清楚的表示:

图 10. SSTable 数据格式转化

这个三个文件写完后,还要做的一件事件就是更新前面提到的 CommitLog 文件,告诉 CommitLog 的 header 所存的当前 ColumnFamily 的没有写到磁盘的最小位置。

在 Memtable 往磁盘中写的过程中,这个 Memtable 被放到 memtablesPendingFlush 容器中,以保证在读时候它里面存的数据能被正确读到,这个在后面数据读取时还会介绍。

数据的写入

数据要写到 Cassandra 中有两个步骤:

1.找到应该保存这个数据的节点

2.往这个节点写数据。客户端写一条数据必须指定 Keyspace、ColumnFamily、Key、Column Name 和 Value,还可以指定 Timestamp,以及数据的安全等级。

数据写入涉及的主要相关类如下图所示:

图 11. Insert 相关类图

大慨的写入逻辑是这样的:

CassandraServer 接收到要写入的数据时,首先创建一个 RowMutation 对象,再创建一个 QueryPath 对象,这个对象中保存了 ColumnFamily、Column Name 或者 Super Column Name。接着把用户提交的所有数据保存在 RowMutation 对象的 Map《string, columnfamily=“” style=“box-sizing: border-box;”》 结构中。接下去就是根据提交的 Key 计算集群中那个节点应该保存这条数据。这个计算的规则是:将 Key 转化成 Token,然后在整个集群的 Token 环中根据二分查找算法找到与给定的 Token 最接近的一个节点。如果用户指定了数据要保存多个备份,那么将会顺序在 Token 环中返回与备份数相等的节点。这是一个基本的节点列表,后面 Cassandra 会判断这些节点是否正常工作,如果不正常寻找替换节点。还有还要检查是否有节点正在启动,这种节点也是要在考虑的范围内,最终会形成一个目标节点列表。最 后把数据发送到这些节点。

接下去就是将数据保存到 Memtable 中和 CommitLog 中,关于结果的返回根据用户指定的安全等级不同,可以是异步的,也可以是同步的。如果某个节点返回失败,将会再次发送数据。下图是当 Cassandra 接收到一条数据时到将数据写到 Memtable 中的时序图。

图 12. Insert 操作的时序图


本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
Cassandra 分布式数据库详解,第 2 部分:数据结构与数据读写
Cassandra分布式数据库详解,第1部分:配置、启动与集群
详解Cassandra0.7配置文件
详解NoSQL数据库使用实例
Cassandra简介
庖丁解LevelDB之数据存储
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服