原文地址: http://www.datastax.com/dev/blog/basic-rules-of-cassandra-data-modeling
选择一个正确的数据模型是Cassandra使用中最难的部分(译者也这么认为)。如果你有关系型数据库开发经验,你会觉得CQL看起来都很相似(和MySQL等),但是你使用它的方式会非常的不同。这篇文章的目的就是解释当你在设计一个Cassandra数据库的时候需要牢记在心里的一些基本规则。如果你遵守这些规则,你会得到很好的拿来就能用的性能提升。更好的是,你的性能将会随着你的集群的节点增加而线性增长。
来自有关系型数据库背景的开发经常会尝试把他们在关系型数据库的设计规则经验放到Cassandra来用。为了避免浪费时间在那些不适用Cassandra的规则上,我需要指出一下非目标:
在Cassandra中写入不是免费的,但是非常廉价。Cassandra在写入吞吐量方面做了高度的优化,几乎所有的写入性能都是平等的(计数器、轻量事务和在list中插入数据除外)。如果你额外的写入可以让你改进读的性能,这一般是一种好的设计。读取是一种更昂贵并且更困难的东西。
反范式设计和冗余数据是Cassandra的设计核心。不要担心这个问题。相比CPU,内存,磁盘IO,网络IO,磁盘的储存空间是非常廉价的,而Cassandra正是遵循这个思路来设计的。为了有更好的读取性能,你往往需要重复数据。
另外Cassandra不提供JOIN ,你不会想在分布式系统中用这些特性。
你的数据模型中有2个非常高优先级的目标
还有一些小规则你需要记住,但是这两个最重要。所以多数情况下,我会把这2个规则作为关注重点。当然你还有一些花哨的技巧可以使用,但是你最好知道如何评测它的效果。
你希望集群中的所有的节点都有大致差不多数量的数据。Cassandra做这个很容易,但不是全自动的。行基于分区主键被分布到集群的各个位置。所以你需要选择一个好的主键,我将简单解释一下。
分区是一组数据共享一个分区key。当你执行一个读取的查询,你希望读取尽量少的分区来获取你需要的数据。
为何如此重要?因为每个分区可能在不同的节点上。代理将会构建许多命令去不同的节点去执行以满足你的请求。这增加了大量的前置操作并且导致各种延迟的情况。而且,即使在单个节点的情况下,由于数据的存储方式导致读取多个分区的数据也比单个分区要慢的多。
你需要尽量减少读取的时候需要的分区数量,为什么不把数据放在一个大分区?你需要干掉规则1,它说要让数据分布在整个集群。
事实是,这两个规则确实经常冲突,所以你需要平衡他们。
最小话查询分区的办法就是让你的数据结构适应你的查询语句。不要依据关系来设计(译注:没错,你的数据会更加抽象化,且不好理解)。基于查询设计。下面是如何做:
尽量尝试去确定你有哪些语句需要执行。这里可能包含大量的注意事项你可能一开始没有考虑到,你需要考虑:
任何一个查询请求的改变都会影响到最优数据模型的设计。
事实上这意味着你可能需要为每个查询建立一张表(译注:也就是说你一开始会有很多表,他们数据会很重复)。如果你需要应付多个查询,那么你需要更多的表。
换句话说,每个表需要高度匹配查询语句需要的答案。如果你需要不同的答案,你需要不同的表。这是你如何优化读操作。记住,重复数据没有问题。你的许多表可能重复了很多数据。
为了展示如何进行一个好的设计,我将带你通过设计解决一个简单的问题。
核心需求,我们有许多用户,我想找到他们。
决定要执行哪些查询。我们希望可以通过用户名,或者用户的email来查询他们。任何一个查询,我们都需要得到他们的所有信息。
尝试创建一个表来满足查询需求,并且只需要用到1个分区。因为我们需要得到用户的所有信息,这需要2张表。
CREATE TABLE users_by_username ( username text PRIMARY KEY, email text, age int ) CREATE TABLE users_by_email ( email text PRIMARY KEY, username text, age int )
现在我们来确认一下是否符合规则:
数据分布均匀?每个用户使用它们自己的分区,所以是的。
最小化分区读取?每个用户我们只需要读取一个分区,所以是的。
现在我们来尝试对“非 - 目标”进行优化,然后又有了下面的设计方式
CREATE TABLE users ( id uuid PRIMARY KEY, username text, email text, age int ) CREATE TABLE users_by_username ( username text PRIMARY KEY, id uuid ) CREATE TABLE users_by_email ( email text PRIMARY KEY, id uuid )
这个数据模型同样将数据分布到所有的节点,但是有个问题,我们需要读取2个分区。一个是users_by_username/users_by_email 然后是 users表。所以读取的成本大体上是之前的2倍。
核心需求:用户被分到不同的组中,我们希望读取分组的所有用户。
决定要执行哪些查询。我们希望获取确切分组的用户的所有信息,不关心排序。
尝试创建一个表来满足查询需求,并且只需要用到1个分区。我们如何让分组分不到不同的分区中?我们可以设计这样的分区主键:
CREATE TABLE groups ( groupname text, username text, email text, age int, PRIMARY KEY (groupname, username) )
注意到主键中有2个部分,groupname,这个是分区主键,username,被称作clustering key (集群主键)。这会使得每个groupname在一个分区中。在一个特定的groupname中,行是按照username排序的。读取分组数据非常简单:
SELECT * FROM groups WHERE groupname = ?
这满足了最小化查询分区的要求,因为我们只需要读取1个分区。但是它并没有在数据均匀分布上做的很好。如果我们考虑有成千上万的小分组,每个分组有几百人,我们将得到一个相当于均匀分布的模型。但是如果有一个分组有1百万用户,所有的负担都会被一个节点承担。
如果我们希望负载均衡,有一些策略我们可以采用。最基本的是添加另一个字段到主键中做成一个复合分区主键,下面是例子:
CREATE TABLE groups ( groupname text, username text, email text, age int, hash_prefix int, PRIMARY KEY ((groupname, hash_prefix), username) )
新的HASH字段hash_prefix,保存了用户名的hash的前缀。比如第一个字节对4取模。和groupname一起,这两个字段组合成了一个复合分区主键。和单个分区不同,现在它分布到4个分区中了。我们的数据更加均匀,但是我们需要读取4次的分区。这是一个规则冲突的例子。你需要在它们间找到一个合适的平衡。
如果你有很多的读操作,而且分组不会太大,那么将取模的值从4改为2是不错的选择。如果你有较少的读取,但是单个分组会增长到很大,将4改为10会更好。
还有一些其他方法可以分割分区,我将会在下面的例子中介绍。
在我继续之前,让我总结一下这个数据模型:我们多次重复了用户信息,每个group一次,你可能希望像这样重建模型来减少重复的数量:
CREATE TABLE users ( id uuid PRIMARY KEY, username text, email text, age int ) CREATE TABLE groups ( groupname text, user_id uuid, PRIMARY KEY (groupname, user_id) )
显然,这最小化了重复。但是我们需要读取多少分区呢?如果分组是1000个用户,我们需要读取1001个分区。这和从一个分区读取数据相比是100倍的差距。如果要求读优先,那这正不是一个好的设计。另一方面,如果读不频繁,但是修改(update)是频繁的,这个模型还是有道理的。当你在设计数据库的时候,一定要确定你的读/写频率。
例3:用户加入分组的时间
假设我们继续之前分组的例子,但是新增一个需求,读取分组中新增的前X个用户。我们可以用和之前有点相似的表:
CREATE TABLE group_join_dates ( groupname text, joined timeuuid, username text, email text, age int, PRIMARY KEY (groupname, joined) )
这里我们使用timeuuid(和时间戳很像,但是不会冲突)作为clustering column 。在一个分组中,行将按照用户加入的时间顺序排序。这允许我们获取最新的用户信息,像下面这样。
SELECT * FROM group_join_dates WHERE groupname = ? ORDER BY joined DESC LIMIT ?
这是非常高效的,我们在同一个分区中顺序查询数据。为了避免总是要用到order by joined desc,这会让查询效率降低,我们可以修改clustering order:
CREATE TABLE group_join_dates ( groupname text, joined timeuuid, username text, email text, age int, PRIMARY KEY (groupname, joined) ) WITH CLUSTERING ORDER BY (joined DESC)
现在我们可以执行更高效的查询了
SELECT * FROM group_join_dates WHERE groupname = ? LIMIT ?
我们之前的例子中,我们有个问题就是如果有一个分组太大,如何把数据均匀的分配到所有节点中。在那个例子里我们随机的分割了数据到同的分区(取模)。但是这个例子不同,我们可以利用我们对查询模板的认识来分区:用时间分区。
比如我们用date来分区(date应该类似于:2016-05-19,所以每天有个分区)
CREATE TABLE group_join_dates ( groupname text, joined timeuuid, join_date text, username text, email text, age int, PRIMARY KEY ((groupname, join_date), joined) ) WITH CLUSTERING ORDER BY (joined DESC)
我们再次使用了复合分区主键,但是这次我们用了加入时间。每天都有一个新的分区。当查询最近的X个用户时,会先找今天的分区,然后昨天,前天,直到我们有X个用户。我们可能会在得到limit个用户之前,查找多个分区。
为了减少分区的查询,我们需要为分区指定一个时间范围,这样你就只需要查询1-2个分区。比如我们平均每天有新3个用户大致上,然后我们按4天一分区,这样,你就可以在1-2个分区查完10个最新用户。
这里提到的最基本的数据模型规则涵盖了现有的所有版本的Cassandra,并且在将来的版本中应该也是一样的。一些其他的小的数据模型问题,比如如何处理墓碑(删除的数据),一样是需要考虑的,但是这些可能在将来的Cassandra版本中可能会改变。
除了这里提到的基本策略,一些Cassandra华丽的功能,像集合,用户自定义数据结构,静态字段,一样可以在读的时候减少分区的使用。在设计的时候不要忘了考虑这些选择。
希望我在你们处理不同的数据库设计的时候已经给予了一些有用的工具。如果你想了解更多,我建议阅读:Datastax’s free, self-paced online data modeling course (DS220) (译者:这是一个英文视频教程)
https://academy.datastax.com/courses/ds220-data-modeling?dxt=blogposting
一路顺风!