Cassandra-数据建模与分析-全-

Cassandra 数据建模与分析(全)

原文:zh.annas-archive.org/md5/b2d5422a8123d40b9326590e1bd1f2be

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

如果你被问到当今 IT 世界的五大热门话题,大数据将是其中之一。实际上,大数据是一个广泛的流行词汇,涵盖了众多组件,而 NoSQL 数据库是其不可或缺的组成部分。

卡桑德拉是最受欢迎的 NoSQL 数据库之一。如今,它提供了大量的技术优势,使你能够在 Web 规模或云环境中突破传统关系型数据库的限制。然而,设计高性能的卡桑德拉数据模型并不是一个直观的任务,尤其是对于那些多年来一直参与关系型数据建模的人来说。在卡桑德拉中简单地模仿关系型数据模型很容易采取次优的方法。

本书是关于如何设计一个更好的模型,利用卡桑德拉提供的卓越的可扩展性和灵活性。卡桑德拉数据建模将帮助你理解和学习释放卡桑德拉最大力量的最佳实践。

从对卡桑德拉的快速介绍开始,我们将逐步引导你从基本的数据建模方法、选择数据类型、设计数据模型和选择合适的键和索引,到通过应用这些最佳实践进行实际应用。

尽管应用程序规模小且基础,你将参与整个开发生命周期。你将经历如何为用 Python 编写的股票市场技术分析应用程序设计一个灵活且可持续的数据模型的设计考虑。业务不断变化,数据模型也是如此。你还将学习如何通过演变数据模型来应对新的业务需求的技术。

运行一个 Web 规模的卡桑德拉集群需要许多仔细的思考,例如数据模型的演变、性能调优和系统监控,这些也可以在你的指尖得到补充。

本书对于任何想要在实用中采用卡桑德拉的人来说都是一本无价的教程。

本书涵盖的内容

第一章, 卡桑德拉的鸟瞰图,解释了大数据和 NoSQL 是什么,以及它们与卡桑德拉的关系。然后,它介绍了重要的卡桑德拉架构组件及其相关特性。本章奠定了所有后续章节所参考的基本知识、概念和能力。

第二章, 卡桑德拉数据建模,解释了为什么使用卡桑德拉进行数据建模与关系型数据建模方法如此不同。在制作卡桑德拉数据模型中常用的技术,即通过查询建模,通过大量的示例进行了更详细的解释。

第三章,CQL 数据类型,通过许多组成数据模型的示例,向您介绍 Cassandra 内置的数据类型。这些数据类型的内部物理存储也提供,以便帮助您理解并可视化 Cassandra 中正在发生的事情。

第四章,索引,讨论了主索引和次索引的设计。Cassandra 数据模型基于用于访问它们的查询。主索引和次索引与关系模型中同名索引不同,这常常使许多新的 Cassandra 数据模型设计师感到困惑。本章清楚地解释了何时以及如何正确使用它们。

第五章,初步设计和实现,对简单股票市场技术分析应用的数据模型进行了设计。本章首先定义了要解决的问题的业务问题、要提供的功能,然后继续设计数据模型和程序。到本章结束时,技术分析应用可以在 Cassandra 上运行以提供实际功能。

第六章,增强版本,描述了对第五章中构建的技术分析应用进行的一系列增强。这些增强涉及数据模型和代码的更改。本章说明了 Cassandra 的数据模型如何灵活地适应这些变化。

第七章,部署和监控,讨论了将应用程序从非生产环境迁移到生产环境时需要考虑的几个重要因素。这些因素包括复制策略、数据迁移、系统监控和基本性能调整。本章在一个实际的 Cassandra 集群部署中突出了这些相关因素。

第八章,最后思考,补充了使用 Cassandra 进行应用程序开发的其他主题,例如适用于不同编程语言的客户端驱动程序、内置安全功能、备份和恢复。它还推荐了一些有用的网站以获取更多信息。最后,对所有章节的快速回顾总结了这本书。

您需要为这本书准备的内容

建议读者在开始开发数据模型和应用程序之前先了解 Cassandra 的基础知识。一本很好的入门书籍是《管理员学习 Cassandra》,作者维贾伊·帕尔萨拉西Packt 出版社

虽然对 Cassandra 的了解不是强制性的,但任何有应用设计和实施背景以及关系数据建模经验的人都会发现这本书很容易与之相关联。

本书参考了最近的 Cassandra 2.0.x 版本。一些代码示例涉及 Cassandra 查询语言 3 (CQL3) 中可用的功能。

此外,由于示例应用程序是用 Python 开发的,因此对 Python 和 NumPy 的基本编程知识就足以顺畅地阅读这本书。推荐的 Python 版本是 2.7.x。一本熟悉 Python 和 NumPy 的好书是 NumPy Cookbook,作者 Ivan Idris,由 Packt Publishing 出版。

所有用于将示例应用程序设置运行所需的工具和包都可以在互联网上免费获得。为了获得示例应用程序的动手经验,建议使用 Ubuntu Linux 操作系统的计算机。

本书面向对象

如果你对 Cassandra 感兴趣,并想开发实际的分析应用程序,那么这本书非常适合你。

将 Cassandra 作为 Web 规模或 Cloud NoSQL 数据库后端使用,可以使你的架构和应用系统真正准备好应对大数据。阅读这本书后,你将了解如何处理和建模数据,以释放 Cassandra 的力量。

惯例

在这本书中,你会发现许多不同风格的文本,用于区分不同类型的信息。以下是一些这些风格的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 处理方式如下所示:“SMA 可以通过 rolling_mean() 函数轻松计算,如 chapter05_007.py 中所示。”

代码块设置如下:

# -*- coding: utf-8 -*-
# program: chapter05_007.py

import pandas as pd

## function to compute a Simple Moving Average on a DataFrame
## d: DataFrame
## prd: period of SMA
## return a DataFrame with an additional column of SMA
def sma(d, prd):
    d['sma'] = pd.rolling_mean(d.close_price, prd)
    return d

当我们希望将你的注意力引向代码块中的特定部分时,相关的行或项目将以粗体显示:

CREATE TABLE stock_ticker (
symbol varchar references stock_symbol(symbol),
tick_date varchar,
open decimal,
high decimal,

任何命令行输入或输出都按如下方式编写:

ubtc01:~$ nodetool status

新术语重要词汇以粗体显示。你在屏幕上看到的单词,例如在菜单或对话框中,在文本中显示如下:“右侧中间面板是运行代码的IPython 控制台。”

注意

警告或重要注意事项以如下框中显示。

小贴士

小贴士和技巧显示如下。

读者反馈

我们始终欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢或不喜欢什么。读者反馈对我们来说很重要,因为它帮助我们开发出你真正能从中获得最大收益的标题。

要发送给我们一般反馈,只需发送电子邮件至 <feedback@packtpub.com>,并在邮件主题中提及书籍的标题。

如果你在一个领域有专业知识,并且你对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南:www.packtpub.com/authors

客户支持

现在你已经是 Packt 书籍的骄傲拥有者,我们有许多事情可以帮助你从购买中获得最大收益。

下载示例代码

您可以从www.packtpub.com下载所有已购买 Packt Publishing 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

下载本书的颜色图像

我们还为您提供了一个包含本书中使用的截图/图表的颜色图像的 PDF 文件。这些颜色图像将帮助您更好地理解输出的变化。您可以从以下链接下载此文件:www.packtpub.com/sites/default/files/downloads/8884OS_ImageBundle.pdf

勘误

尽管我们已经尽一切努力确保我们内容的准确性,错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。

查看之前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分下。

海盗行为

互联网上对版权材料的盗版是一个持续存在的问题,涉及所有媒体。在 Packt,我们非常重视保护我们的版权和许可证。如果您在互联网上发现任何形式的非法复制我们的作品,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过<copyright@packtpub.com>与我们联系,并提供疑似盗版材料的链接。

我们感谢您在保护我们的作者和我们为您提供有价值内容的能力方面的帮助。

问题

如果您对本书的任何方面有问题,您可以通过<questions@packtpub.com>联系我们,我们将尽力解决问题。

第一章. Cassandra 的鸟瞰图

想象一下,如果我们把时钟拨回到 20 世纪 90 年代,你是一名应用架构师。每当需要为你的应用选择合适的数据库技术时,你会选择哪种数据库技术?我敢打赌 95%(或更多)的时间你会选择关系数据库。

关系数据库自 20 世纪 70 年代以来一直是数据管理解决方案中最占主导地位的形式。当时,应用系统通常是孤立的。应用用户及其使用模式是已知且可控的。关系数据库需要满足的工作负载可以确定并估算。除了工作负载的考虑之外,数据模型还可以按照关系理论推荐的方式进行规范化。此外,关系数据库提供了许多好处,如支持事务、数据一致性和隔离性。关系数据库非常适合这些用途。因此,不难理解为什么关系数据库如此受欢迎,为什么它是应用开发中持久数据存储的事实标准。

然而,随着互联网的普及和大量在其上运行的 Web 应用,用户及其使用模式(因此规模)、生成的工作负载和数据模型灵活性都消失了。这些 Web 应用的典型例子包括全球电子商务网站、社交媒体网站、视频社区网站等。它们在极短的时间内产生了大量数据。还应注意的是,这些应用生成数据不仅结构化,而且半结构化和非结构化。由于当时关系数据库是事实标准,开发者和架构师没有太多选择,只能被迫对其进行调整以支持这些 Web 应用,尽管他们知道关系数据库不是最优的,并且有许多局限性。很明显,需要找到一种不同类型的使能技术来突破这些挑战。

我们正处于信息爆炸的时代,这是由于网络和移动应用中用户生成数据和内容的持续增加。生成数据不仅量大且速度快,而且种类繁多。这种快速增长的、种类多样的数据通常被称为大数据

没有人对大数据有一个明确、正式的定义。然而,人们普遍认为,大数据的最基本特征与大量、高速和多样性有关。大数据给采用传统数据处理方式的信息系统带来了真实、新的挑战。这些系统并非为网络规模设计,且为了有效地进行扩展,成本效益并不高。因此,你可能会问自己,我们是否有任何替代方案。

机遇与挑战并存。新一代的数据管理产品应运而生。上一段问题的最新答案是 NoSQL。

什么是 NoSQL?

应对大数据挑战的需求导致了新的数据管理技术和技术的出现。这些技术和技术与使用超过 40 年的普遍关系型数据库技术截然不同。它们统称为NoSQL

NoSQL 是一个术语,用于描述不基于关系数据模型的数据存储。它包括许多不同的数据库技术和产品。如图所示,数据平台景观图,有超过 150 种不同的数据库产品属于非关系型学校,如nosql-database.org/中提到的。Cassandra 是最受欢迎的之一。其他流行的 NoSQL 数据库产品,仅举几个例子,包括 MongoDB、Riak、Redis、Neo4j 等等。

什么是 NoSQL?

数据平台景观图(来源:451 Research)

那么,NoSQL 提供了哪些好处?与关系型数据库相比,NoSQL 克服了关系数据模型处理不佳的弱点,如下所示:

  • 巨大的结构化、半结构化和非结构化数据量

  • 灵活的数据模型(模式)易于更改

  • 网络规模应用的扩展性和性能

  • 成本更低

  • 关系数据模型与面向对象编程之间的阻抗不匹配

  • 内置复制

  • 支持敏捷软件开发

备注

NoSQL 数据库的限制

许多 NoSQL 数据库不支持事务。它们广泛使用复制,因此集群中的数据可能会暂时不一致(尽管最终是一致的)。此外,NoSQL 数据库中不可用范围查询。此外,灵活的模式可能会导致高效搜索的问题。

之前提到了大量结构化、半结构化和非结构化数据。我想深入探讨的是,不同的 NoSQL 数据库为每种数据提供不同的解决方案。需要考虑的主要因素是 NoSQL 数据库类型,这将在下一节介绍。

所有 NoSQL 数据库都提供了一种灵活的数据模型,易于更改,其中一些甚至可能是无模式的。在关系型数据库中,关系数据模型被称为模式。在关系型数据库中存储数据之前,您需要理解要存储的数据,根据关系型数据库理论设计数据模型,并在关系型数据库中预先定义模式。这是一种非常结构化的方法,适用于结构化数据。这是一个规定性的数据建模过程。如果数据模型稳定,那就绝对没问题,因为不需要进行很多更改。但如果数据模型在未来不断变化,您不知道需要更改什么?您无法事先全面规定。这会导致许多不可避免的补救措施;比如说,例如,数据修补来更改模式。

相反,在 NoSQL 数据库中,您不需要全面规定。您只需要描述要存储的内容。您不受关系型数据库理论的约束。您可以在必要时随时更改数据模型。数据模型是无模式的,是一个活生生的对象。它随着时间的推移而演变。这是一个描述性的数据建模过程。

对于 Web 规模的应用程序,可扩展性和性能指的是系统可扩展的能力,最好是水平扩展,以支持 Web 规模的工作负载,而不会显著降低系统性能。关系型数据库只能扩展到由非常少数节点组成的集群。这意味着使用关系型数据库构建的这些 Web 规模应用程序的上限相当低。此外,在集群关系型数据库中更改模式是一项复杂度高的重大任务。完成这项任务所需的处理能力如此之大,以至于系统性能不可避免地受到影响。大多数 NoSQL 数据库都是为了服务 Web 规模应用程序而创建的。它们原生支持水平扩展,而性能下降非常小。

现在让我们谈谈金钱。传统上,大多数高端关系数据库是商业产品,要求用户支付巨额软件许可费。此外,要运行这些高端关系数据库,底层硬件服务器通常也是高端的。结果是,运行强大关系数据库的软硬件成本特别高。相比之下,大多数 NoSQL 数据库是开源的,由社区驱动,这意味着你需要支付的软件许可费用比其他数据库低一个数量级。NoSQL 数据库能够在通用机器上运行,这可能导致可能的故障或崩溃。因此,机器通常配置为集群。高端硬件服务器不需要,因此硬件成本大幅降低。需要注意的是,当 NoSQL 数据库投入生产时,仍需要一些支持成本,但与商业产品相比,这肯定要少得多。

关系数据模型和面向对象编程之间存在一代沟。关系数据模型是 20 世纪 70 年代的产品,而面向对象编程在 20 世纪 90 年代变得非常流行。被称为阻抗不匹配的根本原因,是关系数据模型在面向对象模型中表达记录或表的一个固有困难。尽管有解决这个困难的方法,但大多数应用开发者仍然感到非常沮丧,因为将两者结合起来。

注意

阻抗不匹配

阻抗不匹配是关系模型和通常在面向对象编程语言中遇到的内存数据结构之间的差异。

内置复制是大多数 NoSQL 数据库提供的一项功能,用于支持多个节点集群中的高可用性。这通常是自动的,对应用开发者来说是透明的。这种功能在关系数据库中也是可用的,但数据库管理员必须自己努力配置、管理和操作它。

最后,关系数据库并不很好地支持敏捷软件开发。敏捷软件开发本质上是迭代的。软件架构和数据模型随着项目的进行而出现和演变,以便逐步交付产品。因此,可以想见,为了满足新的需求而改变数据模型的需求不可避免地很频繁。关系数据库是结构化的,不喜欢变化。NoSQL 可以通过其无模式特性为敏捷软件开发团队提供这种灵活性。更好的是,NoSQL 数据库通常允许实时实施更改,而无需停机。

NoSQL 数据库类型

现在你已经知道了 NoSQL 数据库的好处,但属于 NoSQL 数据库范畴的产品相当多样。如何在众多 NoSQL 数据库中为自己选择合适的数据库?选择哪种 NoSQL 数据库适合你的需求实际上取决于手头的用例。这里要考虑的最重要因素是 NoSQL 数据库的类型,它可以细分为四大主要类别:

  • 键值对存储

  • 列族存储

  • 基于文档的存储库

  • 图数据库

NoSQL 数据库类型决定了你可以使用的数据模型。深入了解每一个都是有益的。

键值对存储

键值对是最简单的 NoSQL 数据库类型。键值存储类似于 Windows 注册表的概念,或者在 Java 或 C#中,是一个映射、一个哈希、一个键值对。每个数据项都表示为一个属性名称,也是一个键,以及它的值。它也是数据库中存储的基本单元。键值对类型的 NoSQL 数据库的例子有Amazon DynamoBerkeley DBVoldemortRiak

在内部,键值对存储在一个称为哈希表的数据结构中。哈希表之所以受欢迎,是因为它在访问数据方面提供了非常好的性能。键值对的键是唯一的,可以非常快速地进行搜索。

键值对可以存储和分布在磁盘存储以及内存中。当在内存中使用时,它可以作为缓存使用,这取决于缓存算法,可以显著减少磁盘 I/O,从而显著提高性能。

另一方面,键值对也有一些缺点,例如不支持范围查询,无法同时操作多个键,以及可能存在的负载均衡问题。

列族存储

在这个上下文中,列不等于关系表中的列。在 NoSQL 世界中,列是一个包含键、值和时间的结构。因此,它可以被视为键值对和时间戳的组合。例子有Google BigTableApache CassandraApache HBase。它们为查询非常大的数据集提供了优化的性能。

列族存储基本上是一个多维映射。它将数据列作为一个行存储,与一个行键相关联。这与关系数据库中的数据行形成对比。列族存储不需要存储 null 列,就像关系数据库中的情况一样,因此它消耗的磁盘空间要少得多。此外,列不受严格模式的约束,你也不需要预先定义模式。

列的关键组件通常被称为主键或行键。列按行键排序存储。属于行键的所有数据都存储在一起。因此,数据的读写操作可以限制在本地节点上,避免在集群中产生不必要的节点间网络流量。这种机制使得数据的查找和检索非常高效。

显然,列族存储对于需要 ACID 事务的系统来说不是最佳解决方案,它缺乏关系数据库(如 SUM())提供的聚合查询支持。

基于文档的存储库

基于文档的存储库是为文档或半结构化数据设计的。基于文档的存储库的基本单元将每个键(一个主标识符)与一个称为文档的复杂数据结构关联起来。文档可以包含许多不同的键/值对、键/数组对,甚至嵌套文档。因此,基于文档的存储库不遵循模式。例如 MongoDBCouchDB

在实践中,文档通常是以 JavaScript 对象表示法JSON)形式存在的松散结构化键/值对集合。基于文档的存储库将文档作为一个整体来管理,避免将文档拆分成键/值对的片段。它还允许将文档属性与文档关联起来。

作为文档数据库不遵循固定模式,搜索性能无法保证。查询文档数据库通常有两种方法。第一种是使用预先准备的材料化视图(例如 CouchDB)。第二种是使用定义在文档值上的索引(例如 MongoDB),其行为与关系数据库索引相同。

图数据库

图数据库是为存储有关网络的信息而设计的,例如社交网络。图用于表示由节点及其关系组成的高度连接的网络。节点和关系可以具有单独的属性。突出的图数据库包括 Neo4JFlockDB

由于图具有独特的特性,图数据库通常提供用于快速遍历图的 API。

图数据库在分片扩展方面尤其困难,因为跨不同机器上的节点图遍历并不提供很好的性能。同时更新所有或部分节点也不是一个简单的操作。

到目前为止,你已经掌握了 NoSQL 家族的 fundamentals。由于这本书专注于 Apache Cassandra 及其数据模型,你需要了解 Cassandra 是什么,并对其架构有一个基本的了解,这样你就可以在设计 NoSQL 数据模型和应用时选择和利用最佳选项。

什么是 Cassandra?

Cassandra 可以简单地用一句话来描述:一个基于对等架构的、大规模可扩展、高度可用的开源 NoSQL 数据库。

Cassandra 现在已经 5 岁了。它是 Apache 软件基金会中的一个活跃的开源项目,因此也被称为 Apache Cassandra。Cassandra 可以在跨多个数据中心的庞大分布式集群中管理大量结构化、半结构化和非结构化数据。它提供线性可扩展性、高性能、容错性和非常灵活的数据模型。

注意

Netflix 和 Cassandra

一个非常著名的 Cassandra 研究案例是 Netflix 将其 Oracle SQL 数据库替换为在云上运行的 Cassandra。截至 2013 年 3 月,Netflix 的 Cassandra 部署由 50 个集群和超过 750 个节点组成。更多信息,请访问案例研究www.datastax.com/wp-content/uploads/2011/09/CS-Netflix.pdf

事实上,Cassandra 提供的许多好处都继承自其两个最优秀的 NoSQL 祖先,Google BigTable 和 Amazon Dynamo。在我们深入探讨 Cassandra 的架构细节之前,让我们先了解一下它们各自的特点。

Google BigTable

Google BigTable 是 Google 的核心技术,特别是针对 Web 规模的数据持久性和管理。它运行着许多 Google 应用程序的数据存储,例如 Gmail、YouTube 和 Google Analytics。它被设计为不牺牲实时响应的 Web 规模数据存储。它具有卓越的读写性能、线性可扩展性和持续可用性。

Google BigTable 是一个稀疏的、分布式的、持久的、多维排序映射。映射由行键索引。

尽管 Google BigTable 提供了许多好处,但其底层设计概念实际上非常简单且优雅。它为接收到的每个数据写入请求使用持久的提交日志,然后将数据写入内存存储(充当缓存)。在固定时间间隔或由特定事件触发时,内存存储通过后台进程被刷新到持久磁盘存储。这种持久磁盘存储被称为 排序字符串表SSTable。SSTable 是不可变的,这意味着一旦写入磁盘,它将永远不会再次更改。单词 sorted 意味着 SSTable 内部的数据是索引和排序的,因此数据可以非常快速地找到。由于写入操作基于日志和内存,它不涉及任何读操作,因此写入操作可以非常快。如果发生故障,提交日志可以用来重放写入操作的序列,以合并保存在 SSTables 中的数据。

通过在内存存储和索引的 SSTables 中查找数据,读操作也非常高效,这些数据随后被合并以返回。

所述的 Google BigTable 的卓越之处确实是有代价的。因为 Google BigTable 本质上是分布式的,它受到著名的CAP 定理的限制,该定理阐述了分布式系统三个特性之间的关系,即一致性、可用性和分区容错性。简而言之,Google BigTable 更倾向于一致性和分区容错性,而不是可用性。

注意

CAP 定理

CAP 是分布式系统三个特性的缩写:一致性(Consistency)、可用性(Availability)和分区容错性(Partition-tolerance)。一致性意味着集群中的所有节点在任何时间点都能看到相同的数据。可用性意味着集群中每个非失败节点接收到的每个请求都必须得到响应。分区容错性意味着当与其他节点组的通信丢失时,节点仍然可以继续工作。这个定理起源于埃里克·A·布鲁尔(Eric A. Brewer),它表明在一个分布式系统中,最多只能实现这三个特性中的两个。

当集群发生故障时,Google BigTable 在保持分区节点的一致性时会有可用性问题。

Amazon Dynamo

Amazon Dynamo 是由 Amazon 开发的专有键值存储。它旨在提供高性能、高可用性和海量数据的持续增长。它是 Amazon 的分布式、高可用性和容错骨架。Dynamo 是一种对等设计,意味着每个节点都是一个对等节点,没有谁是管理数据的权威节点。

Dynamo 在集群的多个节点上使用数据复制和自动分片。想象一下,一个 Dynamo 集群由许多节点组成。节点上的每个写操作都会复制到另外两个节点。因此,集群内部有三份数据副本。如果其中一个节点因任何原因失败,仍然可以检索到两份数据副本。自动分片确保数据在集群中分区。

注意

自动分片

NoSQL 数据库产品通常支持自动分片,以便它们可以原生地自动将数据分布到数据库集群中。数据和负载会在集群中的节点之间自动平衡。当某个节点因任何原因失败时,可以快速且透明地替换失败的节点,而不会中断服务。

Dynamo 主要关注集群的高可用性,最重要的思想是最终一致性。在考虑 CAP 定理时,Dynamo 更倾向于分区容错和可用性而非一致性。Dynamo 引入了一种称为最终一致性的机制来支持一致性。在某个时间点,集群中可能会出现暂时的不一致性,但最终所有节点都将接收到最新的一致更新。在一段足够长的时间没有进一步变化的情况下,所有更新都可以预期在整个集群中传播,并且所有节点的副本最终都将是一致的。在现实生活中,更新只需要极短的时间就能达到最终一致性。换句话说,这是在一致性和延迟之间的一种权衡。

注意

最终一致性

最终一致性不是不一致性。它是一种比关系数据库中典型的原子性一致性隔离持久性(ACID)一致性更弱的一致性形式。它意味着在复制节点之间更新数据时,可能会存在短暂的不一致性间隔。换句话说,副本是异步更新的。

Cassandra 的高级架构

Cassandra 运行在点对点架构上,这意味着集群中的所有节点都有平等的责任,除了其中一些节点是种子节点,用于在启动时让其他非种子节点获取有关集群的信息。每个节点持有数据库的一部分。Cassandra 提供了在集群中所有节点上的自动数据分布和复制。提供了参数来自定义分布和复制行为。一旦配置完成,这些操作将在后台处理,并且对应用程序开发者是完全透明的。

Cassandra 是一个列族存储,为应用程序开发者提供了极大的无模式灵活性。它旨在管理大型集群中的大量数据,而不存在单点故障。由于在集群中复制了相同数据的多个副本,因此每当一个节点因任何原因失败时,其他副本仍然可用。复制可以配置以满足不同的物理集群设置,包括数据中心和机架位置。

集群中的任何节点都可以接受来自客户端的读或写请求。连接到请求客户端的节点充当该特定请求的协调器。协调器确定哪些节点负责持有请求的数据,并充当客户端和节点之间的代理。

Cassandra 借鉴了 Google BigTable 的 commitlog 机制以确保数据持久性。每当节点接收到写数据请求时,它会被写入 commitlog。正在更新的数据随后会被写入一个称为 memtable 的内存结构。当 memtable 满了之后,memtable 中的数据会被刷新到一个磁盘存储结构,即 SSTable。写操作会自动根据行键进行分区,并复制到持有相同分区的其他节点。

Cassandra 提供线性可扩展性,这意味着集群的性能和容量与其中的节点数量成正比。

分区

横向扩展和增量扩展的能力是 Cassandra 的关键设计特性。为了实现这一点,Cassandra 需要动态地将数据分区到集群中的节点集合中。

集群是 Cassandra 中最外层的结构,由节点组成。它也是 keyspace 的容器。在 Cassandra 中,keyspace 类似于关系数据库中的模式。每个 Cassandra 集群都有一个系统 keyspace 来存储系统级元数据。它包含复制设置,用于控制数据在集群中的分布和复制方式。通常,一个 keyspace 分配给一个集群,但一个集群可能包含多个 keyspace。

理论上最小的集群包含一个节点和三个或更多节点的集群,这更加实用。每个节点持有不同分区范围内数据的副本,并且每秒钟在集群中交换信息。

客户端向任何节点发出读取或写入请求。接收请求的节点成为协调器,充当客户端的代理执行之前所述的操作。数据在集群中分布,节点寻址机制称为一致性哈希。因此,集群可以被视为一个哈希环,因为集群中的每个节点或环都被分配一个唯一的令牌,以便每个节点负责从其分配的令牌到前一个节点令牌范围内的数据。例如,在下面的图中,一个集群包含四个具有唯一令牌的节点:

分区

Cassandra 的一致性哈希

在版本 1.2 之前,令牌是手动计算和分配的,从版本 1.2 开始,令牌可以自动生成。每一行都有一个分区器使用的行键,用于计算其哈希值。哈希值决定了存储行第一个副本的节点。分区器只是一个用于计算行键哈希值的哈希函数,它还影响数据在集群中的分布或平衡方式。当发生写入操作时,行的第一个副本总是放置在具有令牌键范围的节点上。例如,行键ORACLE的哈希值为6DE7,位于 4,000 到 8,000 的范围内,因此行首先被放置在底部节点。所有剩余的副本都是根据复制策略进行分布的。

注意

一致性哈希

一致性哈希允许集群中的每个节点独立确定给定行键的副本节点。它只涉及对行键进行哈希处理,然后比较该哈希值与集群中每个节点的令牌。如果哈希值落在节点令牌之间,以及环中前一个节点的令牌(令牌按顺时针方向分配给节点)之间,那么该节点就是该行键的副本节点。

复制

Cassandra 使用复制来实现高可用性和数据持久性。每个数据都在配置有称为复制因子的参数的多个节点上进行复制。协调器在其范围内指挥数据的复制。它将数据复制到环中的其他节点。Cassandra 为客户端提供了各种可配置选项,以查看数据如何进行复制,这被称为复制策略。

复制策略是确定副本放置在哪些节点上的方法。它提供了许多选项,例如机架感知、非机架感知、网络拓扑感知等。

Snitch(侦听器)

Snitch(侦听器)确定访问哪些数据中心和机架,以便使 Cassandra 能够了解网络拓扑,从而有效地路由请求。它影响在考虑数据中心的物理设置和机架的情况下,副本的分布方式。节点位置可以通过机架和数据中心以及节点的 IP 地址来确定。以下图示了一个跨两个数据中心的集群示例,以便更好地说明复制因子、复制策略和侦听器之间的关系:

Snitch

多数据中心集群

每个数据中心有两个机架,每个机架分别包含两个节点。这里每个数据中心的复制因子设置为三。有两个数据中心,总共有六个副本。节点位置,即数据中心和机架位置,遵循节点 IP 地址分配的惯例。

种子节点

在一个 Cassandra 集群中,一些节点被指定为其他节点的种子节点。它们被配置为集群中首先启动的节点。它们还简化了新节点加入集群的初始化过程。当一个新节点上线时,它将与种子节点通信以获取集群中其他节点的信息。这种通信机制被称为八卦。如果一个集群跨越多个数据中心,最佳实践是在每个数据中心拥有不止一个种子节点。

八卦和故障检测

节点需要定期(每秒)通信以交换状态信息(例如,死亡或存活),关于它们自己和它们所知的其他节点。Cassandra 使用八卦通信协议来传播状态信息,也称为流行病协议。它是一种对等通信协议,为集群中的节点提供了一个去中心化、定期和自动的方式,以与其他最多三个节点交换它们自己和它们所知的其他节点的状态信息。因此,所有节点都可以快速了解集群中的所有其他节点。八卦信息也被每个节点本地持久化,以允许快速重启。

Cassandra 使用一个非常高效的算法,称为Phi 累积故障检测算法,来检测节点的故障。该算法的思路是,故障检测不是通过一个表示节点是否上线的布尔值来表示。相反,算法输出一个值,表示在死亡和存活之间的连续怀疑水平,以及它对节点已失败的信心程度。在分布式环境中,由于网络性能、波动的工作负载和其他条件,可能会发生假阴性。该算法考虑了所有这些因素,并提供了概率值。如果一个节点已失败,其他节点将定期尝试与它进行八卦,以查看它是否重新上线。节点可以据此从八卦状态及其历史记录中本地确定,并相应地调整路由。

写入路径

下图描述了构成写入路径的组件及其执行顺序:

写入路径

Cassandra 写入路径

当发生写入操作时,数据将立即追加到磁盘上的 commitlog 以确保写入持久性。然后 Cassandra 将数据存储在 memtable 中,这是一个热数据和新鲜数据的内存存储。当 memtable 满时,memtable 数据将通过顺序 I/O 刷新到称为 SSTable 的磁盘文件中,从而避免了随机 I/O。这就是为什么写入性能如此之高的原因。在刷新后,commitlog 将被清除。

由于有意采用顺序 I/O,一行数据通常存储在多个 SSTable 文件中。除了其数据外,SSTable 还有一个主索引和一个bloom filter。主索引是行键列表以及数据文件中行的起始位置。

注意

布隆过滤器

布隆过滤器是主索引的一个样本子集,具有非常快速的确定性算法来检查一个元素是否是集合的成员。它用于提升性能。

对于写操作,Cassandra 通过各种写一致性级别支持可调一致性。写一致性级别是确认成功写入的副本数量。它可以在一系列写一致性级别上进行调整,如图所示:

写入路径

Cassandra 写一致性级别

以下描述了图中的术语:

  • ANY: 这是最低的一致性(但可用性最高)

  • ALL: 这是最高的一致性(但可用性最低)

  • ONE: 这至少提供一个副本

  • TWO: 这至少提供两个副本

  • THREE: 这至少提供三个副本

  • QUORUM: 这通过容忍一定程度的故障来确保强一致性,故障程度由 (replication_factor / 2) + 1(向下取整到最接近的整数)确定

  • LOCAL_QUORUM: 这适用于多数据中心和机架感知,但没有数据中心间流量

  • EACH_QUORUM: 这适用于多数据中心和机架感知

两个极端是左边的ANY,表示弱一致性,和右边的ALL,表示强一致性。实践中非常常见的THREE一致性级别。QUORUM可以选择为最佳值,如给定公式计算。在这里,复制因子是多个节点上数据的副本数量。LOCAL QUORUMEACH QUORUM都支持多数据中心和机架感知的写一致性,与前面所示略有不同。

读取路径

反之,以下图显示了构成读取路径的组件及其执行顺序:

读取路径

Cassandra 读取路径

当读取请求到达一个节点时,要返回的数据是从所有相关的 SSTables 和任何未刷新的 memtables 中合并的。时间戳用于确定哪个是最新的。合并的值也存储在写入通过行缓存中,以提高未来的读取性能。

与写一致性级别类似,Cassandra 还提供了可调的读取一致性级别,如图所示:

读取路径

Cassandra 读取一致性级别

以下描述了图中的术语:

  • ALL: 这是最高的一致性(但可用性最低)

  • ONE: 这至少提供一个副本

  • TWO: 这至少提供两个副本

  • THREE: 这至少提供三个副本

  • QUORUM: 这通过容忍一定程度的故障来确保强一致性,故障程度由 (replication_factor / 2) + 1(向下取整到最接近的整数)确定

  • LOCAL_QUORUM: 这适用于多数据中心和机架感知,但没有数据中心间流量

  • EACH_QUORUM: 这适用于多数据中心和机架感知

读取一致性级别是成功、一致读取时接触的副本数量,几乎与写入一致性级别相同,只是这里没有任何选项。

修复机制

Cassandra 提供了三种内置的修复机制:

  • 读取修复

  • 暗示传递

  • 反熵节点修复

在读取过程中,仅连接并服务客户端的协调器(即节点)会根据数据的一致性级别和最快的副本数量联系多个节点,通过内存比较进行一致性检查。由于它不是一个专用节点,Cassandra 缺乏单点故障。它还会在后台检查所有剩余的副本。如果发现副本不一致,协调器将发出更新以恢复一致性。这种机制称为读取修复

暗示传递旨在减少在重新加入集群时恢复失败节点的时间。它通过牺牲一点读取一致性来确保绝对的写入可用性。如果在写入发生时副本已关闭,另一个健康的副本将存储一个提示。更糟糕的是,如果所有相关的副本都关闭了,协调器将本地存储提示。提示基本上包含失败副本的位置、受影响的行键以及正在写入的实际数据。当负责令牌范围的节点再次上线时,提示将被传递以恢复写入。因此,更新在完全传递之前不能被读取,导致不一致的读取。

另一种修复机制称为反熵,它是一个副本同步机制,用于确保所有节点上的数据都是最新的,并且由管理员手动运行。

Cassandra 的特性

为了使本章简短,以下项目符号列表涵盖了 Cassandra 提供的一些主要特性:

  • 用 Java 编写,因此提供原生 Java 支持

  • 结合了 Google BigTable 和 Amazon Dynamo

  • 灵活的非模式化列族数据模型

  • 支持结构化和非结构化数据

  • 去中心化、分布式对等架构

  • 多数据中心和机架感知的数据复制

  • 位置透明

  • 云支持

  • 具有容错性,没有单点故障

  • 自动且透明的故障转移

  • 弹性、大规模和线性可扩展

  • 在线节点添加或删除

  • 高性能

  • 内置数据压缩

  • 内置缓存层

  • 写入优化

  • 可调一致性,提供从非常强的一致性到不同级别的最终一致性选择

  • 提供类似于 SQL 的Cassandra 查询语言CQL),一种模仿 SQL 的INSERTUPDATEDELETESELECT语法的类似语言

  • 开源并由社区驱动

摘要

在本章中,我们从 20 世纪 70 年代开始回顾了一点点历史。我们完全控制着那些相当稳定的数据模型和相对简单的应用程序。在那些日子里,关系型数据库是一个完美的选择。随着面向对象编程的出现和互联网上网络应用程序的爆炸式增长,数据的本质已经从结构化扩展到半结构化和非结构化。此外,应用程序也变得更加复杂。关系型数据库再也无法完美无缺。大数据的概念被创造出来描述这样的挑战,而 NoSQL 数据库为关系型数据库提供了一种替代的解决方案。

NoSQL 数据库种类繁多。它们提供了一些共同的优点,并且可以根据 NoSQL 数据库类型进行分类。Apache Cassandra 是 NoSQL 数据库之一,它是 Google BigTable 和 Amazon Dynamo 的结合。其架构的优雅性继承了这两个父母的 DNA。

在下一章中,我们将探讨 Cassandra 支持的灵活数据模型。

第二章:Cassandra 数据建模

在本章中,我们将打开通往 Cassandra 数据建模世界的大门。我们将简要介绍其构建模块,与关系数据模型的主要区别,以及如何在 Cassandra 数据模型上构建查询的示例。

Cassandra 通过使用从其 Google BigTable 父级继承的术语来描述其数据模型组件,例如列族、列、行等。其中一些术语也存在于关系型数据模型中。然而,它们的含义却完全不同。这常常会让有关系型背景的开发人员和管理员感到困惑。乍一看,Cassandra 数据模型似乎反直觉,非常难以把握和理解。

在关系型世界中,你通过创建实体并根据关系理论指导下的指南将它们关联起来来建模数据。这意味着你可以只关注数据的逻辑视图或结构,而不必考虑应用程序如何访问和操作数据。目标是拥有一个符合关系型指导原则的稳定数据模型。应用程序的设计可以单独完成。例如,你可以通过构建不同的 SQL 语句来回答不同的查询,这在数据建模过程中并不需要你关心。简而言之,关系型数据建模是面向过程的,基于明确的关注点分离。

相反,在 Cassandra 中,你将上述步骤颠倒过来,始终从你想要在应用程序的查询中回答的问题开始。查询对底层数据模型产生了相当大的影响。你还需要考虑物理存储和集群拓扑。因此,查询和数据模型是孪生兄弟,因为它们是同时诞生的。Cassandra 数据建模是基于对查询在 Cassandra 内部如何工作的清晰理解的结果导向型。

由于 Cassandra 独特的架构,关系型数据库中的许多简单事物,如序列和排序,不能假设。它们在实现时需要你特别处理。此外,它们通常是你在数据建模过程中需要提前做出的设计决策。这可能是在获得卓越的可伸缩性、性能和容错能力的过程中所付出的代价。

为了享受阅读这本书,建议你暂时以关系型和 NoSQL 两种方式思考。尽管你可能不会成为 Cassandra 的朋友,但你将会有一个令人耳目一新的体验,意识到世界上存在不同的工作方式。

Cassandra 数据模型有什么独特之处?

如果让我用一句话来描述 Cassandra 的数据模型,我会说它是一个非关系型数据模型,仅此而已。这意味着你需要忘记你在关系型数据库中进行数据建模的方式。

你需要根据关系理论来建模数据。然而,在 Cassandra 以及其他 NoSQL 数据库中,除了数据本身之外,你还需要关注应用。这意味着你需要考虑在应用中如何查询数据。对于那些来自关系世界的人来说,这是一个范式转变。接下来的章节中会给出示例,以确保你理解为什么不能将关系理论应用于 Cassandra 中的数据建模。

在 Cassandra 数据建模中,另一个重要的考虑因素是你需要考虑 Cassandra 集群的物理拓扑。在关系型数据库中,主要目标是通过对数据进行规范化来消除数据冗余,以有一个单一的数据源。这使得关系型数据库很容易实现 ACID 一致性。相关的存储空间需求也得到了优化。相反,Cassandra 是设计用于在难以实现 ACID 一致性和必须进行复制的海量规模分布式环境中工作。你必须意识到在 Cassandra 数据建模过程中这些差异。

Map 和 SortedMap

在 第一章 中,你学习了 Cassandra 的存储模型是基于 BigTable,一个列式存储。列式存储是一个多维映射。具体来说,它是一个称为 Map 的数据结构。以下是一个声明 Map 数据结构的示例:

Map<RowKey, SortedMap<ColumnKey, ColumnValue>>

Map 数据结构提供了高效的关键字查找,并且其排序特性提供了高效的扫描。RowKey 是一个唯一键,可以存储一个值。内部的 SortedMap 数据结构允许存储可变数量的 ColumnKey 值。这是 Cassandra 使用以实现无模式并且允许数据模型随时间自然演化的技巧。需要注意的是,每个列都有一个客户端提供的时间戳与之关联,但在数据建模过程中可以忽略。Cassandra 在内部使用时间戳来解决事务冲突。

在关系型数据库中,列名只能是字符串,并存储在表元数据中。在 Cassandra 中,RowKeyColumnKey 都可以是字符串、长整数、通用唯一标识符或任何类型的字节数组。此外,ColumnKey 存储在每个列中。你可能认为反复存储 ColumnKey 值会浪费存储空间。然而,这为 Cassandra 带来了一个非常强大的特性。RowKeyColumnKey 可以存储数据,而不仅仅是 ColumnValue。我们目前不会深入探讨这一点;我们将在后面的章节中重新讨论。

注意

通用唯一标识符

通用唯一标识符(UUID)是互联网工程任务组(IETF)标准,请求评论(RFC)4122,旨在使分布式系统能够在不进行重大中央协调的情况下唯一标识信息。它是一个由 32 个小写十六进制数字表示的 128 位数字,以五组由连字符分隔的形式显示,例如:0a317b38-53bf-4cad-a2c9-4c5b8e7806a2

逻辑数据结构

有几个逻辑构建块可以构建 Cassandra 数据模型。以下将逐一介绍它们。

列是 Cassandra 中最小的数据模型元素和存储单元。尽管它也存在于关系型数据库中,但在 Cassandra 中是不同的事物。如图所示,列是一个具有戳记和可选的生存时间(Time-To-Live,TTL)值的名称-值对:

列

列的元素

名称和值(在SortedMap中分别为ColumnKeyColumnValue)是字节数组,Cassandra 提供了一组内置数据类型,这些数据类型会影响值的排序顺序。这里的戳记用于冲突解决,并在写入操作期间由客户端应用程序提供。生存时间(Time-To-Live)是一个可选的过期值,用于标记在过期后删除的列。然后,在压缩过程中物理删除该列。

上一个级别是行,如图所示。它是一组具有唯一行键的可排序列,也称为主键:

行

行的结构

行键可以是与列相同的任何内置数据类型。可排序的意思是列按其列名排序存储。

注意

排序顺序非常重要,因为 Cassandra 不能像我们在关系型数据库中那样按值排序。

不同行中的列可能有不同的名称。这就是为什么 Cassandra 既是行导向又是列导向。应该指出的是,行没有戳记。此外,行不能分割以存储在集群的两个节点之间。这意味着如果行存在于一个节点上,整个行都存在于该节点上。

列族

上一个级别是列族。如图所示,它是一个具有名称的行集合的容器:

列族

列族的结构

列族中的行键必须是唯一的,并用于排序。列族在关系型数据库中类似于表,但你不应该过分依赖这个想法。列族通过允许不同行中的不同列提供了更大的灵活性。任何列都可以在任何时候自由添加到任何列族中。再次强调,这有助于 Cassandra 的无模式性。

列族中的列按比较器排序。比较器决定了 Cassandra 在查询中返回列时的排序和顺序。它接受列名的数据类型为长整型、字节和 UTF8,以及列在行内存储的排序顺序。

物理上,列族存储在磁盘上的单个文件中。因此,保持相关列在同一个列族中对于节省磁盘 I/O 和提高性能很重要。

键空间

如下所示图所示,最外层数据模型元素是键空间:

键空间

键空间的结构

键空间是一组列族和超列族,将在下一节中介绍。它在关系世界中类似于模式或数据库。每个 Cassandra 实例都有一个系统键空间来存储系统级元数据。

键空间包含复制设置,控制数据在集群中的分布和复制方式。通常,一个集群中只有一个键空间。

超列和超列族

如下所示图所示,超列是一组列的命名映射,而超列族只是超列的集合:

超列和超列族

超列和超列族的结构

超列在 Cassandra 的早期版本中很受欢迎,但不再推荐使用,因为它们不受 Cassandra 查询语言(CQL),一种类似于 SQL 的语言来操作和查询 Cassandra 的支持,并且必须通过使用低级的 Thrift API 来访问。在大多数情况下,一个列族就足够了。

注意

Thrift

Thrift 是一个用于开发可扩展跨语言服务的软件框架。它结合了一个软件栈和一个代码生成引擎来构建与多种编程语言高效且无缝工作的服务。它被用作 远程过程调用 (RPC) 框架,并由 Facebook 公司开发。现在它是 Apache 软件基金会的一个开源项目。

还有其他替代方案,例如,Protocol Buffers、Avro、MessagePack、JSON 等等。

集合

Cassandra 允许集合,即集合、列表和映射,作为数据模型的一部分。集合是一种复杂类型,可以在查询中提供灵活性。

Cassandra 允许以下集合:

  • 集合:这些提供了一种保持唯一值集的方法。这意味着可以轻松解决跟踪唯一值的问题。

  • 列表:这些适合维护集合中值的顺序。列表按所选类型的自然顺序排序。

  • 映射:这些类似于键值对的存储。它们对于在单行内存储类似表的数据很有用。它们可以作为一种在没有连接的情况下存储数据的解决方案。

在这里,我们只提供了一个简要的介绍,我们将在后续章节中重新讨论集合。

没有外键

外键在关系数据库中用于维护引用完整性,这定义了两个表之间的关系。它们用于在关系数据模型中强制关系,以便不同但相关的表中的数据可以连接起来以回答查询。Cassandra 没有引用完整性的概念,因此不允许连接。

无连接

外键和连接是关系数据模型中规范化的产物。Cassandra 既没有外键也没有连接。相反,它鼓励并在此数据模型去规范化时表现最佳。

事实上,在关系型世界中,去规范化并非完全被禁止,例如,建立在关系数据库之上的数据仓库。在实践中,去规范化是解决高度复杂的关系型查询性能不佳的问题的一种解决方案,这些查询涉及大量表连接。

注意

在 Cassandra 中,去规范化是正常的。

通过适当的数据建模,可以在 Cassandra 中避免外键和连接。

无序列

在关系数据库中,序列通常用于为代理键生成唯一值。Cassandra 没有序列,因为它在点对点分布式系统中实现起来极其困难。然而,有一些解决方案,如下所述:

  • 使用部分数据生成唯一键

  • 使用 UUID

在大多数情况下,最佳实践是选择第二个解决方案。

计数器

计数列是一个特殊列,用于存储持续计数的数值。计数可以是增加或减少,且不需要时间戳。

计数列不应用于生成代理键。它只是设计用来存储适合分布式计数的分布式计数器。同时请注意,更新计数器不是幂等的。

注意

幂等(Idempotent

幂等最初是数学中的一个术语。但在计算机科学中,幂等被更广泛地用来描述一个操作,如果执行一次或多次,将产生相同的结果。

存活时间(Time-To-Live)

存活时间(TTL)仅在列上设置。单位是秒。当在列上设置时,它将自动递减,然后在服务器端自动过期,无需客户端应用程序的任何干预。

典型用例包括生成安全令牌和一次性令牌、自动清除过时的列等。

二级索引

需要记住的一个重要事项是,Cassandra 中的二级索引与关系数据库中的二级索引并不相同。Cassandra 中的二级索引可以创建来查询不是主键部分的列。一个列族可以拥有多个二级索引。在幕后,它被实现为一个由 Cassandra 内部进程自动维护的独立隐藏表。

二级索引不支持集合,并且不能创建在主键本身上。主键和二级索引之间的主要区别在于,前者是一个分布式索引,而后者是一个本地索引。主键用于确定节点位置,因此对于给定的行键,其节点位置可以立即找到。然而,二级索引仅用于在本地节点上索引数据,并且可能无法立即知道所有匹配行的位置,除非检查了集群中的所有节点。因此,性能是不可预测的。

在后续章节中,我们将提供有关二级索引的更多信息。

查询建模

在上一节中,我们获得了关于关系型数据库和 Cassandra 之间差异的基本理解。最重要的差异是,关系型数据库通过关系来建模数据,而 Cassandra 通过查询来建模数据。现在让我们从一个简单的例子开始,看看查询建模意味着什么。

关系版本

下图显示了股票报价应用的简单关系数据模型:

关系版本

股票报价应用的关系数据模型(来源:Yahoo! Finance)

stock_symbol表是一个实体,代表股票主信息,如股票的符号、股票的描述以及股票交易的交易所。stock_ticker表是另一个实体,存储股票在交易日的开盘价、最高价、最低价、收盘价和成交量的价格。显然,这两个表基于symbol列存在关系。这是一个众所周知的一对多关系。

下面的两个表是数据定义语言DDL):

CREATE TABLE stock_symbol (
symbol varchar PRIMARY KEY,
description varchar,
exchange varchar
);

CREATE TABLE stock_ticker (
symbol varchar references stock_symbol(symbol),
tick_date varchar,
open decimal,
high decimal,
low decimal,
close decimal,
volume bigint,
PRIMARY KEY (symbol, tick_date)
);

考虑以下三种情况:首先,我们想要列出所有交易所中所有股票及其描述。这个 SQL 查询非常简单:

// Query A
SELECT symbol, description, exchange
FROM stock_symbol;

第二,如果我们想知道纳斯达克交易所上市的所有股票的每日收盘价和描述,我们可以编写以下 SQL 查询:

// Query B
SELECT stock_symbol.symbol, stock_symbol.description,
stock_ticker.tick_date, stock_ticker.close
FROM stock_symbol, stock_ticker
WHERE stock_symbol.symbol = stock_ticker.symbol
AND stock_symbol.exchange = ''NASDAQ'';

此外,如果我们想知道 2014 年 4 月 24 日纳斯达克交易所上市的所有股票的每日收盘价和描述,我们可以使用以下 SQL 查询:

// Query C
SELECT stock_symbol.symbol, stock_symbol.description,
stock_ticker.tick_date, stock_ticker.open,
stock_ticker.high, stock_ticker.low, stock_ticker_close,
stock_ticker.volume
FROM stock_symbol, stock_ticker
WHERE stock_symbol.symbol = stock_ticker.symbol
AND stock_symbol.exchange = ''NASDAQ''
AND stock_ticker.tick_date = ''2014-04-24'';

依靠关系数据模型,我们可以简单地编写不同的 SQL 查询来返回不同的结果,而无需对底层数据模型进行任何更改。

Cassandra 版本

现在,让我们转向 Cassandra。上一节中的 DDL 语句可以稍作修改,以在 Cassandra 中创建列族,或表,如下所示:

CREATE TABLE stock_symbol (
symbol varchar PRIMARY KEY,
description varchar,
exchange varchar
);

CREATE TABLE stock_ticker (
symbol varchar,
tick_date varchar,
open decimal,
high decimal,
low decimal,
close decimal,
volume bigint,
PRIMARY KEY (symbol, tick_date)
);

它们乍一看似乎是正确的。

对于查询 A,我们可以以完全相同的方式查询 Cassandra 的stock_symbol表:

// Query A
SELECT symbol, description, exchange
FROM stock_symbol;

下图描述了stock_symbol表的逻辑和物理存储视图:

Cassandra 版本

查询 A 的 Cassandra 数据模型

stock_symbol表的主键只涉及一个单独的列,symbol,它也用作列族的行键和分区键。我们可以将stock_symbol表视为前一小节中提到的排序映射数据结构:

Map<RowKey, SortedMap<ColumnKey, ColumnValue>>

分配的值如下:

RowKey=AAPL
ColumnKey=description
ColumnValue=Apple Inc.
ColumnKey=exchange
ColumnValue=NASDAQ

到目前为止,一切顺利,对吧?

然而,没有外键和连接,我们如何在 Cassandra 中获取Query BQuery C相同的查询结果?这确实突显了我们需要另一种方式来做。简短的答案是使用反规范化。

对于Query B,我们想要的是在纳斯达克交易所上市的股票的当日收盘价和描述。涉及的列有symboldescriptiontick_datecloseexchange。前四列很明显,但为什么我们需要exchange列呢?exchange列是作为查询的过滤器的必要条件。另一个含义是,exchange列必须是行键,或者至少是行键的一部分。

记住两个规则:

  1. 行键被视为分区键以定位存储该行的节点。

  2. 行不能跨越两个节点。

在由 Cassandra 支持的分布式系统中,我们应该尽可能减少不必要的网络流量。换句话说,查询需要与之交互的节点越少,数据模型的表现就越好。我们必须迎合集群拓扑以及数据模型的物理存储。

因此,我们应该为Query B创建一个类似于之前的列族:

// Query B
CREATE TABLE stock_ticker_by_exchange (
exchange varchar,
symbol varchar,
description varchar,
tick_date varchar,
close decimal,
PRIMARY KEY (exchange, symbol, tick_date)
);

stock_ticker_by_exchange的逻辑和物理存储视图如下所示:

Cassandra 版本

Query B的 Cassandra 数据模型

行键是exchange列。然而,这次,列键不再是symboltick_dateclosedescription。现在有 12 个列,包括APPL:2014-04-24:APPL:2014-04-24:closeAPPL:2014-04-24:descriptionAPPL:2014-04-25:APPL:2014-04-25:closeAPPL:2014-04-25:descriptionFB:2014-04-24:FB:2014-04-24:closeFB:2014-04-24:descriptionFB:2014-04-25:FB:2014-04-25:closeFB:2014-04-25:description,分别。最重要的是,列键现在是动态的,并且能够在单行中存储数据。这种动态使用的行称为宽行,与包含stock_symbol表静态列的行相对——称为瘦行。

一个列族存储的是瘦行还是宽行,取决于主键是如何定义的。

注意

如果主键只包含一个列,则行是一个瘦行。

如果主键包含多个列,则称为复合主键,行就是一个宽行。

在任何情况下,主键定义中的第一列都是行键。

最后,我们来到了 Query C。同样,我们使用了反规范化。Query CQuery B 的不同之处在于在 2014 年 4 月 24 日添加了一个额外的日期过滤器。你可能认为可以重用 stock_ticker_by_exchange 表来处理 Query C。答案是错误的。为什么?线索是主键,它由三个列组成,分别是 exchangesymboltick_date。如果你仔细观察 stock_ticker_by_exchange 表的列键,你会发现由于 symboltick_date 列的存在,列键是动态的。因此,Cassandra 是否能够在不知道你确切想要哪些符号的情况下确定列键?否定。

Query C 选择合适的列族应类似于以下代码:

// Query C
CREATE TABLE stock_ticker_by_exchange_date (
exchange varchar,
symbol varchar,
description varchar,
tick_date varchar,
close decimal,
PRIMARY KEY ((exchange, tick_date), symbol)
);

这次你应该注意主键的定义了。有趣的是,exchangetick_date 列还有一个额外的括号。让我们看看 stock_ticker_by_exchange_date 的逻辑和物理存储视图,如图所示:

Cassandra 版本

Query C 的 Cassandra 数据模型

你应该注意这里的列键数量。它只有六个,而不是 stock_ticker_by_exchange12 个。列键仍然是根据 symbol 列动态的,但行键现在是 NASDAQ:2014-04-24,而不是 Query B 中的仅仅 NASDAQ。你还记得之前提到的额外一对括号吗?如果你以这种方式定义主键,你打算使用多个列作为行键和分区键。这被称为复合分区键。目前,你只需要知道这个术语即可。更多信息将在后面的章节中给出。

到目前为止,你可能已经感到头晕目眩,尤其是对于那些在关系数据模型方面有多年经验的人。我也发现 Cassandra 数据模型在第一次接触时非常难以理解。然而,你应该意识到关系数据模型和 Cassandra 数据模型之间的细微差别。你必须非常小心地处理你的查询。查询始终是设计 Cassandra 数据模型的起点。作为一个类比,查询是一个问题,数据模型是答案。你只是使用数据模型来回答查询。这正是查询建模的含义。

数据建模考虑因素

除了查询建模之外,在设计 Cassandra 数据模型时,我们还需要牢记一些重要的要点。我们还可以考虑一些在本节中将要介绍的良好模式。

数据重复

在关系型数据模型中,去规范化是一个邪恶,但在 Cassandra 中不是。事实上,这是一个好习惯。这仅仅是因为 Cassandra 不使用高端磁盘存储子系统。Cassandra 喜欢商品级硬盘驱动器,因此磁盘空间便宜。由于去规范化导致的数据重复绝对不再是问题;Cassandra 欢迎它。

排序

在关系型数据库中,可以使用 SQL 查询中的ORDER BY子句轻松控制排序。或者,可以创建一个二级索引来进一步加快排序操作。

然而,在 Cassandra 中,排序是按设计进行的,因为在创建列族时必须确定如何比较数据。列族的比较器决定了读取时行是如何排序的。此外,列按其列名排序,也是通过比较器来排序的。

宽行

使用宽行进行排序、分组和高效过滤是很常见的。此外,还可以使用瘦行。你只需要考虑行包含的列数。

值得注意的是,对于存储瘦行的列族,列键会在每个列中重复存储。尽管这会浪费一些存储空间,但在廉价的商品硬盘上并不是问题。

桶分区

尽管宽行可以容纳高达 20 亿个变量列,但这仍然是一个硬限制,无法阻止大量数据填满一个节点。为了突破 20 亿列的限制,我们可以使用一种称为桶分区的折衷技术来跨多个节点拆分数据。

桶分区需要客户端应用程序生成一个桶 ID,这通常是一个随机数。通过将桶 ID 包含在复合分区键中,可以将数据段拆分并分布到不同的节点。然而,不应滥用此方法。将数据拆分到多个节点会导致读取操作消耗额外的资源来合并和重新排序数据。因此,这是一个昂贵的操作,并且不是一个理想的方法,因此应该只作为最后的手段。

无值列

列键可以存储值,如通过查询建模部分所示。在 Cassandra 中不存在“非空”概念,因此列值可以存储空值而不会出现任何问题。简单地将数据存储在列键中,而将空值留在列中,这种称为无值列的做法有时是故意为之。这是 Cassandra 中的一种常见做法。

无值列的一个动机是 Cassandra 的按列键排序功能。尽管如此,也有一些限制和注意事项。列键的最大大小为 64 KB,而列值的最大大小为 2 GB。因此,列键的空间是有限的。此外,仅使用时间戳作为列键可能会导致时间戳冲突。

时间序列数据

什么是时序数据?它是指任何基于时间变化的变量,例如处理器利用率、传感器数据、点击流和股票行情。之前介绍的股票报价数据模型就是一个例子。Cassandra 非常适合存储时序数据。为什么?因为一行可以容纳多达 20 亿个变量列。它是在存储模型基础上的单一布局。因此,Cassandra 可以以极快的速度处理大量时序数据。TTL(Time To Live)是另一个简化数据管理的优秀特性。

在本书的后半部分,将开发一个完整的股票报价技术分析应用,以进一步解释使用 Cassandra 处理时序数据的细节。

Cassandra 查询语言

其他作者通常从 CQL(Cassandra Query Language)开始介绍 Cassandra 数据模型。在本章中,我采用了一种不同的方法。我试图在我们对 Cassandra 如何处理其物理存储有一个明确理解之前,避免对 CQL 进行过于深入的探讨。

CQL 的语法设计得非常类似于 SQL。这种意图对于习惯在关系型世界中编写 SQL 语句的人来说是好的,以便迁移到 Cassandra。然而,由于 CQL 和 SQL 之间的高度相似性,如果我们使用 CQL 来解释如何在 Cassandra 中建模数据,那么抛弃关系型思维模式就更加困难。这最终可能会导致更多的混淆。我更喜欢从微观角度观察数据模型与物理存储之间的关系的方法。通过这样做,你可以更快地抓住关键点,更清楚地理解其内部工作机制。CQL 将在下一章中详细介绍。

摘要

在本章中,我们了解了 Cassandra 数据模型的基础知识,现在我们对列、行、列族、键空间、计数器和其他相关术语都很熟悉。还给出了关系数据模型和 Cassandra 数据模型之间主要差异的比较,以解释一开始看起来可能令人震惊和反直觉的查询建模概念。然后介绍了一些关于数据建模和典型使用模式的重要考虑因素。最后,解释了为什么故意推迟引入 CQL。

本章只是关于 Cassandra 数据建模的第一部分。在下一章中,我们将继续游览的第二部分,即 Cassandra 查询语言。

第三章。CQL 数据类型

在本章中,我们将概述 Cassandra 查询语言,并详细探讨 Cassandra 支持的丰富数据类型集。我们将遍历数据类型,研究它们的内部存储结构。如果您想了解 Cassandra 在幕后如何实现它们,可以参考 Cassandra 的 Java 源代码。对于那些尚未安装和设置 Cassandra 的读者,您可以参考 第五章,初步设计和实现,以获取快速流程。

CQL 简介

Cassandra 在 0.8 版本中引入了 Cassandra 查询语言(CQL),作为传统 Thrift RPC API 的 SQL 类似替代品。截至本文撰写时,最新的 CQL 版本是 3.1.7。我不想带你们回顾所有旧版本,因此,我将只关注 3.1.7 版本。需要注意的是,CQL 版本 3 与 CQL 版本 2 不兼容,并且在许多方面有所不同。

CQL 语句

CQL 版本 3 提供了一个与 SQL 非常相似的模式。从概念上讲,它使用表格以列的行来存储数据。它由三种主要类型的语句组成:

  • 数据定义语句:这些语句用于设置和更改数据在 Cassandra 中的存储方式

  • 数据操作语句:这些语句用于创建、删除和修改数据

  • 查询语句:这些语句用于查找数据

CQL 不区分大小写,除非单词被双引号包围。它定义了一系列具有固定语言意义的保留词。它区分了保留词和非保留词。保留词不能用作标识符。它们确实是保留给语言的。非保留词仅在特定上下文中具有特定意义,但可以用作标识符。CQL 关键词列表可以在 DataStax 的文档中找到,网址为 www.datastax.com/documentation/cql/3.1/cql/cql_reference/keywords_r.html

CQL 命令行客户端 – cqlsh

Cassandra 包含了一个支持 CQL 的交互式终端,称为 cqlsh。它是一个基于 Python 的命令行客户端,用于运行 CQL 命令。要启动 cqlsh,请导航到 Cassandra 的 bin 目录并输入以下命令:

  • 在 Linux 上,输入 ./cqlsh

  • 在 Windows 上,输入 cqlsh.batpython cqlsh

如以下图所示,cqlsh 在启动时显示集群名称、Cassandra、CQL 和 Thrift 协议版本:

CQL 命令行客户端 – cqlsh

cqlsh 连接到本地节点上运行的 Cassandra 实例

我们可以使用 cqlsh 通过附加主机(无论是主机名还是 IP 地址)和端口号作为命令行参数来连接到其他节点。

如果我们想使用 SimpleStrategy(将在第六章“增强版本”中解释)作为其复制策略,并设置单个节点 Cassandra 集群的复制因子为 1 来创建一个名为 packt 的 keyspace,我们可以在 cqlsh 中输入以下截图所示的 CQL 语句。

本书将广泛使用此实用程序来演示如何使用 CQL 定义 Cassandra 数据模型:

CQL 命令行客户端 – cqlsh

在 cqlsh 中创建 keyspace packt

提示

下载示例代码

您可以从您在 www.packtpub.com 的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您。

原生数据类型

CQL 版本 3 支持许多基本的列数据类型。它还支持集合类型和 Cassandra 可用的所有数据类型。以下表格列出了支持的基本数据类型及其对应含义:

类型 描述
ascii ASCII 字符串
bigint 64 位有符号长整型
blob 随意字节(无验证)
Boolean TrueFalse
counter 计数列(64 位有符号值)
decimal 可变精度十进制数
double 64 位 IEEE 754 浮点数
float 32 位 IEEE 754 浮点数
inet 可以是 4 字节长(IPv4)或 16 字节长(IPv6)的 IP 地址,应作为字符串输入
int 32 位有符号整数
text UTF8 编码的字符串
timestamp 允许输入日期的字符串常量时间戳
timeuuid 通常用作“无冲突”时间戳的类型 1 UUID
uuid 类型 1 或类型 4 UUID
varchar UTF8 编码的字符串
varint 可变精度整型

表 1. CQL 版本 3 基本数据类型

Cassandra 实现

如果我们查看 Cassandra 的 Java 源代码,CQL 版本 3 的原生数据类型在 org.apache.cassandra.cql3.CQL3Type 接口中的 Native enum 中声明,如下截图所示:

Cassandra 实现

声明 CQL 版本 3 原生数据类型的 Cassandra 源代码

有趣的是,TEXTVARCHAR 确实都是 UTF8TypeAsciiTypeLongTypeBytesTypeDecimalType 等类的 Java 类在 org.apache.cassandra.db.marshal 包中声明。

注意

Cassandra 源代码可在 GitHub 上找到,网址为 github.com/apache/cassandra

了解原生数据类型的 Java 实现,使我们能够更深入地理解 Cassandra 如何处理它们。例如,Cassandra 使用org.apache.cassandra.serializers.InetAddressSerializer类和java.net.InetAddress类来处理INET数据类型的序列化和反序列化。

一个不太长的例子

这些原生数据类型用于 CQL 语句中,以指定要存储在表列中的数据类型。现在让我们创建一个实验性表,其中包含每种原生数据类型的列(除了计数器类型,因为它需要一个单独的表),然后向其中插入一些数据。在创建名为table01的表之前,我们需要指定键空间,在这个例子中是packt,如下截图所示:

一个不太长的例子

创建table01以说明每种原生数据类型

我们使用默认值创建表,但还有其他选项可以配置新表以进行优化,包括压缩、压缩、故障处理等。只有一个列的PRIMARY KEY子句也可以指定,即rowkey ascii PRIMARY KEY。然后向table01插入一个样本记录。我们使用INSERT语句来完成,如下截图所示:

一个不太长的例子

table01插入一个样本记录

我们现在在table01中有数据。我们使用cqlsh来查询表。为了比较,我们还使用另一个名为 Cassandra CLI 的 Cassandra 命令行工具,以获得对行的底层视图。让我们在终端上打开 Cassandra CLI。

注意

Cassandra CLI 实用工具

Cassandra CLI 用于在单个键空间或单个表的基础上设置存储配置属性。要启动它,您需要导航到 Cassandra bin 目录并输入以下命令:

  • 在 Linux 上,./cassandra-cli

  • 在 Windows 上,cassandra.bat

注意,它在 Cassandra 3.0 中被宣布为已弃用,应使用cqlsh代替。

以下截图显示了cqlsh中的SELECT语句和 Cassandra CLI 中的list命令的结果。然后我们将逐列进行说明:

一个不太长的例子

cqlsh 和 Cassandra CLI 中样本行的比较

ASCII

在内部,数据值'ABC'被存储为每个单独字符的十六进制表示的字节值,'A''B''C'分别表示为0x410x420x43

Bigint

这个很简单;数字1000000000的十六进制表示为0x000000003b9aca00,长度为 64 位,内部存储。

BLOB

BLOB数据类型用于存储大型二进制对象。在我们之前的例子中,我们将文本'ABC'作为BLOB插入到blobfield中。其内部表示为414243,这只是一个十六进制表示的字节流。

显然,BLOB 字段可以接受各种数据,正因为这种灵活性,它不能对其数据值进行验证。例如,数据值 2 可以解释为整数 2 或文本 '2'。如果不了解我们想要的解释,BLOB 字段可以对数据值进行检查。

BLOB 字段的另一个有趣之处在于,如前一个截图中的 cqlsh 中的 SELECT 语句所示,返回的 blobfield 数据值对于 'ABC' 文本来说是 0x414243。我们知道从上一个部分,0x410x420x43 分别是 'A''B''C' 的字节值。然而,对于 BLOB 字段,cqlsh 在其数据值前加上 '0x',使其成为一个所谓的 BLOB 常量。BLOB 常量是一系列字节,它们的十六进制值以 0xX+ 开头,其中 hex 是一个十六进制字符,例如 [0-9a-fA-F]

CQL 还提供了一些 BLOB 转换函数,用于将原生数据类型转换为 BLOB,反之亦然。对于 CQL 支持的每个 <native-type>(除了 BLOB,原因很明显),<native-type>AsBlob 函数接受一个 <native-type> 类型的参数,并将其作为 BLOB 返回。相反,blobAs<Native-type> 函数将 BLOB 转换回 <native-type>。如前一个截图中的 INSERT 语句所示,我们使用了 textAsBlob()text 数据类型转换为 BLOB

注意

BLOB 常量

BLOB 常量是在 CQL 版本 3.0.2 中引入的,以便用户输入 BLOB 值。在 CQL 的旧版本中,为了方便起见,支持将 BLOB 作为字符串输入。现在这已被弃用,并将在未来版本中删除。它仍然被支持,只是为了允许更平滑地过渡到 BLOB 常量。尽快更新客户端代码以切换到 BLOB 常量是必要的。

布尔值

boolean 数据类型也非常直观。它仅仅是内部存储中的一个字节,可以是 0x00,表示 False,或者 0x01,表示 True

十进制

decimal 数据类型可以存储可变精度的十进制数,基本上是 Java 中的 BigDecimal 数据类型。

双精度

double 数据类型在其内部存储中是一个双精度 64 位 IEEE 754 浮点数。

浮点数

float 数据类型在其内部存储中是一个单精度 32 位 IEEE 754 浮点数。

注意

BigDecimal、double 或 float?

doublefloat 之间的区别显然是浮点数值的精度长度。doublefloat 都使用十进制数的二进制表示,其基数在很多情况下是一个近似值,而不是一个绝对值。double 是一个 64 位值,而 float 是一个更短的 32 位值。因此,我们可以说 double 比浮点数更精确。然而,在这两种情况下,仍然存在精度损失的可能性,这在处理非常大的数字或非常小的数字时可能会非常明显。

相反,BigDecimal是为了克服这种精度差异的损失而设计的。它是一种精确表示数字的方式。它的缺点是运行时性能较慢。

无论何时你处理金钱或精度是必须的,BigDecimal是最好的选择(或在 CQL 原生数据类型中为decimal),否则doublefloat应该足够好。

Inet

inet数据类型是为了存储IP 版本 4IPv4)和IP 版本 6IPv6)格式的 IP 地址值而设计的。示例记录中的 IP 地址192.168.0.1在内部存储时被表示为四个字节;192被存储为0xc01680xa800x0010x01,分别。需要注意的是,无论存储的 IP 地址是 IPv4 还是 IPv6,端口号都没有存储。如果需要,我们需要另一个列来存储它。

我们也可以存储 IPv6 地址值。以下UPDATE语句将inetfield更改为 IPv6 地址2001:0db8:85a3:0042:1000:8a2e:0370:7334,如下面的截图所示:

Inet

cqlsh 和 Cassandra CLI 中 inetfield 样本行的比较

Note

互联网协议版本 6

互联网协议版本 6(IPv6)是互联网协议IP)的最新版本。它是由 IETF 开发的,以解决 IPv4 地址耗尽这一长期预期的难题。

IPv6 使用 128 位地址,而 IPv4 使用 32 位地址。这两个协议不是为互操作性设计的,这使得向 IPv6 的过渡变得复杂。

IPv6 地址通常表示为用冒号分隔的八组每组四个十六进制数字,例如2001:0db8:85a3:0042:1000:8a2e:0370:7334

cqlsh中,每组四个十六进制数字的前导零被移除。在 Cassandra 的内部存储中,IPv6 地址值占用 16 字节。

Int

int数据类型是一个原始的 32 位有符号整数。

Text

text数据类型是一个 UTF-8 编码的字符串,接受 Unicode 字符。如前所述,"ABC"的字节值0x410x420x43被内部存储。我们可以通过更新textfield来测试text字段中的非 ASCII 字符,如下面的截图所示:

text数据类型是由非 ASCII 和 ASCII 字符组合而成的。四个非 ASCII 字符被表示为它们的 3 字节 UTF-8 值,分别是0xe8b5840xe6ba900xe68f900xe4be9b

然而,ASCII 字符仍然以字节值存储,如截图所示:

Text

textfield 数据类型的实验

时间戳

timestampfield的值编码为一个 64 位有符号整数,表示自称为“纪元”的标准基准时间以来的毫秒数:1970 年 1 月 1 日 00:00:00 GMT。timestamp数据类型可以作为整数用于 CQL 输入,或作为 ISO 8601 格式的字符串字面量。如下面的屏幕截图所示,2014 年 5 月 1 日 16:02:03 在+08:00 时区内的内部值为0x00000145b6cdf878或自纪元以来的 1,398,931,323,000 毫秒:

时间戳

时间戳数据类型的实验

timestamp数据类型包含一个日期部分和一个时间部分,如果只需要日期值,则可以省略一天中的时间。Cassandra 将使用 00:00:00 作为省略时间部分的默认值。

注意

ISO 8601

ISO 8601 是表示日期和时间的国际标准。其完整的参考编号是 ISO 8601:1988(E),其标题为“数据元素和交换格式 – 信息交换 – 日期和时间的表示。”

ISO 8601 根据所需的粒度级别描述了大量的日期/时间格式。格式如下。请注意,“T”在字符串中字面表示时间元素的开始。

  • 年份:YYYY(例如,1997)

  • 年和月:YYYY-MM(例如,1997-07)

  • 日期:YYYY-MM-DD(例如,1997-07-16)

  • 日期加上小时和分钟:YYYY-MM-DDThh:mmTZD(例如,1997-07-16T19:20+01:00)

  • 日期加上小时、分钟和秒:YYYY-MM-DDThh:mm:ssTZD(例如,1997-07-16T19:20:30+01:00)

  • 日期加上小时、分钟、秒和秒的十进制分数:YYYY-MM-DDThh:mm:ss.sTZD(例如,1997-07-16T19:20:30.45+01:00)

其中:

  • YYYY = 四位年份

  • MM = 月份的两位数字(01=一月,等等)

  • DD = 月份的两位数字(01 至 31)

  • hh = 小时的两位数字(00 至 23)(不允许 am/pm)

  • mm = 分钟的两位数字(00 至 59)

  • ss = 秒的两位数字(00 至 59)

  • s = 表示秒的十进制分数的一个或多个数字

  • TZD = 时区标识符(Z 或+hh:mm 或-hh:mm)

时间可以表示为协调世界时UTC)与特殊的 UTC 标识符“Z”或与小时和分钟的时区偏移量一起表示的本地时间。偏移量“+/-hh:mm”表示使用一个本地时区,该时区比 UTC 快“hh”小时和“mm”分钟或慢“hh”小时和“mm”分钟。

如果未指定时区,则使用处理写请求的 Cassandra 协调节点所在的时区。因此,最佳实践是使用时间戳指定时区,而不是依赖于 Cassandra 节点上配置的时区,以避免任何歧义。

Timeuuid

timeuuid 数据类型的值是一个包含其生成时间的 Type 1 UUID,并按时间戳排序。因此,它非常适合需要无冲突时间戳的应用程序。一个有效的 timeuuid 使用自 00:00:00.00 UTC 以来 100 个时间间隔的时间(60 位),用于防止重复的时钟序列号(14 位),以及 IEEE 801 MAC 地址(48 位)来生成一个唯一的标识符,例如,74754ac0-e13f-11e3-a8a3-a92bc9056ee6

CQL v3 提供了多个函数,使 timeuuid 的操作变得方便:

  • dateOf():在 SELECT 语句中使用,用于提取 timeuuid 列的时间戳部分

  • now():用于生成一个新的唯一 timeuuid

  • minTimeuuid() 和 maxTimeuuid():这些用于根据条件时间组件作为其参数返回类似于 UUID 的结果

  • unixTimestampOf():在 SELECT 语句中使用,用于提取 timeuuid 列的时间戳部分作为原始的 64 位整数时间戳

下图使用 table01timeuuidfield 来演示这些 timeuuid 函数的使用:

Timeuuid

时间 UUID 函数的演示

注意

Timestamp 或 Timeuuid?

时间戳适合存储日期和时间值。然而,在需要无冲突的唯一时间戳的情况下,时间 UUID 更为合适。

UUID

UUID 数据类型通常用于避免值冲突。它是一个 16 字节的值,接受类型 1 或类型 4 UUID。CQL v3.1.6 或更高版本提供了一个名为 uuid() 的函数,可以轻松生成随机的类型 4 UUID 值。

注意

Type 1 或 type 4 UUID?

Type 1 使用生成 UUID 数据类型的计算机的 MAC 地址和自公历采用以来 100 纳秒间隔的数量来生成 UUID。如果 MAC 地址没有重复,则保证跨计算机的唯一性;然而,考虑到现代处理器的速度,同一机器上对类型 1 生成器的简单实现进行连续调用可能会产生相同的 UUID,从而否定唯一性的属性。

Type 4 使用随机数或伪随机数。因此,它是推荐的 UUID 类型。

Varchar

基本上,varchartext 相同,这在源代码中相同的 UTF8Type 中可以明显看出。

Varint

使用 varint 数据类型来存储任意精度的整数。

Counter

counter 数据类型是一种特殊的列,其用户可见的值是一个 64 位有符号整数(尽管在内部更为复杂),用于存储一个递增计数特定事件的数值。当向给定的计数器列写入新值时,它会被添加到计数器的上一个值。

计数器非常适合在分布式环境中快速计数,这使得它在实时分析任务中非常有价值。counter数据类型是在 Cassandra 0.8 版本中引入的。计数器列表必须使用counter数据类型。计数器只能存储在专用表中,并且不能在计数器列上创建索引。

小贴士

计数器类型禁忌

  • 不要将counter数据类型分配给作为主键的列

  • 不要在包含除counter数据类型和主键之外任何内容的表中使用counter数据类型

  • 不要使用counter数据类型为代理键生成顺序号;请使用timeuuid数据类型

我们使用CREATE TABLE语句创建计数器表。然而,不允许在计数器表上使用INSERT语句,因此我们必须使用UPDATE语句来更新计数器列,如下面的截图所示。

Cassandra 使用counter而不是name来表示该列是计数器数据类型。计数器的值存储在该列的值中。

这是一篇非常好的文章,解释了在分布式环境中计数器是如何工作的内部机制。www.datastax.com/dev/blog/whats-new-in-cassandra-2-1-a-better-implementation-of-counters

以下截图显示计数器的值存储在列的值中:

计数器

计数器数据类型的实验

集合

Cassandra 在其数据模型中也支持集合来存储少量数据。集合是一种复杂类型,可以提供极大的灵活性。支持三种集合:Set、List 和 Map。每个集合中存储的数据类型需要定义,例如,时间戳集合定义为set<timestamp>,文本列表定义为list<text>,包含文本键和文本值的映射定义为map<text, text>,等等。此外,集合中只能使用原生数据类型。

Cassandra 读取整个集合,并且集合在内部不进行分页。集合的最大项数是 64K,最大项大小也是 64K。

为了更好地展示这些集合上的 CQL 支持,让我们在packt键空间中创建一个表,其中包含每个集合的列,并向其中插入一些数据,如下面的截图所示:

集合

在集合上进行实验

注意

如何更新或删除一个集合?

CQL 也支持更新和删除集合中的元素。您可以参考 DataStax 文档中的相关信息,网址为www.datastax.com/documentation/cql/3.1/cql/cql_using/use_collections_c.html

正如原生数据类型的情况一样,让我们逐一介绍以下每个集合。

设置

CQL 使用集合来保持唯一元素集合。集合的好处是 Cassandra 会自动跟踪元素的唯一性,我们作为应用程序开发者,无需为此烦恼。

CQL 使用花括号({})来表示由逗号分隔的值集合。一个空集合简单地表示为{}。在先前的例子中,尽管我们以{'Lemon', 'Orange', 'Apple'}的形式插入集合,但输入顺序并未保留。为什么?

原因在于 Cassandra 存储集合的机制。内部,Cassandra 将集合的每个元素存储为一个单独的列,其列名是原始列名后跟冒号和元素值。如前所述,'Apple''Lemon''Orange'的 ASCII 值分别是0x4170706c650x4c656d6f6e0x4f72616e6765。因此,它们被存储在三个列中,列名为setfield:4170706c65setfield:4c656d6f6esetfield:4f72616e6765。由于 Cassandra 内置的列名排序特性,集合的元素会自动排序。

列表

列表按照所选类型的自然顺序排序。因此,当不需要唯一性且需要保持顺序时,它非常适用。

CQL 使用方括号([])来表示由逗号分隔的值列表。一个空列表表示为[]。与集合不同,Cassandra 会保留列表的输入顺序。Cassandra 也会将列表的每个元素存储为一个列。但这次,列具有相同的名称,由原始列名(在我们的例子中为listfield),一个冒号和一个在更新时生成的 UUID 组成。列表的元素值存储在列的值中。

映射

Cassandra 中的映射是一个类似于字典的数据结构,具有键和值。当您想在单个 Cassandra 行中存储类似表的数据时,它非常有用。

CQL 也使用花括号({})来表示由逗号分隔的键值映射。每个键值对由冒号分隔。一个空映射简单地表示为{}。可以想象,每个键/值对存储在一个列中,其列名由原始映射列名后跟冒号和该对的键组成。对的值存储在列的值中。类似于集合,映射会自动对其项目进行排序。因此,可以将映射想象为集合和列表的混合体。

用户定义类型和元组类型

Cassandra 2.1 引入了对用户定义类型UDT)和元组类型的支持。

用户定义的类型在键空间级别声明。用户定义的类型简化了处理一组相关属性。我们可以将一组相关属性定义为一种类型,并单独或作为一个单一实体访问它们。我们可以将我们的 UDTs 映射到应用程序实体。Cassandra 2.1 引入的另一种新类型是元组类型。元组是一个没有标签的、固定长度的类型化位置字段集合。

我们可以在表中使用用户定义和元组类型。然而,为了支持未来的功能,用户定义或元组类型的列定义需要使用frozen关键字。Cassandra 将具有多个组件的冻结值序列化为一个单一值。这意味着我们无法更新 UDT 值的部分。整个值必须被覆盖。Cassandra 将冻结 UDT 的值视为一个BLOB

我们在packt键空间中创建了一个名为contact的 UDT(用户定义类型),并使用它来定义table04中的contactfield。此外,我们还有一个名为tuplefield的列,用于在行中存储一个元组。请注意 UDT 和元组的INSERT语句的语法。对于 UDT,我们可能使用点符号来检索 UDT 列的组件,例如在我们下面的例子中的contactfield.facebook。如图所示在cassandra-cli中,contactfield被存储为一个单一值,00000001620000000163000000076440642e636f6d

值按照顺序将每个 UDT 组件以格式连接,格式为一个 4 字节的长度指示组件值的长度,以及组件值本身。因此,对于contactfield.facebook0x00000001是长度,0x62是字符'a'的字节值。Cassandra 对元组也应用相同的处理:

用户定义类型和元组类型

用户定义和元组类型的实验

注意

更多信息可以在 DataStax 的文档中找到,文档可通过以下链接获取:www.datastax.com/documentation/cql/3.1/cql/cql_using/cqlUseUDT.html

摘要

本章是 Cassandra 数据模型的第二部分。我们已经学习了 Cassandra 查询语言(CQL)的基础,它提供了一个类似 SQL 的语言来实现 Cassandra 数据模型并在其中操作数据。然后提供了一个非常详细的说明,包括大量原生数据类型、更高级的集合以及新的用户定义和元组类型的示例,以帮助您了解如何为您的数据模型选择合适的数据类型。还解释了每种数据类型的内部存储,以便您了解 Cassandra 如何实现其数据类型。

在下一章中,我们将学习 Cassandra 查询的另一个重要元素——索引。

第四章:索引

毫无疑问,Cassandra 可以轻松地存储大量数据。然而,如果我们不能高效地在这样的数据深渊中找到我们想要的东西,那么这一切都是没有意义的。Cassandra 通过主索引和二级索引提供了非常好的支持,以搜索和检索所需的数据。

在本章中,我们将探讨 Cassandra 如何使用主索引和二级索引来突出显示数据。在理解了它们之后,我们就可以设计一个高性能的数据模型。

主索引

Cassandra 是一个基于列的数据库。每一行可以有不同的列数。单元格是值的占位符,时间戳数据通过行和列来标识。每个单元格可以存储小于 2 GB 的值。行通过分区进行分组。每个分区的单元格数量限制在行数乘以列数小于 20 亿的条件之下。每一行通过行键来标识,该键决定了存储该行的机器。换句话说,行键决定了行的节点位置。一个表的所有行键列表称为主键。主索引仅仅创建在主键上。

主键可以定义在单个列或多个列上。在任一情况下,表的主键的第一个组件是分区键。每个节点存储表的数据分区,并维护其管理的数据的主键。因此,每个节点都知道它可以管理的行键的范围,并且可以通过扫描相关副本上的行索引来定位行。节点管理的主键范围由分区键和一个称为分区器的集群范围配置参数确定。Cassandra 提供了三种分区器选择,将在本章后面介绍。

主键可以通过 CQL 关键字PRIMARY KEY来定义,包括要索引的列。想象一下,我们想要将每日股票报价存储到名为dayquote01的 Cassandra 表中。CREATE TABLE语句创建了一个具有简单主键的表,该主键仅涉及一个列,如下面的截图所示:

主索引

symbol字段被分配为dayquote01表的唯一键。这意味着具有相同symbol的所有行都存储在同一个节点上。因此,这使得这些行的检索非常高效。

或者,主键可以通过显式的PRIMARY KEY子句来定义,如下面的截图所示:

主索引

与关系数据库不同,Cassandra 不对主键强制唯一约束,因为在 Cassandra 中没有主键违规。使用现有行键的INSERT语句是允许的。因此,在 CQL 中,INSERTUPDATE的行为相同,这被称为UPSERT。例如,我们可以将两个记录插入到dayquote01表中,具有相同的符号,并且不会发出主键违规的警告,如下面的截图所示:

主索引

返回的查询结果只包含一行,而不是预期的两行。这是因为主键是符号,后一个INSERT语句中的行覆盖了前一个INSERT语句创建的记录。对于重复的主键没有警告。Cassandra 简单地、默默地更新了行。这种静默的 UPSERT 行为有时可能会在应用逻辑中产生不良影响。

小贴士

因此,对于应用程序开发者来说,在应用逻辑中处理重复主键情况非常重要。不要依赖 Cassandra 为您检查唯一性。

实际上,当我们知道 Cassandra 的内部存储引擎如何存储行时,Cassandra CLI 显示的以下截图会使这种行为的原因变得更加清晰:

主索引

行键是 0001.HK。它用于定位哪个节点被用来存储该行。每次当我们插入或更新具有相同行键的行时,Cassandra 都会盲目地定位该行并相应地修改列,即使使用了INSERT语句。

虽然单列主键并不罕见,但由多个列组成的复合主键更加实用。

复合主键和复合分区键

复合主键由多个列组成。列的顺序很重要。复合主键的结构如图所示:

复合主键和复合分区键

列 1 到 A 被用作 Cassandra 的分区键,以确定分区所在的节点位置。其余的列,即列 B 到 N,被称为排序列,用于数据的排序。排序列用于定位数据节点中的唯一记录。默认情况下,它们是有序的,并且可以在SELECT语句中使用ORDER BY [DESC]子句。此外,我们可以使用LIMIT 1子句获取排序键的MINMAX值。我们还需要在WHERE子句的谓词中使用排序列。在构建查询时,我们不能省略任何一个。

要定义复合主键,必须在CREATE TABLEALTER TABLE语句中使用显式的PRIMARY KEY子句。我们可以为dayquote03表定义一个复合主键,如下面的截图所示:

复合主键和复合分区键

因为主键的第一部分(即symbol)与简单主键相同,所以分区键与dayquote01中的相同。因此,无论主键是复合的还是简单的,节点位置都是相同的,就像在这个例子中一样。

那么,简单主键(symbol)和这个复合主键(symbol, price_time)之间有什么区别呢?额外的字段price_time指示 Cassandra 通过price_time的值保证分区内行的聚类或排序。因此,复合主键按price_time对相同符号的行进行排序。我们在dayquote03表中插入两条记录,并选择所有记录以查看效果,如下面的截图所示:

复合主键和复合分区键

如预期地返回了两条记录(与dayquote01中仅返回一条记录相比)。此外,结果按price_time的值排序。以下截图显示了dayquote03表中行的内部视图:

复合主键和复合分区键

行键仍然是分区键,即0001.HK。然而,Cassandra 将 CQL SELECT语句返回的两个行存储为其存储中的一个单独的内部行。聚类列的值用作未在PRIMARY KEY子句中指定的列的前缀。由于 Cassandra 按列名排序存储内部列,因此 CQL SELECT语句返回的行是按顺序存储的。简而言之,在物理节点上,当分区键的行按聚类列的顺序存储时,行的检索效率非常高。

现在你已经知道复合主键的第一部分是分区键。如果我们需要继续存储0001.HK的 3,000 条每日报价(大约 10 年),尽管 CQL SELECT语句返回 3,000 条虚拟行,但 Cassandra 需要根据分区键将这些 3,000 条虚拟行作为一个完整的行存储在节点上。随着每日报价的存储越来越多,整个行的大小在节点上会越来越大。随着时间的推移,行将迅速变得巨大,并因此由于集群不平衡而引发严重的性能问题。解决方案是 Cassandra 提供的一个称为复合分区键的功能。

复合分区键将数据分散到多个节点上。它由PRIMARY KEY子句中的额外一组括号定义。让我们创建另一个具有复合分区键的表dayquote04,以便说明其效果。现在,exchangesymbol列是复合分区键的成员,而price_time列是一个聚类列。我们向dayquote04中插入相同但符号不同的两条记录,如下面的截图所示:

复合主键和复合分区键

如以下截图所示,返回了两个内部行,它们的行键分别为SEHK:0001.HKSEHK:0002.HK。在内部,Cassandra 将复合分区键中的列连接起来作为一个内部行键。简而言之,原始的没有复合分区键的行现在被分割成两个行。由于行键现在不同,相应的行可以存储在不同的节点上。聚类列price_time的值仍然用作内部列名的前缀,以保留数据的顺序:

复合主键和复合分区键

时间序列数据

Cassandra 非常适合处理时间序列类型的数据,例如 Web 服务器日志文件、使用数据、传感器数据、SIP 数据包等。前几节中的dayquote01dayquote04表用于存储每日股票报价,是时间序列数据的一个例子。

我们在上一个部分中已经看到,复合分区键是一种避免过度压榨行的更好方法。它基于符号限制行的尺寸。然而,这仅仅部分解决了问题。符号行的尺寸仍然会在一段时间内增长。您还有其他建议吗?我们可以在表中定义一个人工列,quote_date,并将复合分区键设置为exchangequote_date,如下面的截图所示:

时间序列数据

现在复合分区键基于每日限制行的尺寸,并使行更加易于管理。这种方式类似于将数据插入由特定日期标记的不同桶中。因此,它被命名为日期桶模式。按日期分区还通过允许您删除quote_date的分区,使表维护更容易。日期桶模式的一个缺点是您总是需要知道分区键才能获取行。因此,在dayquote05中,您不能使用ORDER BY DESCLIMIT 1子句获取最新的quote_date值。

日期桶模式为应用程序开发者提供了一个设计选项,以实现更平衡的集群,但集群的平衡程度取决于许多因素,其中最重要的因素是分区器的选择。

分区器

分区器基本上是一个哈希函数,用于计算行键的TOKEN()(哈希值),因此它决定了数据如何在集群的节点之间分布。选择分区器决定了哪个节点用于放置数据的第一个副本。每一行数据都由一个分区键唯一标识,并通过TOKEN()的值在集群中分布。Cassandra 提供了以下三个分区器:

  • Murmur3Partitioner(自 1.2 版本以来为默认值)

  • RandomPartitioner (版本 1.2 之前的默认值)

  • ByteOrderedPartitioner

Murmur3Partitioner

Murmur3Partitioner 提供比 RandomPartitioner 更快的哈希和改进的性能。在几乎所有情况下,它都是默认的分区策略,也是新集群的正确选择。它使用 MurmurHash 函数,该函数创建分区键的 64 位哈希值。哈希值的可能范围是从 -2⁶³ 到 +2⁶³ -1。当使用 Murmur3Partitioner 时,你可以通过在 CQL SELECT 语句中使用 TOKEN() 函数来翻页查看所有行。

RandomPartitioner

RandomPartitioner 在 Cassandra 版本 1.2 之前是默认的分区器。它通过使用行键的 MD5 哈希值将数据均匀地分布在节点之间。哈希值的可能范围是从 0 到 2¹²⁷ -1。MD5 哈希函数在性能上较慢,这就是为什么 Cassandra 转向了 Murmur3 哈希。当使用 RandomPartitioner 时,你可以通过在 CQL SELECT 语句中使用 TOKEN() 函数来翻页查看所有行。

ByteOrderedPartitioner

如其名称所示,ByteOrderedPartitioner 用于有序分区。这个分区器按照键字节进行排序。令牌是通过查看分区键数据的实际值并使用键的前导字符的十六进制表示来计算的。例如,如果你想按字母顺序分区行,你可以使用其十六进制表示 0x42 分配一个 B TOKEN()

使用 ByteOrderedPartitioner 允许通过主键进行有序扫描,就像你在关系型表的传统索引中移动游标一样。这种类型的范围扫描查询在 RandomPartitioner 中是不可能的,因为键是按照它们的 MD5 哈希顺序存储的,而不是按照键的顺序。

显然,对行执行范围扫描听起来像是 ByteOrderedPartitioner 的一个期望特性。有方法可以通过使用二级索引来实现相同的功能。相反,出于以下原因不建议使用 ByteOrderedPartitioner

  • 负载均衡困难:需要更多的管理开销来平衡集群。ByteOrderedPartitioner 要求管理员根据他们对分区键分布的估计手动计算分区范围。

  • 顺序写入可能导致热点问题:如果应用程序倾向于一次写入或更新一个顺序的行块,写入将不会在集群中分布。它们都会流向一个节点。这对于处理时间戳数据的应用程序来说通常是一个问题。

  • 多表负载不均衡:如果应用程序有多个表,这些表可能有不同的行键和不同的数据分布。为一张表平衡的有序分区器可能会在同一个集群中的另一张表上造成热点和不均匀的分布。

分页和令牌函数

当使用 RandomPartitionerMurmur3Partitioner 时,行按其值的哈希值排序。因此,行的顺序没有意义。使用 CQL,即使使用 RandomPartitionerMurmur3Partitioner,也可以通过 TOKEN() 函数遍历行,如下面的截图所示:

分页和令牌功能

ByteOrderedPartitioner 以与键值相同的方式排列令牌,而 RandomPartitionerMurmur3Partitioner 以完全无序的方式分配令牌。TOKEN() 函数使得可以遍历无序分区器的结果。它实际上直接使用令牌查询结果。

二级索引

由于 Cassandra 只允许每个表有一个主键,因此它支持在主键之外的列上创建二级索引。好处是可以在 WHERE 子句中快速、高效地查找匹配索引列的数据。每个表可以有多个二级索引。Cassandra 使用二级索引来查找不使用行键的行。在幕后,二级索引作为由 Cassandra 内部进程自动维护的单独、隐藏的表实现。与关系数据库一样,保持二级索引的更新不是免费的,因此应避免不必要的索引。

注意

主索引和二级索引之间的主要区别在于,主索引是一个分布式索引,用于定位存储行键的节点,而二级索引是一个本地索引,仅用于索引本地节点上的数据。

因此,在没有检查集群中的所有节点的情况下,二级索引将无法立即知道所有匹配行的位置。这使得二级索引的性能不可预测。

注意

当使用等值谓词时,二级索引是最有效的。这确实是一个限制,必须至少有一个等值谓词子句,以希望限制需要读入内存的行集。

此外,二级索引不能创建在主键本身上。

注意

注意!

Cassandra 中的二级索引与传统的 RDBMS 中的二级索引并不相同。它们不类似于 RDBMS 中的 B-tree 索引。它们更像是哈希。因此,范围查询在 Cassandra 的二级索引上不工作,只有等值查询在二级索引上工作。

我们可以使用 CQL 的 CREATE INDEX 语句在定义表之后在列上创建索引。例如,我们可能想添加一个 sector 列来指示股票所属的部门,如下面的截图所示:

二级索引

如果我们想在 dayquote06 中搜索属于 Properties 的符号,我们可能会运行以下命令,如下面的截图所示:

二级索引

由于sector不在主键中,我们无法直接通过sector查询 Cassandra。相反,我们可以在列sector上创建一个二级索引来实现这一点,如下面的截图所示:

二级索引

索引名称dayquote06_sector_idx是可选的,但必须在键空间内是唯一的。如果您不提供名称,Cassandra 将分配一个类似于dayquote06_idx的名称。我们现在可以通过sector查询 Cassandra 的每日股票报价。

您可以看到,在先前的截图中的WHERE谓词子句中,主键列不存在,Cassandra 使用二级索引来查找匹配选择条件的行。

多个二级索引

Cassandra 支持在表上创建多个二级索引。如果至少有一个列参与了二级索引,则执行WHERE子句。因此,我们可以在WHERE子句中使用多个条件来过滤结果。当WHERE谓词子句中的多个数据匹配条件时,Cassandra 将首先处理最不频繁出现的条件,以提高查询效率。

当尝试执行可能昂贵的查询,例如范围查询时,Cassandra 需要ALLOW FILTERING子句,该子句可以对其他非索引列的值应用额外的过滤器,以对结果集进行过滤。由于它扫描所有节点上的所有行,因此它运行得非常慢。ALLOW FILTERING子句用于显式指导 Cassandra 在WHERE子句上执行该可能昂贵的查询,而不创建二级索引,尽管性能不可预测。

二级索引的注意事项

二级索引最适合具有许多行且包含较少唯一值的表,在关系数据库术语中称为低基数,这对关系数据库人员来说可能是不直观的。一个特定列中存在的唯一值越多,查询和维护索引的开销就越大。因此,它不适合查询大量记录以获取少量结果。

小贴士

不要对具有低基数值的列进行索引。Cassandra 将二级索引仅存储为数据节点上的本地行的哈希多映射或位图索引,您可以在issues.apache.org/jira/browse/CASSANDRA-1472中参考它。

在以下情况下应避免使用二级索引:

  • 在大量行中,对于少量结果的高基数列

    在高基数列上的索引将导致为非常少的查询结果进行多次搜索。对于包含唯一值的列,从性能角度来看,使用索引以方便查询是可以接受的,只要对索引列族的查询量适中,并且不是持续负载。

  • 在使用计数器列的表中

  • 在频繁更新或删除的列上

    Cassandra 存储墓碑(行中的一个标记,表示某个列已被删除。在压缩过程中,标记的列在索引(一个隐藏的表)中被删除,直到墓碑限制达到 100 K 个单元格。超过这个限制后,使用索引值进行的查询将失败。

  • 在大型分区中查找行

    在大型集群中对索引列进行查询通常需要从多个数据分区收集响应。随着集群中机器数量的增加,查询响应速度会变慢。

小贴士

需要注意的重要要点

  • 不要在高基数列上建立索引

  • 不要在具有计数列的表中使用索引

  • 不要在频繁更新或删除的列上建立索引

  • 不要滥用索引在大型分区中查找行

摘要

在本章中,我们学习了主索引和辅助索引。复合主键、复合分区键和分区器等相关主题也进行了介绍。通过解释 Cassandra 的内部存储和内部工作原理,你现在应该能够说明主索引和辅助索引之间的区别,并在数据模型中正确使用它们。

在下一章中,我们将开始构建使用 Cassandra 和 Python 的第一个版本的技术分析应用程序。还将提供如何将 Python 连接到 Cassandra 并收集市场数据的快速安装和设置指南。

第五章. 初步设计和实现

基于前几章中解释的 Cassandra 数据模型组件,现在是时候将它们应用到实际的工作应用中了。我们将开始定义在数据模型中真正想要存储和查询的内容,设置环境,编写程序代码,并最终测试应用。

要构建的应用是一个股票筛选器应用,它将历史股票报价存储在 Cassandra 数据库中,用于技术分析。该应用从互联网上的免费来源收集股票报价数据,然后应用一些技术分析指标来找出买卖参考信号。为了使您能够轻松理解应用的功能,这里给出技术分析的一个简短快速介绍。尽管在架构上过于简化,功能上也不完整,但它确实为您提供了进一步改进更多高级功能的基础。

注意

免责声明

应假定本书中讨论的方法、技术或指标将是有利可图的,不会导致损失。不能保证所提出的策略和方法将成功,或者您将成为一个有利可图的交易者。任何交易系统或交易方法的历史表现和结果并不一定预示未来的结果。您不应使用无法承受损失的资金进行交易。本书中讨论和展示的例子仅用于教育目的。这些不是购买或出售任何订单的招揽。我对您的交易结果不承担任何责任。没有表示任何账户将或可能实现与本书中讨论的类似利润或损失。交易风险非常高。在做出任何投资或交易决策之前,鼓励您咨询认证的财务顾问。

股票筛选器应用

在本节中,我们将学习一些关于示例应用的背景信息。然后,我们将讨论数据源、初始数据模型以及应用的高级处理逻辑。

金融分析简介

股票筛选器是一个使用一定套标准来筛选大量符合您偏好的股票的实用程序。它类似于股票搜索引擎,而不是网站搜索引擎。筛选标准可能基于基本面和/或技术分析方法。

首先,让我们看看什么是基本面分析。

注意

基本面分析

基本面分析涉及分析一家公司的历史和当前财务报表和健康状况,其管理和竞争优势,以及其竞争对手和市场,以便评估和计算公司股票的价值并预测其可能的价格走势。目标是进行财务预测并找出被低估的股票(换句话说,就是便宜的股票)用于买入并持有。

相比之下,技术分析是一种完全不同的方法。

注意

技术分析

技术分析是一种股票分析方法,通过研究过去的市场数据(主要是价格和成交量)来预测价格走势。技术分析的基本原理是市场价格反映了所有相关信息,因此分析关注的是交易模式的过去历史,而不是外部驱动因素,如经济、基本面和新闻事件。

在本书中,技术分析仅用于股票筛选器应用。由于技术分析侧重于价格行为,股票筛选器应用需要股票价格数据作为其输入,然后应用技术分析技术来确定股票是否满足买入或卖出条件。每当满足这种条件时,我们就可以说触发了交易信号。

股票筛选器应用程序的概念设计如图所示:

金融分析简介

我们将从左到右依次解释前面的图示。数据提供者是股票报价数据的来源,这些数据是从互联网上的免费数据提供者收集的,例如雅虎财经。需要注意的是,雅虎财经提供免费的每日收盘价EOD)股票报价数据,因此提供每日股票报价。如果您希望股票筛选器产生日内信号,您需要寻找其他数据提供者,他们通常提供广泛的付费服务。历史数据是一个存储历史股票报价数据的仓库。股票筛选器是本章要开发的应用程序。最后,警报列表股票筛选器找到的交易信号列表。

在我们继续进行股票筛选器的高级设计之前,我想强调建立历史数据仓库的原因。主要有三个原因。首先,它可以节省大量网络带宽,避免从数据提供者(实际上,雅虎财经提供了多达 10 年的历史价格数据)反复下载历史股票报价数据。其次,它作为规范的数据模型,这样股票筛选器就不需要适应不同数据提供者的不同数据格式。最后,即使股票筛选器与互联网断开连接,它仍然可以对历史数据进行技术分析。

股票报价数据

技术分析只关注价格动作。那么,价格动作是什么?价格动作简单地说就是股票价格的运动。它包含在技术分析和图表模式分析中,试图发现价格看似随机运动中的秩序。

在单日,股票的价格动作可以总结为四个重要的价格:

  • 开盘价:这是当天的起始价格

  • 最高价:这是当天的最高价格

  • 最低价:这是当天的最低价格

  • 收盘价:这是当天的收盘价格

这四个价格通常被缩写为 OHLC。除了 OHLC 之外,衡量在特定时间段内给定股票交易量的另一个指标被称为成交量。对于完整交易日,成交量被称为日成交量。

只有开盘价、最高价、最低价、收盘价和成交量(OHLC)这五个属性,就提供了进行股票技术分析所需的所有必要和充分的数据。现在我们知道了技术分析的输入,但我们如何获取它们呢?

许多网站提供免费且易于获取的股票报价数据,特别适合业余或零售交易者。以下是一些供您参考的网站:

然而,有一个需要注意的问题,即股票报价数据可能存在错误,例如,最高价和最低价不正确。在这本书中,我选择了雅虎财经作为主要的数据流提供商。以下截图是名为GS的股票的历史价格样本:

股票报价数据

当你滚动到网页底部时,你会看到一个链接下载到电子表格。当你点击这个链接时,可以下载历史股票报价数据作为逗号分隔值CSV)文件。以下截图显示了 CSV 文件的一个片段:

股票报价数据

当然,我们可以从网站上手动下载历史股票报价数据。然而,当我们需要每天下载许多不同股票的数据时,这变得不切实际。因此,我们将开发一个程序来自动收集数据流。

初始数据模型

我们现在知道,单个每日价格动作包括股票代码、交易日期、开盘价、最高价、最低价、收盘价和成交量。显然,在连续交易日的价格动作序列是时间序列性质的,Cassandra 非常适合存储这种类型的数据。

如前所述,将收集到的股票报价数据本地存储在仓库中是有益的。因此,我们将实现一个 Cassandra 数据库中的表作为仓库。

我们可以使用 CQL 定义一个名为quote的表来存储历史价格:

// table to store historical stock quote data
CREATE TABLE quote (
  symbol varchar, // stock symbol
  price_time timestamp, // timestamp of quote
  open_price float, // open price
  high_price float, // high price
  low_price float, // low price
  close_price float, // close price
  volume double, // volume
  PRIMARY KEY (symbol, price_time) // primary key
);

列的数据类型和名称是自解释的。

设计 Cassandra 数据模型的一个有用技术是想象行内部存储的视觉表示。以下是一个这样的例子:

初始数据模型

根据主键的设计,行键是symbol,聚类列是price_time。预计随着更多历史股票报价数据的添加,行将变成宽行。如果没有内部存储图,在初始数据模型设计阶段可能不容易发现这一点。目前,我们只需注意潜在的宽行问题,并保持现状(一个可能的解决方案是日期桶模式)。

处理流程

以下图显示了股票筛选器的处理流程,它通过更详细的步骤序列详细阐述了概念设计。每个构建块的解释从顶部开始,如下面的截图所示:

处理流程

数据馈送提供者数据馈送数据馈送适配器数据映射器和归档器组成。Yahoo! Finance 被选为数据馈送。数据馈送适配器用于处理如果我们切换到其他数据馈送提供者时的不同连接性和接口方法。数据映射器和归档器针对不同的股票报价数据格式,并将它们标准化为quote表的相应列。

quote表是历史数据存储库,之前已经解释过。

我们现在将重点转向核心股票筛选器股票筛选器的核心是股票筛选器引擎,它使用筛选规则历史数据上,这些数据通过数据范围器过滤。筛选规则被一个或多个技术分析信号使用,以便当技术分析信号的条件满足时,股票筛选器引擎生成警报。

股票筛选器引擎生成的警报以警报列表的形式呈现,可以保留为记录或通过其他方式分发。

基本上,数据馈送提供者股票筛选器不需要在同一个进程中运行。它们以异步模式工作。这意味着数据馈送提供者可以收集、映射和归档历史股票报价数据到历史数据存储库,而股票筛选器可以独立分析和生成警报。

我们已经提出了应用程序的高级设计,接下来要做的事情可能是看看它如何实现。

系统设计

在本节中,我们将选择适合各种系统组件的适当软件。

操作系统

在考虑实施时,第一个基本选择是操作系统。最重要的限制条件是它必须得到 Cassandra 的支持。对于这本书,我选择了 Ubuntu 14.04 LTS 64 位版本,可以在官方 Ubuntu 网站上获取,www.ubuntu.com/。你应该能够通过遵循详细的安装说明轻松地设置你的 Linux 系统。

然而,使用任何其他由 Cassandra 支持的操作系统(如 Microsoft Windows 和 Mac OS X)完全取决于你。请遵循相应操作系统的安装说明来设置你的机器。我已经考虑了 Stock Screener 的可移植性。正如你将在后续章节中看到的,Stock Screener 应用程序被设计和开发成与大量操作系统兼容。

Java 运行时环境

由于 Cassandra 是基于 Java 的,因此需要一个Java 运行时环境JRE)作为先决条件。我使用了 Oracle Java SE 运行时环境 7 64 位版本 1.7.0_65。可以在以下 URL 获取:www.oracle.com/technetwork/java/javase/downloads/jre7-downloads-1880261.html

当然,我已经下载了 Linux x64 二进制文件,并遵循了www.datastax.com/documentation/cassandra/2.0/cassandra/install/installJreDeb.html上的说明来正确设置 JRE。

在撰写本文时,Java SE 已更新到版本 8。然而,我尚未测试 JRE 8,DataStax 也建议对于 Cassandra 2.0 使用 JRE 7。因此,在这本书中,我将坚持使用 JRE 7。

Java 本地访问

如果你想在 Linux 平台上将 Cassandra 用于生产部署,Java 本地访问JNA)是必需的,以提高 Cassandra 的内存使用。安装和配置完成后,Linux 不会交换Java 虚拟机JVM),从而避免任何与性能相关的问题。即使要安装的 Cassandra 不是用于生产用途,这也是一种最佳实践。

要在 Ubuntu 上安装 JNA,只需在终端中使用以下命令通过 Aptitude 软件包管理器:

$ sudo apt-get install libjna-java

Cassandra 版本

我使用了由 DataStax Community 分发的 Cassandra 版本 2.0.9,适用于 Debian 或 Ubuntu。安装步骤在www.datastax.com/documentation/getting_started/doc/getting_started/gettingStartedDeb_t.html上有很好的文档说明。

安装过程通常需要几分钟,具体取决于你的网络带宽和机器的性能。

注意

DataStax

DataStax 是一家位于加利福尼亚州圣克拉拉的计算机软件公司,它在 DataStax Enterprise 产品中提供 Apache Cassandra 的商业企业级支持。它还为 Apache Cassandra 社区提供巨大的支持。

编程语言

现在是时候将我们的注意力转向用于实现股票筛选器应用程序的编程语言了。对于这本书,我选择了 Python。Python 是一种为开发速度而设计的面向高级的编程语言。它是开源的、免费的,并且跨平台。它拥有几乎涵盖你所能想象到的几乎所有流行算法的丰富库集。

如果你不太熟悉 Python,不必害怕学习 Python。Python 的设计使得与其他编程语言(如 C++)相比,学习起来非常容易。编写 Python 程序几乎就像编写伪代码,这可以加快开发速度。

此外,还有许多用于数据分析的知名 Python 库,例如 NumPy、SciPy、pandas、scikit-learn 和 matplotlib。你可以利用它们快速构建一个功能齐全的应用程序,包括所有功能。对于股票筛选器应用程序,你将广泛使用 NumPy 和 pandas。

当谈到高性能时,Python 也可以利用 Cython,这是一个用于 Python 程序的优化静态编译器,可以使程序运行得和原生 C 或 C++程序一样快。

Python 的最新主要版本是 Python 3。然而,仍然有许多程序是用 Python 2 编写的。这是由于 Python 3 向后不兼容性造成的,使得许多用 Python 2 编写的库迁移到 Python 3 变得非常漫长。因此,预计 Python 2 和 Python 3 在未来相当长一段时间内会共存。对于这本书,使用 Python 2.7.x。

以下步骤用于在 Ubuntu 中使用终端安装 Python 2.7:

$ sudo apt-get –y update
$ sudo apt-get –y upgrade
$ sudo apt-get install python-pip python-dev \
$ python2.7-dev build-essential

安装完成后,输入以下命令:

$ python --version

你应该能看到 Python 返回的版本字符串,这告诉你安装已经成功。

许多 Python 初学者面临的一个问题是各种库包的繁琐安装。为了解决这个问题,我建议读者下载 Anaconda 发行版。Anaconda 完全免费,包括近 200 个最流行的 Python 科学、数学、工程和数据分析包。尽管它体积相当大,但它让你摆脱了 Python 包的烦恼。Anaconda 可以在continuum.io/downloads下载,在那里你可以选择合适的 Python 版本和操作系统。按照安装说明安装 Anaconda 非常简单,所以这里不会详细说明步骤。

Cassandra 驱动程序

系统环境的最后一个是 Python 连接到 Cassandra 数据库的驱动软件。实际上,有几种选择,例如 pycassa、Cassandra 驱动程序和 Thrift。我选择了 DataStax 分发的 Apache Cassandra Python 驱动程序 2.0。它仅支持 CQL 3 和 Cassandra 在 1.2 版本中引入的新二进制协议。更详细的信息可以在www.datastax.com/documentation/developer/python-driver/2.0/common/drivers/introduction/introArchOverview_c.html找到。

驱动程序可以很容易地在 Ubuntu 终端中使用 pip 安装:

$ pip install cassandra-driver

注意

pip

pip 是一个用于安装和管理 Python 库包的命令行包管理系统。其项目页面可在 GitHub 上找到,github.com/pypa/pip

集成开发环境

Spyder 是一个开源的、跨平台的集成开发环境IDE),通常用于 Python 的科学编程。它由 Anaconda 自动安装,并集成了 NumPy、SciPy、matplotlib、IPython 和其他开源软件。它也是我最喜欢的 Python 开发环境。

还有许多其他优秀且流行的 Python IDE,如 IPython 和 Eclipse。本书中的代码对这些 IDE 友好。

系统概述

好的,我们已经了解了 Stock Screener 应用程序的主要系统组件,并决定了它们的实现。以下图展示了应用程序实现的系统概述:

系统概述

值得注意的是,系统将首先在单个 Ubuntu 机器上开发,然后在一个单节点 Cassandra 集群上开发(在第七章中,我们将集群扩展到双节点集群)。这限制了 Cassandra 卓越的集群能力。然而,从软件开发的角度来看,最重要的是完全实现所需的功能,而不是将大量精力分散在系统或基础设施组件上,这些组件的优先级较低。

代码设计和开发

我们现在进入开发阶段。我将逐步向您展示应用程序构建块的编码。从逻辑上讲,将构建两个核心模块,即数据源提供者和 Stock Screener。首先,我们将构建数据源提供者。

数据源提供者

数据源提供者实现了以下三个任务:

  1. 从 Yahoo! Finance 收集历史股票报价数据。

  2. 将接收到的数据转换为标准格式。

  3. 将标准化数据保存到 Cassandra 数据库中。

Python 有一个著名的数据分析库,称为 pandas。它是一个开源库,提供高性能、易于使用的数据结构和数据分析工具,特别是针对时间序列数据。您可以访问pandas.pydata.org/获取更多详细信息。

收集股票报价

pandas 在其pandas.io.data包中提供了一个DataReader函数。DataReader从各种互联网来源提取金融数据到称为DataFrame的数据结构中。Yahoo! Finance 是支持的互联网来源之一,使得收集历史股票报价数据变得轻而易举。参考以下 Python 代码,cha pter05_001.py

# -*- coding: utf-8 -*-
# program: chapter05_001.py

## web is the shorthand alias of pandas.io.data
import pandas.io.data as web
import datetime

## we want to retrieve the historical daily stock quote of
## Goldman Sachs from Yahoo! Finance for the period
## between 1-Jan-2012 and 28-Jun-2014
symbol = 'GS'
start_date = datetime.datetime(2012, 1, 1)
end_date = datetime.datetime(2014, 6, 28)

## data is a DataFrame holding the daily stock quote
data = web.DataReader(symbol, 'yahoo', start_date, end_date)

## use a for-loop to print out the data
for index, row in data.iterrows():
    print index.date(), '\t', row['Open'], '\t', row['High'], \
          '\t', row['Low'], '\t', row['Close'], '\t', row['Volume']

需要简要说明。pandas 提供了一个非常实用的数据结构,称为DataFrame,它是一个具有不同类型列的二维标签数据结构。您可以将其视为电子表格或 SQL 表。它通常是 pandas 中最常用的对象。

以下是一个使用 Spyder 编写和测试chapter05_001.py代码的截图:

收集股票报价

Spyder IDE 的左侧是您编写 Python 代码的地方。右侧中间面板是IPython 控制台,用于运行代码。

数据转换

除了DataFrame中的数据外,您还可以选择性地传递索引(行标签)和列(列标签)。可以通过访问索引和列属性分别访问行和列标签。例如,您可以回顾table.csv的截图,并看到 Yahoo! Finance 返回的列名分别是日期开盘价最高价最低价收盘价成交量调整后收盘价,分别。DataReader使用日期作为返回的DataFrame的索引。其余的列名成为DataFrame的列标签。

chapter05_001.py中的最后一个 for 循环也值得注意。DataFrame有一个名为iterrows()的函数,用于遍历其行作为(索引,列)对。因此,for 循环使用iterrows()遍历每日股票报价,我们简单地打印出索引(通过date()函数转换为字符串),以及通过传递相应的列标签到行的开盘价最高价最低价收盘价成交量列。调整后收盘价是经过股票分割、合并和股息调整的收盘价。我们不使用它,因为我们想专注于纯价格。

请注意,来自不同来源的股票报价数据可能有不同的格式,不用说,列名也不同。因此,在将它们映射到我们的标准化数据模型时,我们需要注意这种细微的差异。DataFrame提供了一个非常方便的方法通过列名检索数据,以及一些有用的函数来操作索引和列。我们可以利用它们来标准化数据格式,如chapter05_002.py所示:

# -*- coding: utf-8 -*-
# program: chapter05_002.py

## web is the shorthand alias of pandas.io.data
import pandas.io.data as web
import datetime

## we want to retrieve the historical daily stock quote of
## Goldman Sachs from Yahoo! Finance for the period
## between 1-Jan-2012 and 28-Jun-2014
symbol = 'GS'
start_date = datetime.datetime(2012, 1, 1)
end_date = datetime.datetime(2014, 6, 28)

## data is a DataFrame holding the daily stock quote
data = web.DataReader(symbol, 'yahoo', start_date, end_date)

## standardize the column names
## rename index column to price_date to match the Cassandra table
data.index.names=['price_date']

## drop extra column 'Adj Close'
data = data.drop(['Adj Close'], axis=1)

## rename the columns to match the respective columns in Cassandra
data = data.rename(columns={'Open':'open_price', \
                            'High':'high_price', \
                            'Low':'low_price', \
                            'Close':'close_price', \
                            'Volume':'volume'})

## use a for-loop to print out the transformed data
for index, row in data.iterrows():
    print index.date(), '\t', row['open_price'], '\t', \
                              row['high_price'], '\t', \
                              row['low_price'], '\t', \
                              row['close_price'], '\t', \
                              row['volume']

在 Cassandra 中存储数据

在将检索到的数据存储到 Cassandra 之前,我们需要在 Cassandra 数据库中创建键空间和表。我们将在chapter05_003.py中创建一个名为packtcdma的键空间和一个名为quote的表来存储历史数据,如下面的代码所示:

# -*- coding: utf-8 -*-
# program: chapter05_003.py

## import Cassandra driver library
from cassandra.cluster import Cluster

## create Cassandra instance
cluster = Cluster()

## establish Cassandra connection, using local default
session = cluster.connect()

## create keyspace packtcdma if not exists
## currently it runs on a single-node cluster
session.execute("CREATE KEYSPACE IF NOT EXISTS packtcdma " + \
                "WITH replication" + \
                "={'class':'SimpleStrategy', " + \
                "'replication_factor':1}")

## use packtcdma keyspace
session.set_keyspace('packtcdma')

## execute CQL statement to create quote table if not exists
session.execute('CREATE TABLE IF NOT EXISTS quote (' + \
                'symbol varchar,' + \
                'price_time timestamp,' + \
                'open_price float,' + \
                'high_price float,' + \
                'low_price float,' + \
                'close_price float,' + \
                'volume double,' + \
                'PRIMARY KEY (symbol, price_time))')

## close Cassandra connection
cluster.shutdown()

代码注释足以解释它在做什么。现在,我们已经准备好了历史数据存储库,接下来是将接收到的数据存储到其中。这正是chapter05_004.py的目的,其中创建了一个 Python 函数来插入数据,如下面的代码所示:

# -*- coding: utf-8 -*-
# program: chapter05_004.py

## import Cassandra driver library
from cassandra.cluster import Cluster
from decimal import Decimal

## function to insert historical data into table quote
## ss: Cassandra session
## sym: stock symbol
## d: standardized DataFrame containing historical data
def insert_quote(ss, sym, d):
    ## CQL to insert data, ? is the placeholder for parameters
    insert_cql = 'INSERT INTO quote (' + \
                 'symbol, price_time, open_price, high_price,' + \
                 'low_price, close_price, volume' + \
                 ') VALUES (' + \
                 '?, ?, ?, ?, ?, ?, ?' + \
                 ')'
    ## prepare the insert CQL as it will run repeatedly
    insert_stmt = ss.prepare(insert_cql)

    ## set decimal places to 4 digits
    getcontext().prec = 4

    ## loop thru the DataFrame and insert records
    for index, row in d.iterrows():
        ss.execute(insert_stmt, \
                   [sym, index, \
                   Decimal(row['open_price']), \
                   Decimal(row['high_price']), \
                   Decimal(row['low_price']), \
                   Decimal(row['close_price']), \
                   Decimal(row['volume']) \
                   ])

虽然chapter05_004.py的代码行数不到十行,但它相当复杂,需要一些解释。

我们可以使用def关键字在 Python 中创建一个函数。这必须后跟函数名和括号内的形式参数列表。构成函数主体的代码从下一行开始,缩进一个制表符。因此,在chapter05_004.py中,函数名为insert_quote(),有三个参数,分别是sssymd

注意

Python 中的缩进

在 Python 中,逻辑行开头的空白(空格和制表符)用于计算行的缩进级别,这反过来又用于确定语句的分组。对此要非常小心。大多数 Python IDE 都有检查缩进的特性。关于 Python 缩进神话的文章值得一读,可在www.secnetix.de/olli/Python/block_indentation.hawk找到。

第二个有趣的事情是prepare()函数。它用于准备由 Cassandra 解析并随后保存以供以后使用的 CQL 语句。当驱动程序使用预定义语句时,它只需要发送绑定参数的值。这避免了每次重新解析语句,从而降低了网络流量和 CPU 利用率。

预定义语句的占位符是?字符,这样参数就可以按顺序传递。这种方法称为位置参数传递。

代码的最后一段是一个 for 循环,它遍历DataFrame并将每一行插入到 quote 表中。我们还使用Decimal()函数将字符串转换为数值。

将它们全部放在一起

所有 Python 代码片段都可以组合起来制作数据馈送提供者。为了使代码更简洁,收集股票报价的代码片段被封装在一个名为collect_data()的函数中,而数据转换的代码片段被封装在transform_yahoo()函数中。完整的程序chapter05_005.py如下所示:

# -*- coding: utf-8 -*-
# program: chapter05_005.py

## import Cassandra driver library
from cassandra.cluster import Cluster
from decimal import Decimal

## web is the shorthand alias of pandas.io.data
import pandas.io.data as web
import datetime

## function to insert historical data into table quote
## ss: Cassandra session
## sym: stock symbol
## d: standardized DataFrame containing historical data
def insert_quote(ss, sym, d):
    ## CQL to insert data, ? is the placeholder for parameters
    insert_cql = "INSERT INTO quote (" + \
                 "symbol, price_time, open_price, high_price," + \
                 "low_price, close_price, volume" + \
                 ") VALUES (" + \
                 "?, ?, ?, ?, ?, ?, ?" + \
                 ")"
    ## prepare the insert CQL as it will run repeatedly
    insert_stmt = ss.prepare(insert_cql)

    ## set decimal places to 4 digits
    getcontext().prec = 4

    ## loop thru the DataFrame and insert records
    for index, row in d.iterrows():
        ss.execute(insert_stmt, \
                   [sym, index, \
                   Decimal(row['open_price']), \
                   Decimal(row['high_price']), \
                   Decimal(row['low_price']), \
                   Decimal(row['close_price']), \
                   Decimal(row['volume']) \
                   ])

## retrieve the historical daily stock quote from Yahoo! Finance
## Parameters
## sym: stock symbol
## sd: start date
## ed: end date
def collect_data(sym, sd, ed):
    ## data is a DataFrame holding the daily stock quote
    data = web.DataReader(sym, 'yahoo', sd, ed)
    return data

## transform received data into standardized format
## Parameter
## d: DataFrame containing Yahoo! Finance stock quote
def transform_yahoo(d):
    ## drop extra column 'Adj Close'
    d1 = d.drop(['Adj Close'], axis=1)

    ## standardize the column names
    ## rename index column to price_date
    d1.index.names=['price_date']

    ## rename the columns to match the respective columns
    d1 = d1.rename(columns={'Open':'open_price', \
                            'High':'high_price', \
                            'Low':'low_price', \
                            'Close':'close_price', \
                            'Volume':'volume'})
    return d1

## create Cassandra instance
cluster = Cluster()

## establish Cassandra connection, using local default
session = cluster.connect('packtcdma')

symbol = 'GS'
start_date = datetime.datetime(2012, 1, 1)
end_date = datetime.datetime(2014, 6, 28)

## collect data
data = collect_data(symbol, start_date, end_date)

## transform Yahoo! Finance data
data = transform_yahoo(data)

## insert historical data
insert_quote(session, symbol, data)

## close Cassandra connection
cluster.shutdown()

股票筛选器

股票筛选器从 Cassandra 数据库中检索历史数据,并应用技术分析技术以产生警报。它包含以下四个组件:

  1. 在指定时间段内检索历史数据

  2. 为时间序列数据编程技术分析指标

  3. 将筛选规则应用于历史数据

  4. 产生警报信号

数据范围

为了利用技术分析技术,需要足够数量的股票报价数据进行计算。我们不需要使用所有存储的数据,因此应该检索数据的一个子集进行处理。以下代码chapte05_006.py从指定日期范围内的quote表中检索历史数据:

# -*- coding: utf-8 -*-
# program: chapter05_006.py

import pandas as pd
import numpy as np

## function to insert historical data into table quote
## ss: Cassandra session
## sym: stock symbol
## sd: start date
## ed: end date
## return a DataFrame of stock quote
def retrieve_data(ss, sym, sd, ed):
    ## CQL to select data, ? is the placeholder for parameters
    select_cql = "SELECT * FROM quote WHERE symbol=? " + \"AND price_time >= ? AND price_time <= ?"

    ## prepare select CQL
    select_stmt = ss.prepare(select_cql)

    ## execute the select CQL
    result = ss.execute(select_stmt, [sym, sd, ed])

    ## initialize an index array
    idx = np.asarray([])

    ## initialize an array for columns
    cols = np.asarray([])

    ## loop thru the query resultset to make up the DataFrame
    for r in result:
        idx = np.append(idx, [r.price_time])
        cols = np.append(cols, [r.open_price, r.high_price, \r.low_price, r.close_price, r.volume])

    ## reshape the 1-D array into a 2-D array for each day
    cols = cols.reshape(idx.shape[0], 5)

    ## convert the arrays into a pandas DataFrame
    df = pd.DataFrame(cols, index=idx, \
                      columns=['close_price', 'high_price', \
                      'low_price', 'close_price', 'volume'])
    return df

函数的前一部分应该容易理解。它执行一个针对特定股票符号和指定日期段的select_cql查询。聚类列price_time使得范围查询成为可能。查询结果集被返回并用于填充两个 NumPy 数组,idx用于索引,cols用于列。然后cols数组被重塑为一个二维数组,其中包含每天的价格和成交量行。最后,使用idxcols数组创建一个DataFrame以返回df

时间序列数据

作为简单的说明,我们使用 10 天的简单移动平均SMA)作为股票筛选的技术分析信号。pandas 提供了一套丰富的函数来处理时间序列数据。SMA 可以通过rolling_mean()函数轻松计算,如chapter05_007.py所示:

# -*- coding: utf-8 -*-
# program: chapter05_007.py

import pandas as pd

## function to compute a Simple Moving Average on a DataFrame
## d: DataFrame
## prd: period of SMA
## return a DataFrame with an additional column of SMA
def sma(d, prd):
    d['sma'] = pd.rolling_mean(d.close_price, prd)
    return d

筛选规则

当计算简单移动平均(SMA)时,我们可以应用一个筛选规则来寻找交易信号。采用一个非常简单的规则:只要交易日的收盘价高于 10 日 SMA,就生成一个买入并持有的信号。在 Python 中,这只是一个利用 pandas 功能的单行代码。太棒了!以下是一个示例:

# -*- coding: utf-8 -*-
# program: chapter05_008.py

## function to apply screening rule to generate buy signals
## screening rule, Close > 10-Day SMA
## d: DataFrame
## return a DataFrame containing buy signals
def signal_close_higher_than_sma10(d):
    return d[d.close_price > d.sma]

股票筛选引擎

到目前为止,我们编写了股票筛选器的组件。现在我们将它们组合起来生成警报列表,如下面的代码所示:

# -*- coding: utf-8 -*-
# program: chapter05_009.py

## import Cassandra driver library
from cassandra.cluster import Cluster

import pandas as pd
import numpy as np
import datetime

## function to insert historical data into table quote
## ss: Cassandra session
## sym: stock symbol
## sd: start date
## ed: end date
## return a DataFrame of stock quote
def retrieve_data(ss, sym, sd, ed):
    ## CQL to select data, ? is the placeholder for parameters
    select_cql = "SELECT * FROM quote WHERE symbol=? " + \"AND price_time >= ? AND price_time <= ?"

    ## prepare select CQL
    select_stmt = ss.prepare(select_cql)

    ## execute the select CQL
    result = ss.execute(select_stmt, [sym, sd, ed])

    ## initialize an index array
    idx = np.asarray([])

    ## initialize an array for columns
    cols = np.asarray([])

    ## loop thru the query resultset to make up the DataFrame
    for r in result:
        idx = np.append(idx, [r.price_time])
        cols = np.append(cols, [r.open_price, r.high_price, \
                         r.low_price, r.close_price, r.volume])

    ## reshape the 1-D array into a 2-D array for each day
    cols = cols.reshape(idx.shape[0], 5)

    ## convert the arrays into a pandas DataFrame
    df = pd.DataFrame(cols, index=idx, \
                      columns=['open_price', 'high_price', \
                      'low_price', 'close_price', 'volume'])
    return df

## function to compute a Simple Moving Average on a DataFrame
## d: DataFrame
## prd: period of SMA
## return a DataFrame with an additional column of SMA
def sma(d, prd):
    d['sma'] = pd.rolling_mean(d.close_price, prd)
    return d

## function to apply screening rule to generate buy signals
## screening rule, Close > 10-Day SMA
## d: DataFrame
## return a DataFrame containing buy signals
def signal_close_higher_than_sma10(d):
    return d[d.close_price > d.sma]

## create Cassandra instance
cluster = Cluster()

## establish Cassandra connection, using local default
session = cluster.connect('packtcdma')
## scan buy-and-hold signals for GS over 1 month since 28-Jun-2012
symbol = 'GS'
start_date = datetime.datetime(2012, 6, 28)
end_date = datetime.datetime(2012, 7, 28)

## retrieve data
data = retrieve_data(session, symbol, start_date, end_date)

## close Cassandra connection
cluster.shutdown()

## compute 10-Day SMA
data = sma(data, 10)

## generate the buy-and-hold signals
alerts = signal_close_higher_than_sma10(data)

## print out the alert list
for index, r in alerts.iterrows():
    print index.date(), '\t', r['close_price']

测试运行

一个端到端测试包括两个部分。首先,我们测试和验证chapter05_005.py,这是完整的数据提供者模块。然后在 Spyder 中运行chapter05_005.py。历史股票报价数据应存储在 Cassandra 数据库中。然后运行并验证股票筛选模块chapter05_009.py,同样在 Spyder 中。

以下截图显示了测试运行的样本筛选。警报列表应该有七个买入并持有的交易信号:

测试运行

摘要

本章内容相当紧凑。我们设计了一个简单的股票筛选应用程序,该程序从雅虎财经收集股票报价数据,其存储库使用 Cassandra。还介绍了应用程序的系统环境以及简要的设置说明。然后我们使用 Python 逐步解释开发了这个应用程序。尽管只使用了一个 Cassandra 表,但基本的行操作逻辑已经得到了演示。

在下一章中,我们将继续增强股票筛选应用程序,以收集一批股票的股票报价数据,并通过几个改进来优化应用程序。

第六章。增强版本

传统上,更改通常不受欢迎,并且关系型数据库开发人员会尽可能地避免更改。然而,业务每天都在变化,尤其是在当前这个快节奏的时代。使用关系型数据库的系统对业务变化的延迟响应会降低企业的敏捷性,甚至威胁到企业的生存。随着 NoSQL 和其他相关技术的发展,我们现在有替代方案来拥抱这样的业务变化。

通过继续增强在第五章“初步设计和实现”中开发的股票筛选器应用程序,将详细解释如何演进现有的 Cassandra 数据模型的技术。同时,还将展示查询建模技术。然后,将相应地修改股票筛选器应用程序的源代码。到本章结束时,将开发一个完整的股票技术分析应用程序。您可以用它作为快速开发您自己的基础。

优化数据模型

在第五章中创建的股票筛选器应用程序,初步设计和实现,足以一次检索和分析一只股票。然而,在实际应用中,仅扫描一只股票看起来非常有限。这里可以稍作改进;它可以处理一组股票而不是单个股票。这组股票将被存储在 Cassandra 数据库中的观察名单中。

因此,股票筛选器应用程序将被修改以分析观察名单中的股票,因此它将根据相同的筛选规则为每个被观察的股票生成警报。

对于产生的警报,将它们保存在 Cassandra 中将有利于回测交易策略和持续改进股票筛选器应用程序。它们可以不时地被审查,而无需即时审查。

注意

回测是一个术语,用于指用现有历史数据测试交易策略、投资策略或预测模型。它也是应用于时间序列数据的一种特殊类型的交叉验证。

此外,当观察名单中的股票数量增长到几百只时,股票筛选器应用程序的用户将很难仅通过参考它们的股票代码来回忆起这些股票。因此,最好将股票名称添加到生成的警报中,使它们更具描述性和用户友好性。

最后,我们可能对找出在特定时间段内特定股票上生成的警报数量以及特定日期上生成的警报数量感兴趣。我们将使用 CQL 编写查询来回答这两个问题。通过这样做,可以展示查询建模技术。

增强方法

增强方法总共包括四个变更请求。首先,我们将对数据模型进行更改,然后代码将增强以提供新功能。之后,我们将再次测试运行增强的股票筛选应用程序。以下图中突出显示了需要修改的股票筛选应用程序的部分。

值得注意的是,股票筛选应用程序中增加了两个新组件。第一个组件是 观察列表,它管理 数据映射器和存档器,以从 Yahoo! Finance 收集观察列表中股票的股票报价数据。第二个组件是 查询。它提供了两个针对 警报列表的查询,用于回测目的:

增强方法

观察列表

观察列表是一个非常简单的表,仅存储其组成部分的股票代码。对于关系型数据库开发者来说,将股票代码定义为主键是非常直观的,对吧?然而,请记住,在 Cassandra 中,主键用于确定存储行的节点。由于观察列表预计不会非常长,将其所有行放在同一个节点上以实现更快的检索会更合适。但我们应该如何做到这一点呢?

我们可以为此特定目的创建一个额外的列,例如 watch_list_code。新表称为 watchlist,并将创建在 packtcdma 键空间中。CQL 语句显示在 chapter06_001.py 中:

# -*- coding: utf-8 -*-
# program: chapter06_001.py

## import Cassandra driver library
from cassandra.cluster import Cluster

## function to create watchlist
def create_watchlist(ss):
    ## create watchlist table if not exists
    ss.execute('CREATE TABLE IF NOT EXISTS watchlist (' + \
               'watch_list_code varchar,' + \
               'symbol varchar,' + \
               'PRIMARY KEY (watch_list_code, symbol))')

    ## insert AAPL, AMZN, and GS into watchlist
    ss.execute("INSERT INTO watchlist (watch_list_code, " + \
               "symbol) VALUES ('WS01', 'AAPL')")
    ss.execute("INSERT INTO watchlist (watch_list_code, " + \
               "symbol) VALUES ('WS01', 'AMZN')")
    ss.execute("INSERT INTO watchlist (watch_list_code, " + \
               "symbol) VALUES ('WS01', 'GS')")

## create Cassandra instance
cluster = Cluster()

## establish Cassandra connection, using local default
session = cluster.connect()

## use packtcdma keyspace
session.set_keyspace('packtcdma')

## create watchlist table
create_watchlist(session)

## close Cassandra connection
cluster.shutdown()

create_watchlist 函数创建表。请注意,watchlist 表由 watch_list_codesymbol 组成的复合主键构成。还创建了一个名为 WS01 的观察列表,其中包含三只股票,AAPLAMZNGS

警报列表

在 第五章,初步设计和实现 中,警报列表非常基础。它由一个 Python 程序生成,列出了收盘价高于其 10 日简单移动平均线的日期,即当时的信号和收盘价。请注意,当时没有股票代码和股票名称。

我们将创建一个名为 alertlist 的表来存储警报,包括股票的代码和名称。包含股票名称是为了满足使股票筛选应用程序更用户友好的要求。同时,请记住,不允许使用连接,并且在 Cassandra 中去规范化确实是最佳实践。这意味着我们不会介意在将要查询的表中重复存储(复制)股票名称。一个经验法则是 一个表对应一个查询;就这么简单。

alertlist 表是通过 CQL 语句创建的,如 chapter06_002.py 中所示:

# -*- coding: utf-8 -*-
# program: chapter06_002.py

## import Cassandra driver library
from cassandra.cluster import Cluster

## function to create alertlist
def create_alertlist(ss):
    ## execute CQL statement to create alertlist table if not exists
    ss.execute('CREATE TABLE IF NOT EXISTS alertlist (' + \
               'symbol varchar,' + \
               'price_time timestamp,' + \
               'stock_name varchar,' + \
               'signal_price float,' + \
               'PRIMARY KEY (symbol, price_time))')

## create Cassandra instance
cluster = Cluster()

## establish Cassandra connection, using local default
session = cluster.connect()

## use packtcdma keyspace
session.set_keyspace('packtcdma')

## create alertlist table
create_alertlist(session)

## close Cassandra connection
cluster.shutdown()

主键也是一个复合主键,由 symbolprice_time 组成。

添加描述性股票名称

到目前为止,packtcdma键空间有三个表,分别是alertlistquotewatchlist。为了添加描述性的股票名称,人们可能会想到只向alertlist添加一个股票名称列。如前所述,这已经完成了。那么,我们是否需要为quotewatchlist添加列?

实际上,这是一个设计决策,取决于这两个表是否将用于处理用户查询。用户查询的含义是,该表将用于检索用户提出的查询所需的行。如果用户想知道 2014 年 6 月 30 日苹果公司的收盘价,这便是一个用户查询。另一方面,如果股票筛选应用程序使用查询来检索其内部处理的行,那么这便不是用户查询。因此,如果我们想让quotewatchlist为用户查询返回行,它们就需要包含股票名称列;否则,它们不需要。

watchlist表仅用于当前设计的内部使用,因此它不需要包含股票名称列。当然,如果将来股票筛选应用程序允许用户维护股票观察列表,那么股票名称也应该添加到watchlist表中。

然而,对于quote来说,这有点棘手。因为股票名称应该从数据提供者那里检索,在我们的案例中是雅虎财经,最适合获取股票名称的时间是在检索相应的股票报价数据时。因此,在quote中添加了一个名为stock_name的新列,如chapter06_003.py所示:

# -*- coding: utf-8 -*-
# program: chapter06_003.py

## import Cassandra driver library
from cassandra.cluster import Cluster

## function to add stock_name column
def add_stockname_to_quote(ss):
    ## add stock_name to quote
    ss.execute('ALTER TABLE quote ' + \
               'ADD stock_name varchar')

## create Cassandra instance
cluster = Cluster()

## establish Cassandra connection, using local default
session = cluster.connect()

## use packtcdma keyspace
session.set_keyspace('packtcdma')

## add stock_name column
add_stockname_to_quote(session)

## close Cassandra connection
cluster.shutdown()

这相当直观。在这里,我们使用ALTER TABLE语句向quote添加了varchar数据类型的stock_name列。

警报查询

如前所述,我们感兴趣的是两个问题:

  • 在指定时间段内,针对某只股票生成了多少个警报?

  • 在特定日期上生成了多少个警报?

对于第一个问题,alertlist足以提供答案。然而,alertlist无法回答第二个问题,因为它的主键由symbolprice_time组成。我们需要为这个问题创建另一个特定的表。这是一个通过查询建模的例子。

基本上,第二个问题的新表结构应该类似于alertlist的结构。我们给这个表起了一个名字,alert_by_date,并在chapter06_004.py中创建了它:

# -*- coding: utf-8 -*-
# program: chapter06_004.py

## import Cassandra driver library
from cassandra.cluster import Cluster

## function to create alert_by_date table
def create_alertbydate(ss):
    ## create alert_by_date table if not exists
    ss.execute('CREATE TABLE IF NOT EXISTS alert_by_date (' + \
               'symbol varchar,' + \
               'price_time timestamp,' + \
               'stock_name varchar,' + \
               'signal_price float,' + \
               'PRIMARY KEY (price_time, symbol))')

## create Cassandra instance
cluster = Cluster()

## establish Cassandra connection, using local default
session = cluster.connect()

## use packtcdma keyspace
session.set_keyspace('packtcdma')

## create alert_by_date table
create_alertbydate(session)

## close Cassandra connection
cluster.shutdown()

chapter06_002.py中的alertlist相比,alert_by_date只是在复合主键中交换了列的顺序。有人可能会认为可以在alertlist上创建一个二级索引来实现相同的效果。然而,在 Cassandra 中,不能在已经参与主键的列上创建二级索引。始终要注意这个限制。

我们现在完成了数据模型的修改。接下来,我们需要增强下一节中的应用逻辑。

代码增强

关于要纳入股票筛选应用程序的新要求,已创建观察列表,我们将在本节中继续实现剩余更改的代码。

数据映射器和归档器

数据映射器和归档器是数据馈送提供器模块的组件,其源代码文件是chapter05_005.py。大部分源代码可以保持不变;我们只需要添加代码到:

  1. 为观察列表代码加载观察列表并检索基于该列表的数据馈送

  2. 检索股票名称并将其存储在报价表中

修改后的源代码显示在chapter06_005.py中:

# -*- coding: utf-8 -*-
# program: chapter06_005.py

## import Cassandra driver library
from cassandra.cluster import Cluster
from decimal import *

## web is the shorthand alias of pandas.io.data
import pandas.io.data as web
import datetime

## import BeautifulSoup and requests
from bs4 import BeautifulSoup
import requests

## function to insert historical data into table quote
## ss: Cassandra session
## sym: stock symbol
## d: standardized DataFrame containing historical data
## sn: stock name
def insert_quote(ss, sym, d, sn):
    ## CQL to insert data, ? is the placeholder for parameters
    insert_cql = "INSERT INTO quote (" + \
                 "symbol, price_time, open_price, high_price," + \
                 "low_price, close_price, volume, stock_name" + \
                 ") VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
    ## prepare the insert CQL as it will run repeatedly
    insert_stmt = ss.prepare(insert_cql)

    ## set decimal places to 4 digits
    getcontext().prec = 4

    ## loop thru the DataFrame and insert records
    for index, row in d.iterrows():
        ss.execute(insert_stmt, \
                   [sym, index, \
                   Decimal(row['open_price']), \
                   Decimal(row['high_price']), \
                   Decimal(row['low_price']), \
                   Decimal(row['close_price']), \
                   Decimal(row['volume']), \
                   sn])

在这里,我们将INSERT语句修改为在insert_quote函数中将股票名称存储到quote中。然后我们添加一个名为load_watchlist的函数:

## retrieve the historical daily stock quote from Yahoo! Finance
## Parameters
## sym: stock symbol
## sd: start date
## ed: end date
def collect_data(sym, sd, ed):
    ## data is a DataFrame holding the daily stock quote
    data = web.DataReader(sym, 'yahoo', sd, ed)
    return data

## transform received data into standardized format
## Parameter
## d: DataFrame containing Yahoo! Finance stock quote
def transform_yahoo(d):
    ## drop extra column 'Adj Close'
    d1 = d.drop(['Adj Close'], axis=1)

    ## standardize the column names
    ## rename index column to price_date
    d1.index.names=['price_date']

    ## rename the columns to match the respective columns
    d1 = d1.rename(columns={'Open':'open_price', \
                            'High':'high_price', \
                            'Low':'low_price', \
                            'Close':'close_price', \
                            'Volume':'volume'})
    return d1

## function to retrieve watchlist
## ss: Cassandra session
## ws: watchlist code
def load_watchlist(ss, ws):
    ## CQL to select data, ? is the placeholder for parameters
    select_cql = "SELECT symbol FROM watchlist " + \
                 "WHERE watch_list_code=?"

    ## prepare select CQL
    select_stmt = ss.prepare(select_cql)

    ## execute the select CQL
    result = ss.execute(select_stmt, [ws])

    ## initialize the stock array
    stw = []

    ## loop thru the query resultset to make up the DataFrame
    for r in result:
        stw.append(r.symbol)

    return stw

在这里,新函数load_watchlistwatch_list执行SELECT查询,以检索特定观察列表代码的观察股票;然后它返回一个symbol列表:

## function to retrieve stock name from Yahoo!Finance
## sym: stock symbol
def get_stock_name(sym):
  url = 'http://finance.yahoo.com/q/hp?s=' + sym + \
  '+Historical+Prices'
  r = requests.get(url)
  soup = BeautifulSoup(r.text)
  data = soup.findAll('h2')
  return data[2].text

def testcase001():
    ## create Cassandra instance
    cluster = Cluster()

    ## establish Cassandra connection, using local default
    session = cluster.connect('packtcdma')

    start_date = datetime.datetime(2012, 1, 1)
    end_date = datetime.datetime(2014, 6, 28)

    ## load the watchlist
    stocks_watched = load_watchlist(session, "WS01")

    ## iterate the watchlist
    for symbol in stocks_watched:
        ## get stock name
        stock_name = get_stock_name(symbol)

        ## collect data
        data = collect_data(symbol, start_date, end_date)

        ## transform Yahoo! Finance data
        data = transform_yahoo(data)

        ## insert historical data
        insert_quote(session, symbol, data, stock_name)

    ## close Cassandra connection
    cluster.shutdown()

testcase001()

这里的更改是一个名为get_stock_name的新函数,它向 Yahoo! Finance 发送一个网络服务请求,并从返回的 HTML 页面中提取股票名称。我们使用一个名为BeautifulSoup的 Python 包来使从 HTML 页面中提取元素变得非常方便。然后get_stock_name函数返回股票名称。

注意

BeautifulSoup是一个为快速周转项目如屏幕抓取而设计的库。它主要解析它所接收的任何文本,并通过解析文本的树遍历找到所需的内容。更多信息可以在www.crummy.com/software/BeautifulSoup/找到。

使用for循环遍历观察列表以检索股票名称和股票报价数据。此外,由于我们需要将股票名称存储在quote表中,insert_quote函数接受股票名称作为新参数,并相应地对INSERT语句和for循环进行少量修改。

那就是关于数据映射器和归档器更改的全部内容。

股票筛选引擎

我们将使用第五章中 Stock Screener Engine 的源代码,初步设计和实现来包含增强功能;为此,我们将执行以下操作:

  1. 与数据映射器和归档器类似,我们将为观察列表代码加载观察列表并在每个股票上扫描警报。

  2. 从报价表中的股票名称列检索股票报价数据。

  3. 将警报保存到alertlist中。

修改后的源代码显示在chapter06_006.py中:

# -*- coding: utf-8 -*-
# program: chapter06_006.py

## import Cassandra driver library
from cassandra.cluster import Cluster

import pandas as pd
import numpy as np
import datetime

## import Cassandra BatchStatement library
from cassandra.query import BatchStatement
from decimal import *

## function to insert historical data into table quote
## ss: Cassandra session
## sym: stock symbol
## sd: start date
## ed: end date
## return a DataFrame of stock quote
def retrieve_data(ss, sym, sd, ed):
    ## CQL to select data, ? is the placeholder for parameters
    select_cql = "SELECT * FROM quote WHERE symbol=? " + \
                 "AND price_time >= ? AND price_time <= ?"

    ## prepare select CQL
    select_stmt = ss.prepare(select_cql)

    ## execute the select CQL
    result = ss.execute(select_stmt, [sym, sd, ed])

    ## initialize an index array
    idx = np.asarray([])

    ## initialize an array for columns
    cols = np.asarray([])

    ## loop thru the query resultset to make up the DataFrame
    for r in result:
        idx = np.append(idx, [r.price_time])
        cols = np.append(cols, [r.open_price, r.high_price, \
                         r.low_price, r.close_price, \
                         r.volume, r.stock_name])

    ## reshape the 1-D array into a 2-D array for each day
    cols = cols.reshape(idx.shape[0], 6)

    ## convert the arrays into a pandas DataFrame
    df = pd.DataFrame(cols, index=idx, \
                      columns=['open_price', 'high_price', \
                      'low_price', 'close_price', \
                      'volume', 'stock_name'])
    return df

由于我们已经将股票名称包含在查询结果集中,我们需要修改retrieve_data函数中的SELECT语句:

## function to compute a Simple Moving Average on a DataFrame
## d: DataFrame
## prd: period of SMA
## return a DataFrame with an additional column of SMA
def sma(d, prd):
    d['sma'] = pd.rolling_mean(d.close_price, prd)
    return d

## function to apply screening rule to generate buy signals
## screening rule, Close > 10-Day SMA
## d: DataFrame
## return a DataFrame containing buy signals
def signal_close_higher_than_sma10(d):
    return d[d.close_price > d.sma]

## function to retrieve watchlist
## ss: Cassandra session
## ws: watchlist code
def load_watchlist(ss, ws):
    ## CQL to select data, ? is the placeholder for parameters
    select_cql = "SELECT symbol FROM watchlist " + \
                 "WHERE watch_list_code=?"

    ## prepare select CQL
    select_stmt = ss.prepare(select_cql)

    ## execute the select CQL
    result = ss.execute(select_stmt, [ws])

    ## initialize the stock array
    stw = []

    ## loop thru the query resultset to make up the DataFrame
    for r in result:
        stw.append(r.symbol)

    return stw

## function to insert historical data into table quote
## ss: Cassandra session
## sym: stock symbol
## d: standardized DataFrame containing historical data
## sn: stock name
def insert_alert(ss, sym, sd, cp, sn):
    ## CQL to insert data, ? is the placeholder for parameters
    insert_cql1 = "INSERT INTO alertlist (" + \
                 "symbol, price_time, signal_price, stock_name" +\
                 ") VALUES (?, ?, ?, ?)"

    ## CQL to insert data, ? is the placeholder for parameters
    insert_cql2 = "INSERT INTO alert_by_date (" + \
                 "symbol, price_time, signal_price, stock_name" +\
                 ") VALUES (?, ?, ?, ?)"

    ## prepare the insert CQL as it will run repeatedly
    insert_stmt1 = ss.prepare(insert_cql1)
    insert_stmt2 = ss.prepare(insert_cql2)

    ## set decimal places to 4 digits
    getcontext().prec = 4

    ## begin a batch
    batch = BatchStatement()

    ## add insert statements into the batch
    batch.add(insert_stmt1, [sym, sd, cp, sn])
    batch.add(insert_stmt2, [sym, sd, cp, sn])

    ## execute the batch
    ss.execute(batch)

def testcase002():
    ## create Cassandra instance
    cluster = Cluster()

    ## establish Cassandra connection, using local default
    session = cluster.connect('packtcdma')

    start_date = datetime.datetime(2012, 6, 28)
    end_date = datetime.datetime(2012, 7, 28)

    ## load the watch list
    stocks_watched = load_watchlist(session, "WS01")

    for symbol in stocks_watched:
        ## retrieve data
        data = retrieve_data(session, symbol, start_date, end_date)

        ## compute 10-Day SMA
        data = sma(data, 10)

        ## generate the buy-and-hold signals
        alerts = signal_close_higher_than_sma10(data)

        ## save the alert list
        for index, r in alerts.iterrows():
            insert_alert(session, symbol, index, \
                         Decimal(r['close_price']), \
                         r['stock_name'])

    ## close Cassandra connection
    cluster.shutdown()

testcase002()

chapter06_006.py 的底部,for 循环负责迭代由新的 load_watchlist 函数加载的 watchlist,这个函数与 chapter06_005.py 中的函数相同,不需要进一步解释。另一个 for 循环内部通过调用新的 insert_alert 函数将扫描到的警报保存到 alertlist

在解释 insert_alert 函数之前,让我们跳转到顶部的 retrieve_data 函数。retrieve_data 函数被修改为同时返回股票名称,因此 cols 变量现在包含六个列。向下滚动一点到 insert_alert

如其名称所示,insert_alert 函数将警报保存到 alertlistalert_by_date。它为这两个表分别有两个 INSERT 语句。这两个 INSERT 语句几乎完全相同,只是表名不同。显然,它们是重复的,这就是反规范化的含义。我们在这里还应用了 Cassandra 2.0 的新特性,称为 批处理。批处理将多个 数据修改语言 (DML) 语句组合成一个单一的逻辑、原子操作。DataStax 的 Cassandra Python 驱动程序通过 BatchStatement 包支持此功能。我们通过调用 BatchStatement() 函数创建一个批处理,然后将准备好的 INSERT 语句添加到批处理中,最后执行它。如果在提交过程中任一 INSERT 语句遇到错误,批处理中的所有 DML 语句将不会执行。因此,这与关系型数据库中的事务类似。

警报查询

Stock Screener 应用程序的最后一次修改是关于警报的查询功能,这些功能对回测和性能测量很有用。我们编写了两个查询来回答两个问题,如下所示:

  • 在指定时间段内,一只股票产生了多少个警报?

  • 在特定日期上产生了多少个警报?

由于我们在数据模型上使用了反规范化,因此执行起来非常容易。对于第一次查询,请参阅 chapter06_007.py

# -*- coding: utf-8 -*-
# program: chapter06_007.py

## import Cassandra driver library
from cassandra.cluster import Cluster

import pandas as pd
import numpy as np
import datetime

## execute CQL statement to retrieve rows of
## How many alerts were generated on a particular stock over
## a specified period of time?
def alert_over_daterange(ss, sym, sd, ed):
    ## CQL to select data, ? is the placeholder for parameters
    select_cql = "SELECT * FROM alertlist WHERE symbol=? " + \
                 "AND price_time >= ? AND price_time <= ?"

    ## prepare select CQL
    select_stmt = ss.prepare(select_cql)

    ## execute the select CQL
    result = ss.execute(select_stmt, [sym, sd, ed])

     ## initialize an index array
    idx = np.asarray([])

    ## initialize an array for columns
    cols = np.asarray([])

    ## loop thru the query resultset to make up the DataFrame
    for r in result:
        idx = np.append(idx, [r.price_time])
        cols = np.append(cols, [r.symbol, r.stock_name, \
                         r.signal_price])

    ## reshape the 1-D array into a 2-D array for each day
    cols = cols.reshape(idx.shape[0], 3)

    ## convert the arrays into a pandas DataFrame
    df = pd.DataFrame(cols, index=idx, \
                      columns=['symbol', 'stock_name', \
                      'signal_price'])
    return df

def testcase001():
    ## create Cassandra instance
    cluster = Cluster()

    ## establish Cassandra connection, using local default
    session = cluster.connect()

    ## use packtcdma keyspace
    session.set_keyspace('packtcdma')

    ## scan buy-and-hold signals for GS
    ## over 1 month since 28-Jun-2012
    symbol = 'GS'
    start_date = datetime.datetime(2012, 6, 28)
    end_date = datetime.datetime(2012, 7, 28)

    ## retrieve alerts
    alerts = alert_over_daterange(session, symbol, \
                                  start_date, end_date)

    for index, r in alerts.iterrows():
        print index.date(), '\t', \
            r['symbol'], '\t', \
            r['stock_name'], '\t', \
            r['signal_price']

    ## close Cassandra connection
    cluster.shutdown()

testcase001()

定义了一个名为 alert_over_daterange 的函数来检索与第一个问题相关的行,然后将其转换为 pandas DataFrame。

然后,我们可以根据 chapter06_007.py 中的相同逻辑来为第二个问题编写查询。源代码显示在 chapter06_008.py

# -*- coding: utf-8 -*-
# program: chapter06_008.py

## import Cassandra driver library
from cassandra.cluster import Cluster

import pandas as pd
import numpy as np
import datetime

## execute CQL statement to retrieve rows of
## How many alerts were generated on a particular stock over
## a specified period of time?
def alert_on_date(ss, dd):
    ## CQL to select data, ? is the placeholder for parameters
    select_cql = "SELECT * FROM alert_by_date WHERE " + \
                 "price_time=?"

    ## prepare select CQL
    select_stmt = ss.prepare(select_cql)

    ## execute the select CQL
    result = ss.execute(select_stmt, [dd])

     ## initialize an index array
    idx = np.asarray([])

    ## initialize an array for columns
    cols = np.asarray([])

    ## loop thru the query resultset to make up the DataFrame
    for r in result:
        idx = np.append(idx, [r.symbol])
        cols = np.append(cols, [r.stock_name, r.price_time, \
                         r.signal_price])

    ## reshape the 1-D array into a 2-D array for each day
    cols = cols.reshape(idx.shape[0], 3)

    ## convert the arrays into a pandas DataFrame
    df = pd.DataFrame(cols, index=idx, \
                      columns=['stock_name', 'price_time', \
                      'signal_price'])
    return df

def testcase001():
    ## create Cassandra instance
    cluster = Cluster()

    ## establish Cassandra connection, using local default
    session = cluster.connect()

    ## use packtcdma keyspace
    session.set_keyspace('packtcdma')

    ## scan buy-and-hold signals for GS over 1 month since 28-Jun-2012
    on_date = datetime.datetime(2012, 7, 13)

    ## retrieve alerts
    alerts = alert_on_date(session, on_date)

    ## print out alerts
    for index, r in alerts.iterrows():
        print index, '\t', \
              r['stock_name'], '\t', \
              r['signal_price']

    ## close Cassandra connection
    cluster.shutdown()

testcase001()

再次强调,反规范化是 Cassandra 的朋友。它不需要外键、引用完整性或表连接。

实现系统更改

我们现在可以逐个实现系统更改:

  1. 首先,我们按顺序运行 chapter06_001.pychapter06_004.py,以对数据模型进行修改。

  2. 然后,我们执行chapter06_005.py以检索观察列表的股票报价数据。值得一提的是,UPSERT 是 Cassandra 的一个非常好的特性。当我们向表中插入相同的行时,我们不会遇到重复的主键。如果行已存在,它将简单地更新该行,否则将插入该行。这使得数据操作逻辑变得整洁且清晰。

  3. 此外,我们运行chatper06_006.py通过扫描观察列表中每只股票的股票报价数据来存储警报。

  4. 最后,我们执行chapter06_007.pychapter06_008.py来查询alertlistalert_by_date,其样本测试结果如下:实施系统更改

摘要

本章通过一系列增强扩展了股票筛选应用程序。我们对数据模型进行了修改,以展示通过查询技术建模以及非规范化如何帮助我们实现高性能应用程序。我们还尝试了 Cassandra 2.0 提供的批量功能。

注意,本章中的源代码未经整理,可以进行某种重构。然而,由于页面数量的限制,它被留作读者的练习。

股票筛选应用程序现在运行在单个节点集群上。

在下一章中,我们将深入探讨将其扩展到更大集群的考虑和流程,这在现实生活中的生产系统中相当常见。

第七章:部署和监控

我们在之前的章节中探讨了 Stock Screener 应用程序的开发;现在是时候考虑如何在生产环境中部署它了。在本章中,我们将讨论在生产环境中部署 Cassandra 数据库最重要的方面。这些方面包括选择合适的复制策略、snitch 和复制因子的组合,以形成一个容错性高、高可用的集群。然后我们将演示将 Stock Screener 应用程序的 Cassandra 开发数据库迁移到生产数据库的过程。然而,集群维护超出了本书的范围。

此外,一个持续运行的实时生产系统当然需要对其健康状况进行监控。我们将介绍监控 Cassandra 集群的基本工具和技术,包括 nodetool 实用程序、JMX 和 MBeans 以及系统日志。

最后,我们将探讨除了使用默认设置之外提高 Cassandra 性能的方法。实际上,性能调整可以在多个级别进行,从最低的硬件和系统配置到最高的应用程序编码技术。我们将重点关注Java 虚拟机JVM)级别,因为 Cassandra 高度依赖于其底层性能。此外,我们还将涉及如何调整表的缓存。

复制策略

本节将介绍 Cassandra 集群的数据复制配置。它将涵盖复制策略、snitch 以及为 Stock Screener 应用程序配置集群。

数据复制

Cassandra,按照设计,可以在全球多个数据中心的大型集群中运行。在这样的分布式环境中,网络带宽和延迟必须在架构中给予关键性的考虑,并且需要提前进行仔细规划,否则可能会导致灾难性的后果。最明显的问题就是时钟同步——这是解决可能威胁整个集群数据完整性的交易冲突的真正手段。Cassandra 依赖于底层操作系统平台来提供时钟同步服务。此外,节点在某个时间点高度可能发生故障,集群必须能够抵御这种典型的节点故障。这些问题必须在架构层面进行彻底的考虑。

Cassandra 采用数据复制来应对这些问题,基于使用空间来交换时间的理念。它简单地消耗更多的存储空间来制作数据副本,以最小化在集群中解决之前提到的问题的复杂性。

数据复制是通过所谓的复制因子在 键空间 中配置的。复制因子指的是集群中每行数据的总副本数。因此,复制因子为 1(如前几章中的示例所示)表示每行数据只有一个副本在单个节点上。对于复制因子为 2,每行数据有两个副本在不同的节点上。通常,大多数生产场景中复制因子为 3 就足够了。

所有数据副本同等重要。没有主副本或从副本。因此,数据复制没有可扩展性问题。随着更多节点的添加,可以增加复制因子。然而,复制因子不应设置超过集群中的节点数。

Cassandra 的另一个独特特性是它了解集群中节点的物理位置以及它们之间的邻近性。Cassandra 可以通过正确的 IP 地址分配方案配置来了解数据中心和机架的布局。这个设置被称为复制策略,Cassandra 为我们提供了两个选择:SimpleStrategyNetworkTopologyStrategy

SimpleStrategy

SimpleStrategy 在单台机器或单个数据中心内的集群中使用。它将第一个副本放置在分区器确定的节点上,然后以顺时针方向将额外的副本放置在下一个节点上,不考虑数据中心和机架的位置。尽管这是创建键空间时的默认复制策略,但如果我们打算拥有多个数据中心,我们应该使用 NetworkTopologyStrategy

NetworkTopologyStrategy

NetworkTopologyStrategy 通过了解集群中节点的 IP 地址来了解数据中心和机架的位置。它通过顺时针机制将副本放置在相同的数据中心,直到达到另一个机架的第一个节点。它试图在不同的机架上放置副本,因为同一机架的节点往往由于电源、网络问题、空调等原因同时失败。

如前所述,Cassandra 通过节点的 IP 地址了解其物理位置。IP 地址到数据中心和机架的映射称为 snitch。简单来说,snitch 确定节点属于哪些数据中心和机架。它通过向 Cassandra 提供有关网络拓扑的信息来优化读取操作,以便读取请求可以有效地路由。它还影响副本在考虑数据中心和机架的物理位置时的分布。

根据不同的场景,有各种类型的 snitch 可用,每种都有其优缺点。以下简要描述如下:

  • SimpleSnitch: 这仅用于单个数据中心的部署

  • DynamicSnitch: 这监控来自不同副本的读操作性能,并根据历史性能选择最佳副本

  • RackInferringSnitch: 这通过数据中心和与 IP 地址对应的机架来确定节点的位置

  • PropertyFileSnitch: 这通过数据中心和机架确定节点的位置

  • GossipingPropertyFileSnitch: 这在添加新节点时使用 gossip 自动更新所有节点

  • EC2Snitch: 这与单个区域的 Amazon EC2 一起使用

  • EC2MultiRegionSnitch: 这用于跨多个区域的 Amazon EC2

  • GoogleCloudSnitch: 这用于跨一个或多个区域的 Google Cloud Platform

  • CloudstackSnitch: 这用于 Apache Cloudstack 环境

注意

Snitch 架构

对于更详细的信息,请参阅 DataStax 制作的文档,www.datastax.com/documentation/cassandra/2.1/cassandra/architecture/architectureSnitchesAbout_c.html

以下图展示了使用RackInferringSnitch和每个数据中心三个副本因子的四个机架中八个节点的集群示例:

网络拓扑策略

提示

集群中的所有节点必须使用相同的 snitch 设置。

让我们先看看数据中心 1中的 IP 地址分配。IP 地址是分组并自上而下分配的。数据中心 1中的所有节点都在同一个123.1.0.0子网中。对于机架 1中的节点,它们都在同一个123.1.1.0子网中。因此,机架 1中的节点 1被分配了 IP 地址123.1.1.1,而机架 1中的节点 2123.1.1.2。同样的规则适用于机架 2,因此机架 2节点 1节点 2的 IP 地址分别是123.1.2.1123.1.2.2。对于数据中心 2,我们只需将数据中心的子网更改为123.2.0.0,然后数据中心 2中的机架和节点相应地改变。

RackInferringSnitch值得更详细的解释。它假设网络拓扑是通过以下规则正确分配的 IP 地址而知的:

IP 地址 = <任意八位字节>.<数据中心八位字节>.<机架八位字节>.<节点八位字节>

IP 地址分配的公式在上一段中显示。有了这种非常结构化的 IP 地址分配,Cassandra 可以理解集群中所有节点的物理位置。

我们还需要了解的是,如图中所示的前三个副本的复制因子。对于具有NetworkToplogyStrategy的集群,复制因子是在每个数据中心的基础上设置的。因此,在我们的例子中,三个副本放置在数据中心 1,如图中虚线箭头所示。数据中心 2是另一个必须有三个副本的数据中心。因此,整个集群中共有六个副本。

我们不会在这里详细说明复制因子、snitch 和复制策略的每一种组合,但我们应该现在理解 Cassandra 如何利用它们来灵活地处理实际生产中的不同集群场景的基础。

为股票筛选器应用程序设置集群

让我们回到股票筛选器应用程序。它在第六章,增强版本中运行的集群是一个单节点集群。在本节中,我们将设置一个可以用于小规模生产的两个节点集群。我们还将把开发数据库中的现有数据迁移到新的生产集群。需要注意的是,对于仲裁读取/写入,通常最好使用奇数个节点。

系统和网络配置

假设操作系统和网络配置的安装和设置步骤已经完成。此外,两个节点都应该安装了新的 Cassandra。两个节点的系统配置相同,如下所示:

  • 操作系统:Ubuntu 12.04 LTS 64 位

  • 处理器:Intel Core i7-4771 CPU @3.50GHz x 2

  • 内存:2 GB

  • 硬盘:20 GB

全局设置

集群被命名为Test Cluster,其中ubtc01ubtc02节点位于同一个机架RACK1,并且位于同一个数据中心NY1。将要设置的集群的逻辑架构如下所示:

全局设置

为了配置一个 Cassandra 集群,我们需要修改 Cassandra 的主配置文件cassandra.yaml中的几个属性。根据 Cassandra 的安装方式,cassandra.yaml位于不同的目录中:

  • 软件包安装:/etc/cassandra/

  • 打包安装:<install_location>/conf/

首先要做的是为每个节点设置cassandra.yaml中的属性。由于两个节点的系统配置相同,以下对cassandra.yaml设置的修改与它们相同:

-seeds: ubtc01
listen_address:
rpc_address: 0.0.0.0
endpoint_snitch: GossipingPropertyFileSnitch
auto_bootstrap: false

使用GossipingPropertyFileSnitch的原因是我们希望 Cassandra 集群在添加新节点时能够自动通过 gossip 协议更新所有节点。

除了cassandra.yaml之外,我们还需要修改与cassandra.yaml相同位置的cassandra-rackdc.properties中的数据中心和机架属性。在我们的例子中,数据中心是NY1,机架是RACK1,如下所示:

dc=NY1
rack=RACK1

配置过程

集群的配置流程(参考以下 bash 脚本:setup_ubtc01.shsetup_ubtc02.sh)如下列举:

  1. 停止 Cassandra 服务:

    ubtc01:~$ sudo service cassandra stop
    ubtc02:~$ sudo service cassandra stop
    
    
  2. 删除系统键空间:

    ubtc01:~$ sudo rm -rf /var/lib/cassandra/data/system/*
    ubtc02:~$ sudo rm -rf /var/lib/cassandra/data/system/*
    
    
  3. 根据前一小节中指定的全局设置,在两个节点上修改 cassandra.yamlcassandra-rackdc.properties

  4. 首先启动种子节点 ubtc01

    ubtc01:~$ sudo service cassandra start
    
    
  5. 然后启动 ubtc02

    ubtc02:~$ sudo service cassandra start
    
    
  6. 等待一分钟,检查 ubtc01ubtc02 是否都处于运行状态:

    ubtc01:~$ nodetool status
    
    

集群设置成功的结果应类似于以下截图,显示两个节点都处于运行状态:

配置流程

旧数据迁移流程

我们现在有了准备好的集群,但它仍然是空的。我们可以简单地重新运行股票筛选器应用程序来重新下载并填充生产数据库。或者,我们可以将开发单节点集群中收集的历史价格迁移到这个生产集群。在后一种方法中,以下流程可以帮助我们简化数据迁移任务:

  1. 在开发数据库中对 packcdma 键空间进行快照(ubuntu 是开发机器的主机名):

    ubuntu:~$ nodetool snapshot packtcdma
    
    
  2. 记录快照目录,在此示例中,1412082842986

  3. 为了安全起见,将快照目录下的所有 SSTables 复制到临时位置,例如 ~/temp/

    ubuntu:~$ mkdir ~/temp/
    ubuntu:~$ mkdir ~/temp/packtcdma/
    ubuntu:~$ mkdir ~/temp/packtcdma/alert_by_date/
    ubuntu:~$ mkdir ~/temp/packtcdma/alertlist/
    ubuntu:~$ mkdir ~/temp/packtcdma/quote/
    ubuntu:~$ mkdir ~/temp/packtcdma/watchlist/
    ubuntu:~$ sudo cp -p /var/lib/cassandra/data/packtcdma/alert_by_date/snapshots/1412082842986/* ~/temp/packtcdma/alert_by_date/
    ubuntu:~$ sudo cp -p /var/lib/cassandra/data/packtcdma/alertlist/snapshots/1412082842986/* ~/temp/packtcdma/alertlist/
    ubuntu:~$ sudo cp -p /var/lib/cassandra/data/packtcdma/quote/snapshots/1412082842986/* ~/temp/packtcdma/quote/
    ubuntu:~$ sudo cp -p /var/lib/cassandra/data/packtcdma/watchlist/snapshots/1412082842986/* ~/temp/packtcdma/watchlist/
    
    
  4. 打开 cqlsh 连接到 ubtc01 并在 production 集群中创建具有适当复制策略的键空间:

    ubuntu:~$ cqlsh ubtc01
    cqlsh> CREATE KEYSPACE packtcdma WITH replication = {'class': 'NetworkTopologyStrategy',  'NY1': '2'};
    
    
  5. 创建 alert_by_datealertlistquotewatchlist 表:

    cqlsh> CREATE TABLE packtcdma.alert_by_date (
     price_time timestamp,
     symbol varchar,
     signal_price float,
     stock_name varchar,
     PRIMARY KEY (price_time, symbol));
    cqlsh> CREATE TABLE packtcdma.alertlist (
     symbol varchar,
     price_time timestamp,
     signal_price float,
     stock_name varchar,
     PRIMARY KEY (symbol, price_time));
    cqlsh> CREATE TABLE packtcdma.quote (
     symbol varchar,
     price_time timestamp,
     close_price float,
     high_price float,
     low_price float,
     open_price float,
     stock_name varchar,
     volume double,
     PRIMARY KEY (symbol, price_time));
    cqlsh> CREATE TABLE packtcdma.watchlist (
     watch_list_code varchar,
     symbol varchar,
     PRIMARY KEY (watch_list_code, symbol));
    
    
  6. 使用 sstableloader 工具将 SSTables 重新加载到生产集群中:

    ubuntu:~$ cd ~/temp
    ubuntu:~/temp$ sstableloader -d ubtc01 packtcdma/alert_by_date
    ubuntu:~/temp$ sstableloader -d ubtc01 packtcdma/alertlist
    ubuntu:~/temp$ sstableloader -d ubtc01 packtcdma/quote
    ubuntu:~/temp$ sstableloader -d ubtc01 packtcdma/watchlist
    
    
  7. ubtc02 上检查生产数据库中的旧数据:

    cqlsh> select * from packtcdma.alert_by_date;
    cqlsh> select * from packtcdma.alertlist;
    cqlsh> select * from packtcdma.quote;
    cqlsh> select * from packtcdma.watchlist;
    
    

虽然前面的步骤看起来很复杂,但理解它们想要实现的目标并不困难。需要注意的是,我们已经将每个数据中心的复制因子设置为 2,以在两个节点上提供数据冗余,如 CREATE KEYSPACE 语句所示。如果需要,复制因子可以在将来更改。

部署股票筛选器应用程序

由于我们已经设置了生产集群并将旧数据移动到其中,现在是时候部署股票筛选器应用程序了。唯一需要修改的是代码以建立 Cassandra 与生产集群的连接。这使用 Python 非常容易完成。chapter06_006.py 中的代码已修改为与生产集群一起工作,作为 chapter07_001.py。创建了一个名为 testcase003() 的新测试用例来替换 testcase002()。为了节省页面,这里没有显示 chapter07_001.py 的完整源代码;只描述了 testcase003() 函数如下:

# -*- coding: utf-8 -*-
# program: chapter07_001.py

# other functions are not shown for brevity

def testcase003():
    ## create Cassandra instance with multiple nodes
    cluster = Cluster(['ubtc01', 'ubtc02'])

    ## establish Cassandra connection, using local default
    session = cluster.connect('packtcdma')

    start_date = datetime.datetime(2012, 6, 28)
    end_date = datetime.datetime(2013, 9, 28)

    ## load the watch list
    stocks_watched = load_watchlist(session, "WS01")

    for symbol in stocks_watched:
        ## retrieve data
        data = retrieve_data(session, symbol, start_date, end_date)

        ## compute 10-Day SMA
        data = sma(data, 10)

        ## generate the buy-and-hold signals
        alerts = signal_close_higher_than_sma10(data)

        ## save the alert list
        for index, r in alerts.iterrows():
            insert_alert(session, symbol, index, \
                         Decimal(r['close_price']), \
                         r['stock_name'])

    ## close Cassandra connection
    cluster.shutdown()

testcase003()

testcase003() 函数开头直接传递给集群连接代码的是一个要连接的节点数组(ubtc01ubtc02)。在这里,我们采用了默认的 RoundRobinPolicy 作为连接负载均衡策略。它用于决定如何在集群中所有可能的协调节点之间分配请求。还有许多其他选项,这些选项在驱动程序 API 文档中有描述。

注意

Cassandra 驱动 2.1 文档

对于 Apache Cassandra 的 Python 驱动 2.1 的完整 API 文档,您可以参考datastax.github.io/python-driver/api/index.html

监控

随着应用程序系统的上线,我们需要每天监控其健康状况。Cassandra 提供了多种工具来完成这项任务。我们将介绍其中的一些,并附上实用的建议。值得注意的是,每个操作系统也提供了一系列用于监控的工具和实用程序,例如 Linux 上的 topdfdu 和 Windows 上的任务管理器。然而,这些内容超出了本书的范围。

Nodetool

nodetool 实用工具对我们来说应该不陌生。它是一个命令行界面,用于监控 Cassandra 并执行常规数据库操作。它包括表格、服务器和压缩统计信息的最重要指标,以及其他用于管理的有用命令。

这里列出了最常用的 nodetool 选项:

  • status:这提供了关于集群的简洁摘要,例如状态、负载和 ID

  • netstats:这提供了关于节点的网络信息,重点关注读取修复操作

  • info:这提供了包括令牌、磁盘负载、运行时间、Java 堆内存使用、键缓存和行缓存在内的有价值节点信息

  • tpstats:这提供了关于 Cassandra 操作每个阶段的活跃、挂起和完成的任务数量的统计信息

  • cfstats:这获取一个或多个表的统计信息,例如读写次数和延迟,以及关于 SSTable、memtable、布隆过滤器和大小的指标。

注意

nodetool 的详细文档可以参考www.datastax.com/documentation/cassandra/2.0/cassandra/tools/toolsNodetool_r.html

JMX 和 MBeans

Cassandra 是用 Java 语言编写的,因此它原生支持 Java 管理扩展JMX)。我们可以使用符合 JMX 规范的工具 JConsole 来监控 Cassandra。

注意

JConsole

JConsole 包含在 Sun JDK 5.0 及更高版本中。然而,它消耗了大量的系统资源。建议您在远程机器上运行它,而不是在 Cassandra 节点所在的同一主机上运行。

我们可以通过在终端中输入jconsole来启动 JConsole。假设我们想监控本地节点,当新连接对话框弹出时,我们在远程进程文本框中输入localhost:71997199是 JMX 的端口号),如下面的截图所示:

JMX 和 MBeans

连接到本地 Cassandra 实例后,我们将看到一个组织良好的 GUI,顶部水平放置了六个单独的标签页,如下面的截图所示:

JMX 和 MBeans

GUI 标签页的解释如下:

  • 概览:此部分显示有关 JVM 和监控值的概述信息

  • 内存:此部分显示有关堆和非堆内存使用情况以及垃圾回收指标的信息

  • 线程:此部分显示有关线程使用情况的信息

  • :此部分显示有关类加载的信息

  • 虚拟机摘要:此部分显示有关 JVM 的信息

  • MBeans:此部分显示有关特定 Cassandra 指标和操作的信息

此外,Cassandra 为 JConsole 提供了五个 MBeans。以下是它们的简要介绍:

  • org.apache.cassandra.db:这包括缓存、表指标和压缩

  • org.apache.cassandra.internal:这些是内部服务器操作,如 gossip 和 hinted handoff

  • org.apache.cassandra.metrics:这些是 Cassandra 实例的各种指标,如缓存和压缩

  • org.apache.cassandra.net:这包括节点间通信,包括 FailureDetector、MessagingService 和 StreamingService

  • org.apache.cassandra.request:这些包括与读取、写入和复制操作相关的任务

注意

MBeans

托管 Bean(MBean)是一个 Java 对象,它代表一个可管理的资源,例如在 JVM 中运行的应用程序、服务、组件或设备。它可以用来收集有关性能、资源使用或问题等问题的统计信息,用于获取和设置应用程序配置或属性,以及通知事件,如故障或状态变化。

系统日志

最基础但也是最强大的监控工具是 Cassandra 的系统日志。系统日志的默认位置位于/var/log/cassandra/下的名为system.log的目录中。它只是一个文本文件,可以使用任何文本编辑器查看或编辑。以下截图显示了system.log的摘录:

系统日志

这条日志看起来很长且奇怪。然而,如果你是 Java 开发者并且熟悉标准日志库 Log4j,它就相当简单。Log4j 的美丽之处在于它为我们提供了不同的日志级别,以便我们控制记录在system.log中的日志语句的粒度。如图所示,每行的第一个单词是INFO,表示这是一条信息日志。其他日志级别选项包括FATALERRORWARNDEBUGTRACE,从最不详细到最详细。

系统日志在故障排除问题中也非常有价值。我们可能需要将日志级别提高到DEBUGTRACE以进行故障排除。然而,在生产 Cassandra 集群中以DEBUGTRACE模式运行将显著降低其性能。我们必须非常小心地使用它们。

我们可以通过调整 Cassandra 配置目录中的log4j-server.properties文件中的log4j.rootLogger属性来更改 Cassandra 的标准日志级别。以下截图展示了 ubtc02 上的log4j-server.properties的内容:

系统日志

需要强调的是,system.loglog4j-server.properties仅负责单个节点。对于两个节点的集群,我们将在各自的节点上拥有两个system.log和两个log4j-server.properties

性能调优

性能调优是一个庞大且复杂的话题,本身就可以成为一门完整的课程。我们只能在这简短的章节中触及表面。与上一节中的监控类似,特定于操作系统的性能调优技术超出了本书的范围。

Java 虚拟机

基于监控工具和系统日志提供的信息,我们可以发现性能调优的机会。我们通常首先关注的是 Java 堆内存和垃圾回收。Cassandra 的环境设置文件cassandra-env.sh中控制了 JVM 的配置设置,该文件位于/etc/cassandra/目录下。以下截图展示了示例:

Java 虚拟机

基本上,它已经包含了为优化主机系统计算出的样板选项。它还附带了解释,以便我们在遇到实际问题时调整特定的 JVM 参数和 Cassandra 实例的启动选项;否则,这些样板选项不应被更改。

注意

www.datastax.com/documentation/cassandra/2.0/cassandra/operations/ops_tune_jvm_c.html可以找到有关如何为 Cassandra 调整 JVM 的详细文档。

缓存

我们还应该注意的一个领域是缓存。Cassandra 包括集成的缓存并在集群周围分布缓存数据。对于特定于表的缓存,我们将关注分区键缓存和行缓存。

分区键缓存

分区键缓存,或简称键缓存,是表的分区索引缓存。使用键缓存可以节省处理器时间和内存。然而,仅启用键缓存会使磁盘活动实际上读取请求的数据行。

行缓存

行缓存类似于传统的缓存。当访问行时,整个行会被拉入内存,在需要时从多个 SSTables 合并,并缓存。这可以防止 Cassandra 再次使用磁盘 I/O 检索该行,从而极大地提高读取性能。

当同时配置了行缓存和分区键缓存时,行缓存尽可能返回结果。在行缓存未命中时,分区键缓存可能仍然提供命中,使磁盘查找更加高效。

然而,有一个注意事项。当读取分区时,Cassandra 会缓存该分区的所有行。因此,如果分区很大,或者每次只读取分区的一小部分,行缓存可能并不有利。它很容易被误用,从而导致 JVM 耗尽,导致 Cassandra 失败。这就是为什么行缓存默认是禁用的。

注意

我们通常为表启用键缓存或行缓存中的一个,而不是同时启用两者。

监控缓存

要么使用nodetool info命令,要么使用 JMX MBeans 来提供监控缓存的帮助。我们应该对缓存选项进行小范围的增量调整,然后使用 nodetool 实用程序监控每次更改的效果。以下图中的nodetool info命令的最后两行输出包含了ubtc02Row CacheKey Cache指标:

监控缓存

在内存消耗过高的情况下,我们可以考虑调整数据缓存。

启用/禁用缓存

我们使用 CQL 通过更改表的缓存属性来启用或禁用缓存。例如,我们使用ALTER TABLE语句来启用watchlist的行缓存:

ALTER TABLE watchlist WITH caching=''ROWS_ONLY'';

其他可用的表缓存选项包括ALLKEYS_ONLYNONE。它们相当直观,我们在此不逐一介绍。

注意

关于数据缓存的更多信息,可以在www.datastax.com/documentation/cassandra/2.0/cassandra/operations/ops_configuring_caches_c.html找到。

摘要

本章重点介绍了将 Cassandra 集群部署到生产环境中的最重要的方面。Cassandra 可以学习理解集群中节点的物理位置,以便智能地管理其可用性、可扩展性和性能。尽管规模较小,我们还是将股票筛选器应用程序部署到了生产环境中。学习如何从非生产环境迁移旧数据对我们来说也很有价值。

然后,我们学习了监控和性能调优的基础知识,这对于一个正在运行的系统来说是必不可少的。如果你有部署其他数据库和系统的经验,你可能会非常欣赏 Cassandra 的整洁和简单。

在下一章中,我们将探讨与应用设计和开发相关的补充信息。我们还将总结每一章的精髓。

第八章. 最后的想法

在前面的章节中,我们快速地经历了一次使用 Python 和 Cassandra 开发技术分析应用程序的过程。我们从理论基础知识开始,逐步设计并开发了一个运行中的应用程序。即使你是计算机编程的新手,也应该能够按顺序阅读这些章节。

现在我们来到了这本书的最后一章。我们将查看与应用程序设计和开发相关的补充信息。然后,我们将快速回顾每一章的基本内容,以便结束这本书。

补充信息

在这里,我们将简要查看关于客户端驱动程序、安全功能、备份和恢复的补充信息。

客户端驱动程序

驱动程序减轻了应用程序开发者处理与底层数据库通信的重复性、低级繁琐工作的负担。这样,应用程序开发者就可以专注于编写业务逻辑。

由于 Cassandra 越来越受欢迎,因此为现代编程语言开发了驱动程序。这极大地简化了应用程序开发者的工作负担,他们习惯了笨拙的 Thrift API。

注意

不同语言的驱动程序

可以在 PlanetCassandra planetcassandra.org/client-drivers-tools/ 上看到常用 Cassandra 驱动程序及其对应的支持编程语言的列表。

现在可用的 Cassandra 驱动程序数量仍在增长,其中许多是开源的。如果你真的需要生产支持,那么 DataStax 值得你考虑。

下面提供了一些关于驱动程序选择的评论:

  • 首先,要支持的编程语言是单个最重要的约束条件。然后是通信协议。Thrift API 历史悠久,但使用起来相当困难。除非你需要支持与较旧版本的 Cassandra 集群一起工作的应用程序,否则强烈建议使用提供 CQL 支持的驱动程序,这将与本书中介绍的数据建模技术保持一致。数据模型的实现也将变得更加容易。

  • 另一个选择因素是驱动程序提供的附加功能,例如节点故障检测、节点故障转移、自动负载均衡和性能。

PlanetCassandra 还提供了关于如何开始使用客户端驱动程序的清晰教程,如下面的截图所示:

客户端驱动程序

安全

安全是一个广泛且复杂的话题。从应用程序开发的角度来看,身份验证、授权和节点间加密是最基本的保障生产应用程序的安全措施。

身份验证

在 Cassandra 中,认证基于内部控制的登录用户名和密码。登录用户名和密码存储在system_auth.credentials表中。内部认证默认是禁用的。我们可以通过修改cassandra.yaml来配置 Cassandra 以启用它。我们还需要增加system_auth键空间的复制因子,因为system_auth键空间与其他键空间没有区别,也可能失败!

一旦启用了内部认证,我们可以使用超级用户账户和诸如CREATE USERALTER USERDROP USERLIST USERS之类的 CQL 语句来创建和管理用户认证。

授权

同样,Cassandra 提供了内部授权,与内部认证协同工作。它借鉴了传统数据库的GRANT/REVOKE范式来管理模式对象的权限。

默认情况下,system键空间中的表授予每个认证用户读取权限。对于用户创建的键空间及其内部对象,我们也可以使用 CQL 语句,即GRANTREVOKELIST PERMISSIONS来管理对象权限。

节点间加密

Cassandra 提供了节点间加密功能,使用安全套接字层SSL)保护集群中节点之间传输的数据,包括 gossip 通信。所有节点都必须在所有节点上拥有所有相关的 SSL 证书。加密可以应用于所有节点、数据中心或机架之间的流量。

我们必须在每个节点的cassandra.yaml文件中设置server_encryption_options,以启用节点间加密选项以及keystoretruststore文件的配置设置,如下面的截图所示:

节点间加密

备份和恢复

备份是在像 Cassandra 这样的大型分布式系统中一个有趣的话题。数据量可能非常庞大,节点数量也可能很大。对整个集群进行一致的备份可能非常棘手。

在我看来,与传统的数据库必须拥有的定期备份不同,Cassandra 中的备份是可选的。备份 Cassandra 集群的需求实际上取决于选择的部署策略。例如,如果集群的节点分布在像纽约、东京和伦敦这样的地理分散区域,并且复制因子设置为三或更高,那么明确地对集群中的数据进行外部备份可能是有益的。这个示例集群具有内置的弹性,每条数据都有多个副本作为其自身的备份。所有三个地理区域同时失败的可能性相当低。

当然,如果你需要遵守政策、法规等,你可能仍然需要定期备份集群。也许你希望系统有所谓的点时间恢复功能。在这些情况下,托管备份是必不可少的。然而,这肯定会复杂化整个系统架构。

总的来说,这是一个针对你实现的设计决策。

有用网站

这里有一些对我们获取最新 Cassandra 信息的有用网站。

Apache Cassandra 官方站点

官方 Cassandra 网站cassandra.apache.org/是获取任何信息的首选之地。最新的发布版本信息可以在其主页上找到。如果你想要深入了解 Cassandra 的内心,或者想要从源代码构建以重新安装 Cassandra 实例,你可能会在那里找到源代码。

就像 Apache 软件基金会下的其他项目一样,我们欢迎你为社区做出贡献。你还可以了解如何加入这个充满热情的开发者团队,以改进这样一个优秀的 NoSQL 数据库。

你还可以找到一个链接到另一个名为 PlanetCassandra 的网站,它值得单独介绍。

PlanetCassandra

PlanetCassandra,planetcassandra.org/,是由 DataStax(一家商业公司)支持的社区服务网站,它提供生产就绪的 Apache Cassandra 产品和服务:

PlanetCassandra

这个网站更多地处理 Cassandra 社区的协作方面。我们可以在那里寻找聚会、参与、网络研讨会、会议和活动,甚至还有教育培训课程。网站最有价值的部分是Apache Cassandra 使用案例,这是一个运行在 Apache Cassandra 上并从中获得实际益处的公司的仓库。

仓库根据多个维度进行分类,具体包括产品目录/播放列表推荐/个性化欺诈检测消息传递物联网/传感器数据未定义。每个仓库条目都有一个名称以及使用案例的公司简介,以及它们如何使用 Cassandra 来推动业务。通过学习这些使用案例,你当然可以学习和产生一些想法。

一定要阅读的案例研究是 Netflix。用例是一个个性化系统,它理解每个人的独特习惯和偏好,并揭示用户可能不知道且不寻找的产品和项目。挑战包括获取经济实惠的容量以存储和处理大量数据,解决 Oracle 遗留的关系型架构的单点故障问题,以及实现国际扩张的业务敏捷性。Netflix 使用了 Cassandra 的商业版本,该版本在多个数据中心提供 100%的可用性和成本效益的扩展。结果是惊人的,如下所示:

  • 首先,系统的吞吐量超过每秒 1000 万次交易

  • 其次,跨各个区域创建和管理新的数据集群几乎不需要费力

  • 最后,Cassandra 可以以最精细的细节捕捉客户查看和日志数据。

非常推荐您阅读此内容,尤其是对于那些考虑从关系型数据库迁移到 Cassandra 的您。

DataStax

本书使用的 Cassandra 版本是一个开源版本,可以在互联网上免费获取。对于大多数系统来说已经足够好了。然而,许多公司仍在寻找基于 Cassandra 的企业级产品以及相关的支持、培训和咨询服务。DataStax(www.datastax.com/)就是其中之一。

DataStax 致力于编制最全面的 Cassandra 文档,如下面的截图所示。该文档可在其网站上免费获取。它还开发和提供 Java、C#、Python 等客户端驱动程序的支持:

DataStax

DataStax 提供 Apache Cassandra 的企业版本,称为 DataStax Enterprise,它具有增强的功能,如高级安全和管理工作,这些功能简化了 Cassandra 集群的日常系统管理。

DataStax Enterprise 包括一个强大的企业级系统管理工具,OpsCenter,允许管理员通过仪表板轻松掌握系统的状态和性能。它监控集群并触发集群变化的警报或通知。备份和恢复操作也得到了极大的简化。

DataStax Enterprise 还将 Cassandra 扩展到支持 Apache Hadoop 和 Solr,作为一个集成的企业平台。

Hadoop 集成

与 Hadoop 集成的 Cassandra 可以成为大数据分析的有力平台。Cassandra 自 0.6 版本以来就能够直接与 Hadoop 集成。它始于对 MapReduce 的支持。从那时起,支持已经显著成熟,现在包括对 Pig 和 Hive 的原生支持。Cassandra 的 Hadoop 支持实现了与Hadoop 分布式文件系统HDFS)相同的接口,以实现输入数据局部性。

Cassandra 为 MapReduce 程序提供了ColumnFamilyInputFormatColumnFamilyOutputFormat类,以实现与 Hadoop 的直接集成。这涉及到数据直接从 MapReduce 的 mapper 中读取 Cassandra 列族,并且包括数据移动。

设置和配置涉及在 Cassandra 节点上叠加一个 Hadoop 集群,为 Hadoop JobTracker 配置一个单独的服务器,并在每个 Cassandra 节点上安装 Hadoop TaskTracker 和 DataNode。

注意

设置和配置程序

将 Cassandra 与 Hadoop 集成的详细步骤可以在以下位置找到:

Cassandra 数据中心中的节点可以从 HDFS DataNode 以及 Cassandra 中获取数据。JobTracker 从客户端应用程序接收 MapReduce 输入。然后它将 MapReduce 作业请求发送给 TaskTrackers 和可选客户端,例如 MapReduce 和 Pig。数据被写入 Cassandra,结果被发送回客户端。

DataStax 还创建了一种简单的方法来使用 Hadoop 与 Cassandra,并将其集成到企业版本中。

摘要

我们从第一章,Cassandra 概览开始,回顾 Cassandra 的基本知识。然后我们讨论了 Cassandra 数据建模的重要方面,例如查询技术建模、丰富的数据类型集和索引。这些技术和知识被整合到一个名为 Stock Screener Application 的股票交易领域的数据分析应用示例中。我们详细解释了应用的每一个细节,尽管速度很快。我们还展示了如何通过更改数据模型和编码来增强初版,以展示 Cassandra 提供的巨大灵活性。然后我们转向计划将增强的系统迁移到具有复制策略、snitch、复制因子、基本监控和性能调整工具的生产就绪集群。

我真的很享受为你写这本书,并且真诚地希望,当你在实际项目中快速使用 Cassandra 时,它对你来说像对我一样有用。你的评论总是受欢迎的,你可以通过 Packt Publishing 联系我。

正如温斯顿·丘吉尔爵士所说:

"现在这并不是结束。这甚至不是结束的开始。但也许,这是开始的结束。"

posted @ 2025-10-01 11:27  绝不原创的飞龙  阅读(4)  评论(0)    收藏  举报