上一篇我们有详细说MemTable,SSTable和Commitlog。其实就是数据在写入的时候,会先存在内存里(MemTable),同时会在Commit log里留一条记录,这条记录只是在系统Crash的时候才用来恢复数据的。在内存里堆积的数据足够多时,会把数据合并到磁盘文件里,生成SSTable。所以,如果在读数据的时候,我们要按什么流程来合并这两部分的数据呢?
从更宏观看,我们在定义一个数据库(KeySpace)的时候,我们定义了数据要有几分复制品(Replica),这是为了保证高可用。那么,在读数据的时候,我们要怎么读这些副本呢?是随机读一个,还是要所有副本都要读取?因为副本之间的同步是有先后之差的,只读其中一个有可能读的不是最新的数据。
从性能的角度,Cassandra会建立很多辅助查询用的Cache、索引和Filter。为什么在这些地方设置性能优化,我们在查询的过程中怎么利用他们?
抱着搞清楚上面三个维度的想法,我们来看看Cassandra是怎么读写的。
Cassandra的写入操作主要包含以下3个步骤:
(1)记录数据到commit log
(2)写数据到memtable(一般情况下一个表有一个memtable)
(3)当commit log的size达到阀值或者memtable的size达到阀值时,将数据flush到SSTable中,flush完成后清空commit log和memtable
Cassandra写操作
(1)对于一个写操作,Cassandra首先将客户端提交的数据和操作记录到commit log中,此操作是为了提升可靠性(起到数据恢复的作用)。
(2)接着Cassandra将数据写入到内存表memtable中,memtable中组织的数据按照key排序。当memtable中的数据大小到达一定限制后,Cassandra才会将memtable中的数据批量刷新到一个SSTable中。这种机制,相当于缓存写回机制(Write-back Cache),优势在于将随机IO写变为顺序IO写, 大大降低了写操作对于存储系统的压力。
(3)SSTable一旦完成写入,就不可变更,只能读取。因此对于Cassandra来说,可以认为只有顺序写,没有随机写操作。
(4)由于SSTable的只读性,因此同一个Column Family的数据可能存储在多个SSTable中,如果一个Column Family中的数据量很大的时候,那么Cassandra需要合并读取多个SSTable和memtable,导致查询效率严重下降。为此Cassandra引入了BloomFilter,每个SSTable都拥有一个BloomFilter。BloomFilter是一个存储在内存中的数据结构,它能够快速判断某个给定的key是否位于某个SSTable中,因此,Cassandra能够快速定位某个key对应的SSTable。
(5)为了避免大量SSTable带来的性能影响,Cassandra通过一种称为压缩(Compaction)的机制来处理随着时间推移而不断膨胀的SSTables。Cassandra定期将多个SSTable合并成一个SSTable,由于每个SSTable中的数据均是有序的,因此只要做一次合并排序就可以完成该任务,这个代价是可以接受的。
(6)Cassandra的数据存储目录下,可以看到三种类型的文件,文件名的格式类似于:
Column Family Name-序号-Data.db
Column Family Name-序号-Filter.db
Column Family Name-序号-index.db
其中Data.db文件是SSTable数据文件,SSTable是Sorted Strings Table的缩写,按照key排序后存储key/value键值字符串。index.db是索引文件,保存的是每个key在数据文件中的偏移位置,而Filter.db则是Bloom Filter算法生产的映射文件。
Cassandra主要支持三种数据压缩策略,Size Tiered Compaction Strategy(STCS),Leveled Compaction Strategy(LCS),Time Window Compaction Strategy(TWCS)。
2.1 Size Tiered Compaction Strategy(STCS) -- 同大小合并
当具有类似大小的SSTable的数目达到阀值(默认为4)时,STCS会将这些SSTables合并成一个新的SSTable,当这些新的SSTable数量增加到阀值,STCS会将他们合并成更大的SSTable。
优点:写占比高的情况下压缩效果好。
缺点:(1)将读操作变慢了,因为根据大小来进行合并的过程并不会对数据进行分组,这样使某个特定行的多个版本很有可能分散在多个SSTable中。
(2)浪费存储空间。由于SSTable是经过一段时间后合并的,一个被删除的记录,它的老版本可能一直存储在旧的SSTable中,直到新的合并出现才会将这些记录删除。因此对于一些经常进行删除操作的系统,其浪费的的空间是很大的。
(3)压缩时占用大量的空间。随着时间的推移,系统中会出现一些很大的SSTable。这时如果需要合并四个很大的SSTable,压缩占用的存储空间就是这四个SSTable大小的总和。
2.2 Leveled Compaction Strategy(LCS) -- 分层合并
LCS将SSTable分层,每一层都拥有各自的SSTables。首先memtable flush数据到L0层,当L0层的SSTabel数量达到阀值(例如4)时,然后L0层4个的SSTabl会和和L1层的10个SSTable进行合并。从L1层开始,每一层的SSTable数量都是上一层的10倍,同时每个SSTable的默认大小为160MB。如果L1层合并后的数据量大于160MB*10,那么LCS会选择L1层的一个SSTable(合并后此SSTable会被删除),与L2层的SSTable进行合并。由于每个SSTable都是有序并且不相交的,因此从L2层也大约只需要选出10个SSTable来进行合并。
这样就解决了STCS出现的问题:
(1)由于每一层的各个SSTable中的数据都有序不相交,可以保证90%的读操作都在一个SSTable中完成,最坏情况是一个记录存在每一层,这样最坏情况下10TB数据也只需要读7层,也就是7个SSTable。
(2)最多只有10%的空间会被浪费。因为最坏的情况是该层的记录和完全存在在下一层中,而且每一层都是这种情况。也就是会所每一层都有10%(下一层数据是上一层的10倍)的数据时冗余的。
(3)在压缩合并操作的开销上,每次只会使用10倍于要压缩的sstable大小的空间。
适用条件:
对于一个更新操作和删除操作比较多的系统,或者读操作占比高的系统,使用分层压缩是比较合适的。因为这种系统会产生同一份数据的多个版本。但是由于这种压缩会在压缩中进行更多的IO操作,所以如果是一个主要是insert操作的系统,建议不要使用分层压缩方法。
2.3 Time Window Compaction Strategy(TWCS)
TWCS通过使用一系列的时间窗口将SSTables进行分组。在compaction阶段,TWCS在最新的时间窗口内使用STCS(合并相同尺寸)去压缩SSTables。在一个时间窗口的结束,TWCS将掉落在这个时间窗口的所有的SSTables压缩层一个单独的SSTable,在SSTable maximum timestamp基础上。一旦一个时间窗口的主要压缩完成了,这部分数据就不会再有进一步的压缩了。这个过程结束之后SSTable开始写入下一个时间窗口。
例如,从上午10点到上午11点,memtables flush到100MB的SSTables中。使用STCS策略将这些SSTables压缩到一个更大的SSTables中。在上午11点的时候,这些SSTables被合并到一个单独的SSTable,而且不会被TWCS再进行压缩了。在中午12点,上午11点到中午12点创建的新的SSTables被STCS进行压缩,在这个时间窗口结束的时候,TWCS压缩开始。注意在每个TWCS时间窗口包含不同大小的数据。
优势:用作时间序列数据,为表中所有数据使用默认的TTL。比DTCS配置更简单。
劣势:不适用于时间乱序的数据,因为SSTables不会继续做压缩,存储会没有边界的增长,所以也不适用于没有设置TTL(Time To Live)的数据。相比较DTCS,需要更少的调优配置。
Cassandra 读操作主要经过以下几个步骤:
(1) 客户端发送一个读请求到Cassandra集群中的单,随机节点(即存储代理节点Storage Proxy);
(2) 该节点根据复制放置策略将读请求发送到N个存储了对应replica的节点;
(3) 收到读请求的节点都要合并读取SSTable和Memtable(在后续详解)
(4) 代理节点必须等待这N个不同节点中的某些节点读响应的返回,才能将读操作成功的消息告诉客户端(根据读一致性水平来确定需要等待读成功响应的节点个数)。Cassandra的读一致性水平(假设副本个数为n)分为以下三种情况。
(a) ONE:返回第一个响应的节点上面的数据,但不保证数据是最新的,通过读修复和一致性检査可保证后续的调用能够读取最新的数据。
(b) QUORUM:査询n个节点,返回至少n/2+1个节点上的最新数据。
(c) ALL:查询n个节点,返回n个节点中的最新数据,一个节点失效将导致读失败。
解析上述(3) 中,单个节点读取数据的过程:
(1)读memtable,如果有返回,如果没有则继续(以下都是这个逻辑)
(2)如果开启了row cache, 读row cache。
注:row cache是存储在内存中,最近被读取到的数据行的缓存。这是基于缓存常用的逻辑:最近读到的数据最有可能被再次读取。如果这行数据有被修改,这行数据的缓存就直接失效了。
(3)读Bloom Filter,Bloom filter用于检查当前查询的partition key位于哪一个SSTable中。
注:Bloom Filter是一种空间效率很高的随机数据结构,本质上就是利用一个位数组来表示一个集合,并能判断一个元素是否属于这个集合。Bloom Filter的这种高效是有误差的:在判断一个元素是否属于某个集合时,有可能会把不属于这个集合的元素误认为属于这个集合(false positive)。
原理:位数组 配合 K个独立hash(y)函数。把数据存入SSTable时,用这K个hash函数对数据算出hash值,并将位数组中这些对应的值的位置都设为1。查找时如果发现所有hash函数对应位都是1说明数据有很大概率存在于这个SSTable中。
P.S. Bloom Filter存储在堆外内存。每TB增长量为1~3GB。
(4)如果开启了partition key cache,读partition key cache。partition key cache是partition index的缓存。如果partition key cache命中了partition key,直接从compression offset map中获取数据的地址。
注:partition key cache 用于标识SSTable中每个Partition的开始位置。存在堆外内存。
compression offset map 用于查找数据对应的块文件(chunk)。在Cassandra中,数据文件会被分块存储,默认一块是64k,如果找到了数据对应的块,你需要整个解压开才能获取到里面的一行数据。compression offset map存在堆外内存,每TB增长量为1~3GB。
(6) 如果partition key cache没有命中,那么读取partition summary,读取partition summary之后从partition index中获得数据的offset。
partition index:存储了partition key和其对应的offset(这里的offset应该是compression offset map的offset)
partition summary:是一个存储了部分partition index的内存数据结构,它存储了间隔X个的partition index,例如,X=20,那么partition index会存储第一个partition key,第20个partition key,第40个partition key...
(7)通过compression offset map获得数据的地址。
(8)从磁盘上的SSTable获得数据。(获得数据的同时,更新row cache)
Read Data