Snowflake弹性数仓论文翻译(下)
在上半部分中,论文中主要介绍了Snowflake的整体、存储与计算结构,详情点击Snowflake弹性数仓论文翻译(上)。以下将继续Snowflake论文的后半部分。
四、亮点功能
Snowflake提供了关系型数仓所期望的许多特性:全面的SQL支持、ACID事务、标准接口、稳定性和安全性、用户支持,当然还有强大的性能和可扩展性。此外,它还引入了一些在相关系统中很少或没有的其他有价值的特性。本节介绍了其中一些我们认为有技术区别的特性。
4.1 Saas服务体验
正如大部分Saas服务一样,Snowflake可以通过浏览器直接与系统交互。一个WebUI是一个关键的区别。WebUI使得从任何位置和环境访问Snowflake都非常容易,同时由于云中已有大量数据,用户只需将Snowflake指向自己的数据并进行查询,而无需下载任何软件。
Snowflake的UI界面可以SQL操作,还可以访问数据库目录、用户和系统管理、监视、使用信息等。
4.2 持续可用性
在过去,数仓的解决方案一般是局域网内的隐藏较好的后端系统,与世界上大部分的网络隔离的。在这种环境中,计划内(软件升级或管理任务)和计划外(故障)的停机时间通常不会对操作产生很大影响。但是,随着数据分析对越来越多的业务任务变得至关重要,持续可用性成为数仓的一个重要需求。
Snowflake提供了满足这些期望的连续可用性。这方面的两个主要技术是故障恢复能力和在线升级能力。
4.2.1 故障恢复能力
Snowflake在体系结构的所有级别上都能容忍单个和相关的节点故障,如图2所示。如今,Snowflake的数据存储层是S3,它跨多个数据中心进行复制,在Amazon术语中称为“可用性区域”(availability zones)或AZs。跨AZs的复制允许S3处理完整的AZ故障,并保证99.99%的数据可用性和99.999999999%的持久性。与S3的体系结构相匹配,Snowflake的元数据存储也在多个AZ之间分布和复制。如果一个节点发生故障,其他节点可以在不影响终用户的情况下接管任务。云服务层的其余服务由多个AZ中的无状态节点组成,负载均衡器负责分发用户请求。因此,单个节点故障甚至是完全的AZ故障都不会对系统范围造成影响,可能是对当前连接到故障节点的用户的一些失败查询。这些用户将被重定向到另一个节点进行下一次查询。
相比之下,虚拟仓库(VM)并不分布在AZs中。这个选择是出于性能考虑。高吞吐是分布式查询执行的关键,而在同一个AZ中,网络吞吐量要高得多。如果其中一个工作节点在查询执行期间失败,则查询会失败,但会透明地重新执行,要么立即替换该节点,要么暂时减少节点数。为了加速节点更换,Snowflake维护了一个小的备用节点池。(这些节点还用于快速VW配置。)
如果全部的AZ变得不可用,那么在该AZ的给定VW上运行的所有查询都将失败,并且用户需要在不同的AZ中主动重新配置VW。由于全部的AZ故障是真正的灾难性和极为罕见的事件,我们今天接受这种部分系统不可用的情况,但希望在将来解决它。
4.2.2 在线升级能力
Snowflake不仅在发生故障时提供连续可用性,而且在软件升级期间也提供连续可用性。系统的设计允许同时部署服务的多个版本,包括云服务组件和虚拟仓库。这是因为所有服务实际上都是无状态的。所有的状态都保存在事务性KV存储服务中,并通过映射层进行访问,映射层负责元数据版本控制和模式演化。每当我们更改元数据模式时,我们都会确保与以前的版本向后兼容。
为了执行软件升级,Snowflake首先将新版本的服务与以前的版本一起部署。然后,用户帐户逐渐切换到新版本,这个时候,相应用户发出的所有新查询都被定向到新版本。同时,保证以前版本的所有查询都完成。一旦所有查询和用户在以前的版本没有查询后,以前版本的所有服务都将被终止和停用。
图3显示了正在进行的升级过程的快照。有两个版本的Snowflake运行并排,版本1(亮)和版本2(暗)。云服务有两个版本,控制两个虚拟仓库(vw),每个都有两个版本。负载平衡器将传入的请求定向到云服务的适当版本。一个版本的云服务只与一个匹配版本的通信。
如前所述,两个版本的云服务共享相同的元数据存储。此外,不同版本的vw能够共享相同的工作节点及其各自的缓存。因此,升级后不需要重新填充缓存。整个过程对用户是透明的,没有停机或性能下降。
在线升级也对我们的开发速度,以及如何处理Snowflake的关键bug产生了巨大的影响。在撰写本文时,我们每周升级一次所有服务。这意味着我们每周都会发布功能和改进。为了确保升级过程顺利进行,升级和降级都在一个特殊的预生产的Snowflake副本不断测试。在极少数情况下,如果我们在产品中发现了一个严重的bug(不一定是在升级过程中),我们可以很快地将其降级到以前的版本,或者实施一个修复程序并执行一个超出原计划的升级。这个过程并不像听起来那么可怕,因为我们不断地测试和使用升级/降级机制。这是高度自动化的和并且严格的。
4.3 半结构化和Schema-Less的数据
Snowflake将标准SQL类型系统扩展为三种半结构化数据类型:VARIANT、ARRAY和OBJECT。VARIANT类型的值可以存储本地SQL类型的任何值(DATE、VARCHAR等),也可以存储可变长度的数组,以及类似JavaScript的对象,字符串到VARIANT值的映射。后者在文献中也被称为documents,由此产生了文档存储的概念(MongoDB,Couchbase)。
数组和对象只是类型VARIANT的严格形式。内部表示是相同的:一个自描述的紧凑二进制序列化类型,它支持快速的键值查找,以及高效的类型测试、比较和hash。因此,VARIANT列可以像其他列一样用作join keys、gourping keys和ordering keys。
VARIANT类型允许Snowflake以ELT(Extract-Load-Transform)方式使用,而不是以传统的ETL(Extract-Transform-Load)方式使用。不需要指定数据的schema或在加载时进行转换。用户可以将JSON、Avro或XML格式的输入数据直接加载到变量列中;Snowflake处理解析和类型推断(参见第4.3.3节)。这种方法在文献中被恰当地称为“schema later”,它允许通过将信息生产者与信息消费者和任何中介分离来解决schema问题。相反,传统ETL管道中数据schema的任何更改都需要组织中多个部门之间的协调,这可能需要数月的时间来执行。
ELT和Snowflake的另一个优点是,以后如果需要转换,可以使用并行SQL数据库查询功能来执行转换,包括连接、排序、聚合、复杂谓词等操作,这些操作在传统ETL工具链中通常缺失或效率低下。在这一点上,Snowflake还具有自定义函数(udf),支持完整的JavaScript语法,并与VARIANT数据类型集成。对udf的支持进一步增加了可以在Snowflake中执行的ETL任务的数量。
4.3.1 后关系执行
对数据重要的操作是提取数据元素,可以按字段名(对于对象)提取,也可以按偏移量(对于数组)提取。Snowflake提供了函数式SQL和类JavaScript的路径标识来支持。内部编码也使得提取非常高效。子元素只是父元素中的指针;不需要复制。提取之后通常会将结果变量值转换为标准SQL类型。同样,编码也使强制转换非常有效。
第二种常见操作是展平数据,即将嵌套结构旋转到多行中。Snowflake使用SQL横向视图来表示展开操作。这种扁平化可以是递归的,允许将文档的层次结构完全转换为一个适合SQL处理的关系表。与展平相反的操作是聚合。Snowflake为此引入了一些新的聚合和分析函数,如ARRAY_ AGG和OBJECT_AGG。
4.3.2 列存及处理
将半结构化数据序列化(二进制)是将半结构化数据集成到关系数据库中的常规选择。不好的方面是,行存储的处理效率一般低于列存储,所以列式关系数据一般会将半结构化数据转换为关系数据。
Cloudera Impala[21](使用Parquet[10])和Google Dremel[34]已经证明,半结构化数据的列式存储是可能的,也是有益的。然而,Impala和Dremel(及其外部化BigQuery[44])要求用户为列式存储提供完整的table schema。为了实现schema-less序列化仍然保持灵活性和列式关系数据库的性能,Snowflake引入了一种新的自动类型推断和列式存储方法。
如第3.1节所述,Snowflake以混合列格式存储数据。在存储半结构化数据时,系统会自动对单个表文件中的文档集合执行统计分析,以执行自动类型推断,并确定哪些(类型化的)类型是常见的。然后从文档中移除相应的列单独存储,单独存储的列使用与本地关系数据相同的列压缩方式。对于这些列,Snowflake甚至会计算物化聚合,以便通过修剪(参见第3.3.3节)使用,就像普通关系数据一样。
在扫描过程中,不同的列可以重新组合成一个VARIANT的列。然而,大多数查询只对原始文档的一部分列感兴趣。在这些情况下,Snowflake会将映射和强制转换表达式下推到scan中,只访问必要的列并将其直接强制转换到目标SQL类型中。
上面描述的优化对于每个表文件都是独立执行的,这使得即使在schema变化的情况下也可以有效地存储和提取。然而,它确实给查询优化带来了挑战,特别是剪枝。假设一个查询在路径表达式上有一个谓词,我们希望使用修剪来限制要扫描的文件集。路径和相应的列可能出现在大多数文件中,但频率仅足以保证某些文件中的元数据。保守的解决方案是简单地扫描没有合适元数据的所有文件。Snowflake通过计算所有路径上的Bloom过滤器(而不是值)来改进此解决方案存在于文件中。这些Bloom过滤器与其他文件元数据一起保存,并在修剪期间由查询优化器进行探测。可以安全地跳过不包含给定查询所需路径的表文件。
4.3.3 乐观转换
由于一些本地SQL类型(尤其是日期/时间值),比如常用的格式(如JSON或XML)中是字符串,因此需要在写入时(插入或更新期间)或读取时(查询期间)将这些值从字符串转换为其实际类型。如果没有schema的提示,这些字符串转换需要在读取时执行,而在以读取为主的查询中,这比在写入期间执行一次转换效率要低。没有类型的数据的另一个问题是缺少合适的元数据进行优化,比如对于日期比较重要。(分析工作通常在日期列上有范围查询。)
但在写入时,自动转换可能会丢失信息。例如,一些字段定义成数字实际上可能不是数字,比如前面填充0的字符串。又比如,看起来像是日期实际上可能是文本内容。Snowflake通过执行乐观数据转换来解决这个问题,并在单独的列中保存转换结果和原始字符串(除非是完全可逆的不保存原始字符串)。如果查询之后需要原始的字符串,则可以轻松地检索或重构。因为有不加载和不访问未使用的列,所以双存储对查询性能的影响是小的。
4.3.4 性能表现
为了评估列式存储、乐观转换和半结构化数据剪枝对查询性能的综合影响,我们使用TPC-H-like数据集和查询进行了测试。
我们创建了两种类型的数据库模式。首先,一个传统的关系型TPC-H模式。第二种是“schema-less”数据库模式,其中每个表都由一列VARIANT类型组成。然后,我们生成聚集(排序)的SF100和SF1000数据集(分别为100GB和1TB),以纯JSON格式存储数据集(即日期变成字符串),并使用关系数据库模式和schema-less数据库模式将数据加载到Snowflake中。我们没有将schema-less数据的字段、类型提供给系统,也没有进行调优。然后,我们在schema-less数据库定义了一些视图,以便能够对所有四个数据库运行完全相同的TPC-H查询集。(在撰写本文时,Snowflake不使用视图进行类型推断或其他优化。)
后,我们对这四个数据库运行了所有22个TPC-H查询,使用了一个中等标准的仓库。图4显示了结果。测试结果是通过三次热缓存运行的结果。标准误差是很少,因此省略了。
可以看出,除了两个查询(SF1000上的Q9和Q17)之外,其他所有查询的schema-less存储和查询处理的开销都在10%左右。对于这两个查询,我们确定了慢的原因是join顺序,这是由distinct的已知错误造成的。我们继续改进半结构化数据的元数据收集和查询优化。
总之,对于具有相对稳定和简单模式的半结构化数据(即在实践中发现的大多数机器生成的数据),其查询性能几乎与传统关系数据查询的性能相当,可以直接使用列存储、列执行和修剪的优势,而无需用户优化。
4.4 时间旅行和复制
在第3.3.2节中,我们讨论了Snowflake如何在多版本并发控制(MVCC)之上实现快照隔离(SI)。对表的写入操作(插入、更新、删除、合并)通过添加和删除整个文件来生成表的更新版本。
当文件被新版本删除时,它们将保留一段可配置的时间(当前长为90天)。文件保留允许Snowflake非常高效地读取表的早期版本;也就是说,在数据库上执行时间旅行。用户可以使用方便的AT或BEFORE语法从SQL访问此功能。时间戳可以是时间,相对时间,或者是相对之前查询语句中的时间(由ID引用)。
Select from my_table at (TIMESTAMP =>’Mon, 01 May 2015 16:20:00 -0700’::timestamp);
Select * from my_table at (OFFSET => -60*5); -- 5 min ago
Select * from my_table before (STATEMENT =>’8e5d0ca9-005e-44e6-b858-a8f5b37c5726’);
相关文章