阅读时间:2021.7
目录
===== 3. Architecture Overview
===== B. Client Interfaces, Parsing, and Planning
===== C. Query Optimization【优化器】
===== Predicate Pushdown(谓词下推)
===== Inter-node Parallelism(节点间并发)
===== Intra-node Parallelism(节点内并发)
===== 5. Query Processing Optimization
===== E. Operating on Compressed Data
===== 1. Introduction
能快速并且简单的从大量数据中提取分析变得越来越重要。
Presto是一个开源的分布式SQL查询引擎,旨在具有适应性(adaptive)、灵活性(flexible)、可扩展性(extensible)。Presto提供 ANSI SQL接口来从存储中查询数据,暴露HTTP接口,支持JDBC,并兼容很多BI工具。
Presto每天在 Facebook 处理千万亿的行,数百PB的数据。具备很多优秀的特性:
- 多租户(multi-tenant)能力。可以并发的运行数以百计的内存、IO、CPU敏感型查询,并且可以扩展到1000+的worker节点
- 联邦查询(federated design)。可以在一个query中查询来自不同数据源的数据
- 极度灵活(flexible)。可以被应用在各种用例场景,不同的限制及性能特性
- 为高性能而设计(high performance)。涉及多个关键特性&优化,包括代码生成、JVM共享等
本文章主要有2个contribution:
- 主要是为了介绍Presto的设计,以及为了实现上面特性而进行的优化及tradeoff
- 其次是介绍一些关键性决定&优化带来的性能结果,以及开发和维护Presto的经验教训
===== 2. Use Cases
Presto在 Facebook 的 Use Case 如下:
- A. Interactive Analytics。Facebook 的 engineer&data scientist 例行的进行小规模数据验证、检验假设等。可能会进行一些试探性查询,只关注第一批返回的数据
- 期望支持 50~100 并发的查询,并能在秒级 or 分钟级返回结果
- B. Batch ETL。批量的ETL查询,支持用户从旧的批处理系统迁移到Presto上。从CPU的角度上看,ETL查询构成了Presto的主要负载
- 资源使用率&集群吞吐比查询延迟更重要
- C. A/B Testing。期望能在小时级别返回完成且精确的结果。也期望能在5-30s内返回小部分数据的查询结果。
- D. Developer/Advertiser Anslytics。一些暴露给客户的报表工具在Presto之上构建,此类查询的数据总量大,但是查询的选择性高(只访问自己的数据),大多数的查询包含join、aggregation、window。
- 期望延迟在50ms-5s,支持百级别并发,并支持高可用
===== 3. Architecture Overview
【基本架构及组件功能】Presto集群由一个 Coordinator & 多个 Worker节点组成。Coordinator节点负责接纳、解析、计划查询以及查询优化;Worker节点负责查询执行。
【处理SQL主要流程】STEP1:Client 发送HTTP请求包含SQL查询给 Coordinator;STEP2:Coordinator 对SQL进行解析、优化,并产生分布式执行计划;STEP3:Coordinator 将执行计划分发给 Worker,然后开始执行 tasks ,并开始列举 splits;STEP4:Worker 运行这些任务,处理对应的splits( 从外部系统拉取数据或者处理其他Worker生成的中间结果)
这里 task & split 的拆分都是由 Coordinator 来完成的。task是指代执行逻辑,split是待处理数据。即先将逻辑(task)分发,随后给逻辑(task) assign 数据(split)
Worker 使用协作式、多任务处理来同时处理来自多个查询的任务。执行尽可能流水线化,并且任务一旦可用,数据就会在任务之间流动。中间数据&状态都存储在内存中。
【Plugin】Presto被设计为可扩展的,并提供多功能的插件接口。插件可以提供数据类型、函数、访问控制实现、时间消费者、排队策略和配置属性。
【Connector】更重要的,插件还提供了 Connector,Connector使得 Presto可以通过Connector API连接外部数据源,Connector API由4部分组成:Metadata API、Data Location API、Data Source API、Data Sink API。
===== 4. System Design
介绍4部分内容:Presto支持的SQL Dialect(A)、查询生命周期的全路径(B、C、D、E)、资源管理机制(F)、异常容忍(G)
===== A. SQL Dialect
Presto 严格遵循 ANSI SQL 规范 [2],没有实现规范中的每一个feature,实现的feature都遵循规范声明。
此外,Presto还实现了一些扩展能力,以提升可用性。比如支持 lambda表达式,以及一些内置高级函数
===== B. Client Interfaces, Parsing, and Planning
【客户端接口】Presto Coordinator暴露 RESTful HTTP接口,并附带命令行接口。同时还提供 JDBC client以便于BI工具连接
【请求解析】使用 ANTLR-based 解析器,将SQL解析成一棵语法树
【逻辑计划】logical planner基于语法树,生成一个由 plan nodes组成的中间代表(intermediate representation,IR)树结构。每个节点表示一个物理 或者 逻辑的operation,其子节点是当前节点的输入。该树结构是完全逻辑的,不包括任何的执行逻辑
===== C. Query Optimization【优化器】
优化器【主要基于RBO完成】 将逻辑计划转化为更物理的结构,代表查询的高效执行策略。该过程的工作原理是贪婪的评估一系列转换逻辑,直到达到一个固定点。每个 Rule 都有一个模式,可以匹配查询计划的子树并确定是否应用转换。结果是替换匹配目标的逻辑等效子计划。Rule比如:谓词&limit下推(predicate and limit pushdown)、列修剪(column pruning)、去相关(decorrelation)
Presto正在通过引入 CBO(cost-based optimization)来增强优化器能力,CBO基于 Cascades framework [13]完成。
目前Presto已经支持了2个 CBO 优化逻辑(将列统计信息考虑在内) —— join策略选择 & join 重排序。
这里讨论一小部分优化器特性。
===== Data Layouts(数据分布)
优化器会利用从 connector的Data Layout API获取的数据物理分布及其他属性(partition、sort、group、index)。
Connector 对于单个 table 会返回多个不同属性的layout,优化器会从中选择最有效的一个来使用 [15][19]。
===== Predicate Pushdown(谓词下推)
优化器会和Connector协同决定是否将 range&equality 通过Connector下推以提高 filter 效率。
如果有多个layout,引擎会选择在谓词列上有索引的layout
===== Inter-node Parallelism(节点间并发)
【stage & task】优化器会识别 执行计划 是否有某些部分可以并行执行,这些部分被称为 stage。每个stage被拆分为一到多个 task,每个 task 都是针对不同的输入执行相同的计算逻辑。
引擎在stage之间进行数据传递(shuffle)的数据会插入到内存buffer中,shuffle会增加延迟、耗费内存、增加CPU负载,因此Optimizer会关注shuffle在plan中的次数。
- Data Layout 属性:数据分布情况决定了 optimizer 的策略,以降低数据shuffle的次数
- Node 属性:除了数据分布外,connector还会暴露一些节点属性(Partition、sort、bucket、group) [24],optimizer也会将其考虑进来作为shuffle的依据。
- Presto 贪婪的选择满足尽可能多的所需属性的分区,以减少 shuffle。当然这也可能会导致分片倾斜(partition skew)
===== Intra-node Parallelism(节点内并发)
Optimizer 使用相同的机制来识别是否可以进行 节点内并发 并受益。节点内并发往往比节点间并发更高效,尤其是对那些对节点有严格并发吞吐控制的场景(增大节点吞吐量)。
paper中最后举了2个例子,都是因为数据本身内在原因而导致的单节点大数据量处理的场景,进行 Intra-node Parallelism 可以有效的提高并发。
下图4中展示了节点内部并发进行计算(ScanHash & HashBuild)的逻辑。
===== D. Scheduling
【Coordinator管理调度task&stage】Coordinator 将 Plan stage 拆分成 task,并调度给 Worker 执行,每个 task 可以被理解为一个可执行的单元。随后 Coordinator 将 task 在 stage 之间通过 shuffle 链接并构成一个树状结构。
【pipeline&operator】一个 task 有多个 pipeline 组成,每个 pipeline 由一系列的 operator 构成,operator 对数据执行独立的定义完整的计算操作。pipeline的数据暂存在本地内存中并完成join。
上图4展示了一个task内部pipeline&operator的组织关系。
为了完成一个query,引擎需要做出2个决策:stage如何被调度 & task如何被调度
【Stage调度 -- Stage Scheduling】
Presto支持2种 Stage 调度策略:all-at-once & phased
- All-at-once:最小化 stage 之间的数据调度延迟,在数据准备好时立即执行。适用于对延迟敏感的场景
- Phased:识别必须同时启动以避免死锁的强连通组件,按照拓扑顺序来执行这些组件
【Task调度 -- Task Scheduling】
Task调度将 Stage 分为2种情况:leaf & intermediate。Leaf stage 从 connector 读取数据;Intermediate stage 仅处理从其他收到的中间结果。
Leaf Stage 的 task 调度给 Worker 的时候会考虑 network&connector 施加的一些约束。
- 比如:share-nothing架构,worker会尽量的和数据调度在一起
- 分析显示,生产集群的大部分CPU都消耗在从 connector中读取数据(decompress、decode、filter、apply transformation)。这种工作负载可以高度并行化,所以如果没有其他约束(比如share-everything架构),worker 节点都会尽可能参与到计算中。
- 对于网络拓扑的考虑,比如会考虑到机架因素
Intermediate Stage 的 task 可以在所有的worker节点上执行,但是仍然要决定task的数量(基于connector配置、plan属性、数据分布等等)。
部分情况下,引擎会在运行时动态调整 task数量,比如后面 Sector IV-E 中介绍的自适应策略。
【Split调度 -- Split Scheduling】
Leaf Stage 必须被分配 split 后才可以运行,而 Intermediate Stage 是一直运行的,且只有当任务中断或者完成时才会终止。
不同的 connector 携带的 split 信息是不同的。比如对于 DFS,split由 file path & offset 组成;对于 Redis ,split由 kv 等信息组成。
【Split分配】当 task 被调度到 worker节点后,Coordinator 开始为 task 分配 split。Presto 要求 Connector 列举小批量的 split,并 lazy 地将他们分配给 task。这会有一些受益:
- 【加速查询】解耦了大量 split 的枚举时间 与 查询时间
- 【快速返回】query 不需要 处理所有的数据就可以产生结果,并且可以在随后快速的被 cancel 或者 达到 limit 而终止。比如内部分析场景
- 【适配CPU能力】Worker 维护一个 split 的队列,Coordinator 可以简单的通过 保证队列最短的逻辑 来分配 split就可以适配 CPU 处理能力不同的场景
- 【降低内存使用】所有的 query 查询都不需要加载所有的 metadata 到内存
lazy 的split枚举策略,会导致无法精准的预估和汇报查询进度
===== E. Query Execution
【本地数据流转 -- Local Data Flow】
【Driver Loop】一旦 split 被分配到 thread上,就会被 driver loop 来处理。driver loop 比 Volcano模式 更适合多任务协同执行,因为 operator 可以在线程 yield之前快速的进入一个确定的状态。此外,driver 可以最大化在每一次quota期间运转,在无须输入的情况下就可以让数据在operator之间流转。
【Page】driver loop operator的数据运转单元称为 page,page 是一个以列编码的多行信息。当 Connector的 Data Source API 收到一个split时,返回page,随后 operator 以 page 为单位对数据进行读写&处理。并且 driver loop 继续在 operator 之间流转 page。
【Shuffle】 -- 利用内存减低延迟,保护内存使用
Presto旨在最大程度减小端到端的延迟,最大化提高资源利用率。Presto通过HTTP协议,使用内存缓冲的shuffle来交换中间数据。Producer保留数据,直到 Consumer拉取完成。该机制相对于将shuffle数据持久化到磁盘的其他产品,提供更低的延迟 [4][21]。
引擎 基于输入&输出缓冲区利用率来调整并发度
- 如果 outbut buffer 持续较高时,会降低并发处理split的数量(背压?)。有2个优点:1. 网络使用更加平均;2. 处理那些赶不上生产速度的客户端
- 对于 接收端(input),引擎监控每个请求平均数据传输量,以计算HTTP请求的并发度,来保证input buffer 大小
【Write】
大部分ETL job的场景都需要写入其他表,写入性能的一个关键点就是写入并发度,即通过 Connector Data Sink API 写入数据的线程数。
Presto使用自适应的策略,当产生数据超过buffer threshold时,增加worker数量。
===== F. Resource Management
【CPU调度 -- CPU Scheduling】
Presto主要对集群的整体吞吐优化,此外,本地(节点)还优化了计算成本低的查询的低周转时间。
节点统计了每个 split 的 CPU 执行时间,并在本地以task级别进行调度决策。
【split执行调度】任何一个split在线程上执行最多运行1s,随后必须放弃线程并返回队列中。当outbut buffer满、input buffer空、系统内存使用超限等场景也会进行任务切换调度。
【选择下一个待执行任务】Presto使用一个 5级的多级反馈队列 [8],任务积累更多的CPU时间,就回去更高级别。每个级别都会配置自己运行时间的比例。
系统提供了一个 low-cost yield signal,所以运行时间长的计算任务可以在operator中停止。如果operator运行超时,那么会收取(charge)对应的时间,并降低其随后的执行概率。这个策略可以给低资源消耗的任务提供更高的优先级。【用户期望小查询快速完成,而并不关系数据量大,计算多的查询的运行时间】
【Memory管理 -- Memory Management】
内存被定义为2类:user内存 & system内存
- user内存 指对输入、计算推理使用的内存
- system内存 指协助做决定的副产品(比如 shuffle buffer),可能与查询类型&输入数据无关
引擎对user & total(user + system)施加了不同的限制。query超过全局或者单节点内存限制会被kill。total内存的限制通常比user内存限制高很多,只有少量query可以超出 total 内存。
查询在每个节点&全局的user内存限制是不同的,这最大程度地允许使用倾斜。
不太可能所有节点同一时间都用到最大内存,所以 overcommit memory是ok的,只要我们保证节点内存不足时,集群状态仍是健康的。这里有2个策略:spilling & reserved pool。
【Spilling】
当节点内存快要用尽时,引擎会调用memory revocation流程 来释放内存(直到可用内存满足最后一个request运行)。Revocation是将状态 spill 到磁盘,Presto spilling支持join & aggregate。而Facebook线上并未配置 spilling,因为全内存操作 延迟更低。
【Reserved Pool】
当节点内存不足,且没有配置Spill,也没有可回收的内存时,Reserved Pool机制将会被使用。
每个节点的查询内存被进一步详细分为general pool& reserved pool。当general pool被耗尽时,使用内存最大的query将被提升为reserved pool运行。
为了防止deal lock,整个cluster同一时间只能有一个query在reserved pool运行。如果general pool内存耗尽,而reserved pool被占用时,所有任务都会暂停,只保留reserved pool中的任务执行。也可以配置成kill query来取消大多数节点的查询阻塞。
===== G. Fault Tolerance
Presto支持通过低级别retry的方式使系统从短暂的异常中恢复,然而并没有很好的内部机制使系统可以应对 Coordinator 或者 Worker 的crash。Coordinator 使整个集群不可用,Worker节点异常使运行在该节点的query均失败。目前只能依赖客户端来自动的重试。
有很多技术来应对异常,比如checkpoint及部分recover技术、基于复制的异常容忍 [6]等。但鉴于成本考虑,这些技术的预计价值尚不明显。尤其是考虑到 MTTF & 1000节点的集群,其中大多数查询都是在几个小时内完成的场景。[17]
===== 5. Query Processing Optimization
===== A. Working with the JVM
Presto 是 Java 实现的,运行在 Hotspot JVM上,性能的优化一定程度局限于平台。没有应用级别的机制来控制 JVM JIT编译器如何生成机器码,可以对代码进行结构化,以便利用 JIT 编译器提供的优化(inlining、loop unrolling、intrinsics等),我们也在探索在 JVM 无法生成最佳机器代码的场景中使用 Graal [22]
Java 的 GC 容易造成突发的影响,Presto 使用 G1 collector [10],该collector处理大对象的能力比较弱。Presto 避免分配大对象和buffer,以适配G1
===== B. Code Generation
引擎的主要特性之一是 代码生成,针对 JVM 字节码,有2种形式
【表达式评估 - Expression Evaluation】
Presto内置一个表达式解释器,可以评估任意的表达式,但是对于评估 billion级别的数据太慢了。为了解决这个问题,Presto生成字节码来解决 constants、function call、refrence to variables、lazy or short-circuiting options
【针对JIT的启发式优化 -- Targeting JIT Optimizer Heuristics】
Presto 对几个关键的 operator 和 operator组合 生成 字节码,该字节码比传统的处理流程更适合 JIT 优化。
这里有3个主要的行为,略。
===== C. File Format Features
【Page & Block】叶子节点的 Scan operator 会调用 Connector API,并收到以列组织的数据 -- Page。Page 由多个Block组成,Block是一个列组织的内存结构
Connector会尽可能的利用文件格式的特性 [20],比如通过文件统计信息(header或者footer记录的range信息)来跳过文件。Reader可以将某些格式的压缩数据直接转换为 block,引擎也可以有效的对这些数据进行处理
上图展示了一些page的压缩编码场景:Directionary-encoded block 对基数较小的数据比较有效。RLE(run-length encoded) block压缩重复数据。同时通过节点内共享字典的方式来提高内存利用率
===== D. Lazy Data Loading
【Lazy materialization】Presto支持惰性物化数据(lazy materialization data)。这个功能可以利用像ORC、Parquet、RCFile这样已经以列组织的压缩数据。
Connector只有在真正访问它的时候才会生成block(read、decompress、decode)。大量的CPU耗费在解压和解码上,filter通常具有高度的选择性,所以当列不常被访问时,这个优化非常有效
===== E. Operating on Compressed Data
Presto会尽可能在压缩数据上运行。
- 当 Page processor评估要对一个dictionary block进行转换或者过滤时,会处理dictionary中的所有数据,这使得引擎在快速无条件循环中处理整个字典。
- Presto也会利用 dictionary block结构来构建hash table
- Presto 还会对中间结果生成压缩数据
===== 6. Performance
===== A. Adaptivity
Presto支持 TPC-DS 所有的查询,本次实验中选择一个内存使用量较低的查询子集。
对比三个场景:
- Raptor:元数据存储在MySQL,实际数据以ORC的方式存储在本地。实验中使用随机的分布策略
- 数据存储在Hive/HDFS 中,没有使用 统计信息(statistics)
- 数据存储在Hive/HDFS 中,使用 统计信息(statistics)
实验证明Presto可以很好的利用存储特性。过程略
===== B. Flexibility
这里截取了生产环境的query统计,证明presto可以适应低延迟(ms级别)的查询 到 ETL 运行好几个小时的job
===== C. Resource Management
这里采集了一些生产环境数据,证明查询并发度不断变化的过程中,CPU一直可以保持大约90%的利用率
===== 8. Related Work
和其他支持SQL产品对比:
- Hive [21] 在 HDFS存储之上提供 SQL 接口,并将 query 编译成 MapReduce [9] 或者 Tez [18] job 来运行
- Spark SQL [4] 构建在 Spark引擎 [23] 之上,解决了很多 MapReduce 系统上的问题。
然而这些系统不支持端到端的 pipeline,也通常会将中间数据持久化到磁盘。这尽管提升了异常容忍的能力,但是增加了延迟,不适用于交互式或低延迟的用户场景
- Vertica [15] ,Teradata,Redshift等系统可以从外部系统读取数据。但是都是围绕内部系统构建的
- Apache Impala [14] 提供交互式响应需求,但是需要在 Hadoop 系统上运行
Presto 可以构建在未知的数据源之上,可以和其他的垂直集成存储(比如Raptor)结合使用。更加开放
Presto构架在很多系统的革新技术之上:
- 编译 query plan 来加速query 执行,借鉴了Neumann [16] & Diaconu et al. [12]
- 尽可能的压缩数据,借鉴了 Abadi et al. [3],并生成压缩的中间结果
- 从 Vertica & C-Store [19] 中选择最佳布局?
- 使用类似 Zhou et al. [24] 的策略,结合计划属性来最小化 shuffle
===== 9. Reference
Theory
- ANSI SQL [2]
Algorithm / Data struct
- G1 collector [10]
Research
- Integrating Compression and Execution in Column-Oriented Database Systems. [3] #563#
- Efficient Processing of Data Warehousing Queries in a Split Execution Environment [5] #85#
- Fault-tolerance in the borealis distributed stream processing system. [6] #212#
- An Experimental Time-Sharing System. [8] #244#
- Cascades framework [13]
- Efficiently Compiling Efficient Query Plans for Modern Hardware. [16] #427#
- Incorporating partitioning and parallel plans into the scope optimizer. [24] #86#
Product
- Volcano [1]、MapReduce [9]、Apache Impala [14]、Vertica [15]、Tez [18]、C-Store [19]、Hive [21]、Graal [22]
- Tenzing A SQL Implementation On The MapReduce Framework [7].
- Microsoft SQL Server Polybase [11]
- Hekaton: SQL Server’s Memory-Optimized OLTP Engine. [12]
- Themis: An I/O-Efficient MapReduce [17]
- Even faster: Data at the speed of Presto ORC [20]
- Spark [23]
- Spark SQL [4]