最新《F1 Query:大规模数据的声明式查询》读后感(1),大数据开发详解

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化资料的朋友,可以戳这里获取

注意这里的“所有”这个词, 太霸气了,初听起来有点反智,因为常识告诉我们没有什么东西是全能的,那么我们就来仔细看看 F1 Query 到底是怎么实现“全能”的。这里说的“全能” 在企业级数据处理领域的主要对应三大类需求:

  • 支持对小规模的 OLTP 式的数据进行高效查询。

  • 支持低延迟地对大批量的(异构)数据进行快速即席查询。

  • 支持对超大规模数据进行可靠的 ETL 处理。

可以看出 Presto 的能力只涵盖其中的第二项,第一项和第三项都是 Presto 所没有的。

设计初衷


F1 Query 之所以被设计出来是因为 Google 内部一些业务需求驱动。

首先数据一定是碎片化的。即使是单个应用也是如此: 一部分数据可能保存在关系型数据库里面,一部分存在 KV Store 里面,还有些可能以日志的形式保存在文件系统里面,那么一个统一的总体数据视图就十分必要了。

设计一定要充分考虑现代数据中心的架构。在这一点上,F1 Query 主要想强调的是,它设计的视野不是某一台机器,或者某几台紧密关联的机器,而是跨数据中心的机器集群。传统的设计方法都是把计算跟存储尽量绑定在一起的,这种架构在数据量不大的时候是很好的选择,但是当发展到如今这种超大数据规模的时代,这种架构已经不是最优的了;而且 Google 机房内带宽很高,要访问的数据到底在计算节点本地还是在远端几乎没有太大的区别,而且数据在分布式文件系统上多副本保存反而可以让我们以更大的并行度去访问,得到更好的查询性能。

这里其实主要就是在说我们也经常说的计算与存储分离啦。

可伸缩性:  客户的需求各种各样,从只影响一两条数据的 OLTP 类需求,到大规模、超大规模的数据处理,不应该随着数据量、请求延迟性的变化而要用完全不同的数据处理引擎来处理,这里面有很大的迁移成本。

可扩展性:用户的需求千奇百怪,用户可能需要支持新的存储格式、存储系统、嵌入新的业务的逻辑等等,一个理想的系统要支持这些扩展性。

其实这些需求都不是 Google 特有的,任何一个大公司甚至任何一个公司在这个大数据的时代都需要这些数据处理的能力,那么我们一起来看看 F1 Query是怎么做到如上的这些特点的。

整体架构


整体架构

整个架构从纵向来看分为三层: F1 Client, F1集群以及各种异构的数据源。而F1集群内部主要的角色主要是5个。

首先是 F1 Master ,  它负责对所有的查询进行监控并且管理所有 F1 Worker 。然后是 F1 Server ,F1 Server 在角色上有点像我们 Data Lake Analytics 的 FrontNode 的角色, 在请求真正执行之前做一些执行计划编译、优化的工作,是整个系统的“前端”,而真正的数据处理是由 F1 Worker 来完成的。

Catalog Service 扮演的元数据中心的角色,各种异构数据的元信息都保存在这个服务里面,也就形成了一个全局的统一视图 – 不管你数据是保存在什么介质里面。(我们 Data Lake Analytics 和 AWS的 Athena Glue都有类似的服务)。

Batch Metadata 保存的是 Batch Execution 模式下任务的一些元信息,比如执行计划之类的。

UDF Server 是 Google 比较创新的一个概念,它是一个 UDF 的仓库,而且是在执行引擎之外的,执行引擎通过 RPC 与 UDF Server 进行交互。

由于整体架构上存储和计算的分离,F1 ServerF1 Worker 都是无状态的,当需要水平扩展的时候,只需要向集群里面加入新的机器就好了,数据层面不需要做任何重新分布的工作。

查询的执行

因为 F1 Query 强调的是跨机房部署,因此查询的请求跟实际的数据很可能不在一个集群里面,当请求到达一台 F1 Server的时候,它首先对查询进行解析,看看查询里面涉及哪些数据源,如果有任何数据源不在这个数据中心里面, 它会看看哪些 F1 Server 离这些数据更近,然后返回一个 F1 Server 的列表给客户端,客户端接到之后,把这个请求重新发给这些新的 F1 Server 进行查询。F1 Query 强调虽然把计算和存储分离了,并且借助高效的网络设置,已经解决了很多数据本地化的问题,但是数据还是离计算越近,性能越好。

一个查询过来之后,首先在接到请求的 F1 Server 上进行编译和优化,然后把这个优化好的执行计划推到执行层,而执行的时候根据客户端指定的模式偏好来选择到底用何种模式来执行。

数据源

F1 ServerF1 Worker 不止可以访问本数据中心的数据,还可以跨数据中心访问数据。F1 Query 同时也像 Presto 一样,可以支持对各种异构数据源的查询。而且跟 Presto 一样,F1 Query 把所有的数据源都抽象成一个关系型的表(因为最终使用的查询语言是SQL嘛),因此隐藏掉了数据源本身的实现细节。不同的数据源之间可以进行关联的JOIN查询,同时借助前面提到的 Catalog Service 来统一管理这些异构数据源的元数据。整个就是一个企业级的大数据库啊,可以看到整个企业里面的所有数据。

除了查询 Catalog Service 管理的数据, F1 Query 还能查询不在这个元数据中心里面的数据,通过一个叫做 DEFINE TABLE(而不是 CREATE TABLE )的语句来对这个要查询的数据源进行描述,描述之后就可以进行查询了:

DEFINE TABLE People(

format = ‘csv’,

path = ‘/path/to/peoplefile’,

columns = ‘name:STRING, DateOfBirth:DATE’

);

SELECT Name, DateOfBirth FROM People WHERE Name = ‘John Doe’;

其实本质上就是创建一个临时表,只在当前的 session 有效,为什么不用 CREATE TEMP TABLE 这种更容易理解的语法呢?这是我始终不大明白的地方。

我们 Data Lake Analytics 也有类似的直接查询裸数据的语法,可以说英雄所见略同啊:

SELECT count(*) FROM

TABLE temp_1

(

col1 int,

col2 string

)

LOCATION ‘oss://test-oss-bucket/tbl1_part/kv1.txt’;

如果要支持一种新的数据源的话,在Presto里面,我们是实现一个 Connector , 而在 F1 Query 里面是实现一个 Table-Valued Function(TVF)。

Data Sink

数据查询出来之后可以直接返回给客户端显示,也可以根据客户端的语句直接插入到另外一个表,这个表可以是被 Catalog Service 管理的表,也可以不是。如果是被管理的表,那么是通过 CREATE TABLE 语法创建出来的。而这个 Data Sink 的表默认是的实现是保存到 Google 的 Colossus 分布式文件系统上面去了。而用户也可以像 DEFINE TABLE 语法一样,可以用 EXPORT DATA 语法指定输出到自定义的表里面去。

在这一点上 F1 Query 貌似没有 Presto 来的灵活,Presto 里面的 Data Sink 可以是任何类型的存储,并且不需要什么特殊的 EXPORT DATA 的语法。

查询语言

F1 Query的查询语言是 SQL 2011 , 他们在这上面做了一些扩展以进行嵌套结构的数据查询。比较值得一提的是,F1 Query 的SQL方言跟 Big QueryDremel 以及 Spanner SQL 是一样的,这样用户可以在这些系统之间很容易进行迁移 – 统一是主旋律啊。

三大执行模式


前面也提到过,F1 Query 支持三种执行模式,他们的名字分别为 Centralized Execution , Distributed Execution 以及 Batch Execution。其中 Centralized ExecutionDistributed Execution 都属于交互式(Interative)的执行模式。

交互式执行模式

所谓的交互式执行模式很容易理解:用户是“在线等”的,因此要求响应时间要短,F1 Query内部对于这种执行模式使用的都是完全基于内存的流式执行策略的。

Centralized Execution

对于中心化的执行,接到这个请求的 F1 Server 直接就执行掉了, 因为这种请求处理的数据量不大,对于资源的要求不高,因此 F1 Server 内部其实是以单线程的 pull-based 模式来执行的:

pull-based模型

之所以把它叫做 pull-based 模型,是因为当这个计划开始执行的时候,上层的算子递归地调用底层算子的 GetNext() 方法来获取它自己的输入。总体来说数据都是被从下向上“拉”出来的,因此叫 pull-based

Distributed Execution

对于 Distributed Execution ,第一个接到这个查询请求的 F1 Server 只是充当一个调度者的角色,真正的执行是由一组 F1 Worker 共同执行。

这种模式的架构就跟 Presto 很像了,这两个角色在 Presto 里面分别叫做 CoordinatorWorker

那么什么时候用 Centralized 模式,什么时候用 Distributed 模式呢? 优化器对 SQL 进行解析,如果发现这个查询最好要用大并发进行分区读的话,那么它会走 Distributed 的模式,否则走的就是 Centralized 模式。

在分布式的执行计划里面,整个执行计划会被分拆成一些执行计划片段( Fragments ), 每个片段由一组 F1 Worker 来执行,这些片段是同时并发执行的,并且内部可能会应用流水线技术。

优化器是怎么把整个执行计划拆分成多个 Fragments 的呢? 优化器使用的是自底向上的策略来拆分的,每个单独的算子对于输入数据的分布(Data Distribution)都会可以有一定的要求的。一般来说这种要求是指数据是否按照某个字段进行分片。典型的例子是 Hash Join , Hash Join 需要数据按照 Group Key 或者 Join Key 进行 hash 分片 – 这就是 HashJoin 算子的数据分布需求。如果当前的数据分布策略能够满足这个算子的要求,那么这个算子保留在当前的 Fragment 里面,否则我们就要在执行计划当中插入一个 Exchange 节点来进行数据的重新分布,同时也划分了Fragment 之间的边界。

分布式模式下的执行计划分片

划分了 Fragment 边界之后下面一件事件就是决定这些 Fragment 的并行度, 并行度的计算也是自底向上的过程,首先最底层的 TableScan 决定了最初的并行度,然后这种并行度的信息会被一层一层地上推给一个叫做 Width Calculator 的模块来逐步计算每个 Fragment 的并行度。比如一个 HashJoin 在一个 50 并行度和一个100 并行度的两个输入 Fragment 之间进行的话,那么这个 HashJoin 算子会选用 100 并行度以照顾比较大的那个输入算子。

感觉这就是在描述Presto的实现啊。在读这篇论文之前我一直搞不清楚的就是这个神奇的 Exchange 算子是怎么来的,看了这篇论文总算搞清楚了。

数据重分布(Reparitition)策略

F1 Query 里面的 Fragment 是并行执行的,整个执行的数据流可以看作一个DAG,数据在流经 Fragment 边界的时候会被一个 Exchange 算子进行重新分布(repartition), 对于每条数据,  数据的发送者利用一个分区函数来计算它的目的地(一个分区值: partition number ),而每个 partition number 对应到目标 Fragment 里面的一个具体的 Worker

而这个 Exchange 的算子是通过 RPC 来实现的(Presto里面也是这样的),  而且数据的发送和接收之间还有流控的机制,这种基于 RPC 的通信机制的并发性还是挺好的,可以做到每个 Fragment 几千个分区,如果要求更高的并发度,那么就要使用 Batch Execution 模式来执行了。

为了达到高效的查询,查询优化器会要求最底层的 TableScan 算子把数据切分成指定的并发度,而具体的 TableScan 算子就会产生 N 个分片描述,然后集群的调度器就会起N个 Worker ,来执行这N个分片的数据扫描操作。有时候数据的分片的个数会比 Worker 的数量要大,这样调度器会动态的把数据分片交给比较空闲的 Worker 去做,这样可以避免数据倾斜。

有些算子本身会作为当前 Fragment 的一个输入,比如 LookupJoin 会作为所在 Fragment 的左边输入,因为 LookupJoin 的两个输入的数据分布规则是一样的(左边输入的数据是根据右边输入数据查询出来的)。相对应的 HashJoin 则需要多个属于不同的Fragment,并且都有自己的多个分区。

一般来说优化器会把 HashJoin 每个输入放在一个单独的 Fragment 里面,除非它本身的数据分布跟 HashJoin 算子已经一样了。HashJoin 的两个输入根据相同的分片函数把数据发送到 HashJoin 所在的 Fragment 里面, 这样才能保证相同的 Key 的数据最后是在同一个分区里面,从而可以让每个分区可以处理一段独立的 Key 的取值空间。(否则数据就 Join 不上啊)

跟 HashJoin 类似, 聚合操作通常也需要对输入进行重新分布, 只不过聚合操作是根据要聚合的 Key 进行数据重分布。而如果聚合函数不是针对特定的 Key 进行聚合(比如 count(*) ),  那么所有的数据会被发送一个分区。这种情况是可以优化的,通常会在目标 Aggregation 算子之前生成另外一个 PartialAggregation 算子,这样做的好处一是提高了总体的并行度,因为多个Worker参与了聚合操作;另外因为做了部分聚合之后,要往下游发的数据变少了,Worker 间传送的总数据也就少了。

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化资料的朋友,可以戳这里获取

外链图片转存中…(img-XbzCtr5G-1715813716262)]
[外链图片转存中…(img-xTKMDsF5-1715813716263)]
[外链图片转存中…(img-DmMkJOrp-1715813716263)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化资料的朋友,可以戳这里获取

  • 23
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值