Cassandra在2008年7月被Facebook开源。Cassandra最初的版本主要是由亚马逊(Amazon)和微软(Microsoft)的一名前雇员编写的。它深受亚马逊首创的分布式key/value数据库Dynamo的影响。Cassandra实现了一个没有单点故障的动态复制模型,但是添加了一个更强大的列家族数据模型。下面我基于对它的理解说说它的一些概念。
cassandra集群的token的区间在 负2的63次方 2的63之间. 集群中所有的节点分分隔这个token环,老版本的cassandra采用静态分隔的方式,需要手工配置每个节点的token范围,并提供工具用于平均分隔这个环,但静态分隔维护困难且易出错,容易形成热点。在新版本的cassandra引入了虚拟节点(vnodes)概念,它把这个token环分隔为很多固定的段,且体多少段由每个节点的“num_tokens”配置决定,默认为256。初始时,cassandra把所以节点的num_tokens值相加,得到这个集群内存在vnodes的数量,然后把token均分,打散分散到各节点中。
数据通过分区器(默认为Murmur3Partitioner)计算出hash值,比较决定落入哪个vnodes。
一旦有数据进集群就不能修改分区器,配置参数为“partitioner”。用于把数据划归哪个vnodes的算法。默认分区器是org.apache.cassandra.dht.Murmur3Partitioner,但为兼容旧版本,还支持RandomPartitioner分区器,它是MD5方式,它比Murmur3Partitioner慢很多,所以被替换。cassandra还支持org.apache.cassandra.dht. OrderPreservingPartitioner分区器,这个分区器就是按key的顺序来分区,且物理存储也是按key的字母表顺序来存储,这就与Hbase的分区规则一样了,对key的设计非常重要,也就是把本该由系统来处理的数据倾斜问题上升为业务自己来处理,确实需要丰富的业务知识才能完成这一任务。如果key较均匀分布确实很棒,区间查询速度会快一些,但如果中key并不能按我们预想的那么均匀,那么它的查询并不会比Murmur3Partitioner快。另外支持的分区器是:ByteOrderedPartitioner,如果key不会转为string,可以考虑使用,这应该是适用于较特殊的领域了。
虽然Murmur3Partitioner随机选择令牌,但它仍然容易受到热点的影响;但是,与保留顺序的分区器相比,这个问题大大减少了。事实证明,为了最小化热点,需要额外的拓扑知识。为了解决这个问题,3.0中增加了对令牌选择的改进。在cassandra中配置"allocate_tokens_for_keyspace"属性。具有特定名称空间,指示分区程序基于该名称空间的复制策略优化令牌选择。如果集群只有一个名称空间,或者所有名称空间都具有相同的复制策略,那么这种方法非常有用。在3.0版本中,这个选项只对Murmur3Partitioner可用。
先看一段名称空间创建的示例:
CREATE KEYSPACE my_keyspace WITH replication =
{'class': 'SimpleStrategy',
'replication_factor': 1} AND durable_writes = true;
上面class就是指需要在这个名称空间上需要实施的复制策略,要写的第一个副本就是按分区器来分派的,replication_factor是指要复制的份数,即数据备份到1台服务器,但不能超过集群机器数量。否则会报"Not enough replicas available for query at consistency QUORUM "异常。命名空间的这个配置可以修改“ALTER KEYSPACE 名称空间 WITH replication = {‘class’: ‘SimpleStrategy’, ‘replication_factor’: ‘1’}”,如果减小了复制份数,需要在每个节点上执行命令: nodetool clean 完成数据的清理。常用的复制策略如下:
通过“endpoint_snitch”属性进行配置,告密者会收集cassandra网络拓扑结构的信息,以便Cassandra能够有效地路由请求。告密者会找出节点与其他节点之间的关系(推断数据中心是复制策略的工作)。通过更新cassandra中的"endpoint_snitch"属性进行配置。它的实现都在包:org.apache.cassandra.locator. 中并且实现 了IEndpointSnitch接口。
cassandra还提供了一个称为动态监控的特性,可以帮助优化读写路由。它是这样工作的。您选择的告密者被另一个称为DynamicEndpointSnitch的告密者包裹。动态告密者从所选的告密者获得对拓扑的基本理解。然后监控对其他节点的请求的性能,甚至跟踪哪些节点正在执行压缩。性能数据用于为每个查询选择最佳副本。这使
Cassandra避免将请求路由到性能较差的副本。cassandra的读取数据时,判断要读的副本是否本地有存储,如果没有在本地存储,则由动态告密者(dynamic snitch)那里知识一个速度最快的节点发送内容,同时向其它节点(由读一致性决定发送几个节点)发送摘要请求。所以配置合适的告密者对读性能非常重要。
动态告密者如何判断节点性能最佳呢?cassandra 修改了的gossip协议,加入了Phi增量故障检测算法。为了避免由于网络波动而频烦修改性能最佳节点,可以合理配置 “dynamic_snitch_badness_threshold”属性,想要成为新的最佳节点,那么它必须比擂主(旧的最佳节点)的性能好于这个阀值才能替换掉擂主,每个节点的得分都会定期重置,以便性能较差的节点能够证明它已经恢复了最佳状态。常用的告密者有:
SimpleSnitch: 它不理解网络的拓扑结构,所以它不知道集群中的机架和数据中心,这使得它不适合多数据中心部署,注意,如果使用了此告密者,那个名称空间的复制策略也要设置为“SimpleStrategy”
PropertyFileSnitch: 用于配置的属性文件cassandra-topology.properties提供网络拓扑。如果使用此告密者或者其它具有机架感知功能的告密者时,那个名称空间的复制策略必须使用“NetworkTopologyStrategy”,这种方式对于频繁更改节点时不太方便,你让cassandra来计算数据中心和机架位置还不能直接告诉它来的更安全和直接,也能达到更好的性能。
GossipingPropertyFileSnitch:这也是一个具有机架感知功能的告密者,它通过gossip协议来传达整个cassandra集群的拓扑结构,每个节点都在cassandra-rackdc.properties文件中定义好自己所属的数据中心和机架信息,然后通过gossip协议来进行交换,最后形成一个整体的拓扑结构。如果存在cassandra-topology.properties文件,那么它也会起作用。
RackInferringSnitch:这个具有机架感知的告密者是跟据节点的ip地址来猜测数据中心和机架的。这个要求集群中的节点采用一致的网络方案。如果两个主机的IP地址的第二段有相同的值,那么它们将被确定位于同一个数据中心。如果两个主机的IP地址的第三段有相同的值,那么它们将被确定在同一个机架上。
cassandra如何执行查询数据操作?对于开发人员可以不关心,但对于架构师却不能不理,理解它对于性能优化,模型设计都会有帮助。
读路径:判断要读的副本是否是本地,如果没有在本地则由动态告密者(dynamic snitch)那里拿到一个性能最好的节点发送内容请求,同时向其它节点(由读一致性决定发送几个节点)发送摘要请求(摘要请求类似于标准读取请求,只是副本返回所请求数据的摘要或散列。),本地协调者从最快节点那拿到了内容和摘要信息,再与其它节点的摘要进行比较,如果摘要相同返回结果给客户端,如果摘要不同,则要发送读修复请求。
查询分以下几部:
Cassandra实现了几个特性来优化SSTable搜索: 键缓存、Bloom过滤器、SSTable索引和摘要索引。
读修复触发时机:
读取修复可以在返回客户机之前或之后执行。如果您正在使用两个更强的一致性级别之一(QUORUM或ALL),那么读取修复将在数据返回给客户机之前进行。如果客户端指定了一个弱一致性级别(比如One),那么在返回客户端之后,可以选择在后台执行read repair。导致给定表的后台修复的读操作的百分比由表的read_repair_chance和dc_local_read_repair_chance选项决定。
因为采用了强一致性,那么副本发生不致的概率非常小,那么代价就不大,在返回给客户机之前进行修复是可取的。如果采弱一致性,谁也不能保证不一致的副本有多少,那么先返回给客户机结果再修复是较明智的设计,至于要不要异步地在后端修复那是由要修改的副本占比决定,read_repair_chance就是那个阀值,超过阀值那么异步地后端修复,没超过直接修复。
读修复:
协调器节点通过为每个请求列选择一个值来合并数据。它比较从副本返回的值,并返回具有最新时间戳的值。如果Cassandra发现不同的值存储在相同的时间戳中,它将按字典顺序比较这些值,并选择值较大的值。这种情况应该非常罕见。合并数据是返回给客户机的值。
cassandra如何落盘对于性能调优至关重要。
如果与客户端连接的节点不在同一个数据中心时,cassandra会在别的数据中心选择一个协调器,并把数据发送给协调器,由协调器发送给本数据中心的其它副本。
当然,这只是写路径的一个简单概述,没有考虑到诸如计数器修改和物化视图之类的变量。对具有实体化视图的表的写操作更为复杂,因为分区必须被锁定。Cassandra在内部利用已记录的批来维护物化视图。要更深入地讨论写路径,请参考Michael Edge在Apache Cassandra Wiki上的精彩描述。
cassandra特别灵活,各环境就可以自定义,向上面的读写路径的参数可配置,告密者、分区器、复制策略等都是可配置的。更进一步,cassandra可以让用户按需求执行自己配置的一致性级别。
级别 | 描述 |
---|---|
ONE, TWO,THREE | 立即返回响应查询的第一个节点所持有的记录。 创建一个后台线程来检查该记录与其他副本上的相同记录。 如果其中任何一个已经过期,则执行读修复将它们同步到最近的值。 |
LOCAL_ONE | 与第一个类似,附加的要求是响应节点位于本地数据中心。 |
QUORUM | 查询所有节点。一旦大多数副本((复制因子/ 2)+ 1)响应,返回给客户机 带有最新时间戳的值。然后,如果需要,在后台对所有剩余的副本执行读修复。 |
LOCAL_QUORUM | 与QUORUM类似,附加的要求是响应节点位于本地数据中心。 |
EACH_QUORUM | 确保每个数据中心中都有一个节点仲裁响应 |
ALL | 查询所有节点。等待所有节点响应,并将带有最新时间戳 的记录返回给客户机。然后,如果需要,在后台执行读修复。 如果任何节点没有响应,则读取操作将失败。 |
级别 | 描述 |
---|---|
ANY | 确保在返回客户机之前将值写入至少一个副本节点,如果有节点写入失败,允许将hints算作写入。 |
ONE, TWO, THREE |
在返回客户机之前,确保将该值写入至少一个、两个或三个节点的提交日志和memtable。 |
LOCAL_ONE | 与上面类似,附加的要求是响应节点位于本地数据中心。 |
QUORUM | 确保至少大多数副本都收到了写操作((复制因子/ 2)+ 1)。 |
LOCAL_QUORUM | 与QUORUM类似,其中响应节点位于本地数据中心。 |
EACH_QUORUM | 确保每个数据中心中都有一个节点仲裁响应。 |
ALL | 确保复制因子指定的节点数在返回到客户机之前接收到写操作。如果有一个副本对写操作没有响应,则操作失败。 |
通过CQL连接上的一致性级别为“ONE”
代码设置一致性
//全局设置一致性
QueryOptions queryOptions = new QueryOptions();
queryOptions.setConsistencyLevel(ConsistencyLevel.ANY);
Cluster cluster = Cluster.builder().addContactPoint("localhost").withPort(9042).withQueryOptions(queryOptions)
.build();
//statement设置一致性
PreparedStatement prepare = session
.prepare("INSERT INTO rjzjh.users (id, userName, com) VALUES (?, ?, ?) IF NOT EXISTS");
UUID id = UUID.fromString("756716f7-2e54-4715-9f00-91dcbea6cf52");
BoundStatement bind = prepare.bind(id, "zjh2", "xplat");
bind.setConsistencyLevel(ConsistencyLevel.ANY);
cassandra像其它nosql数据库一样不支持完整的ACID语义,但它仍支持轻量级的事物。它包含有下列语议
IF NOT EXISTS语法、、、、、、、、、、、、、、、、、、、、、、、、、
cqlsh> INSERT INTO hotel.hotels (id, name, phone) VALUES (
‘AZ123’, ‘Super Hotel at WestWorld’, ‘1-888-999-9999’) IF NOT
EXISTS;
UPDATE…IF
UPDATE hotel.hotels SET name=‘Super Hotel Suites at WestWorld’
… WHERE id=‘AZ123’ IF name=‘Super Hotel at WestWorld’;
检查事务结果:
private void insert() {
PreparedStatement prepare = session
.prepare("INSERT INTO rjzjh.users (id, userName, com) VALUES (?, ?, ?) IF NOT EXISTS");
UUID id = UUID.fromString("756716f7-2e54-4715-9f00-91dcbea6cf52");
BoundStatement bind = prepare.bind(id, "zjh2", "xplat");
bind.setConsistencyLevel(ConsistencyLevel.ANY);
ResultSet rs = session.execute(bind);
System.out.println("applied=" + rs.wasApplied());
if (rs.wasApplied()) {
Row row = rs.one();
System.out.println("applied=" + row.getBool("[applied]"));
} else {
Row row = rs.one();
System.out.println("applied=" + row.getBool("[applied]") + ",userName=" + row.getString("userName"));
}
}
当错误发生时,会输出旧的数据完整值:
applied=false
applied=false,userName=zjh
除了常规一致性级别外,条件写语句还可以具有串行一致性级别,CAS依赖Paxos协议来达成分布式共识。这些操作有一个paxos阶段和一个提交阶段。前者的一致性级别是在使用setSerialConsistencyLevel()的语句上设置的,而后者的一致性级别是使用常用的setConsistencyLevel()方法定义的。串行一致性当参与的节点正在协商提议的写时,必须回复的节点数量。下面显示了两个可用选项
级别 | 描述 |
---|---|
SERIAL | 这是默认的串行一致性级别,指示节点仲裁必须响应。 |
LOCAL_SERIAL | 类似于SERIAL,但表示事务只涉及本地数据中心中的节点。 |
虽然轻量级事务仅限于单个分区,Cassandra提供了一个批处理机制,允许将对多个分区的修改分组到一个语句中。批处理事物支持的语义如下:
三种批类型:
代码如下:
private void insertBatch() {
SimpleStatement st1 = new SimpleStatement("INSERT INTO rjzjh.users (id, userName, com) VALUES (?, ?, ?)",
UUID.fromString("756716f7-2e54-4715-9f00-91dcbea6cf53"), "zjh3", "xplat3");
SimpleStatement st2 = new SimpleStatement("INSERT INTO rjzjh.users (id, userName, com) VALUES (?, ?, ?)",
UUID.fromString("756716f7-2e54-4715-9f00-91dcbea6cf54"), "zjh4", "xplat4");
BatchStatement batch = new BatchStatement();
batch.add(st1);
batch.add(st2);
ResultSet rs = session.execute(batch);
System.out.println("applied=" + rs.wasApplied());
}
CQL:
cqlsh> BEGIN BATCH
INSERT INTO hotel.hotels (id, name, phone)
VALUES ('AZ123', 'Super Hotel at WestWorld', '1-888-999-9999');
INSERT INTO hotel.hotels_by_poi (poi_name, id, name, phone)
VALUES ('West World', 'AZ123', 'Super Hotel at WestWorld',
'1-888-999-9999');
APPLY BATCH;
批量操作指是的原子批,并不能提高性能,并不是批处理。第一次使用批量更新时,用户常常会混淆批量更新的性能。但事实批处理并不能提高性能,实际上会降低性能,并可能导致垃圾收集压力。
cassandra的查询限制条件较多,查询的条件必需是在PRIMARY KEY中出现的列,其它列不能用于查询。如:
PRIMARY KEY (hotel_id, date, room_number)
查询条件可以是: WHERE hotel_id=‘AZ123’ and date>‘2016-01-05’ and date<‘2016-01-12’;
但不能是:WHERE hotel_id=‘AZ123’ and room_number=101; 这样会报错:InvalidRequest: code=2200 [Invalid query] message=“PRIMARY KEY column “room_number” cannot be restricted as preceding column “date” is not restricted”
也就是说,前面的查询条件“date”没有出现时并不能出现后面的查询条件“room_number”。cassandra可以允许分区键“hotel_id”没有出现,只需要上关键字:“ALLOW FILTERING”,如:
WHERE date=‘2016-01-25’ ALLOW FILTERING;
但cassandra不推荐使用: ALLOW FILTERING 因为它可能导致非常昂贵的查询。如果您发现自己需要这样的查询,您将需要重新检查数据模型,以确保您已经设计了支持查询的表。
由于查询有诸多的限制,需要对模型的设计比较慎重,但在某些场景下面,却实又存在对非PRIMARY KEY上的查询要求,如之前的设计由于新的需求导致查询字段并没有出现在PRIMARY KEY中,这个时候可以考虑采用二级索引来实现。语法示例:
CREATE INDEX user_last_name_idx ON my_keyspace.user (last_name);
这样我也就可以直接用这个字段进行查询:WHERE last_name = ‘Nguyen’;
二级索引主要的难点是不能规整地映射到分区中,有两种主要的方法来支持二级索引进行分区:基于文档的分区和基于词条的分区。cassandra和Elasticsearch等采用的是基于文档分区的二级索引,它在分个分区上各自维护自己的二级索引,每个分区只管自己文档面不关心其它分区的文档,文档分区索引也称为本地索引,并不是全局索引。在读取时一般来说需要读所有分区(除非做做特殊处理才能达到只读一个分区)的二级索引,查询到结果后进行合并最后返回正结果。这种查询分区数据为听方法称为分散/聚集,很明显这种二级索引的代价确实高昂。所以要求我们能构建合适的分区方案,尽量由单个分区就能满足二级索引查询。
由于这个原因,涉及二级索引的查询通常涉及更多的节点(也许是全部节点),这样会使得查询的开销大大增加。我们不能比较随意设置我们的二级索引,cassandra对二级索引字段的优化有些要求,下面三种情况不能建索引:
高基数:字段全是唯一的,如:id ,email
低基数: 字段全是一样的,如:男、女
经常性做删除和更新的字段
Cassandra 3.4版本包含了二级索引的另一种实现,称为SSTable Attached secondary Index (SASI)。SASI是由苹果公司开发的,并作为Cassandra的二级索引API的开源实现发布。顾名思义,SASI索引是作为每个SSTable文件的一部分计算和存储的,而前面提到的旧的二级索引实现原理是做为一张独立的“隐藏”表。它的创建语法示例:
CREATE CUSTOM INDEX user_last_name_sasi_idx ON user (last_name)
USING 'org.apache.cassandra.index.sasi.SASIIndex';
SASI索引提供了传统二级索引实现不了的功能,比如在索引列上执行不等式(大于或小于)查询的能力。还可以使用新的CQL LIKE关键字对索引列进行文本搜索。例如,您可以使用以下查询来查找姓氏以N开头的用户:
SELECT * FROM user WHERE last_name LIKE 'N%';
尽管SASI索引确实比传统索引执行得更好,因为它不需要从其他表读取数据,但它无论如何也逃脱不了需要多节点的查询汇聚需求。
上面提及的二级索引,由于PRIMARY KEY没有改变,也就是意味了它的数据存储位置是不变的,无论做什么索引都不能改变需要查多个节点做汇聚步骤,查询的开销避免不了,物化视图则是最彻底的以空间换时间的方案,它可以把之间的非PRIMARY KEY列提升为PRIMARY KEY,由cassandra完成原数据到物化视图的数据更分发的工作。如:
它比较适合二级索引处理不了的情形,如“高基”字段,如上面的确认号"confirm_number",它不可能存在相同的确认号,那用它来做物化视图中的分区键就比较合适了。它也可以简化应用,如果要满足不同场景的查询需要多个逻辑模型,虽然是同一份数据,应用程序也要保证多份物理模型的同步工作,有了物化视图,就无需这么麻烦,应用程序只同步一份数据,通过物化视图,cassandra内部保证了多份模型的数据一致性。它会改变写路径,对写的性能会有些损耗。创建物化视图示例:
cqlsh> CREATE MATERIALIZED VIEW
reservation.reservations_by_confirmation
AS SELECT *
FROM reservation.reservations_by_hotel_date
WHERE confirm_number IS NOT NULL and hotel_id IS NOT NULL and
start_date IS NOT NULL and room_number IS NOT NULL
PRIMARY KEY (confirm_number, hotel_id, start_date, room_number);
主键列的分组使用与普通表相同的语法。最常见的用法是首先将附加列作为分区键,然后是基表主键列,用于物化视图的聚类列。AS SELECT子句标识了我们希望物化视图包含的基表中的列。我们可以引用单独的列,但在本例中,通过使用通配符*选择所有列都作为视图的一部分。物化视图也有一些限制条件:
物化视图必须包含基表主键中的所有列。这个限制使Cassandra避免将基表中的多行折叠成物化视图中的一行,这将大大增加管理更新的复杂性。
WHERE子句提供了对过滤的支持。注意,必须为物化视图的每个主键列指定一个过滤器,即使它非常简单,只需指定值不为空即可。
建表语句可以带“COMPACT STORAGE”关键字表示需要对这张表进行压缩存储。如:
CREATE TABLE playlists_2 ( id uuid, song_id uuid, title text,
PRIMARY KEY (id, song_id )
) WITH COMPACT STORAGE;
cassandra在存储到SSTable时,它会把记录数据进行合并压缩存储,如未采用压缩存储的数据:
$ sstable2json Metrics/playlists_1/*Data*
[
{
"columns": [
[
"7db1a490-5878-11e2-bcfd-0800200c9a66:",
"",
1436971955597000
],
[
"7db1a490-5878-11e2-bcfd-0800200c9a66:title",
"Ojo Rojo",
1436971955597000
]
],
"key": "62c3609282a13a0093d146196ee77204"
},
{
"columns": [
[
"aadb822c-142e-4b01-8baa-d5d5bdb8e8c5:",
"",
1436971955602000
],
[
"aadb822c-142e-4b01-8baa-d5d5bdb8e8c5:title",
"Guardrail",
1436971955602000
]
],
"key": "444c3a8a25fd431cb73e14ef8a9e22fc"
}
]
采用存储压缩的数据格式:
[
{
"columns": [
[
"7db1a490-5878-11e2-bcfd-0800200c9a66",
"Ojo Rojo",
1436972070334000
]
],
"key": "62c3609282a13a0093d146196ee77204"
},
{
"columns": [
[
"aadb822c-142e-4b01-8baa-d5d5bdb8e8c5",
"Guardrail",
1436972071215000
]
],
"key": "444c3a8a25fd431cb73e14ef8a9e22fc"
}
]
跟据相关的测试,结果显示无压缩存储将多需要大约 35% 的存储空间(并且任何额外的处理都需要如此)。这样就清楚地知道了在我们写入的数据卷当中,非压缩存储中 35% 的存储是没有被有效利用正常工作的。那么那里为什么会有一个选项?cassandra不直接全部实施压缩存储?
压缩存储也有它的限制条件,就是除了Partition Key和Clustering Key两部分组成的Primary Key字段外,只能出现列非Partition Key,而且也不能新增和删除表的字段。如果在你的数据模式中需要更多的列值,你有两个选择:不使用压缩存储,或者将他们序列化成单一列数据值。
与java的静态字段类似,静态列可以被多条记录共用,它对于同一个Partition Key使用的字段都是都存储一份的,这样不但大大减少了存储空间,而且对于管理也及为方便。下面是一个定义的示例:
CREATE TABLE "iteblog_users_with_status_updates" (
"username" text,
"id" timeuuid,
"email" text STATIC,
"encrypted_password" blob STATIC,
"body" text,
PRIMARY KEY ("username", "id")
);
上面由“STATIC”关键字标识的列都属于静态列。静态列也有下面的限制条件:
如果表没有定义 Clustering columns(又称 Clustering key),不能添加静态列.
如果建表的时候指定了 COMPACT STORAGE,这时候也不允许存在静态列。
如果列是 partition key/Clustering columns 的一部分,那么这个列不能说明为静态列。
静态列的这些特性用于做表的关联非常有用。因为 Cassandra 中是不支持 join 的,静态列相当于把两张表进行了 join 操作。我们可以对事实表进行建模,对把维表做成一个自定义类型,并设置为静态列。还有就是对明细表建模时把那个对应主档表的关联字段设置为partition key,那么再把主档表想引入的字段全部设置为静态列,是不是就达到了join的目的?而且此时,对主档的任何修改只要带一个“partition key”查询条件更新就可以了。这样明细表的所有关联在“partition key”下的记录都被同时修改了,对于在静态字段的任何distinct都不用担心性能。