原文地址:
在原文基础上有所删减

Cassandra内部架构

上两篇分析了Cassandra集群架构,下面我们剖析Cassandra内部架构。Cassandra守护进程管理各种内存和磁盘中的数据结构,如下图所示。

Commit Logs(提交日志)用于记录对磁盘的写操作,作为故障恢复机制。 在Cassandra中写入效率高的原因之一就是所有Keyspace共享一个提交日志,因此只要写操作将数据追加到副本上的提交日志中(就协调器节点而言),就算写入完成。

SSTables(Sorted String Tables)为Cassandra提供磁盘存储。 当Cassandra执行写操作时,Cassandra会先将数据写入Memtables内存表,并且定期刷新到磁盘,写入SSTables,以提高写入性能。

Key Caches(键缓存)和Row Caches(行缓存)用于缓存经常访问的数据,以提高读性能。 对于小表,可以启用行缓存。但对于数据量较大的表不建议启用行缓存,虽然新版Cassandra已经将这块区域移至堆外内存,但也会引起堆外OOM问题。Cassandra对行缓存也是默认关闭的。键缓存建议全部开启,Cassandra默认也是开启的。

关键特性

Hinted Handoff(提示移交)

考虑如下场景:一个写请求到达Cassandra,但是负责这部分数据的节点却由于网络、硬件故障或其他原因而不可用。为了保证整个集群在这种情况下的可用性,Cassandra实现了一个称为提示移交(hinted handoff)的机制。可以把一个提示看做是一个小即时贴,上面记录着写请求的内容。如果写操作所属的节点失败了,Cassandra接收到该请求的节点会创建一个提示,包含这样一条备忘信息:”我有一个给节点B的写请求信息。这个请求现在挂起了,等到节点B回来的时候请告知我,那时我会把写请求送交给它。”也就是说,写操作的提示信息将会从节点A移交给节点B。

提示移交允许Cassandra对于写操作永远可用,降低离线节点恢复服务之后的不一致的时间。之前讨论过一致性级别,0.6版本中引入的一致性级别ANY,这个一致性级别意味着有一个提示移交就可以认为写操作是成功的。也就是说,即使只有一个提示被记录下来,写操作也就可以认为是成功了。

一些对提示移交的顾虑,在Cassandra社区内部就已经提出过了。起先,这似乎是一个深思熟虑且精巧的设计,可以保证数据库的持久性,并且,因为这种方法已经在很多分布式计算模式中出现过,比如Java消息服务(JMS),似乎不会有什么问题。在具有持久性的”保障传递”JMS队列中,如果消息无法发送给接收者,JMS会等待一个给定时间,然后重传消息,直到消息被成功接收。但是在实际系统中,不论是对于JMS的可靠传输还是对于Cassandra的提示移交,都存在一个问题:如果节点离线持续一段时间,其他节点上会堆积相当多的提示信息。之后当其他节点发现掉线节点重新在线的时候,请求会如潮水般涌向这个节点,而此时,这个节点本身正处在自己最脆弱的状态(它刚刚从故障中恢复过来,正在努力恢复工作)。

作为对这个问题的方案,可以完全关闭提示移交,或者加强对集群的健康检查监控,及时发现问题节点并及时恢复,以避免出现大量提示堆积。

Bloom Filter(布隆过滤器)

这不是Cassandra独有的特性,ApacheHadoop、GoogleBigtable和Squid缓存服务器也使用了这个算法以提高性能。

布隆过滤器是一种提升性能的手段,得名于其发明者Burton Bloom。布隆过滤器是一种用于判断一个元素是否是一个集合成员的快速、但不确定的算法。称其为不确定性方法是因为布隆过滤器可能会得到一个”假阳性”结果,但是它不会得到”假阴性”的结果,也就是说判断为属于不一定确实属于,但判断为不属于则一定不属于。布隆过滤器将数据集里的值映射为一个位数组,并将一个大数据集凝炼为一个摘要字符串。按照定义,摘要字符串会占用远少于原始数据的内存空间。布隆过滤器位于内存之中,这样可以减少查找键值时的磁盘访问,从而改善性能。磁盘访问通常会比内存访问慢很多。所以,布隆过滤器可以看做是一类特殊的缓存。当进行查询时,在访问磁盘之前首先检查布隆过滤器。因为不会有假阴性结果,所以,如果布隆过滤器显示元素不存在就是真的不存在。但如果Bloom Filter显示这个元素在集合之中,那就再去访问磁盘,确认是否存在。

因为算法比较经典,网上讲解的资源很多,所以具体算法细节自行百度即可。

Tombstone(墓碑)

你可能了解关系型数据库中的”逻辑删除”这个概念。逻辑删除是指,应用并不直接执行SQL的delete语句,而是使用一个update语句,把某列的值变为”已删除”之类的内容。在Cassandra中,有个与此类似的概念,称为墓碑。这就是所有删除操作的做法,区别是它自动为你执行的。当你执行一个删除操作时,数据并不会被立刻删除。它被视为一个更新操作,在相应的字段的值上放一个墓碑。墓碑是一个删除标记,当执行合并SSTable时,比墓碑时间更老的内容都会被真正删除。有一个相关的设置:GarbageCollectionGraceSeconds(垃圾回收时延)。这个时间是服务器对一个墓碑进行垃圾回收之前的等待的时间。这个时间默认是864000秒,也就是10天。Cassandra会一直跟踪墓碑的年龄,一旦某个墓碑的寿命比GCGraceSeconds长了,就会回收。这个时延的设计目的是留下足够长的时间以便于恢复数据,如果一个节点宕机超过这个时间,那么它也会被认为是发生故障了,应该被替换掉。

读写流程

写操作

写操作流程

  1. 将数据写入节点时,首先将其存储到提交日志中,以便在节点发生故障时可以恢复写入。

  2. 数据的副本写入内存表Memtables中,可以在无需访问磁盘的情况下进行后续读取操作或后续的更新。

  3. 如果正在使用行缓存,并且缓存中已经存在该行的较旧副本,则该旧副本将被新值替换。

  4. Cassandra监视Memtables的大小。如果内存表达到某个阈值大小,Cassandra会将内存表数据写入磁盘的SSTables中。如果不同的写操作在同一行中更新了不同的列,则将产生多个SSTable文件。 Cassandra具有压缩机制,该机制会定期运行以合并SSTables。

  5. 最后,如果启用了”提示移交(hinted handoff)”功能,并且如果协调器检测到节点在写入过程中已无响应(由于网络问题,过载情况,垃圾收集暂停等),则丢失的写入操作将作为”提示”存储在协调器上。提示包含未能接受写入的节点的标识符以及要写入的数据。当Gossip协议发现发生故障的节点已恢复重连时,协调器节点将重发先前失败的操作的提示,并从协调器中删除提示。提示数据有过期时间,以防止它们在协调器上累积。监视群集的运行状况非常重要,可以在提示过期之前重新启动或更换已关闭的节点。

写性能

Cassandra最出色的是写操作性能。 一旦将数据记录到提交日志中,Cassandra就可以完成写操作,其他操作异步进行。 而且性能不会随节点的扩展而降低。下面的示例中,我们向表中写入几条记录,平均写入延迟大约为0.69毫秒。 事实证明,Cassandra在生产规模的集群中每秒可提交多达一百万次写入。

1
2
3
4
5
6
7
8
nodetool tablestats gps 

Total number of tables: 36
----------------
Keyspace : gps
..
Write Count: 6
Write Latency: 0.06933333333333333 ms

读流程

Cassandra中的读比写更复杂。 当客户端使用读查询连接到协调器节点时,读操作开始。 像写请求一样,协调器节点使用分区程序确定哪些节点保存数据的副本。 像写一样,读的性能取决于查询所需的一致性。 读一致性是指在将结果视为有效之前需要达成共识的副本数量(参考Cassandra可调一致性的使用及原理)。 如果对结果没有共识,Cassandra将在内部运行”读修复”操作,强制更新Cassandra副本上旧值,然后再将结果返回给客户端。 这是影响性能的一个关键因素。

对于读操作,Cassandra需要执行几个步骤。

读取流程

  1. 查询副本时,首先要查找行缓存(如果启用)。如果所需的数据在行缓存中命中,则可以立即返回。

  2. Cassandra将检查键缓存(如果启用)。如果在键缓存中找到了分区键,Cassandra可以使用该键通过读取内存中的压缩偏移量来了解数据的存储位置。

  3. 接下来,Cassandra将检查该内存表Memtable,以查看是否存在所需的数据。

  4. 之后,Cassandra从磁盘上的SSTables中获取数据,并将其与Memtable中的数据组合起来,以构建所查询数据的视图。

  5. 最后,如果启用行缓存,Cassandra会将数据写入在行缓存中(以加快后续对相同数据的读取),并将结果返回到协调器节点。

这些步骤略有简化。 Cassandra具有其他优化功能,例如上文中提到的布隆过滤器,可通过缩小要搜索的键池来加快分区键查找的过程。

总结

本文剖析了Cassandra节点的内部架构,包括JVM内存结构和磁盘结构两部分。JVM部分包括Memtables、Key Caches、Row Caches;磁盘部分包括:Commit Logs、SSTables、Hints。并介绍了Cassandra的几个关键特性:提示移交、布隆过滤器和墓碑机制。最后对Cassandra的读写操作流程进行了分析。