基于trino实现Sort-Based Shuffle

背景

最近一年内,trino借助Tardigrade项目对批处理能力进行了优化,使其可处理ETL任务。然而,目前社区exchange shuffle实现采用了基于Hash-Based的设计方案。该方案存在一个重要缺陷:随着集群规模的增大,shuffle partition配置的数量也会增加,从而导致更多的exchange中间数据文件被产生。

例如,当partition数为N时,每个map task都将产生N个partition shuffle文件。因此,当map task的数量为M时,就会产生M*N个文件。当M和N较大时,就会导致大量海量的小文件。这些文件最终会导致大量低效的IO操作,并且内存中需要保存大量文件操作句柄和临时buffer,这很容易导致OOM,从而给性能和稳定性带来问题。为此,必须采用更有效的设计方案来优化这一问题。

这个问题在Tardigrade项目中也重点描述到,目前社区版本的Tardigrade能力只适合于在20个节点以内的小集群使用(https://github.com/trinodb/trino/wiki/Fault-Tolerant-Execution

image.png
image.png

当前,主流批处理引擎不断演进,针对shuffle阶段存在的问题,逐步采用sort-base shuffle技术解决。为此,我们通过调研各引擎的演进方案,结合trino现状,巧妙地设计了一套自己的sort-base shuffle实现方案。

Spark Shuffle演进

Spark采用了Hash-Based Shuffle作为早期的数据分片策略。然而,随着1.1版本的推出,Sorted-Based Shuffle得以引入,后续在2.0版本中删除了Hash-Based Shuffle。Sorted-Based Shuffle的应用,让Spark由原本只能用于中小规模数据处理的平台,重新定位为可处理大规模数据处理的平台,进一步稳固了它在数据处理领域的领导地位。
image.png

说一下spark Sorted-Based Shuffle实现,下面的图很好的展示这种模式下的shffle write和shuffle read。
image.png
shuffle write的大致流程:

  1. ShuffleWriter迭代式地读取每一个原始数据计算并确定其所属的分区(partition id),并将其存储于内存中的Shuffle buffer中。
  2. 当Spark Executor内存管理机制发现内存不足时,就会触发Spill操作,将Shuffle buffer中的数据写入到磁盘的Spill数据文件中。同时,为了方便后续的读取操作,还需要记录每一个分区在Spill文件中的位置(offset和length信息)。
  3. 在Shuffle Write过程中,Spill文件会不断地增多。因此,在任务执行完毕后,需要将所有Spill数据文件进行合并(merge)成一个Data数据文件,并按照分区进行排序。同时,还需要为Data文件生成一个Index文件,用于记录每一个分区在Data文件中的位置(offset和length信息)。
  4. ShuffleWriter消费完row数据后,就需要对多个spill数据文件进行merge成一个data数据文件(也是按partition id排序),然后再生成一个index文件记录各个partition id在data数据文件的位置[offset, length]

Shuffle read的流程比较简单,其主要目的是按照确定的分区进行读取,具体流程如下:

  1. ShuffleReader首先读取Index文件,分析出每一个分区的位置信息。
  2. 然后根据所得到的位置信息,去读取Data数据文件中的Shuffle数据。

Flink Shuffle演进

先看一下flink shuffle演变:

  • 在2020年的Flink 1.12版本中,基于sort的批处理shuffle实现被引入,随后持续进行了性能和稳定性优化。
  • 在2021年的Flink 1.13版本中,sort-shuffle已经实现并且可用于生产系统中。

引入 Sort-Shuffle 的意义(来自flink社区的说明)
我们之所以要在 Flink 中引入 sort-shuffle 的实现,一个重要的原因是 Flink 原本的基于 hash 的实现对大规模批作业不可用。这个也是被现有的其他大规模分布式计算系统所证明的:

  • 稳定性方面:对于高并发批作业,基于 hash 的实现会产生大量的文件,并且会对这些文件进行并发读写,这会消耗很多资源并对文件系统会产生较大的压力。文件系统需要维护大量的文件元数据,会产生文件句柄以及 inode 耗尽等不稳定风险。
  • 性能方面:对于高并发批作业,并发读写大量的文件意味着大量的随机 IO,并且每次 IO 实际读写的数据量可能是非常少的,这对于 IO 性能是一个巨大的挑战,在机械硬盘上,这使得数据 shuffle 很容易成为批处理作业的性能瓶颈。

Flink Sort-Shuffle实现
Flink Sort-Shuffle实现的整体设计思路与Spark相似,但在细节上存在差异。
Flink通过对shuffle数据的文件结构进行优化,采用多个数据区域的方式实现按partition id排序,而不是全局排序。相比于Spark的设计,这种方法避免了spill文件的合并,减少了数据文件的读写和复制过程。然而,需要注意的是,由于索引文件中记录的信息相对较多,这也会导致索引文件的大小相对较大。
image.png
image.png

trino如何实现

方案选择

相较于Spark和Flink的实现方式,Trino结合自身的shuffle实现最终设计上参考了Flink的Sort-based Shuffle。具体实现方式为将Shuffle数据文件根据Partition ID进行局部排序,从而实现如下优势:

  1. 在性能上表现更优秀。一方面,避免了一次Spill和Merge的IO读写过程,另一方面Trino Shuffle采用基于Remote Storage的存储,如果选择Spill and Merge方案,则无法应用本地Disk复制的Zero-copy等提升性能的技术。
  2. 功能实现方面也更加简单,无需Spill过程。

实现

目前,Trino的exchange shuffle实现逻辑主要集中在plugin/trino-exchange-filesystem模块中。shuffle的读写入口主要在FileSystemExchangeSink和FileSystemExchangeSource。本文不会深入讨论相关实现代码,而是重点强调核心逻辑。

具体而言,shuffle写操作先将Page数据依据Partition ID划分,并缓存在内存中。接着,基于Trino的Memory Revoke机制,触发内存数据排序和写入shuffle文件,并建立index索引信息文件。相比之下,shuffle读操作只需要依据index索引查找对应Partition ID的shuffle数据即可。需要注意的是,目前Trino的Memory Revoke机制仅支持operator算子级别,如果要支持FileSystemExchangeSink的Memory Revoke,需要进行一定的扩展。

下面展示了sort-based shuffle生成的shuffle数据文件,并与hash-based shuffle进行了比较。

-- 以下是sort-based shuffle产生的shuffle数据文件
/tmp/exchange2/00448f.20230209_032131_00002_9gph8.external-exchange-19.16/0/shuffle.data
/tmp/exchange2/00448f.20230209_032131_00002_9gph8.external-exchange-19.16/0/shuffle.index
/tmp/exchange2/00448f.20230209_032131_00002_9gph8.external-exchange-19.16/0/committed

-- 以下是hash-based shuffle产生的shuffle数据文件
/tmp/exchange2/9782b8.20230216_070725_00000_ayw3f.external-exchange-9.5/0/0_0.data
/tmp/exchange2/9782b8.20230216_070725_00000_ayw3f.external-exchange-9.5/0/10_0.data
/tmp/exchange2/9782b8.20230216_070725_00000_ayw3f.external-exchange-9.5/0/11_0.data
/tmp/exchange2/9782b8.20230216_070725_00000_ayw3f.external-exchange-9.5/0/12_0.data
/tmp/exchange2/9782b8.20230216_070725_00000_ayw3f.external-exchange-9.5/0/13_0.data
/tmp/exchange2/9782b8.20230216_070725_00000_ayw3f.external-exchange-9.5/0/14_0.data
/tmp/exchange2/9782b8.20230216_070725_00000_ayw3f.external-exchange-9.5/0/15_0.data
/tmp/exchange2/9782b8.20230216_070725_00000_ayw3f.external-exchange-9.5/0/16_0.data
/tmp/exchange2/9782b8.20230216_070725_00000_ayw3f.external-exchange-9.5/0/17_0.data
/tmp/exchange2/9782b8.20230216_070725_00000_ayw3f.external-exchange-9.5/0/18_0.data
/tmp/exchange2/9782b8.20230216_070725_00000_ayw3f.external-exchange-9.5/0/19_0.data
/tmp/exchange2/9782b8.20230216_070725_00000_ayw3f.external-exchange-9.5/0/1_0.data
/tmp/exchange2/9782b8.20230216_070725_00000_ayw3f.external-exchange-9.5/0/20_0.data
/tmp/exchange2/9782b8.20230216_070725_00000_ayw3f.external-exchange-9.5/0/21_0.data
/tmp/exchange2/9782b8.20230216_070725_00000_ayw3f.external-exchange-9.5/0/22_0.data
/tmp/exchange2/9782b8.20230216_070725_00000_ayw3f.external-exchange-9.5/0/23_0.data
/tmp/exchange2/9782b8.20230216_070725_00000_ayw3f.external-exchange-9.5/0/2_0.data
/tmp/exchange2/9782b8.20230216_070725_00000_ayw3f.external-exchange-9.5/0/3_0.data
/tmp/exchange2/9782b8.20230216_070725_00000_ayw3f.external-exchange-9.5/0/4_0.data
/tmp/exchange2/9782b8.20230216_070725_00000_ayw3f.external-exchange-9.5/0/5_0.data
/tmp/exchange2/9782b8.20230216_070725_00000_ayw3f.external-exchange-9.5/0/6_0.data
/tmp/exchange2/9782b8.20230216_070725_00000_ayw3f.external-exchange-9.5/0/7_0.data
/tmp/exchange2/9782b8.20230216_070725_00000_ayw3f.external-exchange-9.5/0/8_0.data
/tmp/exchange2/9782b8.20230216_070725_00000_ayw3f.external-exchange-9.5/0/9_0.data
/tmp/exchange2/9782b8.20230216_070725_00000_ayw3f.external-exchange-9.5/0/committed

测试

接下来我们对trino的sort-base shuffle进行测试。

稳定性测试

测试环境:

  • 1000G数据集tpcds,iceberg表。
  • 批处理模式。retry-policy=TASK。
  • query11
  • trino worker实例:8core32G,数量为3个。
  • exchange storage为hdfs存储

测试结果:

  1. 对于Hash-based shuffle,当shuffle partition数量超过500后,其稳定性开始下降,大概率出现失败情况。
  2. 而对于Sort-based shuffle,由于shuffle文件数不会随着shuffle partition数线性增加,因此即使在partition数量较大的情况下,其稳定性表现仍然较好。
场景Hash-based shuffleSort-based shuffle
fault_tolerant_execution_partition_count=50successsuccess
fault_tolerant_execution_partition_count=100successsuccess
fault_tolerant_execution_partition_count=500failedsuccess
fault_tolerant_execution_partition_count=1000failedsuccess

以下是Hash-based shuffle场景下造成查询失败的异常堆栈,主要原因是在hdfs上并发打开大量shuffle文件以进行读写操作,最终导致了hdfs瓶颈问题的出现。

Query 20230207_110508_00004_s3xnj failed: org.apache.hadoop.ipc.RemoteException(java.io.IOException): File /tmp/exchange2/477088.20230207_110508_00004_s3xnj.external-exchange-19.9/4/52_0.data could only be replicated to 0 nodes instead of minReplication (=1).  There are 3 datanode(s) running and 3 node(s) are excluded in this operation.
        at org.apache.hadoop.hdfs.server.blockmanagement.BlockManager.chooseTarget4NewBlock(BlockManager.java:1832)
        at org.apache.hadoop.hdfs.server.namenode.FSDirWriteFileOp.chooseTargetForNewBlock(FSDirWriteFileOp.java:265)
        at org.apache.hadoop.hdfs.server.namenode.FSNamesystem.getAdditionalBlock(FSNamesystem.java:2586)
        at org.apache.hadoop.hdfs.server.namenode.NameNodeRpcServer.addBlock(NameNodeRpcServer.java:880)
        at org.apache.hadoop.hdfs.protocolPB.ClientNamenodeProtocolServerSideTranslatorPB.addBlock(ClientNamenodeProtocolServerSideTranslatorPB.java:517)
        at org.apache.hadoop.hdfs.protocol.proto.ClientNamenodeProtocolProtos$ClientNamenodeProtocol$2.callBlockingMethod(ClientNamenodeProtocolProtos.java)
        at org.apache.hadoop.ipc.ProtobufRpcEngine$Server$ProtoBufRpcInvoker.call(ProtobufRpcEngine.java:507)
        at org.apache.hadoop.ipc.RPC$Server.call(RPC.java:1034)
        at org.apache.hadoop.ipc.Server$RpcCall.run(Server.java:1003)
        at org.apache.hadoop.ipc.Server$RpcCall.run(Server.java:931)
        at java.security.AccessController.doPrivileged(Native Method)
        at javax.security.auth.Subject.doAs(Subject.java:422)
        at org.apache.hadoop.security.UserGroupInformation.doAs(UserGroupInformation.java:1926)
        at org.apache.hadoop.ipc.Server$Handler.run(Server.java:2854)

性能对比

测试环境:

  • 100G数据集tpcds,iceberg表。
  • 批处理模式。retry-policy=TASK;fault-tolerant-execution-partition-count=24
  • trino worker实例:8core32G,数量为3个。
  • exchange storage为hdfs存储

测试结果:

  1. 针对测试结果进行分析,发现Sort-based shuffle存在大约10%的性能退化,相较于Hash-based shuffle来说。
  2. Hash-based shuffle在shuffle partition较小的情况下表现出性能优势,因此非常适合小型群集的使用场景。
场景总耗时(单位:秒)
Hash-based shuffle2919
Sort-based shuffle3262

总结

本文针对Trino在处理ETL任务中shuffle阶段存在的问题进行研究,结合Spark和Flink的Sort-based Shuffle实现,提出了一套针对Trino的sort-base shuffle方案。

与Hash-based Shuffle相比,Sort-based Shuffle在处理大规模数据时表现更优,稳定性也更好。但在性能方面存在一定的退化,需要根据具体使用场景进行权衡。

本文主要介绍了Sort-based Shuffle的实现流程和Trino的具体实现方式,同时对稳定性和性能进行了测试分析。通过本文的研究,可以给Trino处理ETL任务时的shuffle阶段提供一种更为有效的优化方案。

trino-server 是一款开放源代码的分布式 SQL 查询引擎。要下载 trino-server,首先需要访问官方网站(https://trino.io/)。在网站的下载页面上,可以找到 trino-server 的最新版本和相关的下载链接。 下载 trino-server 有两种方式。首先,可以直接下载预编译的二进制发行版。这些发行版为各种操作系统提供了编译好的二进制文件,包括 Linux、MacOS 和 Windows。下载适用于目标操作系统的二进制发行版,然后解压缩到希望安装 trino-server 的目录。 第二种方式是通过源代码自行构建 trino-server。这需要在本地设置开发环境,并确保安装了必要的依赖项。通过从 GitHub 上克隆 trino-server 仓库,并使用构建工具(例如 Maven)构建项目,可以编译生成 trino-server 的可执行文件。构建完成后,可将生成的二进制文件拷贝到目标位置,即可完成下载和安装。 无论是下载预编译的二进制发行版还是通过源代码自行构建 trino-server,一旦下载完成,还需要进行一些配置工作。这包括编辑配置文件以指定必要的参数,如节点数量、端口号等。配置完成后,可以通过命令行启动 trino-server,然后使用相应的客户端工具连接和查询 trino-server。 总而言之,下载 trino-server 可以通过访问官方网站,并选择下载预编译的二进制发行版或通过源代码自行构建。下载后,需要进行配置并启动 trino-server,之后即可使用 trino-server 进行 SQL 查询操作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值