内容


Apache Cassandra3.X 系列,第 2 部分

数据模型深入解释

Comments

系列内容:

此内容是该系列 6 部分中的第 # 部分: Apache Cassandra3.X 系列,第 2 部分

敬请期待该系列的后续内容。

此内容是该系列的一部分:Apache Cassandra3.X 系列,第 2 部分

敬请期待该系列的后续内容。

本文是 Apache Cassandra3.X 系列的第二篇文章,主要向读者介绍 Cassandra 数据模型,包括与关系型数据库之间的模型差别、Cassandra 自身的数据模型特性介绍及实验案例、源代码等,也会介绍 Cassandra 数据库的相关参数详细含义及注意事项。

数据模型介绍

初步介绍

前面一篇文章我粗略地介绍了 Cassandra 的设计特点及基本特性,这篇文章我会重点介绍它的数据模型。

首先我们需要知道,Cassandra 的数据模型借鉴了谷歌 BigTable 的设计思想,包括以下四个概念:

  1. 键空间(KeySpace):相当于关系型数据库模型中的数据库,是最上层的命令空间;
  2. 列族 ColumnFamily:相当于关系型数据库中的表,但它比表更稀疏;
  3. 行 Row:表示一个数据对象,存在于 ColumnFamily 当中;
  4. 列 Column:相当于属性,是存储的基本单元。

除了这些以外,Cassandra3.0 开始引入了物化视图(Materialized Views)概念,用于加快分布在集群内的数据查询效率,后面会重点介绍。

Cassandra 各主要概念之间的包含关系如图 1 所示。

图 1 Cassandra 主要概念关系图

在上面这张图里,KeySpace 中的 Settings,主要设置副本数量、Hash 策略。ColumnFamily 中的 Settings 主要设置 key 缓存、读修复概率、列的排序方式等属性。Column 是 Cassandra 存储的基本单元,它是一个三元组(name、value、timestamp)。一般情况下它不支持基于列值的查询。V1.1 版本之后(目前是 3.10 版本),Cassandra 对它的 Column 扩展了一个新的属性,即 TTL(存活时间)。

如图 2 所示,该图初略地概括了关系型数据库模型和 Cassandra 数据模型之间的对比。

图 2 关系型数据库模型 VS Cassandra 数据模型

用一句话概括,KeySpace 是一个或者多个列族的容器(Container),列族是一系列行的容器,每一行包括了若干个有序的列,列族呈现的是数据的结构,每一个 KeySpace 有至少一个列族。

KeySpace

创建 KeySpace

创建 KeySpace 的基本语法如清单 1 所示。

清单 1 创建 KeySpace 的基本语法

CREATE  KEYSPACE | SCHEMA  IF NOT EXISTS keyspace_name
WITH REPLICATION = Map
AND DURABLE_WRITES = true | false

假设我们想要设置一个副本策略是 SimpleStrategy 的 class,副本数量是 3,如清单 2 所示。

清单 2 设置一个副本策略

KEYSPACE Excelsior
WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 3 };

如果我们想要设置支持跨数据中心备份的 NetworkToplogyStrategy 策略,我们需要先知道数据中心的名字,可以通过 nodetool 命令获得这些信息,如清单 3 所示。

清单 3 设置支持跨数据中心备份的 NetworkToplogyStrategy 策略

$ nodetool status
Datacenter: datacenter1
=======================
Status=Up/Down
|/ State=Normal/Leaving/Joining/Moving
-- Address Load Tokens Owns Host ID Rack
UN 127.0.0.1 46.59 KB 256 100.0% dd867d15-6536-4922-b574-e22e75e46432 rack1

从清单里面我们可以知道当前所使用的数据中心名字是 datacenter1,和前面类似,可以采用如下语句创建 KeySpace,如清单 4 所示。

清单 4 创建 KeySpace

CREATE KEYSPACE NTSkeyspace WITH REPLICATION = { 'class' : 'NetworkTopologyStrategy','datacenter1' : 1 };

注意,如果想要在生产环境下使用 NetworkToplogyStrategy,需要改变默认的 SimpleSnitch 为 network-aware-snitch,在 snitch 配置文件中定义超过 1 个数据中心名称。

如果设置 DURABLE_WRITES 为 false,那么在写入数据到 keyspace 的时候会绕开提交日志。采用这种方式可能会丢失数据。注意,不要在使用 SimpleStrategy 的时候使用这个属性。

KeySpace 源码解释

如果我们想要删除一个 keySpace,你会怎么写代码?要知道 KeySpace 是非常重要的共享资源,会有很多人同时在访问,如果又有多个人同时想要提交删除请求怎么办?怎么保护呢?最简单的方式是采用同步代码块方式,如清单 5 所示,这是删除 KeySpace 的源代码。

清单 5 删除 KeySpace 源代码

synchronized (Keyspace.class)
{
Keyspace t = schema.removeKeyspaceInstance(keyspaceName);
if (t != null)
{
for (ColumnFamilyStore cfs : t.getColumnFamilyStores())
t.unloadCf(cfs);
t.metric.release();
}
return t;
}

首先要从 NonBlockingHashMap(这里是 keyspaceInstances)里移除数据,如清单 6 所示。

清单 6 从 NonBlockingHashMap 里移除数据

public Keyspace removeKeyspaceInstance(String keyspaceName)
{
return keyspaceInstances.remove(keyspaceName);
}

如果我们发现表中存在列族,那么需要遍历 ConcurrentHashMap(ColumnFamilyStores),找到所有的列族,并且删除它们,最后才能正式推出同步代码块,确认 KeySpace 已经被彻底删除了。

Super Column

Cassandra 的数据模型包括两个维度:行和列。如果一些用户需要更多的维度,比如说你想要通过接收人进行消息的分组,例如下面这个例子,如清单 7 所示。

清单 7 通过接收人进行消息的分组

“alice”:{
“ccd10d-d200-11e2-b7f6-29cc17aeed4c”:{
“sender”: “bob”,
“sent”: “2017-08-01 19:29:00+0100”,
“subject”: “hello”,
“body”: “hi”
      }
}

基于这个原因,Cassandra 引入了 Super Column。也就是说,列可以包含列。你可以在一个 Super Column 里包含多个列,多个 Super Column 之间的列数量可以是不同的。允许你删除 Super Column 里面的一个或者多个列。

大家是不是觉得这样做很好?确实,可以让我们更加容易定义数据结构,但是,你有没有发现问题?我想问题是有的。如果你需要读取一个子列,这时候你不得不反序列化整个 Super Column,随着单个 Super Column 所包含的数据量增大而产生更多消耗。对应地,数据压缩过程中的合并过程也会很耗时。

从 V0.8.1开始,Cassandra 引入了组合列概念,既保留了原有 Super Column 的优点,又解决了上面提到的缺点。从 V2.0.0 开始,Cassandra 正式淘汰了 Super Column。

Column Family

你可以把 Cassandra 的列族(Column Family)想象成为 Map 里面放了 Map,通过 Row Key 连接,通过列键(Column Key)连接内置的 Map。注意,两个 Map 都是排序的。

定义表的几种设计方式

如图 3 所示,该图描述的是列族里面的一行,每个 Column Key 对应一个 Colum Value。

图 3 列族里面的一行

如图 4 所示,该图描述的是超级列族里的一行,每个 Super Column Key 下面有若干个子列键和对应的值。

图 4 超级列族里的一行

如果我们想要查看每个列族的阈值,同时也查看以下 memtable 的相应配置信息,我们可以通过 nodetool 命令查看,如清单 8 所示:

清单 8 查看列族阈值信息

$ ./bin/nodetool -h localhost cfstats
Keyspace: dev
Read Count: 1
Read Latency: 0.897 ms.
Write Count: 2
Write Latency: 0.051 ms.
Pending Tasks: 0
Column Family: data
SSTable count: 2
Space used (live): 9530
Space used (total): 9530
Memtable Columns Count: 1
Memtable Data Size: 26
Memtable Switch Count: 1
Read Count: 1
Read Latency: 0.897 ms.
Write Count: 2
Write Latency: 0.020 ms.
Pending Tasks: 0
Key cache capacity: 200000
Key cache size: 2
Key cache hit rate: 0.0
Row cache: disabled
Compacted row minimum size: 51
Compacted row maximum size: 86
Compacted row mean size: 73

物化视图(Materialized Views)

在引入物化表之前,如果你想要去查询一个列,并且这个列不是分区键的话,那么你只能使用二级索引。如果我们需要从集群中的大多数节点上查询数据(当数据基数很大的时候),又想查询速度很快,我们应该怎么办?我们可以采用物化表。

假设我们用一张球员的得分表,我们有以下几项查询需求想要实现:

1. 通过比赛查询谁是得分最高的球员?

2. 通过比赛和比赛日,查询谁是得分最高的球员?

3. 通过比赛和月份,查询谁是得分最高的球员?

我们首先需要创建表,如清单 9 所示。

清单 9 创建表

CREATE TABLE scores
(
  user TEXT,
  game TEXT,
  year INT,
  month INT,
  day INT,
  score INT,
  PRIMARY KEY (user, game, year, month, day)
)

接下来我们需要创建物化视图用于呈现所有时间的最高得分。物化视图实际上是预先对数据进行排序,所以我们这里要选择合适的排序方式,这里选择的是 score 列,如清单 10 所示。

清单 10 创建物化视图

CREATE MATERIALIZED VIEW alltimehigh AS
       SELECT user FROM scores
       WHERE game IS NOT NULL AND score IS NOT NULL AND user IS NOT NULL AND year IS NOT NULL AND month IS NOT NULL AND day IS NOT NULL
       PRIMARY KEY (game, score, user, year,
month, day)
       WITH CLUSTERING ORDER BY (score desc)

相应地,为了满足另外两个查询需求,我们也分别创建了物化视图,如清单 11 所示。

清单 11 创建物化视图

CREATE MATERIALIZED VIEW dailyhigh AS
       SELECT user FROM scores
       WHERE game IS NOT NULL AND year IS NOT NULL AND month IS NOT NULL AND day IS NOT NULL AND score IS NOT NULL AND user IS NOT NULL
       PRIMARY KEY ((game, year, month, day),
score, user)
       WITH CLUSTERING ORDER BY (score DESC)
CREATE MATERIALIZED VIEW monthlyhigh AS
       SELECT user FROM scores
       WHERE game IS NOT NULL AND year IS NOT
        NULL AND month IS NOT NULL AND score IS NOT NULL AND user IS NOT NULL AND day IS NOT
        NULL
       PRIMARY KEY ((game, year, month), score,
        user, day)
       WITH CLUSTERING ORDER BY (score DESC)

我们尝试插入一些数据,如清单 12 所示。

清单 12 插入数据

INSERT INTO scores (user, game, year, month, day, score) VALUES ('pcmanus', 'Coup', 2015,
        05, 01, 4000)
INSERT INTO scores (user, game, year, month, day, score) VALUES ('jbellis', 'Coup', 2015,
        05, 03, 1750)
INSERT INTO scores (user, game, year, month, day, score) VALUES ('yukim', 'Coup', 2015, 05, 03, 2250)
INSERT INTO scores (user, game, year, month, day, score) VALUES ('tjake', 'Coup', 2015, 05,
        03, 500)
INSERT INTO scores (user, game, year, month, day, score) VALUES ('jmckenzie', 'Coup', 2015,
        06, 01, 2000)
INSERT INTO scores (user, game, year, month, day, score) VALUES ('iamaleksey', 'Coup',
        2015, 06, 01, 2500)
INSERT INTO scores (user, game, year, month, day, score) VALUES ('tjake', 'Coup', 2015, 06,
        02, 1000)
INSERT INTO scores (user, game, year, month, day, score) VALUES ('pcmanus', 'Coup', 2015,
        06, 02, 2000)

清单 13 查询数据

SELECT user, score FROM alltimehigh WHERE game ='Coup' LIMIT 1 
user    | score
-------------------
pcmanus | 4000

也可以查询每日最高得分,如清单 14 所示。

清单 14 查询每日最高得分

SELECT user, score FROM dailyhigh WHERE game = 'Coup'
AND year = 2015 AND month = 06
AND day = 01 LIMIT 1 user | score ------------------- iamaleksey |2500

实现原理

图 5 实现原理

图 5 中的"base replica"从本地读取数据,然后创建对应的视图。如果视图里的主键(Primary Key)已经在基准表里被更新了,那么这时候会生成一个墓碑(tombstone),以确保旧的数据不会再被呈现在视图里。关于墓碑概念,我会在系列文章中介绍。而确保最终基准表和视图之间一致性的是 batchlog,我也会在其他文章中深入介绍。你可以在 system.built_materializedviews 表里面查询到自己创建的物化视图。

注意点

1. 物化视图没有正常表的那些写入属性。物化视图写入前需要额外的读取动作,并且需要在每个副本上检查数据的一致性,以确保视图里面的数据一并更新;

2. 物化视图实质上是基准表的 CQL 行的对照;

3. 数据量很少情况下不适合使用物化视图;

4. 如果存在大量的分区墓碑,那么物化视图的查询性能可能会受到影响。物化视图在查询过程中一定会针对每一个数据产生一个墓碑(为了确保数据一致性)。

Composite Key(组合键)

一个组合键由一个或者多个主键字段组成。你可以通过使用括号方式区分组成复合分区键的字段,包含在主键定义里面,但是又在内置的括号以外的字段属于集群列。这些列在一个分区内部形成逻辑集合,便于检索数据,如清单 15 所示。

清单 15 创建多个主键

CREATE TABLE Cats (
  block_id uuid,
  breed text,
  color text,
  short_hair boolean,
  PRIMARY KEY ((block_id, breed), color, short_hair)
);

清单 15 所示的组合分区键是 block_id 和 breed。集群列是 color 和 short_hair。Cassandra 会把具有相同 block_id,但是不太 breed 的数据存储在不同的节点,而这两个字段都相同的数据会被存储在相同的节点上。

迁移案例示例

我们来设想这么一个场景,我们的网站需要包含用户表和商品表,一个用户可以选择多款商品,同样,一款商品可以被多个用户选择,这就构成了多对多的关系。如果按照关系型数据库的设计方式,如图 6 所示,我们看看下面这张 E-R 图。

图 6 用户信息 E-R 图

如果我们想在 Cassandra 里同样实现完全一样的功能,就数据库设计方案而言,那么我们可以有以下几种方案:

1.关系模型的精确副本

图 7 关系模型的精确副本

这种模型支持通过 user id 查询用户数据,也可以通过 item id 查询 item 数据。但是如果你想查询某个用户喜欢的所有商品,或者所有喜欢某种商品的用户,在 Cassandra 数据库里就不那么简单了。所以,这种设计方式是比较差的一种方式。

2.具有自定义索引的规范化实体

图 8 具有自定义索引的规范化实体

这种模型方式使 user id 和 item id 的映射关系存储了两次,第一次是通过 item id,第二次是通过 user id。假设我们想要拿到某位购买的商品的名字以及商品 id,我们首先需要查询 Item_By_User 表获取这位用户所有的商品,然后对于每一个 item id,我们需要需要去 item 表获得对应的 item title。针对这种方式的优化方式是针对 Item_by_User 表和 User_by_Item 进行优化,如方案 3 所示。

3.归一化到自定义索引的规范化实体

图 9 归一化到自定义索引的规范化实体

采用这种模式,title 和 usernmae 已经被分别包含在 User_By_Item 表和 Item_By_User 表。这样就可以快速地通过用户 id 搜索所有的商品名称,也可以通过商品 id 搜索对应的用户名字。对于这个用例来说,这样的设计比较合理。

4.部分去规范化实体

图 10 部分去规范化实体

方案 4 通过采用 Super Column 方式(已经淘汰,建议采用 composite columns 方式替代)构建了两张大表,这种方式不是说不可以,但是一旦数据量增大后,容易出现查询性能问题。

方案 3 是最佳选择,但是设计时我们应该增加时间戳 column。

属性解释

针对 KeySpace 的属性

num_tokens

默认值是 256。大家知道,Cassandra 的数据存储是由 N 个节点组成的环形,每个节点通过虚拟节点(vnodes)区分,数据被存储在节点上,每个节点随机地被分配一定数量的令牌(tokens)。一个节点有越多的令牌,意味着它也存储了更多的数据。

hinted_handoff_enabled

默认值是 true。这个值是为了设置是否开启或者关闭 hinted handoff 特性。如果你想要针对数据中心进行设置,需要通过这种方式:hinted_handoff_enabled: DC1,DC2。Hinted 特性是为了避免出现数据脏读(数据更新过程中一个节点离线了,等大家都更新完毕后它又活过来了,造成"僵尸"数据的出现),hint 就是为了避免出现这种情况而向 Coordinator 节点写入信息。我会在后续的文章里具体介绍 hinted_handoff 的实现方法及源代码。

max_hint_window_in_ms

针对无响应节点的最大 hint 时间。这段时间结束之后,新的 hints 不会再生成,直到这个离线节点恢复响应。如果这个节点再次宕机,那么新的中断时间开始。这个值的默认值是 3 小时(10800000 微秒)。

memtable_flush_after

这个值的单位是分钟,指的是一个 memtable 应该在写入磁盘前内存中保留多长时间的数据,默认是 24 小时(1440 分钟)。我们会每隔 10 秒钟检查一次 memtable 里面存放的数据。在时间周期范围内,如果列族的主键 memtabl 或者任意的 secondary 索引 memtables 有收到数据,它们需要写入磁盘。

如果我们把这个值设置得很小,那么可能会造成很多个 memtables 同时过期,我们可以通过调整 memtable_flush_writers 和 memtable_flush_queue_size 的值减少对于磁盘的压力,但是依然会造成磁盘 I/O 压力。最好的方式是调整其他 memtable 的触发阈值。

partitioner

默认值是 org.apache.cassandra.dht.Murmur3Partitioner。通过分区键分布的行横贯整个集群的所有节点。针对这个参数,我们可以配置为 Murmur3Partitioner 、RandomPartitioner、ByteOrderedPartitioner、OrderPreservingPartitioner(已淘汰)中的任意一个。

disk_failure_policy

设置 Cassandra 处理磁盘失败的方式,默认值是 stop,推荐值是 stop 或者 best_effort。配置可选值包括:

  • die:针对文件系统失败或者单个 SSTable 失败而杀死 JVM 进程并关闭 gossip 和 Thrift,节点可以被替换。
  • stop_paranoid:仅针对单个 SSTable 失败关闭 gossip 和 Thrift。
  • stop:关闭 gossip 和 Thrift,让节点有效死亡,但是保留使用 JMX 检查的途径。
  • best_effort:关闭磁盘失败并保持针对余下可用 SSTables 的请求,意味着你可以在 ONE 一致性时丢弃数据。
  • ignore:仅仅记录所有文件系统错误,其余错误都会被直接忽略。

seed_provider

记录所有 seed 主机地址。Seed 中文叫种子,它的作用也确实和"种子"差不多,这些"种子"节点会记录整个集群的状态,其他节点可以通过从这些"种子"节点同步状态来了解整个集群的当前实际状况。

compaction_throughput_mb_per_sec

用于设置横贯节点的压缩数据总的吞吐量。你越快速地插入数据,你越应该快速地压缩数据,用于保证 SSTable 的数量。推荐值是 16 到 32MB/s。

listen_address

Cassandra 绑定用于连接到其他节点的 IP 地址,默认值是 localhost(主机名),推荐设置为 IP 地址。

endpoint_snitch

默认值是 org.apache.cassandra.locator.SimpleSnitch。Cassandra 使用 snitches(告密者)锁定节点的位置和路由请求。主要有以下几个值可以被用于设置:

  • SimpleSnitch:用于单一数据中心的部署。这种方式不对数据中心或者机架信息进行处理,也就是说,本地运行几个节点的架构方式可以采用这种方式。
  • GossipingPropertyFileSnitch:推荐用于生产环境配置项。
  • PropertyFileSnitch:针对机架和数据中心计算相似值。
  • Ec2Snitch:针对 EC2 用户的部署方式。
  • Ec2MultiRegionSnitch:使用公开的 IP 地址作为广播地址,你必须相应设置 seed 地址。
  • RackInferringSnitch:针对机架和数据中心计算相似值。

针对 Column Family 的属性

压缩存储(Compact Storage)

当我们建列族的时候,如果使用这个 WITH COMPACT STORAGE 关键字,并且使用的是复合主键方式,每次存储数据的时候列名也会一起存储,并且使存储在磁盘上的同一个列。如清单 16 示例代码所示。

清单 16 压缩存储

CREATE TABLE sblocks(
block_id uuid,
subblock_id uuid,
PRIMARY KEY(block_id, subblock_id)
)
WITH COMPACT STORAGE

注意,如果使用的是组合组件,并且也使用了压缩表策略,那么需要指定至少一个 Clustering Column(集群列)。并且,压缩表创建完毕后是不可以增加或者删除列的。

Bloom_filter_fp_chance

从字面上理解,Bloom_filter_fp_chance 这个参数控制着存储在磁盘上的 SSTables 的过滤精度。说得通俗一些,每次尝试检索数据时,如果我们在内存中已经存放了完整的数据映射表(数据具体在哪个 SSTable 上),那么我们的每次检索耗时会很短,如果我们保存在内存中的精度不高,那会出现我们搜索了很长时间的 SSTable,结果还是没能找到数据。同时,越精准也意味着需要更大的内存。

这个值为 0 表示最大化启用 Bloom Filter,1 表示关闭 Bloom Filter,推荐使用 0.1 这个值。

这个值对应着不同的压缩策略,如果设置为 0.01,那么需要设置压缩策略:SizeTieredCompactionStrategy,如果设置为 0.1,压缩策略:LeveledCompactionStrategy。

结束语

本文是"Apache Cassandra3.X"系列文章的第二篇,主要对数据模型进行了深入介绍,包括 KeySpace、Super Column、Column Family、Composite Key 等的设计理念及源代码解释,也针对一个从 RDBMS 到 Cassandra 的数据库设计迁移案例进行了举例说明。接下来介绍了与数据模型相关的各个属性,包括 KeySpace 属性、Column Family 属性,由于属性较多,这里并没有完全举例,只是列举了几个比较常用的,并且需要根据实际情况设定值的属性。

参考资源

参考 developerWorks 上的 Apache Cassandra,了解更多 Apache Cassandra 知识。

参考书籍 《Cassandra The Definitive Guide 2nd Edition》 Jeff Carpenter&Eben Hewitt


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Open source
ArticleID=1050016
ArticleTitle=Apache Cassandra3.X 系列,第 2 部分: 数据模型深入解释
publish-date=09202017