Cassandra 读/插入/删除操作的实现

Cassandra 数据模型概述

关于数据模型的基本概念先参考 http://wiki.apache.org/cassandra/DataModel 。 注意:这篇文章中关于 Column Families 和 Rows 的描述是不准确的,而且缺少 column family store (CFS) 这一重要概念。 下面对 Column Families 做进一步的说明。

 

Column-family 数据模型

为了简化,我们先忽略 SuperColumn。

在 Cassandra 中一个 Keyspace 可以看作一个二维索引结构:

  • 第一层索引所用的 key 为 (row-key, cf-name), 即用一个 row-key 和 column-family-name 可以定位一个 column family。column family 是 column 的集合。
  • 第一层索引所用的 key 为 column-name, 即通过一个 column-name 可以在一个 column family 中定位一个 column。

Column 是这个数据模型里面最基本的数据单元,它是一个三元组 (name, value, timestamp)。 一个 column family 里面,所有的 column 是按照 column-name 排序的。所以可以根据 column-name 快速找到 column。

数据定位

Cassandra 通过 (row-key, cf-name) 对来定位一个 column-family,具体过程如下:

  • 首先根据 row-key 生成一个哈希值,根据哈希值确定在网络中哪几个节点上放置该 column family。
  • 在每一个放置了该 column family 的节点上,具有相同的 cf-name 的 column-family 会被保存在一起,称为一个 column family store (CFS)。在一个 CFS 中,各个 column family 是按照 row-key 排序的。所以,在该节点上,首先通过 cf-name 来找到对应的 CFS,然后用 row-key 在这个 CFS 中查找这个 column family。

需要注意的地方是,上面的定位过程中 row-key 被使用了两次。 总结来说,在每个节点本地看来,一个 CFS 相当于数据库的一个表, Column Family 相当于表中的一行, Column 相当于一行中的一个域。

 


数据的本地存储

在阅读关于本地存储实现的代码分析之前,建议先阅读关于 Google 的 Big Table 系统的论文:"Bigtable: A Distributed Storage System for Structured Data"。为了简化讨论,以下均不考虑 super column。

 

数据结构

数据模型相关的数据结构可以分成两部分:内存中的数据结构和磁盘上的数据结构。

每个 Cassandra 节点在内存中维护一系列层次性的数据结构

  • 在最顶层的是 Table 类,一个 Table 对应于一个 keyspace。它是 ColumnFamilyStore 对象的集合,可以通过 column family 的名字查找到对应的 CFS;
  • ColumnFamilyStore 类包含两个主要的域:
    • 一个 Memtable 对象。该对象保存了最近的对该 CFS 中的 Column family 的修改,这些修改还没有被 flush 到磁盘上。
    • 一个 SStableTracker 对象。这是一个 SStableReader 对象的集合,每个 SSTableReader 对象维护了关于磁盘上的 CF 数据的必要信息。

Memtable 对象是一个修改的集合,修改包括两种:插入和删除。在 Cassandra 中,删除并不直接删除数据,而只是添加一条特殊的修改记录;修改某一个 column 也并不直接修改原有的数据,而是插入一条新的记录。一条修改记录对应一个 ColumnFamily 对象,需要注意的是,在 Cassandra 的代码里面,ColumnFamily 对象表示对某个 Column family 的修改,而不是表示这个 CF 的全部内容。Memtable 类包含一个 NonBlockingHashMap<DecoratedKey, ColumnFamily> 域,这个哈希表用于从 row-key 查找对应的 ColumnFamily 对象。

ColumnFamily 对象是一个 Column 对象的集合,同样的,每一个 Column 对象表示对某一列的修改。

在一定的条件下,Memtable 里面的修改记录会被 flush 到磁盘上,flush 之后会创建一个新的 Memtable。数据在磁盘上按照 SStable (Sorted String Table) 的格式保存,一个 SSTable 包括三个文件

  • cfname-seqno-Data.db:保存修改记录的主数据文件;
  • cfname-seqno-Index.db:从 row-key 到相应的 ColumnFamily 在 data.db 文件中的偏移量的索引;
  • cfname-seqno-Filter.db:Bloom filter,是 row-key 的集合,用于快速检查一个 row-key 对应的 CF 修改是否在 cfname-seqno-Data.db 文件中存在。

其中,cfname 为 column family 的名字,seqno 用于区分同一个 CFS 的多个 SSTable,seqno 依次递增。

Data.db 文件按照 row-key 的顺序保存了 Memtable 中所有的 CF 修改信息。Memtable 中每个 ColumnFamily 在 Data.db 文件中按照如下格式保存

  1. bloom filter length
  2. bloom filter:关于 column name 的 Bloom filter,用于快速查找某个列是否被修改;
  3. index size
  4. index info 表。由于 column 按照 name 的顺序保存,所以可以通过这个索引表来快速查找。每个 index info 包括
    1. first column name:起始 column
    2. last column name:结束 column
    3. offset:起始 column 在 column 表中的偏移量
    4. width:起始 column 到结束 column 之间所有 column 的大小之和
  5. column 表:column 的修改信息。

逻辑上看,在 SSTable 中所有的 CF 按照 row-key 排序,每个 CF 的 column 按照 column name 排序,所以利用上述的多级索引结构可以很快查找到一个 column。

 


本地读/插入/删除操作的实现

Cassandra 支持三个主要的操作:read, insert, remove。操作的基本数据单元是一个 column。

在传统的文件系统或者数据库中,insert 操作需要对系统的元信息和数据进行更新,而 remove 操作则需要对元信息进行更新。Cassandra 的设计思路与这些系统不同,无论是 insert 还是 remove 操作,都是在已有的数据后面进行追加,而不修改已有的数据。这种设计称为 Log structured 存储,顾名思义就是系统中的数据是以日志的形式存在的,所以只会将新的数据追加到已有数据的后面。Log structured 存储系统有两个主要优点:

  • 数据的写和删除效率极高。传统的存储系统需要更新元信息和数据,因此磁盘的磁头需要反复移动,这是一个比较耗时的操作,而 Log structured 的系统则是顺序写,可以充分利用文件系统的 cache,所以效率很高;
  • 错误恢复简单。由于数据本身就是以日志形式保存,老的数据不会被覆盖,所以在设计 journal 的时候不需要考虑 undo,简化了错误恢复。

但是,Log structured 的存储系统也引入了一个重要的问题:读的复杂度和性能。理论上说,读操作需要从后往前扫描数据,以找到某个记录的最新版本。相比传统的存储系统,这是比较耗时的。

插入操作

Cassandra 实现单次插入操作的函数为 CassandraServer.insert,该函数的原型如下:

public void insert(String table, String key, ColumnPath column_path, byte[] value, long timestamp, int consistency_level)

其中,ColumnPath 类描述了需要插入的数据在 Cassandra 的索引层次结构中的路径,包括 column_family, super_column, column 三个主要的域。由这个函数的参数就可以唯一地确定数据需要插入的位置。Cassandra 首先通过 table, key, column_family 几个参数确定存储该数据的节点,然后将 table, key, column_family, super_column, column 这几项信息封装在一个 RowMutation 数据结构里面,分配到选定的节点上执行插入操作。

我们先来看一下 RowMutation 这个数据结构,它包括三个主要的域:

  • table:数据所在的 table;
  • key:column family 对应的 row-key;
  • modifications:类型为 Map<String, ColumnFamily>,是一个 CF 的集合,每个 CF 是对一个 column family 的修改。

RowMutation 对象被发送到每个节点之后,节点调用 Table.apply 函数来将修改插入到本地存储中。本地执行插入操作的流程如下:

  1. 将修改记录到 commit log 里面,需要注意的是,每次添加修改到 commit log 之后并不立即将 commit log flush 到磁盘上,所以该操作不会阻塞;
  2. 对 RowMutation 中的每个 CF,找到对应的 CFS,然后调用 ColumnFamilyStore.apply 函数。该函数将 CF 加入到 memtable 中,如果 memtable 的大小或者包含的对象个数超过阈值,则创建一个新的 memtable,并返回当前的 memtable 以便将它的内容 flush 到 SSTable 中,否则返回 null;
  3. 对于每个 CFS 的 apply 函数返回的非空的 memtable,Table.apply 函数都需要将它 flush 到 SSTable 中持久保存。flush 的操作由 ColumnFamilyStore.maybeSwitchMemtable 函数来执行,大致流程如下:
    1. 调用 submitFlush 函数将 memtable flush 到 SSTable。flush 分为两个阶段:(1)对 memtable 中的 CF 按照 row-key 进行排序;(2)将排序好的 CF flush 到 SSTable 中。前一个操作是计算密集型的,而后一个操作是 I/O 密集型的,所以将这两个操作分配到两个不同的线程池中执行,提高并行性。
    2. 当 memtable 的内容确信已经写到磁盘上之后,需要删除 commit log 中相应的修改记录。submitFlush 函数返回一个 condition,当负责 flush 的线程完成了写磁盘操作,就会 signal 这个 condition。所以,switchMemtable 函数在 submitFlush 函数返回之后,会启动一个新的线程,等待该 condition 被 signal,之后就可以安全地执行删除 commit log 的操作了。

删除操作

删除一个 column 其实只是插入一个关于这个 column 的墓碑(tombstone),并不直接删除原有的 column。该墓碑被作为对该 CF 的一次修改记录在 Memtable 和 SSTable 中。墓碑的内容是删除请求被执行的时间,该时间是接受客户端请求的存储节点在执行该请求时的本地时间(local delete time),称为本地删除时间。需要注意区分本地删除时间和时间戳,每个 CF 修改记录都有一个时间戳,这个时间戳可以理解为该 column 的修改时间,是由客户端给定的。

由于被删除的 column 并不会立即被从磁盘中删除,所以系统占用的磁盘空间会越来越大,这就需要有一种垃圾回收的机制,定期删除被标记了墓碑的 column。垃圾回收是在 compaction 的过程中完成的。Compaction 过程在后面介绍。

读操作

Cassandra 中读操作的接口有以下五个:

public ColumnOrSuperColumn get(String table, String key, ColumnPath column_path, ConsistencyLevel consistency_level)
public List<ColumnOrSuperColumn> get_slice(String keyspace, String key, ColumnParent column_parent, SlicePredicate predicate,
 ConsistencyLevel consistency_level)
public Map<String, ColumnOrSuperColumn> multiget(String table, List<String> keys, ColumnPath column_path, ConsistencyLevel consistency_level)
public Map<String, List<ColumnOrSuperColumn>> multiget_slice(String keyspace, List<String> keys, ColumnParent column_parent,
 SlicePredicate predicate, ConsistencyLevel consistency_level)
public List<KeySlice> get_range_slices(String keyspace, ColumnParent column_parent, SlicePredicate predicate, KeyRange range,
 ConsistencyLevel consistency_level)

这些接口形式上虽然不同,但是它们的底层操作都是一样的:对客户端指定的key集合 keys = {k_1, k_2, ...} 中的每个 k_i,利用 (k_i, cf-name) 确定一个 Column Family,然后根据一定的标准从中选择出一个 column 集合并返回。

存储节点首先根据 cf-name 在本地找到相应的 CFS,这个 CFS 包括了一个或者多个 Memtable以及多个 SSTable。每个 Memtable 或者 SSTable 里面都可能包含 keys 集合中的一部分 key。本地的读操作主要有两个步骤:

  • 对每一个 Memtable 或者 SSTable,读取一个结果集合 r = {(k, cf)},其中每个 k 都属于集合 keys,且 cf 中的 column 必须满足客户端指定的条件。每个结果集合都已经根据 k 来排好序。
  • 对上一步得到的多个结果集合进行归并(merge)为一个结果集合:将 k 相等的 cf 合并为一个 cf,最后得到最终的结果集合 R = {(k_1, cf_1), ....}。

由于每个结果集合 r 都已经根据 k 来排序,所以可以使用多路归并算法来找到这些集合中所有 k 相等的 cf。cassandra 使用 Apache 的 IteratorUtils.collatedIterator 类来实现多路归并,这个类实现了一个最小堆,可以从给定的一个迭代器集合里面找到下一个值(next)最小的迭代器并返回它的值。cassandra 得到每个集合 r 的迭代器,并且将他们作为参数传给 collatedIterator。由于 collatedIterator 每次调用 next() 都会返回给定的迭代器中值最小的值,所以所有 k 相等的 cf 会被连续返回。接下来就是对这些 k 相等的 cf 进行合并了。

cf 中的每一个 column 都是关于这个 column 的一次修改,而我们要得到最新的修改,所以合并 cf 的过程就是保留同名的 column 中时间戳最新的那个 column。

可见,为了提高写的性能,cassandra 增加了读操作的复杂度,因此读操作的性能会相对较低。不过,可以通过在每个存储节点上增加 cache 来提高读的性能,在 Cassandra 0.6.1 版本里面已经加入了这个优化。这种设计体现了 Log structured 文件系统最初提出者的思想:写操作是主要的性能瓶颈,而读操作可以通过 cache 来提高性能,因此需要让文件系统尽量优化写。

Compaction

简单地说,compaction 就是将一个 CFS 中的多个 SSTable 合并为一个。Cassandra 里面 compaction 机制主要有三个功能:

  • 垃圾回收:上面提到cassandra并不直接删除数据,因此磁盘空间会消耗得越来越多,compaction 会把标记为删除的数据真正删除;
  • 提高读效率:compaction 将多个 SSTable 合并为一个,因此能提高读操作的效率;
  • 生成 MerkleTree:在合并的过程中会生成关于这个 CFS 中数据的 MerkleTree,用于与其他存储节点对比以及修复数据。

Cassandra 中的 compaction 分为 minor, major, cleanup, readonly, anti-entropy 几种,它们各实现了上面几个功能中的一个或者几个,其中 major compaction 实现了上述所有三个功能,因此我们在这里只对它进行介绍。MerkleTree 以及数据修复在Cassandra 中的数据一致性中介绍。

cassandra 每增加一个新的 SSTable,就会调用 CompactionManager.submitMinorIfNeeded 函数,这个函数当该 CFS 中的 SSTable 数量达到一定阈值就会触发 compaction 操作,具体的 compaction 操作在 doCompaction 函数中完成。compaction 操作其实就是将一个 CFS 的所有 SSTable 合并成一个 SSTable,然后将原有的 SSTable 删除。如果是 major compaction 的话,在合并过程中不会保留标记为删除的 column 或者 CF。

Commit Log

在数据库领域,commit log 可以分为 undo-log, redo-log 以及 undo-redo-log 三类,由于 cassandra 不会覆盖已有的数据,所以无须使用 undo 操作,因此它的 commit log 使用的是 redo log。commit log 的文件格式以及相关的操作在 CommitLog.java 文件开始的注释里面已经有比较详细的说明:

 

我们在这里只是对那里没有说明的几个要点进行补充:

  • Cassandra 中所有 Table 共用一个 commit log,那么如何区分不同 CF 的 commit log 记录呢?cassandra 在启动加载配置文件的时候会给所有的 CF 分配一个全局的 ID,这个 ID 在 commit log 中被用于区分不同 CF 的记录,这个 ID 也被用作 CommitLogHeader 中两个数组的下标。
  • Cassandra 每次重启的时候会从 commit log 中恢复数据,最后删除 commit log 文件。
  • 如果对 cassandra 的配置文件进行了修改,增加或者删除了一个 CF,那么原有的 commit log 就不能工作了,有可能引起数据的丢失。因此,安全的做法是:
    • 在修改配置之前,先重启一次 cassandra,并且保证没有任何写操作,经过这次重启,上次的 commit log 已经被删除;
    • 修改配置,并再次重启 cassandra,这样就能保证 commit log 头部的格式是正确的。

 


分布式读/插入/删除操作

Cassandra 的一个特性是可以让用户指定每个读/插入/删除操作的一致性级别(consistency level)。Casssandra API 目前支持以下三种一致性级别:

  • ZERO:只对插入或者删除操作有意义。负责执行操作的节点把该修改发送给所有的备份节点,但是不会等待任何一个节点回复确认,因此不能保证任何的一致性。
  • ONE:对于插入或者删除操作,执行节点保证该修改已经写到一个存储节点的 commit log 和 Memtable 中;对于读操作,执行节点在获得一个存储节点上的数据之后立即返回结果。
  • QUORUM:假设该数据对象的备份节点数目为 n。对于插入或者删除操作,保证至少写到 n/2+1 个存储节点上;对于读操作,向 n/2+1 个存储节点查询,并返回时间戳最新的数据。

如果用户在读和写操作的时候都选择 QUORUM 级别,那么就能保证每次读操作都能得到最新的更改。另外,Cassandra 0.6 以上的版本对插入和删除操作支持 ANY 级别,表示保证数据写到一个存储节点上。与 ONE 级别不同的地方是,ANY 把写到 hinted handoff 节点上也看作成功,而 ONE 要求必须写到最终的目的节点上。

 

 

 


最终一致性的维护

简单地说,维护系统中数据的最终一致性的方法就是定期地检查数据备份是否一致,如果不一致则及时采取同步措施。在 Cassandra 里面,通过三个机制保证系统的最终一致性:

  • anti-entropy
  • read repair
  • hinted handoff

anti-entropy

Anti-Entropy 是 Cassandra 维护最终一致性的主要机制,entropy 就是熵的意思,在物理学中代表混乱、不一致的程度,anti-entropy 则是维护一致性的意思。

Cassandra 使用分布式哈希表(DHT)来确定存储某一个数据对象的节点。在 DHT 里面,负责存储的节点以及数据对象都被分配一个 token。token 只能在一定的范围内取值,比如说如果用 MD5 作为 token 的话,那么取值范围就是 [0, 2^128-1]。存储节点以及对象根据 token 的大小排列成一个环,即最大的 token 后面紧跟着最小的 token,比如对 MD5 而言,token 2^128-1 的下一个 token 就是 0。Cassandra 使用以下算法来分布数据:

  • 首先,每个存储节点被分配一个随机的 token,该 token 代表它在 DHT 环上的位置;
  • 然后,用户为数据对象指定一个 key(即 row-key),Cassandra 根据这个 key 计算一个哈希值作为 token,再根据 token 确定对象在 DHT 环上的位置;
  • 最后,该数据对象由环上拥有比该对象的 token 大的最小的 token 的节点来负责存储;
  • 根据用户在配置时指定的备份策略,将该数据对象备份到另外的 N-1 个节点上。网络中总共存在该对象的 N 个副本。

因此,每个存储节点最起码需要负责存储在环上位于它与它的前一个存储节点之间的那些数据对象,而且这些对象都会被备份到相同的节点上。我们把 DHT 环上任何两点之间的区域称为一个 range,那么每个存储节点需要存储它与前一个存储节点之间的 range。

因为 Cassandra 以 range 为单位进行备份,所以每个节点需要定期检查与它保存了相同的 range 的节点,看是否有不一致的情况。一种最简单的方法如下:

  • 为 range 中每个数据对象进行一次消息摘要,然后将 range 内的所有对象的token和消息摘对的集合 {(token, digest)} 要发送到需要检查的邻居;
  • 邻居也生成自己维护的该 range 中对象的消息摘要,将两个列表按照 token 排序之后通过归并算法就能找出所有不一致的和遗漏的数据对象;
  • 邻居将这些不一致和遗漏的对象发送给发起修复的节点。

这个算法的优点是精确,可以精确到每个对象,但是缺点是网络开销太大,因为一个range内的对象数量可能很多。由于不一致的对象比例往往是比较低的,所以传递所有对象的摘要值开销一般会比接下来传递需要修复的对象本身开销大。

为了降低开销,Cassandra 的 anti-entropy 算法降低了比较的精确程度:将 range 划分为 sub-range,取 sub-range 内的所有对象的摘要值的异或(XOR)作为该 sub-range 的摘要值,最后只是比较 sub-range 以及它的摘要值,如果发现某个 sub-range 不一致,则将整个 sub-range 内的对象都修复。那么,如何划分以及比较 sub-range 呢?Cassandra 使用了 MerkleTree。

Cassandra 中的 MerkleTree 是一个二叉树,整个树代表一个完整的 range,而树的每个叶子节点代表一个 sub-range,记录了该 sub-range 的摘要值,每个内部节点的摘要值是它两个子节点的摘要值的异或。因为只有代表同一个 range 的两个 MerkleTree 才能进行有意义的比较,而在 Cassandra 里面,由于存在不一致的情况,所以同一个 range 的两个备份的起始或者结束 token 可能会不同。比如,节点 A 的 range 是 {3, 4, 5, 6, 8, 10},而节点 B 上虽然也备份了这个 range 上的数据,但是由于存在不一致,这个 range 的 token 集合可能是 {4, 5, 6, 8, 10}。所以,如果直接从这两个 range 生成两棵树的话,它们之间的比较是不准确的。为了解决这个问题,Cassandra 中所有的 MerkleTree 所代表的 range 都是整个 DHT 环,比如以 MD5 为 token 的话,range 就是 [0, 2^128)(等价于[0, 0])。

从一个 range 构造 MerkleTree 分为两个步骤:

  1. 划分 sub-range 并且生成树结构
  2. 计算叶子节点的摘要值。

划分 sub-range 在 AntiEntropyService.Validator.prepare() 函数中进行。算法如下:

 

keys 是 range 中的数据对象的 token 列表。算法每次选出一个 token, 算法通过随机抽样来判断 range 中 token 的分布,再根据分布来划分 range:token 密集的部分划分得比较细,token 稀疏的部分划分得粗,以保证每个 sub-range 包含的 token 数量大致相等。具体的划分操作由 MerkleTree.split() 函数执行:

    

该函数调用 splitHelper 函数进行递归划分,如果一个 token 落在一个叶子节点上,则将该叶子节点对应的 sub-range 从中点划分为两个 sub-range,这样就能保证 token 越密集,划分的 sub-range 越小。需要特别注意的是,每个 sub-range 都是从中点划分的,而不是在 token 的位置划分,这为后面比较两个树的算法带来了便利。因此,如果 DHT 环的总长度是2^n的话,每个 sub-range 的长度是2^k (1<=k<=n),各个 sub-range 的 k 可以不同。

建立了树结构之后,通过 AntiEntropyService.Validator.add() 和 AntiEntropyService.Validator.complete() 函数计算叶子节点的摘要值。对于有 token 的 sub-range,首先生成每一个 token 对应对象的 SHA-256 摘要值,然后将它们进行异或得到 sub-range 的摘要值;对于没有 token 的 sub-range,摘要值设置为0。

给定两棵 MerkleTree,可以使用 MerkleTree.difference() 函数来对它们进行比较,该函数返回两棵树中最大连续不一致的 sub-range 列表。这个函数所接受的树必须是使用上述的 prepare 函数生成的,即每个 sub-range 在划分时都在中点划分。算法首先比较两个树的根节点的摘要值,如果不相等,则分别比较两个左子树的摘要值以及两个右子树的摘要值,如果其中一对子树的摘要值不相等,则递归比较这一对子树。在递归的过程中,算法收集所有最大的连续不一致的 sub-range。

上面已经把 MerkleTree 的原理讲完了,结合 Cassandra wiki 上关于 Anti-Entropy 的文章以及源代码应该就可以比较好地理解 Cassandra 中 AE 的工作原理了。

最后就是找到两个 range 不一致的 sub-range 之后,如果进行修复的问题了。节点会对不一致的 sub-range 进行一次 AntiCompaction,得到一个临时的 SSTable,最后通过 Streaming API 将这些文件发送到需要修复的备份节点。备份节点只需要将收到的 SSTable 作为一个新的 SSTable 保存下来,后续的读和 compaction 就会得到新的数据。

read repair

read repair 是指在客户端读取某一个 column 的时候,执行客户请求的存储节点会负责检查该 column 的各个备份是否一致,如果不一致则修复。修复的时机由用户指定的 consistency level 来决定。如果是 ONE,那么节点在获得存储节点返回的第一个结果后立即返回给客户,并且修复会在后台进行;如果是 QUORUM,那么节点执行以下的协议:

       // 1. Get the N nodes from storage service where the data needs to be
       // replicated
       // 2. Construct a message for read/write
        * 3. Set one of the messages to get the data and the rest to get the digest
       // 4. SendRR ( to all the nodes above )
       // 5. Wait for a response from at least X nodes where X <= N and the data node
        * 6. If the digest matches return the data.
        * 7. else carry out read repair by getting data from all the nodes.
       // 5. return success

执行节点向所有的 N 个备份节点发送读数据请求,但是只有其中一个要求返回数据,而其他的只需要返回对该数据的摘要,执行节点检查数据与摘要是否匹配,如果不匹配则发起 read repair 操作。(Cassandra 0.6.1 版本中,上述协议的实现似乎与描述的不一致,执行节点只会选择其中一个存储节点返回的摘要与数据的摘要进行比较,而不是将数据与 X 个节点返回的摘要都比较一遍。这样的实现可能会遗漏部分不一致的情况。)

read repair 的具体操作是:从所有备份节点(N 个)读取数据,然后选出最新的修改版本,再将该版本写到这些备份节点上,从而使得它们的版本一致。修改版本的新旧是根据时间戳来决定的。

hinted handoff

首先可以看看 Cassandra wiki 上关于 hinted handoff 的介绍文章

负责中转的节点将中转的数据与自己的数据存储在一起,然后在 System Table 里面增加一些引用。具体可以看 HintedHandoffManager.java 文件开始的注释:

/**
 * For each table (keyspace), there is a row in the system hints CF.
 * SuperColumns in that row are keys for which we have hinted data.
 * Subcolumns names within that supercolumn are host IPs. Subcolumn values are always empty.
 * Instead, we store the row data "normally" in the application table it belongs in.
 *
 * So when we deliver hints we look up endpoints that need data delivered
 * on a per-key basis, then read that entire row out and send it over.
 * (TODO handle rows that have incrementally grown too large for a single message.)
 *
 * HHM never deletes the row from Application tables; there is no way to distinguish that
 * from hinted tombstones!  instead, rely on cleanup compactions to remove data
 * that doesn't belong on this node.  (Cleanup compactions may be started manually
 * -- on a per node basis -- with "nodeprobe cleanup.")
 *
 * TODO this avoids our hint rows from growing excessively large by offloading the
 * message data into application tables.  But, this means that cleanup compactions
 * will nuke HH data.  Probably better would be to store the RowMutation messages
 * in a HHData (non-super) CF, modifying the above to store a UUID value in the
 * HH subcolumn value, which we use as a key to a [standard] HHData system CF
 * that would contain the message bytes.
 *
 * There are two ways hinted data gets delivered to the intended nodes.
 *
 * runHints() runs periodically and pushes the hinted data on this node to
 * every intended node.
 *
 * runDelieverHints() is called when some other node starts up (potentially
 * from a failure) and delivers the hinted data just to that node.
 */

分布式删除问题

在一个完全分布式的系统里面,删除数据是一个很难的问题,Cassandra 中删除数据的策略是比较有启发性的。

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值