Spark 3.2.0 版本新特性 push-based shuffle | 青训营笔记

1,077 阅读10分钟

这是我参与「第四届青训营 」笔记创作活动的第10天。

背景

在 Spark 计算基础设施的大规模部署中,Spark Shuffle 正在成为潜在的扩展瓶颈和集群效率低下的原因,主要存在的一下几点问题:

  • 数据存储在本地磁盘,没有备份
  • IO 并发:大量 RPC 请求(M*R)
  • IO 吞吐:随机读、写放大(3X)
  • GC 频繁,影响 NodeManager

这导致了整体作业缓慢,甚至是长期运行工作的失败。

因为解决此类缓慢和故障问题不仅会影响到开发人员的生产力,而且还导致基础设施的高运营成本。

为了优化该问题,有很多公司都做了思路相近的优化,push shuffle

Magnet

为了解决这些问题,Spark 实现了 Magnet,这是一种新颖的基于推送(push-based)的 shuffle 服务。

5a9537a5e821485352e6294ede324ec.jpg Magnet 旨在保持当前 Spark shuffle 操作的容错优势,该操作以基于排序的方式物化了中间 shuffle 数据,同时克服了上述问题。

在 Magnet 的设计和实现,必须克服几个挑战。

首先,Magnet 需要在 shuffle 操作过程中提高磁盘 I/O 效率。

它应该避免从磁盘中读取单个小的 shuffle 块,这会损害磁盘吞吐量。

其次,Magnet 应该有助于缓解潜在的 Spark ESS 连接故障,以提高大规模 Spark 集群中 shuffle 操作的整体可靠性。

第三,Magnet 需要应对潜在的落后者和数据倾斜,这在具有实际生产工作负载的大型集群中很常见。

最后,Magnet 需要在不产生过多内存或 CPU 开销的情况下实现这些优势。

这对于使 Magnet 成为一种可扩展的解决方案,从而处理具有数千个节点和 PB 级别每天 shuffle 数据的集群来说至关重要。

Magnet 主要流程

Magnet shuffle 服务背后的核心思想是基于推送的 shuffle 概念,其中 mapper 生成的 shuffle 块也被推送到远程 shuffle 服务,以按每个 shuffle 分区进行合并。push-based shuffle 的 shuffle 写入路径如下图所示:

7d4c0dbc3dca2bbca408318487a6f9a.jpg 在 map 任务生成其 shuffle 文件后,它准备将 shuffle 块推送到远程 ESS。它将 shuffle 文件中的连续块组装成 MB 大小的块,并将该组数据推到相应的 ESS。大于特定大小的 Shuffle 块将被跳过,因此我们不会推送可能来自大型倾斜分区的块。map 任务以一致的方式确定这个分组和相应的 ESS 目的地,从而将属于同一个 shuffle 分区的不同 mappers 的块推到同一 ESS。分组完成后,这些块的传输将移交给专用线程池,然后 map 任务就完成了。通过这种方式,我们将任务执行线程与块传输线程解耦,在 I/O 密集型数据传输和 CPU 密集型任务执行之间实现了更好的并行性。ESS 接受远程推送的 shuffle 块,并将相同分区的 shuffle 数据合并到相应的 shuffle 文件中。这是以尽力而为的方式完成的,这并不能保证所有块都被合并。但是,ESS 确实保证在合并期间不会发生数据重复或损坏。

fb4a4ec61316f7ec8086727dcd9bf65.jpg

在基于推送的 shuffle 数据读取路径上,reduce 任务可以从合并的 shuffle 文件和 map 任务生成的原始 shuffle 文件中获取其任务输入(如上图)。ESS 在从合并的 shuffle 文件中读取时可以执行大的顺序 I/O 而不是小的随机 I/O,从而显着提高 I/O 效率。利用这一点,reduce 任务更愿意从合并的 shuffle 文件中获取它们的输入。由于块推送/合并过程是尽力而为的,reduce 任务可以使用未合并的块来填充合并的 shuffle 文件中的任何漏洞。如果合并的 shuffle 文件变得不可用,他们甚至可以完全回退到获取未合并的块。Magnet 的高效磁盘 I/O 模式进一步为构建高性能 Spark 集群提供了更大的灵活性,因为它更少依赖 SSD 来实现良好的 shuffle 性能。

Spark driver 负责协调 map 和 reduce 任务中基于推送的 shuffle。在 shuffle 写入路径上,Spark driver 为给定 shuffle 的 map 任务确定要使用的 ESS 列表。这个 ESS 列表作为任务上下文的一部分发送到 Spark executors,这使 map 任务能够在块组和远程 ESS 目的地之间提出上述一致的映射。Spark 驱动程序进一步协调 map 和 reduce 阶段之间的转换。一旦所有的 map 任务都完成了,Spark 驱动程序会等待一段可配置的时间,然后通知所有选择的 ESS 进行这次 shuffle 以完成合并操作。当 ESS 收到终结请求时,它停止接受来自给定 shuffle 的任何新块。它还向驱动程序响应每个最终 shuffle 分区的元数据列表,其中包括有关合并的 shuffle 文件的位置和大小信息以及指示哪些块已合并的位图。一旦 Spark 驱动程序从所有 ESS 接收到此类元数据,它就会启动 reduce 阶段。此时,Spark 驱动程序拥有 shuffle 数据位置的完整视图,现在在合并的 shuffle 文件和原始 shuffle 文件之间进行了 2 次复制。Spark 驱动程序利用此信息来协调 reduce 任务的输入位置。此外,合并的 shuffle 文件的位置为 reduce 任务创建了自然的位置偏好。Spark 驱动程序利用该信息可以在存储了 shuffle 信息的机器上启动 reduce 任务,如下图所示:

a65720210b676ff7e098284883d2244.jpg

push-based shuffle 的优势

Push-based shuffle 为 Spark shuffle 带来了几个关键好处。

提高磁盘 I/O 效率

使用 push-based shuffle,shuffle 服务在访问 shuffle 文件中的 shuffle 数据时,从小的随机读取切换到大的顺序读取,显着提高了磁盘 I/O 效率,特别是对于基于 HDD 的 shuffle 存储。在 shuffle 写路径上,即使对小块进行两次 shuffle 数据写入,整体的 I/O 效率还是有提升的。这是因为小的随机写入可以从多个级别的缓存中受益,例如操作系统页面缓存和磁盘缓冲区。因此,小随机写入可以实现比小随机读取高得多的吞吐量。改进的磁盘 I/O 效率的效果反映在本博文后面显示的性能数据中。关于 I/O 效率提升的更详细分析包含在我们的 VLDB 论文中。

缓解 shuffle 的可靠性/可扩展性问题

Spark 原生 shuffle 操作要成功,需要每个 reduce 任务成功地从所有 map 任务中获取每个相应的 shuffle 块,这在拥有数千个节点的繁忙集群中通常无法满足。Magnet 通过多种方式实现了更好的 shuffle 可靠性:

•Magnet 采用尽力而为的方法来合并块。块推送/合并过程中的失败不会影响 shuffle 的过程。•通过基于推送的 shuffle,Magnet 有效地生成了 shuffle 中间数据的第二个副本。只有在无法从原始 shuffle 文件或合并的 shuffle 文件中获取 shuffle 块时,才会发生 shuffle fetch 失败。

通过 reduce 任务的位置感知调度,它们通常在合并后的 shuffle 文件所在的机器上启动,这允许它们绕过 ESS 读取 shuffle 数据。这使得 reduce 任务对 ESS 可用性或性能问题更具弹性,从而缓解前面提到的可扩展性问题。

在块推送过程中处理 stragglers

在 Spark 的普通 shuffle 操作中,由于多个任务通常是并发运行的,任务中的掉队者(即一些任务运行速度明显慢于其他任务)的影响可以被其他任务隐藏。使用基于推送的 shuffle,如果在块推送操作中有任何落后者,它可能会暂停执行很长时间的作业。这是因为块推送操作介于 shuffle map 和 reduce 阶段之间。当存在落后者时,可能根本没有任务在运行。然而,通过提前终止技术,Magnet 可以在块推送过程中有效地处理掉队者。Magnet 不是等待 push 过程完全完成,而是限制它在 shuffle map 和 reduce 阶段之间等待的时间。Magnet 的尽力而为的特性使其能够容忍由于提前终止而未合并的块。如下图所示:

1020c9e740b8e633e78951afad09c02.jpg

与 Spark 原生集成

Magnet 与 Spark 原生集成,这带来了多种好处:

•Magnet 不依赖于其他外部系统。这有助于简化 Magnet shuffle 服务的部署、监控和生产。•通过与 Spark 的 shuffle 系统的原生集成,Magnet shuffle 服务中的元数据可以暴露给 Spark 驱动程序。这使 Spark 驱动程序能够实现更好的性能(通过任务的位置感知调度)和更好的容错(通过回退到原始 shuffle 块)。•Magnet 与现有的 Spark 功能(例如自适应查询执行)配合得很好。Spark AQE 的承诺之一是能够动态优化 skew join,这也需要对 shuffle 进行特殊处理。Spark AQE 会在多个 reducer 任务之间划分一个倾斜的 shuffle 分区,每个任务只从 mapper 任务的一个子范围中获取 shuffle 块。由于合并后的 shuffle 文件不再保持每个单独的 shuffle 块的原始边界,因此无法按照 Spark AQE 要求的方式划分合并后的 shuffle 文件。由于 Magnet 保留原始 shuffle 文件和合并后的 shuffle 文件,因此它可以委托 AQE 处理偏斜分区,同时优化非偏斜分区的 shuffle 操作。

性能对比

我们在 LinkedIn 上使用真实的生产作业评估了 Magnet 的性能,我们看到了非常不错的结果。在下表中,我们展示了运行一个 ML 特征生成作业的性能结果,该作业具有数十个 shuffle 阶段和接近 2 TB 的 shuffle 数据。与 Spark 中的原生 shuffle 相比,Magnet 取得了非常好的性能结果。请注意,Magnet 将 shuffle fetch 等待时间减少了 98%。这可以通过 Magnet 的高效随机磁盘 I/O 和减少任务的位置感知调度来实现。此外,这个作业并不完全使用 Spark SQL,因为它混合使用了 Spark SQL 和非 SQL 代码组成其计算逻辑。在优化 shuffle 操作时,Magnet 只承担很少的工作本身。

Total shuffle fetch wait time (min)Total executor task runtime (min)End-to-end job runtime (min)
Vanilla Spark shuffle206365077142
Magnet shuffle445 (-98%)29928 (-41%)31 (-26%)

我们还在 LinkedIn 引入了含有大量 shuffle 的作业。我们的一个生产集群中估计有 15% 的 shuffle 工作负载已迁移到 Magnet。在这些作业中,我们看到 shuffle fetch 等待时间、任务总运行时间和作业端到端运行时间也相应的减少。如下图所示,启用 Magnet 的 Spark 作业平均减少了 3-4 倍的随机提取等待时间。此外,我们已经看到本地访问的 shuffle 数据量增加了大约 10 倍,这表明基于推送的 shuffle 大大改善了数据局部性。最后,我们已经看到作业运行时间在集群高峰时段变得更加稳定。随着我们加入更多的作业,Magnet 将更多的 shuffle 工作负载转换为优化路径,从而减轻了 shuffle 服务的压力,并为集群带来更多好处。另一方面,Magnet 可能会将 shuffle 临时存储需求加倍。我们正在通过为 shuffle 文件切换到 zstd 压缩编解码器来缓解这种情况,与默认压缩编解码器相比,这有可能将 shuffle 文件大小减少 50%。

image.png

04a31a7e522438b1673ccfc4c4a9d4e.jpg