Cassandra3 物化视图技术解密

简介: 在这篇博文中,我们将深入研究Cassandra 3.0的全新物化视图功能。我们将看到它是如何在内部实现的,您应该如何使用它来充分利用其性能以及需要避免哪些警告。本文中Cassandra == Apache Cassandra™, 物化视图是Materialized Views译文为什么是物化视图?Cassandra数据模型的关键点之一是非规范化,即复制数据以便更快地访问。

在这篇博文中,我们将深入研究Cassandra 3.0的全新物化视图功能。我们将看到它是如何在内部实现的,您应该如何使用它来充分利用其性能以及需要避免哪些警告。

本文中Cassandra == Apache Cassandra™, 物化视图是Materialized Views译文

为什么是物化视图?

Cassandra数据模型的关键点之一是非规范化,即复制数据以便更快地访问。以牺牲磁盘空间以获取更低读取延迟。

如果您的数据本质上是不可变的(如时间序列数据/传感器数据),那么您可以轻松上手,非常迷人。

但是,需要非规范化的可变数据总是痛点。一般来说,人们最终采取以下策略:

  • 对不可变数据进行非规范化处理
  • 对于可变数据,要么:

    • 接受将它们标准化,接受额外的读开销,但不关心变动
    • 非规范化处理,但接受先读后写,再处理更新这些开销
  • 因为非规范化大多数时候用于不同的读取模式,你可以依靠第三方索引解决方案(如Datastax Enterprise SearchStratio Lucene-based secondary index SASI二级索引)作业
    对于经常变化的数据两种解决方案都不理想,因为它会给开发人员带来很多开销(在客户端额外读取或同步更新数据)

物化视图的目的是为了减轻开发者的痛苦,但它不会奇迹般地解决了非规范化的所有开销。

物化视图创建语法

以下是创建物化视图的语法:

CREATE MATERIALIZED VIEW [IF NOT EXISTS] keyspace_name.view_name AS
SELECT column1, column2, ... 
FROM keyspace_name.base_table_name 
WHERE column1 IS NOT NULL AND column2 IS NOT NULL ... 
PRIMARY KEY(column1, column2, ...)

在第一个视图中,很明显物化视图需要一个基表。物化视图,从概念上讲,仅仅是另一种方式来呈现基表数据,具有不同的主键,不同的访问模式。

细心的读者应该注意到where子句 column1 IS NOT NULL AND column2 IS NOT NULL ...。当然,此子句保证将用作视图主键的所有列都不为null。

创建物化视图的一些约束项

  • AS SELECT column1, column2, … 子句让你选择其中的列基表要复制到视图中。基表主键列会被包含在内的。
  • WHERE column1 IS NOT NULL AND column2 IS NOT NULL … 自居保证了视图的主键列没有空列
  • PRIMARY KEY(column1, column2, …) 子句中应该包含基表的所有主键列,再加上至多有一个列,它不能是基本表的主键的一部分,这些主键列顺序并不重要,可自行抉择
    一例胜千言
CREATE TABLE user(
   id int PRIMARY KEY,
   login text,
   firstname text,
   lastname text,
   country text,
   gender int
);
 
CREATE MATERIALIZED VIEW user_by_country 
AS SELECT *  //denormalize ALL columns
FROM user
WHERE country IS NOT NULL AND id IS NOT NULL
PRIMARY KEY(country, id);
 
INSERT INTO user(id,login,firstname,lastname,country) VALUES(1, 'jdoe', 'John', 'DOE', 'US');
INSERT INTO user(id,login,firstname,lastname,country) VALUES(2, 'hsue', 'Helen', 'SUE', 'US');
INSERT INTO user(id,login,firstname,lastname,country) VALUES(3, 'rsmith', 'Richard', 'SMITH', 'UK');
INSERT INTO user(id,login,firstname,lastname,country) VALUES(4, 'doanduyhai', 'DuyHai', 'DOAN', 'FR');
 
SELECT * FROM user_by_country;
 
 country | id | firstname | lastname | login
---------+----+-----------+----------+------------
      FR |  4 |    DuyHai |     DOAN | doanduyhai
      US |  1 |      John |      DOE |       jdoe
      US |  2 |     Helen |      SUE |       hsue
      UK |  3 |   Richard |    SMITH |     rsmith
 
SELECT * FROM user_by_country WHERE country='US';
 
 country | id | firstname | lastname | login
---------+----+-----------+----------+-------
      US |  1 |      John |      DOE |  jdoe
      US |  2 |     Helen |      SUE |  hsue

在上面的例子中,我们希望按国家代码查找用户,因此加入the WHERE country IS NOT NULL子句。我们还需要包含原始表的主键(AND id IS NOT NULL)

视图的主键由国家/地区作为分区键组成。由于同一个国家/地区可能有许多用户,因此我们需要将用户ID添加为clustering列以区分它们。

WHERE xxx IS NOT NULL子句的基本原理是保证基表中的空值不会被非规范化为视图。例如,未设置其国家/地区的用户将不会被复制到视图中,主要是因为SELECT * FROM user_by_country WHERE country = null没有意义,因为country是主键的一部分。此外,将来,您可以使用除IS NOT NULL之外的其他子句,主要使用用户定义函数来过滤要非规范化的数据。

约束的基本原理(基表的所有主键列,加上最多一个不属于基表主键的列)是为了避免主键的空值。

例:

CREATE MATERIALIZED VIEW user_by_country_and_gender 
AS SELECT *  //denormalize ALL columns
FROM user
WHERE country IS NOT NULL AND gender IS NOT NULL AND id IS NOT NULL
PRIMARY KEY((country, gender),id)
 
INSERT INTO user(id,login,firstname,lastname,country,gender) VALUES(100,'nowhere','Ian','NOWHERE',null,1);
INSERT INTO user(id,login,firstname,lastname,country,gender) VALUES(100,'nosex','Jean','NOSEX','USA',null);

在上面的示例中,用户' NOWHERE '和' nosex '无法非规范化到视图中,因为作为视图主键一部分列是null。

技术实现

A 物化视图更新步骤

以下是在基表中插入/更新/删除数据时的操作顺序

  1. 如果设置了系统属性cassandra.mv_enable_coordinator_batchlog,则协调器将创建批处理日志
    image
  1. 协调者发送修改至所有副本,并等待一致性级别要求的ack数
    image
  1. 每个副本正在获取要在基表中插入/更新/删除的分区上的本地锁
    image
  1. 每个副本都在基表的分区上执行本地读取
    image
  1. 每个副本使用以下语句创建本地批处理日志:

    • DELETE FROM user_by_country WHERE country ='old_value'
    • INSERT INTO user_by_country(country,id,...)VALUES('FR',1,...)
      image
  2. 每个副本都异步执行批处理日志。对于批处理日志中的每个语句,它使用CL = ONE针对配对的视图副本(稍后解释)执行
    image
  1. 每个副本在本地执行基表的修改(mutation)
    image
  2. 每个副本都释放基表分区上的本地锁
    image
  3. 如果本地修改成功,则每个副本都会向协调器发送ack
    image
  4. 如果协调器接收到与一致性级别要求一样多的ack,则客户端确认该修改(mutation)是成功的
    image
  1. 可选的,如果设置了系统属性cassandra.mv_enable_coordinator_batchlog,并且如果QUORUM确认的是由所接收的协调,协调者的batchlog会被删掉
    image

B 配对视图副本定义

在详细解释一些技术步骤的基本原理之前,让我们定义什么是配对视图副本。以下是源代码中的正式定义:

The view natural endpoint is the endpoint which has the same cardinality as this node in the replication factor.

The cardinality is the number at which this node would store a piece of data, given the change in replication factor.

如果keyspace 的复制策略是NetworkTopologyStrategy,我们在计算基数时过滤环以仅包含本地数据中心中的节点。基数是副本索引位置的意思

例如,如果我们有以下ring环:

  • A,T1 - > B,T2 - > C,T3 - > A.
    对于令牌T1,在RF = 1时,将包括A,因此T1的A的基数为1.

对于令牌T1,在RF = 2时,将包括B,因此T1的B的基数为2.
对于令牌T3,在RF = 2时,将包括A,因此T3的A的基数为2。

对于基表令牌为T1且视图标记为T3的视图,节点之间的配对将为:

  • A写入C(对于T1,A的基数为1,对于T3,C的基数为1)
  • B写入A(对于T1,B的基数为2,对于T3,A的基数为2)
  • C写入B(对于T1,C的基数为3,对于T3,B的基数为3)

C本地锁定基表分区

读者应该想知道为什么每个副本都需要在基表分区上获取本地锁,因为锁定很昂贵。此锁定的原因是为了保证在基表分区上进行并发更新时的视图更新一致性。

假设我们在用户(id = 1)上有2个并发更新,其原始国家/地区为英国:

  1. UPDATE ... SET country ='US'WHER id = 1
  2. UPDATE ... SET country ='FR'WHER id = 1
    如果没有本地锁定,我们的视图将发生穿插修改

image

用户(id = 1)现在在视图表中有2个条目(country =' US '和country =' FR '),这是不对的。

使用本地锁定修复此问题

image

实际上,操作的顺序1)读取基表数据2)删除视图旧分区3)插入视图新分区必须要原子执行,因此需要锁定

D 本地batchlog用于视图异步更新

在每个副本上创建的用于视图更新的本地batchlog可确保即使出现故障(例如,因为视图副本暂时关闭),最终也会提交视图更新。

使用一致性级别ONE是因为每个基表副本负责更新其配对的视图副本,因此一致性级别ONE就足够了。

此外,每个配对视图副本的更新是异步执行的,例如,副本在处理到基表更新之前不会阻塞并等待确认。本地batchlog保证在出现错误时自动重试。

E 视图数据一致性级别

客户端在基表上请求的一致性级别得到遵守,例如,如果需要QUORUM(RF = 3),协调器只有在从基表副本收到2个ack时才会确认成功写入。在这种情况下,客户端确保基表更新在3副本中至少2个副本上进行了持久化

视图表的一致性保证较弱。与上面的例子中,我们只有该视图将被更新,保证最终上3副本中至少2副本以上进行持久化

与基表相比,一致性保证的主要区别在于最终一致(异步本地批处理日志,非强一致)。在协调器接收基表副本2个ACK的时候,我们都不能肯定 view表已经更新上至少2个副本。

F 协调者批处理日志

系统属性cassandra.mv_enable_coordinator_batchlog仅对某些边界情况有帮助。并不能防范所有边界case,同时花费高额代价,协调者batchlog一般情况下没意义,参数cassandra.mv_enable_coordinator_batchlog被默认禁用。

性能考虑

与正常突变相比,具有物化视图的基表上的mutation将产生以下额外成本:

  • 基表分区上的本地锁定
  • 基表分区上的本地read-before-write
  • 物化视图的本地批处理日志
  • 可选地,协调器批处理日志
    实际上,大多数性能热点都是由本地先读后写入引起的,但这个只开销一次,并不取决于与表关联的视图个数。

但是,增加视图数量会对群集范围的写入吞吐量产生影响,因为对于每个基表更新,您将向群集添加额外的(DELETE + INSERT)* nb_of_views负载。

话虽如此,比较普通表和具有视图的表之间的原始写吞吐量是没有意义的。比较手动非规范化表(使用已记录的批处理客户端)和使用物化视图的同一表之间的写入吞吐量更为明智。在这种情况下,具有物化视图的自动服务器端方案明显胜出,因为:

  1. 它为先前读写节省了网络流量
  2. 它为已记录的非规范化表mutation批量节省了网络流量
  3. 它消除了开发人员不得不保持与基表同步的非规范化表的痛苦
    值得一提的另一个性能考虑因素是热点。与手动非规范化方案类似,如果您的视图分区键选择不当,您最终会在群集中出现热点。我们的用户表的一个简单示例是创建物化视图user_by_gender
// THIS IS AN ANTI-PATTERN !!!!
CREATE MATERIALIZED VIEW user_by_gender
AS SELECT * FROM user
WHERE id IS NOT NULL AND gender IS NOT NULL
PRIMARY KEY(gender, id) 

根据上面的观点,所有用户最终只会有2个分区:男性和女性。您当然不希望群集中存在此类热点。

现在,物化视图和二级索引相比,读性能如何?

根据二级索引的实现,读取性能可能会有所不同。如果执行scatter-gather操作,则读取性能将与数据中心/集群中的节点数密切相关。

注:二级索引会发起全集群节点查询。

读取操作也总是由两个不同的读取路径组成:

  • 读取磁盘上的索引以查找相关的主键
  • 从C *中读取源数据
    话虽这么说,很明显物化视图会给你更好的读取性能,因为读取是直接的,只需一步即可完成。我们的想法是您在写入时支付开销以获得读取时的增益。

实际上,在查询方面,物化视图与高级二级索引实现相比松散,因为只允许精确匹配,远程扫描(给我的用户在“UK”和“US”之间的国家/地区)将破坏您的读取性能。

物化视图和操作

在本章中,我们将讨论在操作方面实现物化视图的影响。

Repair & hints

  • 可以独立于其基表修复视图
  • 如果基表被修复,由于基于mutation的修复(通过写入路径进行修复,与正常修复不同),视图也将被修复
  • 对视图的读取修复行为与正常的读取修复相似
  • 基表上的读取修复也将修复视图
  • 基表上的提示重放将触发相关视图的更新

schema:

  • 物化视图可以修改为任何标准表(压缩,压缩,......)。使用ALTER MATERIALIZED VIEW命令
  • 您不能从实例化视图使用的基表中删除列,即使此列不是视图主键的一部分
  • 您可以向基表添加新列,其初始值将在关联视图中设置为null
  • 你不能删除基表,你必须先删除所有相关的视图

The shadowable view tombstone

对于那些只想了解深层内部的人来说,这部分纯粹是技术性的。你可以放心地跳过它

在物化视图的开发过程中,一些问题出现在墓碑和查看时间戳上。我们来看这个例子:

CREATE TABLE base (a int, b int, c int, PRIMARY KEY (a));
 
CREATE MATERIALIZED VIEW view AS
    SELECT * FROM base
    WHERE a IS NOT NULL
    AND b IS NOT NULL
    PRIMARY KEY (a, b);
 
//Insert initial data
INSERT INTO base (a, b, c) VALUES (0, 0, 1) USING TIMESTAMP 0;
 
//1st update
UPDATE base SET b = 1 USING TIMESTAMP 2 WHERE a = 0;
 
//2nd update
UPDATE base SET b = 0 USING TIMESTAMP 3 WHERE a = 0;

ts是时间戳的简写

在初始数据插入时,视图将包含此行:pk = (0,0), row_ts=0, c=1@ts0

在1st更新,视图状态是:

  • pk=(0,0), row@ts0, row_tombstone@ts2, c=1@ts0 (DELETE FROM view WHERE a=0 AND b=0)
  • pk=(0,1), row@ts2, c=1@ts0 (INSERT INTO view … USING TIMESTAMP 2)
    行(0,0)在逻辑上不再存在,因为行逻辑删除时间戳>行时间戳,到目前为止一直很好。在第二次更新时,视图状态为:
  • pk=(0,0), row@ts3, row_tombstone@ts2, c=1@ts0 (INSERT INTO view …)
  • pk=(0,1), row@ts2, row_tombstone@ts3, c=1@ts0 (DELETE FROM view WHERE a=0 AND b=1)
    由于我们将b重新设置为0,因此再次重新插入视图行(0,0),但每列的时间戳不同。((a,b) = (0,0)@ts3 but c=1@ts0 因为没有修改列c。

问题是,现在,如果你读取视图分区(0,0),列c值将被旧行墓碑@ts2遮蔽,所以 SELECT * FROM view WHERE a=0 AND b=0将返回(0, 0,null)这是错误的......

一个天真的解决方案是在第二次更新后将列c时间戳升级到3,例如pk=(0,0), row@ts3, row_tombstone@ts2, c=1@ts3

但是,如果以后有另一个UPDATE,UPDATE base SET c=2 USING TIMESTAMP 1 WHERE a=0 AND b=0 该如何做?如果我们遵循先前的规则,我们将在视图中为列c设置时间戳为1,它将被先前的值覆盖( (c=1@ts3)…

开发团队提出了一个解决方案:shadowable tombstone!有关详细信息,请参阅CASSANDRA-10261

源代码注释中可隐式墓碑(shadowable tombstone)的正式定义是:

如果行时间戳(primaryKeyLivenessInfo().timestamp())低于删除时间戳,则仅可存在shadowable row tombstone。也就是说,如果一行具有带时间戳A的可阴影墓碑,并且对具有时间戳B的那一行进行更新,使得B> A,那么该shadowable tombstone将被该更新“遮蔽”。目前,shadowable row deletions的唯一用途是物化视图,请参阅CASSANDRA-10261。

有了这个实现,在1st更新,视图状态是:

  • pk=(0,0), row@ts0, shadowable_tombstone@ts2, c=1@ts0 (DELETE FROM view WHERE a=0 AND b=0)
  • pk=(0,1), row@ts2, c=1@ts0 (INSERT INTO view … USING TIMESTAMP 2)
    在第二次更新时,视图状态变为:
  • pk=(0,0), row@ts3, shadowable_tombstone@ts2, c=1@ts0 (INSERT INTO view …)
  • pk=(0,1), row@ts2, shadowable_tombstone@ts3, c=1@ts0 (DELETE FROM view WHERE a=0 AND b=1)

现在,当读取视图分区(0,0)时,由于可隐藏的逻辑删除 (ts2)被新的行时间戳(ts3)遮蔽,因此即使其时间戳(ts0)比可隐式墓碑时间戳(ts2)低,也会读出列c值

简而言之:

  • 如果可阴影的墓碑时间戳>行时间戳,则可阴影的墓碑表现得像普通的墓碑
  • 如果可阴影的墓碑时间戳<行时间戳,请忽略此可隐藏的逻辑删除(就像它不存在一样)

译至原文
微信群和钉钉群交流
为了营造一个开放的 Cassandra 技术交流,我们建立了微信群和钉钉群,为广大用户提供专业的技术分享及问答,定期在国内开展线下技术沙龙,专家技术直播,欢迎大家加入。

微信群:
image

钉钉群
image

钉钉群入群链接:https://c.tb.cn/F3.ZRTY0o

相关实践学习
深入解析Docker容器化技术
Docker是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。Docker是世界领先的软件容器平台。开发人员利用Docker可以消除协作编码时“在我的机器上可正常工作”的问题。运维人员利用Docker可以在隔离容器中并行运行和管理应用,获得更好的计算密度。企业利用Docker可以构建敏捷的软件交付管道,以更快的速度、更高的安全性和可靠的信誉为Linux和Windows Server应用发布新功能。 在本套课程中,我们将全面的讲解Docker技术栈,从环境安装到容器、镜像操作以及生产环境如何部署开发的微服务应用。本课程由黑马程序员提供。 &nbsp; &nbsp; 相关的阿里云产品:容器服务 ACK 容器服务 Kubernetes 版(简称 ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情: https://www.aliyun.com/product/kubernetes
相关文章
|
SQL 分布式计算 Java
数据治理之元数据管理的利器——Atlas入门宝典(二)
随着数字化转型的工作推进,数据治理的工作已经被越来越多的公司提上了日程。作为Hadoop生态最紧密的元数据管理与发现工具,Atlas在其中扮演着重要的位置。但是其官方文档不是很丰富,也不够详细。所以整理了这份文档供大家学习使用。
3603 1
数据治理之元数据管理的利器——Atlas入门宝典(二)
|
11月前
|
存储 人工智能 关系型数据库
4年10亿美金,Neon用Serverless PG证明:AI需要的不是“大”,而是“隐形”
AnalyticDB PostgreSQL 版基于Neon架构隆重推出满足 AI 时代应用开发需求的Serverless版本,并且在这之上搭载了结构化分析、向量检索、BM25全文检索和图检索,通过一套引擎满足 AI 应用丰富的数据诉求,支持MCP和OpenAI协议,为企业全面拥抱 AI 配备了数据存储、分析和应用的 “关键” 能力,帮助企业火箭式启动跑赢时代。
|
12月前
|
存储 关系型数据库 MySQL
【赵渝强老师】TiDB的体系架构
TiDB是由PingCAP公司自主研发的开源分布式关系型数据库,支持HTAP(混合事务分析处理),具备弹性扩缩容、金融级高可用、实时分析等特性,兼容MySQL协议。其架构分为存储集群(行存TiKV与列存TiFlash)、调度集群(PD实例)和计算集群(TiDB实例)。相比传统单机数据库,TiDB优势显著:纯分布式设计、高扩展性、自动故障恢复、ACID事务支持及丰富的工具生态,适用于高可用与强一致要求的场景。
430 10
|
运维 监控 Python
自动化运维:使用Python脚本实现日常任务
【9月更文挑战第24天】在现代的软件开发周期中,运维工作扮演着至关重要的角色。本文将介绍如何利用Python编写简单的自动化脚本,来优化和简化日常的运维任务。从备份数据到系统监控,Python的易用性和强大的库支持使其成为自动化运维的首选工具。跟随这篇文章,你将学习如何使用Python编写自己的自动化脚本,提高运维效率,减少人为错误,并最终提升整个开发流程的质量。
|
存储 NoSQL 算法
使用图数据库进行复杂数据建模:探索数据关系的无限可能
【8月更文挑战第17天】图数据库以其高效的关系查询能力、直观的数据表示方式、灵活的数据模型和强大的可扩展性,在复杂数据建模和查询中展现出了巨大的潜力。随着大数据和人工智能技术的不断发展,图数据库的应用领域也将不断拓展和深化。对于需要处理复杂关系网络和数据关联性的场景来说,图数据库无疑是一个值得深入研究和应用的强大工具。
|
存储 SQL Prometheus
【TiDB原理与实战详解】1、原理与基础优化~学不会? 不存在的!
TiDB 是一款开源的分布式关系型数据库,具备水平扩展、高可用性和强一致性等特点,适用于高并发、低延迟的大规模数据处理场景。其架构设计灵感源自 Google 的 Spanner 和 F1,并兼容 MySQL。TiDB 集群由 TiDB Server(无状态 SQL 层)、PD(元数据管理模块)和 TiKV Server(分布式存储层)组成,还包含 TiFlash(列存储引擎)以加速分析型查询。TiDB 支持分布式事务和多种事务模式,适用于 OLTP 和 HTAP 场景,如电商平台和金融系统。此外,TiDB 的部署要求包括高性能硬件配置和特定网络设置,以确保系统的稳定性和高效运行。
|
缓存 运维 监控
Cassandra 性能压测及调优实战
掌握Cassandra分布式数据库性能压测及性能调优 作者:孤池
4625 1
Cassandra 性能压测及调优实战
|
存储 NoSQL 大数据
大数据存储:HBase与Cassandra的对比
【7月更文挑战第16天】HBase和Cassandra作为两种流行的分布式NoSQL数据库,在数据模型、一致性模型、数据分布、查询语言和性能等方面各有千秋。HBase适用于需要强一致性和与Hadoop生态系统集成的场景,如大规模数据处理和分析。而Cassandra则更适合需要高可用性和灵活查询能力的场景,如分布式计算、云计算和大数据应用等。在实际应用中,选择哪种数据库取决于具体的需求和场景。希望本文的对比分析能够帮助读者更好地理解这两种数据库,并做出明智的选择。
1287 1
|
存储 SQL 分布式计算
TiDB整体架构概览:构建高效分布式数据库的关键设计
【2月更文挑战第26天】本文旨在全面概述TiDB的整体架构,深入剖析其关键组件和功能,从而帮助读者理解TiDB如何构建高效、稳定的分布式数据库。我们将探讨TiDB的计算层、存储层以及其他核心组件,并解释这些组件是如何协同工作以实现卓越的性能和扩展性的。通过本文,读者将能够深入了解TiDB的整体架构,为后续的学习和实践奠定坚实基础。
|
存储 Java Linux
Netty ByteBuf 的零拷贝(Zero Copy)详解
Netty ByteBuf 的零拷贝(Zero Copy)详解
493 0