[Practical Cassandra]CH2
当为开始为你的keyspace创建数据模型的时候,首要事情就是忘记你知道的关于关系型数据建模的一切。关系型数据模型是被设计为用来高效存储,关系型查找,以及联系起关注点的。而Cassandra是被设计作为高性能和存储海量数据的。 与关系型数据库不同的是,Cassa
当为开始为你的keyspace创建数据模型的时候,首要事情就是忘记你知道的关于关系型数据建模的一切。关系型数据模型是被设计为用来高效存储,关系型查找,以及联系起关注点的。而Cassandra是被设计作为高性能和存储海量数据的。
与关系型数据库不同的是,Cassandra环境下的数据模型是建立在应用要求的查询模式上的。这意味着你在数据建模之前必须了解应用的读/写模式。这一点对于索引同样适用,Cassandra中建立索引是基于特定类型的查询要求的,而不像关系型数据库中那样作为性能优化功能的。
本章中,我们会指出创建关系型数据建模方式与Cassandra数据建模方式的关键不同。我们将用一个存储时间序列数据的例子来进行讲解。
Cassandra数据模型
为了了解如何在Cassandra环境下对应用建模,你需要先明白Cassandra数据模型是如何工作的。Cassandra的数据分布方式来源于Amazon的Dynamo,而其数据表示则来源于Google的BigTable。
当使用CQL建表时,你不仅要告诉Cassandra你的数据的名称和类型,还要告知如何存储和分布你的数据。这是通过PRIMARY KEY操作符实现的。PRIMARY KEY告诉Cassandra存储系统基于这个键来分布数据,这通常被称作partition key。当PRIMARY KEY包含多个字段时(类似组合键),第一个字段作为partition key(决定数据如何分布),其余字段作为聚集键(clustering key,决定数据如何存储在磁盘)。聚集键允许你预先通过key中的字段group你的数据。使用组合键的行在Cassandra中称宽行(wide rows),但这里的宽行说的是Cassandra存储在磁盘上的行,而不是你查询是展示给你的行。
Listing3.1 示例使用单一字段PRIMARY KEY时数据如何存储
—————————————————————————————————————
CREATE TABLE animals (
name TEXT PRIMARY KEY,
species TEXT,
subspecies TEXT,
genus TEXT,
family TEXT
);
SELECT * FROM animals;
name |family | genus | species | subspecies
_ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ _ __ _ _ __ _ _
dog | Canidae | Canis | C. lupus | C. l. familiaris
cat | Felidae | Felis | F. catus | null
duck | Anatidae | Anas | A. platyrhynchos | null
wolf | Canidae | Canis | C. lupus | null
_ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ _
Listing 3.2 示例当使用组合键时数据如何存储
—————————————————————————————————————
CREATE TABLE animals (
name TEXT,
species TEXT,
subspecies TEXT,
genus TEXT,
family TEXT,
PRIMARY KEY(family, genus)
);
SELECT * FROM animals;
name | family | genus | species | subspecies
_ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ _
dog | Canidae | Canis | C. lupus | C. l. familiaris
wolf | Canidae | Canis | C. lupus | null
cat | Felidae | Felis | F. catus | null
duck | Anatidae | Anas | A. platyrhynchos | null
—————————————————————————————————————
从图3.2可以看出,当使用组合键时,wolf和dog的数据存储在相同的服务器上。这是因为我们设置了利用“family”字段partition和用“genus”字段聚集。这意味着相同family的数据会存储在相同副本集合,并通过genus排序或聚集。 这样使得可以通过已知的family和genus值快速查找数据。
模型查询———并非数据查询
使用Cassandra,当创建数据模型时首先要考虑的是查询的性能。在Cassandra中,行不会被切分到多个接点。也就是说如果一行存在于某个节点上,那整行都会存于节点。如果你对某个key频繁读写,就产生了热点。热点在频繁查询某key(row)导致服务器Spike时产生。某个机器上的高负载会导致集群范围的混乱,因为信道会back up。行的大小也要引起注意,一行应该在磁盘存储能力内,如果一行有数十亿列,那或许会超过所在节点可用磁盘空间的大小。
列表3.3中,我们可以看到关系型数据库存储event logs的典型方式。自增的ID字段,event时间,事件类型对应的ID,以及一些该事件的信息。尽管你可以在Cassandra中模拟这个模型,但这并不是一种高效的做法,因为它需要两次查询(一次查这一事件行,一次通过事件类型ID查事件类型),因为Cassandra不支持join操作。
Listing 3.3 示例关系型数据模型作Log存储
—————————————————————————————————————
CREATE TABLE events (
id INT PRIMARY KEY,
time TIME,
event_type INT references event_types(id),
data text
);
SELECT * FROM events;
id | time | event_type | data
_ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ _ __ _ _ __ _ _
1 | 16:41:33.90814 | 4 | some event data
2 | 16:59:48.9131 | 2 | some event data
3 | 17:12:12.12758 | 4 | some event data
4 | 17:32:17.83765 | 1 | some event data
5 | 17:48:57.10934 | 0 | some event data
—————————————————————————————————————
为了解决没有join操作的问题,我们只要每次都将event_type的值存在一列中即可。这样反范式化数据对Cassandra来说显然不frowned upon。Cassandra中的数据建模应该遵循“磁盘空间很廉价,所以复制数据(甚至多次)也不是问题”这个看法。事实上,范式化数据对于Cassandra来说反而是反模式的。Cassandra模型可能很相似,但是几点关键的不同导致了性能和可用性上的巨大差别。
列表3.4展示了上述关系型版本的Cassandra复制版,但存储的是时间类型本身而不是其ID。值得注意的是这个模型的每行存储一个单一事件,这导致了查询某个时间或某个事件类型的事件数据变得困难。你需要知道事件对应的ID来获取事件的数据,但不需要遍历查找整个列族(ColumnFamily)。
Listing 3.4 示例Cassandra作Log存储的数据模型(复制RDBMS的)
—————————————————————————————————————
CREATE TABLE events (
id UUID PRIMARY KEY,
time TIMESTAMP,
event_type TEXT,
data text
);
—————————————————————————————————————
我们可以通过索引解决其中的一些问题,但代码中却没有想象中那么高效。比如说你要获取某个小时里的所有事件,我们可以在time字段上加上索引,但这回 导致过高负载,因为这些事件需要从分散在集群中各节点中的行中获取。
为了补偿这一点,鉴于我们事先知道获取的小时值,我们可以将小时变成key,利用Cassandra的动态表(dynamic tables)我们可以确保某个小时内的所有事件物理上都存储在一行中。当你使用组合键时,Cassandra将key off 第一个字段,而随后的字段作为列名的一部分。列表3.5中,我们调整代码,以一种便于快速查找小时对应所有事件的方式组织表中的数据存储。
Listing 3.5 示例Cassandra作Log存储的数据模型(低负载)。
—————————————————————————————————————
CREATE TABLE events (
hour TIMESTAMP,
id UUID,
time TIMESTAMP,
event_type TEXT,
data text
PRIMARY KEY(hour, id)
);
—————————————————————————————————————
通过这个新模型,我们可以很方便地查找小时中所有事件,因为我们将同个小时中的数据物理地存储在磁盘上的一行中。这对于一个低流量的应用来说非常合适。然而,如果你的应用存在大量读写,你可能就要将一行切分为多行,因为小时内的事件都存放在一行会变得难以查询,而且会导致热点(因为所有的读写都在磁盘上同个文件中进行)。我们可以将一行切分为多行来解决问题,只要我们有多个可供查询的字段的信息。在这个例子中,就是event_type。我们还能通过确保数据按照event_time排序(而不是ID)来进一步提高性能。这样做还是得我们能够基于事件的时间来做区间查询(rang queries)。在列表3.6中,我们用到组合键以及聚集排序(clustering order)来解决热点问题,同时根据事件时间进行了倒序排序。
Listing 3.6 示例Cassandra作Log存储(优化后的)
—————————————————————————————————————
CREATE TABLE events (
hour TIMESTAMP,
id TIMEUUID,
time TIMESTAMP,
event_type TEXT,
data text,
PRIMARY KEY((hour, event_type), time)
) WITH CLUSTERING ORDER BY (time DESC);
SELECT * FROM events;
hour | event_type | time | data | id
_ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ _ __ _ _ __ _ _
2013-06-13 11:00:00 | click | 2013-06-13 11:00:00 | some data | 3...
2013-06-13 11:00:00 | page_view | 2013-06-13 11:00:01 | some data | 2...
2013-06-13 11:00:00 | error | 2013-06-13 11:00:05 | some data | 0...
2013-06-13 11:00:00 | redirect | 2013-06-13 11:00:09 | some data | 1...
—————————————————————————————————————
既然我们有了一个存储我们的events数据的机制,我们可能还想跟踪事件上的度量(metrics)。Cassandra不支持以一种ad hoc的方式创建聚合度量(aggregate metrics)。为了跟踪特定的聚合信息,我们首先要清楚我们要跟踪何种聚合度量。在这个例子中,我们将通过跟踪小时内某event types的发生次数。这类跟踪的一种好候选方式是Cassandra Counter 列。
列表3.7展示为一个counter列族建表的做法。
Listing 3.7 示例带Counter列的建表
—————————————————————————————————————
CREATE TABLE event_metrics (
hour TIMESTAMP,
event_type TEXT,
count COUNTER,
PRIMARY KEY(hour, event_type)
);
CREATE TABLE url_metrics (
hour TIMESTAMP,
url TEXT,
count COUNTER,
PRIMARY KEY(hour, url)
);
—————————————————————————————————————
既然我们为我们的event_metrics创建了带有counters的表,我们已经能通过使用BATCH声明来在同一时间更新我们的counters。列表3.8展示了如何在一个原子的批处理中插入事件数据的同时更新我们的counters。
Listing 3.8 示例用Atomic Counter Batch插入和更新数据。
—————————————————————————————————————
INSERT INTO events (hour, id, time, event_type, data)
VALUES ('2013-06-13 11:00:00', NOW(), '2013-06-13 11:43:23',
'click', '{"url":"http://example.com"}')
BEGIN COUNTER BATCH
UPDATE event_metrics SET count _ count _ 1
WHERE hour _ '2013-06-13 11:00:00'
AND event_type _ 'click'
UPDATE url_metrics SET count _ count _ 1
WHERE hour _ '2013-06-13 11:00:00'
AND url _ 'http://example.com'
APPLY BATCH;
集合类(Collections)
Cassandra将集合类作为数据模型的一部分。这些类被用来提供灵活的查询方式。
Sets
Cassandra sets 提供了一种无须先读后写(read-before-write)的维护唯一项集(a unique set of items)的方式。这以为着你可以轻松解决如跟踪唯一e-mail地址或唯一IP地址这样的问题。集合中的数据按照元素类型自然顺序排好。Listing 3.9展示了如何创建一个带有set类型的表,以及如何查询。
Listing 3.9 使用Set的例子
—————————————————————————————————————
CREATE TABLE users (
email TEXT PRIMARY KEY,
portfolios SET@UUID:,
tickers SET@TEXT:
);
UPDATE users set
portfolios = portfolios + {756716f7-2e54-4715-9f00-91dcbea6cf50},
tickers = tickers + {'AMZN'}
WHERE email = 'foo@bar.com';
UPDATE users set
portfolios = portfolios + {756716f7-2e54-4715-9f00-91dcbea6cf50},
tickers = tickers + {'GOOD'}
WHERE email = 'foo@bar.com';
email | portfolios | tickers
_ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ _ __ _ _ __ _ _
foo@bar.com | {756716f7-2e54-4715-9f00-91dcbea6cf50} | {'AMZN', 'GOOG'}
UPDATE users SET
tickers = tickers - {'AMZN'}
WHERE email = 'foo@bar.com';
email | portfolios | tickers
_ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ _ __ _ _ __ _ _foo@bar.com | {756716f7-2e54-4715-9f00-91dcbea6cf50} | {'GOOG'}
DELETE tickers FROM users
WHERE email = 'foo@bar.com';
email | portfolios | tickers
_ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ _ __ _ _ __ _ _
foo@bar.com | {756716f7-2e54-4715-9f00-91dcbea6cf50} | null
Lists
当不需要唯一性时而需要维护顺序时,Cassandra lists很好用。比如前面的例子,现在我们要为我们的用户指定top 5 tickers。列表3.10展示了使用lists的一个例子。
Listing 3.10 使用Lists的例子
—————————————————————————————————————
ALTER TABLE users ADD top_tickers list
UPDATE users
set top_tickers = ['GOOD']
WHERE email = 'foo@bar.com';
UPDATE users
set top_tickers = top_tickers + ['AMZN']
WHERE email = 'foo@bar.com';
email | portfolios | tickers | top_tickers
_ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ _ __ _ _ __ _ _
foo@bar.com | {756716f7-2e54... } | null | ['GOOG', 'AMZN']
Listing 3.10 使用Lists的例子
—————————————————————————————————————
UPDATE users
SET top_tickers[1] = 'FB'
WHERE email = 'foo@bar.com';
email | portfolios | tickers | top_tickers
_ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ _ __ _ _ __ _ _
foo@bar.com | {756716f7-2e54...} | null | ['GOOG', 'FB']
UPDATE users
SET top_tickers = top_tickers - ['FB']
WHERE email = 'foo@bar.com';
email | portfolios | tickers | top_tickers
_ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ _ __ _ _ __ _ _
foo@bar.com | {756716f7-2e54...} | null | ['GOOG']
—————————————————————————————————————
Maps
Cassandra maps 提供了字典的风格。当你需要在一行中存储表格式的数据时很有用。它可以减轻没有join操作的不适,或者避免在一列中存储JSON数据。列表3.22展示了使用maps的例子。
Listing 3.11 使用maps的例子
—————————————————————————————————————
ALTER TABLE users ADD ticker_updates map
UPDATE users
SET ticker_updates = {'AMZN':'2013-06-13 11:42:12' }
WHERE email _ 'foo@bar.com';
email | portfolios | ticker_updates
_ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ _ __ _ _ __ _ _
foo@bar.com | {756716f7...} | {'AMZN': '2013-06-13 11:42:12-0400'}
UPDATE users
SET ticker_update['GOOD'] = '2013-06-13 12:51:31'
WHERE email = 'foo@bar.com';
email | portfolios | ticker_updates
_ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ _ __ _ _ __ _ _
foo@bar.com | {756716f7... } | {'AMZN': '2013-06-13 11:42:12-0400',
'GOOG': '2013-06-13 12:51:31-0400'}
DELETE ticker_updates['AMZN']
FROM user
WHERE emial = 'foo@bar.com';
email | portfolios | ticker_updates
_ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ __ _ _ _ __ _ _ __ _ _
foo@bar.com | {756716f7...} | {'GOOG': '2013-06-13 12:51:31-0400'}
总结
Cassandra中的数据建模方式在有的习惯使用关系型数据库的人看来可能不适应。这是为了在海量数据的情况下获得高负载作出的妥协。本章可以看出关系型数据库建模技术那一套在Cassandra的世界里不再使用;对查询建模,而不是数据;反范式化和复制数据并不是坏事--事实上,它们还是被推荐干的。另外,牢记任何时候将你的数据范式化都是不推荐的做法。Collections已经很强大,尽管但数据结构中的数据量非常大的时候会影响性能。