欢迎投稿

今日深度:

Cassandra从thrift升级到CQL3指南——原文《A thrift to CQL3 upgrade guide》,

Cassandra从thrift升级到CQL3指南——原文《A thrift to CQL3 upgrade guide》,


此文章于2014年10月17日发表在 NoSql 并加以 Cassandra thrift 列族 的标签 by 屁民瑞威.

由于屁民瑞威从Cassandra 2.x才开始接触,所以直接使用的是CQL来访问Cassandra,但是在学习Nutch 2.2.1时出现了Gora这个ORM框架,Nutch通过Gora写入到Cassandra的数据采用的是Thrift方式,另外基于某些以前的网络文章也是thrift方式,所以屁民瑞威认为有必要了解一下相关的知识。本文章是在最开始学习Cassandra时看到的,当时也只是简单的了解了一下,后面也没有特别注意,为了方便各位同行,屁民瑞威简单翻译一下吧。

前言

该指南只描述CQL语言的第三个版本CQL3。为了避免混淆,大多数时候我们将尝试使用术语CQL3而不是CQL,但是如果没有指明的话,也表示这个意思。

此外,CQL3在Cassandra 1.1时还是beta的(语言本身是beta,而不仅仅是其实现),只有在Cassandra 1.2时才是最终版本,并且将会有一些重大的语法改变以及在beta和最终版本间添加很多特性。这个指南描述了CQL3的最终版本,特别的——这些描述或使用的特性在Cassandra 1.1还不可用。

介绍

CQL3(Cassandra查询语言)提供了一个新的API和Cassandra进行交互。旧有的thrift API暴露了Cassandra的内部存储结构实在是有点直接,而CQL3则是在内部结构上提供了一个抽象的层。这是个好事情,因为它允许从API隐藏一些容易分散注意和无用的实现细节(比如range ghosts),允许为通用的编码/方言(例如下面将要讨论的CQL3 collections)提供本地的语法,而不是让每个客户端或客户端库重新实现它们不同的、不兼容的方式。不管怎么样,CQL3提供的抽象层意味着thrift的用户如果要把已有的应用程序迁移到CQL3,他们就将必须懂得这个抽象的基础。这就是本文章将打算做的事——文章讲解了如何从thrift迁移到CQL3。为了实现这样的目的,本文章也讲解了CQL3的实现基础,也可以让那些希望了解它的朋友感点兴趣。

但是在讨论这个事的核心前,让我们说明一下什么时候应该使用CQL3。如上描述,我们相信CQL3更简单,而且对于Cassandra来说,CQL3也是比thrift更好的API。所以,鼓励在新项目/应用程序中使用CQL3。但是thrift API已经存在了,已有的应用程序没有必要升级到CQL3。本质上,CQL3和thrift使用同样的存储引擎,所以所有提升该引擎的特性将同样影响到两种API。因此,本指南适合这样的情况:1、拥有使用thrift的应用程序;2、希望迁移到CQL3。

最后,我们需要注意CQL3并没有要求从本质上改变Cassandra建模的方式。主要的建模实践和以前一样的:有效的建模方式仍然基于配置数据——数据通过反规范化被一起访问,通过存储引擎提供排序——大量用于查询。总之,CQL3声称能够使得建模更简单。

本文所使用的词汇

在以前,thrift API让从关系数据库世界转过来的人员觉得使用“rows”和“columns”很疑惑,但是它们却跟SQL中的意思不一样。CQL3修正了在模型中出现 的这些词语,现在row和column与SQL一样了。我们相信这对于新手是一个进步,但是不幸的是,为了实现这样的效果,当你想从thrift迁移到 CQL3时会有短暂的困惑——“thrift”的row并不总是等于“CQL”的row,“CQL3”的column也不总是等于”thrift”的 column。

为了防止这些困惑,我们将使用下面的规范:

  • 当我们讨论thrift的row时使用“internal row”。我们使用“internal”这个术语是因为这是和内部实现一致的(thrift直接暴露的)。术语“row”表示CQL3的row,有时也用“CQL3 row”。
  • 我们使用术语“cell”代替thrift/internal的列。那么“column”就表示CQL3的列。
  • 术语“column family”用于thrift,“table”用于CQL3——虽然它们可以认为是一样的。

总而言之,为了达到本文章的目的,内部行(internal row)包含“cell”,CQL3 row包含列(column),这两个概念不总是能够直接映射的:本文章将解释两个概念何时相同或异同。

标准列族

静态列族

在thrift中,静态列族(static column family)是其每个内部行(internal row)拥有或多或少的同样的一组单元格名集合(cell names),集合是有限的。典型的例子是用户简介——拥有有限的属性,每个具体的简介有一些子集。

这些静态列族(static column family)通常在thrift中是这样定义的【定义1】:

1 2 3 4 5 6 7 8 9 create column family user_profiles     with key_validation_class = UTF8Type      and comparator = UTF8Type      and column_metadata = [        {column_name: first_name, validation_class: UTF8Type},        {column_name: last_name, validation_class: UTF8Type},        {column_name: email, validation_class: UTF8Type},        {column_name: year_of_birth, validation_class: IntegerType}      ]

用户简介在内部是这样存储的

profiles_schema_full

这个等同于在CQL3中这样定义这些列族

1 2 3 4 5 6 CREATE TABLE user_profiles (       user_id text PRIMARY KEY,       first_name text,       last_name text,       year_of_birth int     ) WITH COMPACT STORAGE

用CQL定义的方式存储的数据是和上面thrift定义的一样的(我们将在后面讲解使用WITH COMPACT STORAGE选项的原因,现在只需要知道这是必要的就行)。所以,对于静态列族来说,internal/thrift的行和CQL3的行是一样的。但是即使thrift的cell有对应的CQL3的列,CQL3还是定义了user_id这个没有映射到cell的列——它被映射到thrift的row key(显然是PRIMARY KEY的功劳)。

现在细心的读者可能注意到CQL定义比thrift多一些信息,也就是CQL定义为row key提供了一个名字(user_id),而thrift定义中并不存在。换句话说,CQL3比thrift使用了更多的元数据,如果你尝试通过【定义1】的方式访问thrift的列族时是没有这些元数据的。CQL3处理这个是通过下面两个方面:

1、如果row key没有名字,CQL3为row key选择默认的名字。如果你尝试使用CQL3访问【定义1】创建的列族就会是这样的情况。对于row key来讲,默认的名字就是key。换句话说,【定义1】是完全等于下面的CQL定义的:

1 2 3 4 5 6     CREATE TABLE user_profiles (       key text PRIMARY KEY,       first_name text,       last_name text,       year_of_birth int     ) WITH COMPACT STORAGE

类似的,如果通过【定义1】的方式创建user_profiles,那么从cqlsh就可以得到:

1 2 3 4 cqlsh:test> SELECT * FROM user_profiles;      key    | email         | first_name | last_name | year_of_birth     --------+---------------+------------+-----------+---------------      tbomba | tb@hotmail.mh |        Tom |  Bombadil |          1954

2、如果你想声明更多用户友好的名字而不是默认的,可以通过下面的方法:

1     ALTER TABLE user_profiles RENAME key TO user_id;

这些语句声明了CQL3缺少的元数据,这对thrift方面来说没有关系,但是在那个语句后,你可以像【定义2】一样通过CQL3访问table。

动态列族

动态列族(或者说宽行列族)是每个内部行(internal row)可能拥有完全不同的cell集合。典型的例子是时间序列的列族。例如,保存每个用户点击过的每个链接的时间轴。通过thrift定义这样的列族应该是这样的【定义3】:

1 2 3 4     create column family clicks     with key_validation_class = UTF8Type      and comparator = DateType      and default_validation_class = UTF8Type

对于指定的用户,他点击的链接在内部是这样存储的:

clicks_schema

换句话说,一个用户点击的url将被存储在一个内部行中(internal row)。由于内部行(internal row)通过比较器(comparator)被排序,在这个例子中比较器是DateTime——点击将按时间排序,允许非常有效的请求指定的用户在指定时间段的点击。
在CQL3中,等同的功能定义应该是这样的【定义4】

1 2 3 4 5 6     CREATE TABLE clicks (       user_id text,       time timestamp,       url text,       PRIMARY KEY (user_id, time)     ) WITH COMPACT STORAGE

这个定义和【定义3】存储数据的方式一样。这和静态列族的例子不同之处在于复合主键。CQL3的工作方式是这样的,映射主键的第一个要素(user_id)到内部行的row key,第二个要素(time)为内部cell的名,最后一个CQL3列(url)将被映射为cell的值。这就是CQL3如何访问宽行:转换一个内部宽行到多个CQL3的行,每一个宽行cell。然而这只是不同的方法查看同样的信息。

现在,和静态情况下一样,这个定义还是比对应的thrift【定义3】多一些信息——提供了用户友好的row key(user_id),cell名(time)和列值(url)。同样的,CQL将选择默认的名字:key用于row key,column1用于cell name,value用于cell value。
换句话说,定义在【定义3】的用户点击这个列族实际上等于CQL3的table:

1 2 3 4 5 6     CREATE TABLE clicks (       key text,       column1 timestamp,       value text,       PRIMARY KEY (key, column1)     ) WITH COMPACT STORAGE

再次说明,“等于”意味着你可以通过CQL3访问thrift定义的列族。例如,你可以通过下面的方式检索点击的时间分片(通过宽行的内部排序):

1 2 3 4 5 6 7 8 9 cqlsh:test> SELECT column1, value                 FROM clicks                 WHERE key = 'tbomba'                   AND column1 >= '2012-10-25 14:31:00'                   AND column1 < '2012-10-25 18:00:00';      column1                  | value     --------------------------+-------------------------      2012-10-25 14:33:14+0000 |    http://www.amazon.fr      2012-10-25 17:47:05+0000 | http://www.datastax.com

该查询将在内部转换成get_slice调用。
当然你也可以定义更多用户友好的名字:

1 2 3 ALTER TABLE clicks RENAME key TO user_id                           AND column1 TO time                           AND value TO url;

这样就可以改写为:

1 2 3 4 5 6 7 8 9 cqlsh:test> SELECT time, url                 FROM clicks                 WHERE user_id = 'tbomba'                   AND time >= '2012-10-25 14:31:00'                   AND time < '2012-10-25 18:00:00';      time                     | url     --------------------------+-------------------------      2012-10-25 14:33:14+0000 |    http://www.amazon.fr      2012-10-25 17:47:05+0000 | http://www.datastax.com

混合静态和动态(列)

在大多数情况下,列族要不是静态的就是动态的,两种方式CQL3都可以很好的进行原生操作。然而在某些情况下,同时使用部分动态列和静态列是非常有用的。

一个典型的例子就是:如果你想添加tag到uesr_profiles,就有两种方式可以对其建模。

第二种技术的优势是:当读取包含tag时整个用户的profle只需要请求一次,而使用两个列族的方式需要读取两次。严格来说,如果你想添加“good guy”和“friendly”到用户profile,你需要插入一个名为“tag:good guy”和”tag:friendly”的cell到用户profile,在Cassandra内部用户的profile看起来是这样的:

profiles_schema2

就像前面博客的文章(http://www.datastax.com/dev/blog/cql3_collections)所解释的,CQL3提供了一个本地的方式执行能够支持集合(collection)的技术。实际上,CQL3的集合(set)的实现方式完全和tag这个例子一样:集合(set)中的每个元素在内部是一个没有值的独立的cell。它和thrift唯一的不同之处在于:使用thrift时,你必须手动为每个tag的cell考虑字符串“tag:”,同时在读取时重新一起排序所有的tag;而CQL3将在你使用集合(set)时透明的处理所有的事。

注意这就是为什么“暴露集合(collection)给Thrift”不是明智的选择。集合(collection)被暴露给Thrift,只是Thrift直接暴露了内部存储引擎,由此暴露了格式化前的集合(collection)元素。换句话说CQL3中的集合(collection)只是一个有用的语法糖。这样的语法糖只能够被提供,是因为这个API(CQL3)是存储引擎上的一个抽象层。

所以,对于新建的CQL3项目,大家应该在需要混合静态和动态列的时候直接使用集合(collection)。集合(collection)是比使用Thrift手动处理这些事的更好的方案。

然而,如果你已经使用某些thrift类似的tag的例子,升级到CQL3而不使用纯静态或纯动态列族将并没有多少直接效果。也就是说,CQL3将认为列族是静态的,因为uesr_profile的定义仍然是【定义1】。但是tag列没有被声明的话(也不能,它们是动态创建的),默认情况下你将不能通过CQL3访问它们。唯一能够完全访问列族的解决方案是移除thrift声明的列,比如更新thrift架构:

1 2 3 4 update column family user_profiles       with key_validation_class = UTF8Type        and comparator = UTF8Type        and column_metadata=[]

一旦你这样做了以后,你就可以通过类似动态情况访问列族,在更新后你可以得到这样的结果:

1 2 3 4 5 6 7 cqlsh> SELECT * FROM user_profiles WHERE key = 'tbomba'      key    | column1       | value     --------+---------------+---------------      tbomba |         email | tb@hotmail.mh      tbomba |    first_name |           Tom      tbomba |     last_name |      Bombadil      tbomba | year_of_birth |      \x07\xa2

当然这也有一些弊端:

  • 从API来看,用户的profile将会暴露为多个CQL行(这个只是API表现的,内部结构没有改变)。这主要是表面上的,但是可以认为比每个profile一个CQL行更丑陋。
  • 一些静态列值的服务端验证会丢失。
  • 一旦静态列值的类型验证被遗弃,它们将对客户端库永远不可用。特别的,就像上面可以看到的,cqlsh展示了一些对人类来说不友好的格式。除非客户端库提供了一种简单的方式反序列化这些值,否则只有手动在客户端代码中处理了。

显然,如果你把很多混合了静态和动态列行为的列族从thrift升级到CQL3将会非常麻烦。这是不幸的,但是还是让我们回忆一下:

  • thrift将不再更新。CQL3带来了更为用户友好的API,但是CQL3并没有做thrift不能实现的。因此,如果你有很多混合了静态和动态行为的列族,更简单的方式还是使用thrift。
  • 如果你真的想升级到CQL3并能够承担相应的负担,把存在的列族迁移到标准的CQL3是可能的。

复合

有一种动态列族的子集值得讨论:比较器(comparator)是CompositeType。举例来说,你需要存储用户的事件,并且你对事件通过日期(每天)和每天按分钟进行分组感兴趣。这是一个很基础的类似前面的点击列族一样的时间序列例子,但是请允许我认为你希望把日期和分钟分开。你可能用thrift定义下面的列族:

1 2 3 4   create column family events       with key_validation_class = UTF8Type        and comparator = 'CompositeType(IntegerType, IntegerType)'        and default_validation_class = UTF8Type

对于一个用户来说,他的事件在内部看起来是这样的:

events_schema

4:120代表了包含4和120两部分的复合cell名。

如果你通过CQL3查询这个列族,你将得到:

1 2 3 4 5 6 7 cqlsh:test> SELECT * FROM events;      key    | column1 | column2 | value     --------+---------+---------+---------      tbomba |       4 |     120 | event 1      tbomba |       4 |    2500 | event 2      tbomba |       9 |     521 | event 3      tbomba |      10 |    3525 | event 4

换句话,对CQL3来说,这个列族等于:

1 2 3 4 5 6 7 CREATE TABLE events (         key text,         column1 int,         column2 int,         value text,         PRIMARY KEY(key, column1, column2)     ) WITH COMPACT STORAGE

就像你看到的一样,复合类型能够很好的被CQL3处理,它能够映射复合cell名的每个部分为一个CQL3列。当然,像上面的例子一样,你可以重新定义用户友好的名字:

1 2 3 4 5 6 7 8 9 10 11    cqlsh:test> ALTER TABLE events RENAME key TO id                                       AND column1 TO day                                       AND column2 TO minute_in_day                                       AND value TO event;     cqlsh:test> SELECT * FROM events;      id     | day | minute_in_day | event     --------+-----+---------------+---------      tbomba |   4 |           120 | event 1      tbomba |   4 |          2500 | event 2      tbomba |   9 |           521 | event 3      tbomba |  10 |          3525 | event 4

注意,在这个例子中建议使用ALTER TABLE语句重命名所有的列。由于技术的限制,如果你一个一个的命名列,你将得到错误的结果。

非压缩的表(Non compact tables)

所有上面的CQL3定义都使用了COMPACT STORAGE选项。实际上,通过thrift创建的列族通常映射到这样的压缩表。为了解释non-compact和compact的异同,考虑下面的non-compact的CQL3表:

1 2 3 4 5 6 7 8 CREATE TABLE comments (         article_id uuid,         posted_at timestamp,         author text,         karma int,         content text,         PRIMARY KEY (article_id, posted_at)     )

该表存储了文章的评论。每个评论都是一个CQL行,它通过article_id来识别是针对哪个文章进行评论的,包含文章发表的时间,每个这样的评论包含作者名,评论内容和“karma”(喜欢这个评论的人数)。

就像前面部分说讲的动态列族的例子,我们拥有一个复合主键,article_id将映射到内部row key,posted_at将映射到cell名。然而,在前面的部分我们只有一个CQL3的列不是主键的一部分(由于使用了compact存储,声明多个列将出现bug),并且映射到内部的cell值。但是在这里,我们拥有3个CQL3的列不是主键的一部分。处理该情况的方法是这样的,在内部这个评论表将使用CompositeType比较器,第一部分将映射到posted_at,第二个将作为cell代表的列的名字。也就是,对于一个指定的文章,内部结构是这样的:

comments_schema

所以这个评论表在内部使用宽行,但是每个CQL3行实际上映射为内部cell的一小片(请注意第一个“空”cell不是错误,这是实现的细节https://issues.apache.org/jira/browse/CASSANDRA-4361)

换句话说,non-compact的CQL3表映射行到宽排序的内部行的静态分片。这是非常有用的,尤其是Cassandra中使用的物化视图。诚然,这非常像thrift中的超级列(下面将讨论超级列),但是要比超级列好:一些CQL3列是文本(author,content),另外的是数字(karma),这在超级列中是不可能的(必须使用BytesType作为子比较器或存储karma数字为字符串)。

这也允许在它们分离成独立的存储引擎cell后更新和删除每个独立的列。

下面看看真实的静态表。前面部分的【定义2】(使用了COMPACT STORAGE)是如何与下面不同的:

1 2 3 4 5 6 CREATE TABLE user_profiles (       user_id text PRIMARY KEY,       first_name text,       last_name text,       year_of_birth int     )

也就是一样的,除了没有COMPACT STORAGE选项。它们的不同之处在于上面的定义将在内部使用只有一个UTF8组件的CompositeType比较器,而不是UTF8Type比较器。这个看起来有点浪费(技术上,CompositeType多了两个字节),但是这样就可以支持集合(collection)。在内部,集合(collection)要求使用CompositeType,换句话说,通过上面的定义你可以:

1 ALTER TABLE user_profiles ADD tags TYPE set

但是你不能在有COMPACT STORAGE的情况下做这样的事。
请注意,虽然non-compact表在内部很少压缩,我们还是强烈建议在新的开发情况下使用它们。这个东西可以让你的表能够在后面使用集合(collection),这比只增加很少的存储费用——更值。

关于超级列

就像我们在前面部分所看到的,CQL3原生支持类似超级列的使用场景,CQL3也没有超级列的很多限制。然而,已经存在的超级列在这篇文章正在写的时候不能通过CQL3访问。CASSANDRA-3237用于使用同等的复合列编码来内部替换超级列。【屁民瑞威注:在Cassandra 2.0的beta版本完成】这个编码将是non-compact的CQL3表。因此,一旦这个补丁完成,超级列就可以通过CQL3像正常的CQL3表一样访问了。这是Cassandra 1.3的roadmap。

 

参考:

《A thrift to CQL3 upgrade guide》

转载声明:本文《Cassandra从thrift升级到CQL3指南——原文《A thrift to CQL3 upgrade guide》》为【屁民部落】原创/翻译文章,转载时请注明出处!

www.htsjk.Com true http://www.htsjk.com/cassandra/34923.html NewsArticle Cassandra从thrift升级到CQL3指南——原文《A thrift to CQL3 upgrade guide》, 此文章于2014年10月17日发表在 NoSql并加以 Cassandrathrift列族的标签 by 屁民瑞威 . 由于屁民瑞威从 Cassandra 2.x 才开始接触...
相关文章
    暂无相关文章
评论暂时关闭