写操作

基于MemTable和SSTable的设计,Cassandra写磁盘都是顺序写,没有seek操作,使得写操作非常快速。 基于commitlog和hinted handoff的基址,写入总是高可用的。在每个row中,写操作是原子的。

Cassandra提供了可调一致性,在执行写操作时可以指定想要的一致性级别。在更高的一致性下,需要同步写入更多的节点,同时也降低了可用性。

  • ANY 最少写入一个节点,即使是hinted handoff情况
  • ONE/TWO/THREE
  • LOCAL_ONE 本地DC
  • QUORUM
  • LOCAL_QUORUM 本地DC
  • EACH_QUORUM 每个DC
  • ALL 写入replication factor的所有副本

写请求处理流程

write-path

  • 客户端连接集群中的某个节点作为协调节点,协调节点使用partitioner确定哪些节点包含对应的副本
  • 协调器向本地副本发送写请求,如果是多DC,本地协调器会选择其它每个DC的远程协调器,向它们发送写请求
  • 在线的需要写入的副本都会收到写请求,对于不在线的节点,会出现不一致的数据,将会被 hinted handoff/read repair/anti-entropy repair 其中的某种机制修复
  • 协调器等待副本响应,一旦达到一致性级别要求的副本数量,即可向客户端回复。
  • 如果某个节点下线,会为它生成hint

副本节点内部的处理流程

inter-node-write-path

  • 副本节点收到写请求后,立即写入commitlog,然后再写入memTable
  • 如果使用了row caching并且row被缓存,则cache中对应的row失效
  • 如果commitlog或memTable达到阈值,则执行flush操作
  • 此时即认为写操作在本节点成功,回复协调节点

另外,

  • 在某个时刻,节点执行flush操作,每个memTable将会被刷到磁盘的SSTable中,然后清空commitlog。
  • 在flush结束后,检查是否需要执行compaction

磁盘文件

commit log

  • 文件名格式:CommitLog-.log

SSTable

  • flush会生成SSTable,每个SSTable包含一组文件
  • 文件命名 --.db
  • 每次生成新的SSTable时,generation都会递增
  • component的可能类型
    • Data.db SSTable数据文件,在Cassandra备份时,仅备份该文件
    • CompressionInfo.db 包含对Data.db压缩情况的元数据
    • Digest.crc32 db文件校验
    • Filter.db 该SSTable的bloom过滤器
    • Index.db 数据文件内row和column的索引,会加载到内存中
    • Summary.db 保存index的摘要
    • Statistics.db 统计数据
    • TOC.txt 该SSTable的component列表

Lightweight Transactions

LWT基于Paxos算法,支持以下语义

  • INSERT语句中的IF NOT EXISTS语句保证不会覆盖相同primary key的记录
  • INSERT语句中的IF EXISTS则仅在primary key存在时执行
  • UPDATE语句中的IF 语句可以在写入执行前提供检查操作,并保证读写的原子性,可以用于扣库存操作
    • 如果条件检查失败,语句会返回当前值,用户程序可以判断是否重试或终止

以上的条件写语句,可以格外指定顺序一致性级别 serial consistency level,用来决定在Paxos协商阶段参与的节点数量

  • SERIAL 至少quorum个节点
  • LOCAL_SERIAL 每个DC至少quorum个节点

Batches

batch操作将多个修改放在一个语句,用来降低客户端和协调器之间的网络耗时。

LWT限制了仅在一个partition上使用,而batch机制允许将多个修改请求放在同一个语句中,且可以跨partitions。

  • 只有修改操作 INSERT/UPDATE/DELETE 操作可以放在batch中
  • 分为logged/unlogged
  • batch不提供事务,但允许将LWT语句放在batch中执行。batch中的多个LWT必须使用同一个partition。
  • batch有尺寸上限

使用场景

  • 对单个partition做多次更新
  • 保持多个表同步
  • 更新具有相同partition key的不同表

注意,batch操作并不会提升修改的性能,它只是避免多次请求的网络延时。batch实际上会损耗性能并导致GC。

关于logged batch和unlogged batch

  • logged batch
    • 协调器发送batch的拷贝(batchlog)给另外两个节点,如果协调器执行batch失败,另外两个节点会定时判断是否有未完成的batch需要执行
    • logged会给协调器增加编排负担;使用 BEGIN BATCH – APPLY BATCH 包裹多条语句。
    • logged batch提供同一个partition下多个修改操作的原子性。对于不同partition的操作,可能在batch执行结束前就能被读到。
  • unlogged batch
    • 不涉及batchlog的操作,适用于快速插入大量数据
    • 不保证不同partition的所有数据都会成功写入,如果batch仅写一个partition就不会有问题

读操作

客户端可以连接集群中的任一节点进行读操作,如果该节点没有需要的数据,则作为协调器去目标节点读取数据。

由于读取SSTable需要文件IO,读操作比写操作要慢。可以通过增加节点、增加内存、使用cache的方式让更多的数据留在内存。

根据一致性级别和副本因子,读操作需要同步等待若干节点的相应,并根据需要执行read repair。

Read Consistency Levels

更高的一致性级别意味着需要读取更多的节点,来保证读到准确的数据。如果两个节点返回值的时间戳不同,则选择时间戳最新的那个。 如果发现旧数据,会执行 read repair进行修复。

读一致性级别:

  • ONE/TWO/THREE 立即返回第一个响应的节点的值,然后在其他副本上检查一致性,对于过期数据执行read repair。可能读到旧值。
  • LOCAL_ONE 本地DC的一个节点
  • QUORUM 读取所有节点,多数节点响应后,返回最近的时间戳,必要时对剩余节点执行read repair
  • LOCAL_QUORUM 类似QUORUM,只要求本地DC
  • EACH_QUORUM 要求每个DC的多数节点响应
  • ALL 读取并等待所有节点响应,返回最近的时间戳并进行read repair

可调的读写一致性级别可以实现一致性和性能之间的权衡。

读请求处理流程

read-path

  • 客户端向某个节点发送读请求,该几点作为协调器节点
  • 根据partition确定副本位置,检查是否有足够的副本满足一致性级别要求
  • 如果一致性要求多DC的读取,协调器还会请求其它DC的协调器执行读请求
  • 如果协调器不是目标副本,会向最快副本发送读请求(由dynamic snitch决定哪个副本是最快的)
  • 向剩余副本发送digest request请求来获取目标数据的哈希值
  • 协调器计算最快副本返回的结果的哈希值,和其它副本返回的哈希值对比
  • 如果相同,且符合一致性级别要求,则返回客户端
  • 如果不同,在返回结果后执行read repair

读请求内部处理流程

inter-node-read-path

  • 副本节点收到读请求后,首先检查row cache,如果包含数据,直接返回
  • 在memTables和SSTables中查询
    • 一个表对应一个内存memTable,但是可能对应多个SSTable
    • 加速SSTable读取的几个特性
      • key caching/bloomguolqi/SSTable index/summary index
  • 通过bloom过滤器检查是否在当前SSTable不存在需要的partition
  • 检查key cache中是否存在partition key对应的offset
  • 如果key cache中不存在对应关系,则通过summary index根据partition获取offset
  • 根据offset去SSTable中获取数据

从SSTable读取数据后,Cassandra会从memTable及SSTable中选择时间戳最近的值进行合并。 最终将合并后的结果返回给客户端或者协调节点。

对于digest的读请求,仅需要返回结果的哈希值。

Read Repair

  • 协调者向多个副本发起读请求后,会检查值的时间戳并返回最新的值
    • 如果同一个时间戳发现了不同的值,则选择字段序最大的那个返回。这种情况很少出现。
  • 如果副本返回的值不同,则发起read repair
  • read repair可能在响应客户端之前或者之后执行,取决于使用的一致性级别
    • 对于QUORUM/ALL级别,需要在返回客户端之前修复
    • 对于弱一致性级别(ONE等),可以在返回客户端后在后台执行

Range Queries, Ordering and Filtering

假如有这样的表定义约束

1
PRIMARY KEY (hotel_id, date, room_number)

则partition key为hotel_id,clustering column为date和room_number。

以下查询是合法的:

1
SELECT * FROM available_rooms_by_hotel_date WHERE hotel_id='AZ123' and date>'2016-01-05' and date<'2016-01-12';

以下查询是非法的:

1
SELECT * FROM available_rooms_by_hotel_date WHERE hotel_id='AZ123' and room_number=101;

WHERE字句有两个规则:

  • partition key中的所有元素都必须给定值
  • clustering column中,如果有某个元素没有指定条件,则不允许对其后的元素指定条件

这个限制基于Cassandra在磁盘上存储数据的方式,是基于partition key分区,基于clustering column排序的,只能读取连续的rows。

当然也可以使用ALLOW FILTERING打破这个限制,但是不建议使用,因为性能极差。此时应该考虑修改表定义。

1
SELECT * FROM available_rooms_by_hotel_date WHERE date='2016-01-25' ALLOW FILTERING; # not recommended

IN字句可以用来指定column的多个可能取值

1
SELECT * FROM available_rooms_by_hotel_date WHERE hotel_id='AZ123' AND date IN ('2016-01-05', '2016-01-12');

如果在partition key上使用IN字句,会导致协调节点和大量的节点通信。此时应该考虑将查询按partition key拆分成多个并行的查询。

SELECT允许对clustering column列的结果重排序

1
2
SELECT * FROM available_rooms_by_hotel_date
WHERE hotel_id='AZ123' and date>'2016-01-05' and date<'2016-01-12' ORDER BY date DESC;

Paging

对于分页操作,一般使用客户端库的API操作。

  • 如果在服务端分页,可以使用分页对象page在内存中操作
  • 如果在请求方分页,服务端无法保存状态,可以将page对象加密序列化后发送给请求端,在请求下一页时再发送。

删除操作

Cassandra中的删除,并是不立即移除数据。

  • 如果Cassandra的删除设计成立即执行,如果某个节点宕机错过删除事件,在它恢复后会认为其它节点丢失了这个数据的写入(反熵),因此又把记录发送给其它节点,导致记录复活
  • Cassandra中使用tombstone墓碑机制执行删除操作

特性:

  • 墓碑是在DELETE操作时放置了一个特殊的标记,如果有副本没有收到删除请求,在副本节点恢复后,将会收到来自其它节点的墓碑信息。
  • 因此DELETE操作相当于一次写操作
  • 墓碑的副作用是,数据存储空间不会在DELETE操作后立即减小。
  • 每个节点都会跟踪墓碑的时间,默认最长保留10天,此时随着compaction操作的执行,墓碑会被删除,相应的磁盘空间也会被释放
    • SSTable无法修改,只会在compaction执行时生成一份新的
  • DELETE操作的本质是写入墓碑,因此写操作的一致性级别对于DELETE操作同样适用。
  • 在一条命令中删除的数据越多,使用的墓碑越少。
  • 如果应用生成了大量的墓碑,将会严重影响读取性能(因为在读取SSTable时需要遍历这些墓碑)

建议:

  • 可以在插入数据时指定TTL属性,Cassandra会自动删除过期数据
  • 对于有时间序列模式的表,可以使用 TimeWindowCompactionStrategy的压缩策略,允许Cassandra一次性删除完整的SSTable。

DELETE支持不同的粒度:

  • 删除一个集合中的数据(set/list/map)
  • 删除非主键的column
  • 删除完整的row
  • 删除符合WHERE字句条件的rows
  • 删除一个partition