这是我参与「第四届青训营 」笔记创作活动的第4天,本节课了解了spark技术栈,开始Spark学习之路。
Apache spark 是一个用于大规模数据处理的一站式分析引擎。它提供了 java、 scala、 python 和 r 的高级 api,同时支持图计算。它还支持一系列丰富的高级工具,包括 sql 和结构化数据处理的 spark sql、机器学习的 mllib、图形处理的 graphx 以及增量计算和流处理的结构化流。近10余年的发展,已经形成了一个庞大的生态,包括开源的数据湖解决方案Delta Lake,也将Spark作为核心计算引擎。
Spark1.0
Hadoop对数据的处理、加工依赖引擎MapReduce,在计算过程中需要将中间数据刷写到磁盘,导致计算效率较低,并且MapReduce编程模型较为复杂,实现简单的WordCount也要写很长的代码。Spark1.0的出现,解决了部分问题,1.0阶段最重要的四个特性。1. 引入内存计算的理念解决中间结果落盘导致的效率低下。早期官网中给出数据,在理想状况下,性能可达到MR的100倍。
Spark2.0
1. 引入 Tungsten engine 进行内存优化
通过运行期间优化那些拖慢整个查询的代码到一个单独的函数中,消除虚拟函数的调用以及利用CPU寄存器来存放中间数据,
- 更好的SQL支持
在SQL支持层面,1.0阶段,SQL的很多功能并不能很好的支持,在2.0阶段,引入了ANSI SQL解析器,并且支持子查询,已经可以运行TPC-DS所有的99个查询,基本覆盖了常见的99%应用场景。在编程API方面,对API做了精简,合并dataframe和datasets,1.6的dataset包含了dataframe的功能,这样两者存在很大冗余,所以2.0将两者统一,保留dataset api,把dataframe表示为dataset[Row],即dataset的子集。
- 引入Structured Stream ing
Structured Streaming是构建在Spark SQL引擎上的流式数据处理引擎,使用户可以像使用静态RDD一样来编写流式计算过程。当流数据连续不断的产生时,Spark SQL将会增量的,持续不断的处理这些数据并将结果更新到结果集中。Structured Streaming系统通过checkpoints和write ahead logs方式保证端到端数据的准确一次性以及容错性。
Spark3.0
Spark3.0在2019年发布,在2.0的基础上又做了大量的优化,以下是最核心的几个关键特性。
动态分区裁剪( Dynamic Partition Pruning )
是指根据运行时推断出的信息来进一步进行分区裁剪,达到数据剪枝优化,在之前的版本中,无法进行动态计算代价,在运行时会扫出大量无效的数据,经过这个优化,性能大概提升了33倍。主要参数 spark.sql.optimizer.dynamicPartitionPruning.enabled = true
自适应查询( Adaptive Query Execution )
查询执行计划的优化,允许 Spark Planner 在运行时执行可选的执行计划,这些计划将基于运行时统计数据进行优化。AQE目前提供了三个功能,动态合并shuffle partitions、动态调整join策略、动态优化倾斜的join,可以通过设置 SQL 配置 spark.sql.adaptive=true 来启用 AQE,这个参数默认值为 false
支持GPU等计算加速调度:大规模机器学习中,计算迭代时间会比较长,AI从业者会使用GPU、FPGA、TPU等加速计算,hadoop3.1已经支持GPU、FPGA。目前Spark的主要两个资源调度管理器Yarn和K8S都已经支持了GPU,spark支持GPU,在技术层面需要提供用户灵活的API,控制GPU资源的使用和分配。
可插拔的CataLog:支持DataSource的自定义扩展。
Spark应用在集群上运行时,包括了多个独立的进程,这些进程之间通过驱动程序(Driver Program)中的SparkContext对象进行协调,SparkContext对象能够与多种集群资源管理器(Cluster Manager)通信,一旦与集群资源管理器连接,Spark会为该应用在各个集群节点上申请执行器(Executor),用于执行计算任务和存储数据。Spark将应用程序代码发送给所申请到的执行器,SparkContext对象将分割出的任务(Task)发送给各个执行器去运行。
需要注意的是每个Spark application都有其对应的多个executor进程。Executor进程在整个应用程序生命周期内,都保持运行状态,并以多线程方式执行任务。这样做的好处是,Executor进程可以隔离每个Spark应用。从调度角度来看,每个driver可以独立调度本应用程序的内部任务。从executor角度来看,不同Spark应用对应的任务将会在不同的JVM中运行。然而这样的架构也有缺点,多个Spark应用程序之间无法共享数据,除非把数据写到外部存储结构中。
Spark对底层的集群管理器一无所知,只要Spark能够申请到executor进程,能与之通信即可。这种实现方式可以使Spark比较容易的在多种集群管理器上运行,例如Mesos、Yarn、Kubernetes。
Driver Program在整个生命周期内必须监听并接受其对应的各个executor的连接请求,因此driver program必须能够被所有worker节点访问到。
因为集群上的任务是由driver来调度的,driver应该和worker节点距离近一些,最好在同一个本地局域网中,如果需要远程对集群发起请求,最好还是在driver节点上启动RPC服务响应这些远程请求,同时把driver本身放在离集群Worker节点比较近的机器上。
Executor
Spark Executor 是集群中工作节点(Worker)中的一个 JVM 进程,负责在 Spark 作业中运行具体任务(Task),任务彼此之间相互独立。Spark 应用启动时,Executor 节点被同时启动,并且始终伴随着整个 Spark 应用的生命周期而存在。如果有 Executor 节点发生了故障或崩溃,Spark 应用也可以继续执行,会将出错节点上的任务调度到其他 Executor 节点上继续运行。 Executor负责运行组成 Spark 应用的任务,并将结果返回给驱动器进程。它们通过自身的块管理器(Block Manager)为用户程序中要求缓存的RDD 提供内存式存储。RDD 是直接缓存在 Executor 进程内的,因此任务可以在运行时充分利用缓存数据加速运算。
2.RDD
RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是 Spark 中最基本的数据处理模型。
- 存储的弹性:内存与磁盘的自动切换;
- 容错的弹性:数据丢失可以自动恢复;
- 计算的弹性:计算出错重试机制;
- 分片的弹性:可根据需要重新分片。
- 分布式:数据存储在大数据集群不同节点上
- 数据集:RDD 封装了计算逻辑,并不保存数据
- 数据抽象:RDD 是一个抽象类,需要子类具体实现
- 不可变:RDD 封装了计算逻辑,是不可以改变的,想要改变,只能产生新的 RDD,在新的 RDD 里面封装计算逻辑
- 可分区、并行计算
RDD处理方式类似于IO,也是装饰者设计模式这里一层一层的RDD只是定义了数据的处理逻辑,并不会真正执行,只有当触发collect方法时才会触发真正的执行过程。IO中有缓冲区;RDD是没有缓冲区的,不保存数据
划分Stage的整体思路:从后往前推,遇到宽依赖就断开,划分为一个Stage。遇到窄依赖,就将这个RDD加入该Stage中,DAG最后一个阶段会为每个结果的Partition生成一个ResultTask。每个Stage里面的Task数量由最后一个RDD的Partition数量决定,其余的阶段会生成ShuffleMapTask。
当RDD对象创建后,SparkContext会根据RDD对象构建DAG有向无环图,然后将Task提交给DAGScheduler。DAGScheduler根据ShuffleDependency将DAG划分为不同的Stage,为每个Stage生成TaskSet任务集合,并以TaskSet为单位提交给TaskScheduler。TaskScheduler根据调度算法(FIFO/FAIR)对多个TaskSet进行调度,并通过集群中的资源管理器(Standalone模式下是Master,Yarn模式下是ResourceManager)把Task调度(locality)到集群中Worker的Executor,Executor由SchedulerBackend提供。
Spark调度
在使用spark-summit或者spark-shell提交spark程序后,根据提交时指定(deploy-mode)的位置,创建driver进程,driver进程根据sparkconf中的配置,初始化sparkcontext。Sparkcontext的启动后,创建DAG Scheduler(将DAG图分解成stage)和Task Scheduler(提交和监控)两个调度task模块。
Spark 作为一个基于内存的分布式计算引擎,Spark采用统一内存管理机制。重点在于动态占用机制。
设定基本的存储内存(Storage)和执行内存(Execution)区域,该设定确定了双方各自拥有的空间的范围,UnifiedMemoryManager统一管理Storage/Execution内存
双方的空间都不足时,则存储到硬盘;若己方空间不足而对方空余时,可借用对方的空间
当Storage空闲,Execution可以借用Storage的内存使用,可以减少spill等操作, Execution内存不能被Storage驱逐。Execution内存的空间被Storage内存占用后,可让对方将占用的部分转存到硬盘,然后"归还"借用的空间
当Execution空闲,Storage可以借用Execution内存使用,当Execution需要内存时,可以驱逐被Storage借用的内存,可让对方将占用的部分转存到硬盘,然后"归还"借用的空间
user memory存储用户自定义的数据结构或者spark内部元数据
Reserverd memory:预留内存,防止OOM,
堆内(On-Heap)内存/堆外(Off-Heap)内存:Executor 内运行的并发任务共享 JVM 堆内内存。为了进一步优化内存的使用以及提高 Shuffle 时排序的效率,Spark 可以直接操作系统堆外内存,存储经过序列化的二进制数据。减少不必要的内存开销,以及频繁的 GC 扫描和回收,提升了处理性能。
Shuffle Write
Shuffle Write,即数据是如何持久化到文件中,以使得下游的Task可以获取到其需要处理的数据的(即Shuffle Read)。在Spark 0.8之前,Shuffle Write是持久化到缓存的,但后来发现实际应用中,shuffle过程带来的数据通常是巨量的,所以经常会发生内存溢出的情况,所以在Spark 0.8以后,Shuffle Write会将数据持久化到硬盘,再之后Shuffle Write不断进行演进优化,但是数据落地到本地文件系统的实现并没有改变。
1)Hash Based Shuffle Write
在Spark 1.0以前,Spark只支持Hash Based Shuffle。因为在很多运算场景中并不需要排序,因此多余的排序只能使性能变差,比如Hadoop的Map Reduce就是这么实现的,也就是Reducer拿到的数据都是已经排好序的。实际上Spark的实现很简单:每个Shuffle Map Task根据key的哈希值,计算出每个key需要写入的Partition然后将数据单独写入一个文件,这个Partition实际上就对应了下游的一个Shuffle Map Task或者Result Task。因此下游的Task在计算时会通过网络(如果该Task与上游的Shuffle Map Task运行在同一个节点上,那么此时就是一个本地的硬盘读写)读取这个文件并进行计算。
Hash Based Shuffle Write存在的问题
由于每个Shuffle Map Task需要为每个下游的Task创建一个单独的文件,因此文件的数量就是:number(shuffle_map_task)*number(result_task)。如果Shuffle Map Task是1000,下游的Task是500,那么理论上会产生500000个文件(对于size为0的文件Spark有特殊的处理)。生产环境中Task的数量实际上会更多,因此这个简单的实现会带来以下问题:
- 每个节点可能会同时打开多个文件,每次打开文件都会占用一定内存。假设每个Write Handler的默认需要100KB的内存,那么同时打开这些文件需要50GB的内存,对于一个集群来说,还是有一定的压力的。尤其是如果Shuffle Map Task和下游的Task同时增大10倍,那么整体的内存就增长到5TB。
- 从整体的角度来看,打开多个文件对于系统来说意味着随机读,尤其是每个文件比较小但是数量非常多的情况。而现在机械硬盘在随机读方面的性能特别差,非常容易成为性能的瓶颈。如果集群依赖的是固态硬盘,也许情况会改善很多,但是随机写的性能肯定不如顺序写的。
2)Sort Based Shuffle Write
在Spark 1.2.0中,Spark Core的一个重要的升级就是将默认的Hash Based Shuffle换成了Sort Based Shuffle,即spark.shuffle.manager从Hash换成了Sort,对应的实现类分别是org.apache.spark.shuffle.hash.HashShuffleManage和org.apache.spark.shuffle.sort.SortShuffleManager。
正如前面提到的,Hash Based Shuffle的每个Mapper都需要为每个Reducer写一个文件,供Reducer读取,即需要产生M*R个数量的文件,如果Mapper和Reducer的数量比较大,产生的文件数会非常多。而Sort Based Shuffle的模式是:每个Shuffle Map Task不会为每个Reducer生成一个单独的文件;相反,它会将所有的结果写到一个文件里,同时会生成一个Index文件。
参考文献
(34条消息) Spark Shuffle详解_帅成一匹马的博客-CSDN博客_sparkshuffle