Spark 原理与实践

236 阅读10分钟

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

内容不是很友好,只看到spark的内存管理,其它的spark core和spark sql都没开始

image.png

大数据处理引擎spark介绍

大数据处理技术栈

image.png

大数据处理链路

image.png

开源大数据处理引擎

Screenshot 2022-07-30 165615.jpg

spark是什么

spark是一个支持多种语言(python,Java,go,sql)的数据处理,数据科学,机器学习引擎,适用于单点或者集群

image.png

  • 机器学习上,可以单机训练机器学习算法,可拓展到大规模集群上

image.png

3.0 & 3.3 new features:

  • adaptive query execution 自适应查询
  • dynamic partition pruning 动态分区裁剪
  • accelerator-aware scheduling 支持GPU等计算加速的调度
  • Bloom filter joins 过滤器提升join查询性能
  • query execution enhancements 增强自适应查询 ...

spark生态&features

之前讲One SQL rules big data all的时候顺带说过的spark,mr,flink本身不提供sql接口,只提供了RDD,一些api,后来才逐渐形成自身的模块,比如spark sql,操作结构化数据,通过操作spark sql查询hive,hbase等数据源,可进行交互式查询;

在其之上,有spark structured streaming,这是spark提供的流式计算框架,来支持高吞吐,可容错处理的实时流式数据;

还有MLlib这样的模块,这是spark提供的机器学习算法程序库,包括常见的分类,回归,聚类等算法

image.png

丰富数据源

  • 内置datasource:text,parquet/orc,json/csv,jdbc
  • 自定义datasource:实现datasource v1/v2 api,hbase/mongo/elastic search,还有很多第三方库

丰富的API/算子

  • RDD:弹性分布式数据集,是一个容错,并行的数据结构,提供了很多算子 image.png
  • SparkSQL->DataFrame:也是一个以RDD为基础的分布式数据集,对hive和sql的操作友好 image.png

spark运行架构&部署方式

标准master/slave架构

image.png

cluster manager:spark的集群管理器,控制整个集群,监控worker节点,负责资源管理和调度;worker节点作为从节点,在yarn种可称为node manager,负责控制计算节点,启动executor(相当于slave,实际执行任务者),负责task执行;对于一个任务而言,driver program相当于一个app master,是整个应用的管理者,一个spark application只有一个driver,负责作业的任务调度,是一个JVM进程,运行程序的main函数,创建一个sparkContext的上下文,来控制应用的生命周期;

用户程序从最开始提交到最后计算:

  1. 用户创建一个sparkContext,sparkContext连接到clusterManager
  2. clusterManager根据用户提交时设置的参数(cpu,内存)为本次submit分配计算资源,启动executor
  3. Driver会将用户程序划分为不同的stage,每个stage含有完全相同的一组task,这些task会 分别作用于一些待处理数据和不同分区;
  4. 在阶段划分完,task也创建完后,drive会向executor发送task,executor在接收到task之后,会下载task的运行时的依赖,准备好task的执行环境,开始执行task,并实时将task的状态汇报给driver,driver会根据回报的状态做状态更新
  5. 不断调用task,发给executor执行,直到所有task都执行正确,或者超限

部署方式

  • spark local mode:本地测试/单进程多线程模式
  • spark standalone mode:需要启动spark的standalone集群的master/worker,类似mr1.o框架,spark框架自带了完整的资源调度管理服务,可以独立部署到一个集群中,而不需要依赖其他系统来为其提供资源管理调度服务。由一个Master和若干个Slave构成,并且以槽(slot)作为资源分配单位。不同的是,Spark中的槽不再像MapReduce1.0那样分为Map 槽和Reduce槽,而是只设计了统一的一种槽提供给各种任务来使用。
  • yarn/k8s:依赖外部资源调度器(yarn/k8s),运行于Hadoop之上,资源管理和调度依赖yarn,分布式存储规则依赖hdfs

Spark三种部署方式

动手搭一下,运行一个简单任务就好了

sparkCore原理解析

RDD

五要素

分区列表compute函数依赖partitioner优先位置
每个RDD都有一个分区,分布在集群的不同节点上,每个分区都会被一个计算任务处理,分区决定了并行计算的数量,创建RDD时,可以指定分区;如果不指定分区个数,比如从collection创建,默认的分区数即CPU核数;如果是从hdfs文件存储中创建的话,默认的分区数就是文件的block数RDD以partition为基本单位,每个RDD实现一个compute函数每一个RDD会依赖其它的RDD,RDD每次转换会生成一个新的RDD,所以会像pipeline一样,前后依赖,如果部分分区数据丢失,可以通过这个依赖关系,重新计算丢失的分区数据,而不是全盘重算有hash partitioner和range partitioner,hashPartitoner作为默认分区器,会对数据的key进行key.hashcode%numpartitions计算,得到的数值会放到对应的分区中,这样能较为平衡的分配器数据到各分区;另一种基于范围的分区器,它是在排序算子中常用到的分区器,比如sortbykey,sortby,orderby等,先对输入的数据的key做采样,来估算key的分布,然后按照指定的排序切分range,尽量让每个partition对应的range里的key分布均匀。列表存取每个partition的优先位置,对于一个hdfs文件来说,这个列表保存的就是每个分区所在的块的位置,按照“移动数据不如移动计算”的理念来说,spark在进行任务调度时,会尽可能选择那些存有数据的worker节点来进行任务计算。

Resilient Distributed Dataset

是一个应用层面的逻辑概念,一个RDD多个分片,RDD就是一个元数据记录集,记录了RDD内存所有的关系数据。

RDD依赖

宽依赖:会产生shuffle,父RDD的每个partition都可能对应多个子RDD分区 窄依赖:父RDD的每个分区至多对应一个子RDD分区(Narrowdependency,Rangedependency,onetooneDependency,pruneDependency)

执行流程

image.png

内存管理

spark作为一个基于内存的分布式计算引擎,其内存管理模块在整个系统中扮演着重要的角色,理解spark内存管理的基本原理,有助于更好的开发spark应用程序和进行性能调优。

主要是对executor的内存管理进行分析,下面内存特指executor中的内存

image.png

在执行spark应用程序时,spark集群会启动driver(主控程序,负责上下文,提交spark作业job,并将作业转化为计算任务task,在各个executor进程间协调任务的调度)和executor(负责在工作节点上执行具体的计算任务,并将结果返回给Driver,同时为需要持久化的RDD提供存储功能)两种JVM进程。

堆内内存,堆外内存

作为一个JVM进程,Executor的内存管理建立在JVM的内存管理之上,Spark对JVM的堆内(On-heap)空间进行了更为详细的分配,以充分利用内存。同时,Spark引入了堆外(Off-heap)内存,使之可以直接在工作节点的系统内存中开辟空间,进一步优化了内存的使用。

image.png

堆内内存

堆内内存的大小,由Spark应用程序启动时的–executor-memory或spark.executor.memory参数配置。Executor内运行的并发任务共享JVM堆内内存,这些任务在缓存RDD和广播(Broadcast)数据时占用的内存被规划为存储(Storage)内存,而这些任务在执行Shuffle时占用的内存被规划为执行(Execution)内存,剩余的部分不做特殊规划,那些Spark内部的对象实例,或者用户定义的Spark应用程序中的对象实例,均占用剩余的空间。不同的管理模式下,这三部分占用的空间大小各不相同(下面会进行介绍)。

Spark对堆内内存的管理是一种逻辑上的“规划式”的管理,因为对象实例占用内存的申请和释放都由JVM完成,Spark只能在申请后和释放前记录这些内存,我们来看其具体流程:

  • 申请内存:
    • Spark在代码中new一个对象实例
    • JVM从堆内内存分配空间,创建对象并返回对象引用
    • Spark保存该对象的引用,记录该对象占用的内存
  • 释放内存:
    • Spark记录该对象释放的内存,删除该对象的引用
    • 等待JVM的垃圾回收机制释放该对象占用的堆内内存

我们知道,JVM的对象可以以序列化的方式存储,序列化的过程是将对象转换为二进制字节流,本质上可以理解为将非连续空间的链式存储转化为连续空间或块存储,在访问时则需要进行序列化的逆过程——反序列化,将字节流转化为对象,序列化的方式可以节省存储空间,但增加了存储和读取时候的计算开销。

对于Spark中序列化的对象,由于是字节流的形式,其占用的内存大小可直接计算,而对于非序列化的对象,其占用的内存是通过周期性地采样近似估算而得,即并不是每次新增的数据项都会计算一次占用的内存大小,这种方法降低了时间开销但是有可能误差较大,导致某一时刻的实际内存有可能远远超出预期。此外,在被Spark标记为释放的对象实例,很有可能在实际上并没有被JVM回收,导致实际可用的内存小于Spark记录的可用内存。所以Spark并不能准确记录实际可用的堆内内存,从而也就无法完全避免内存溢出(OOM, Out of Memory)的异常。

虽然不能精准控制堆内内存的申请和释放,但Spark通过对存储内存和执行内存各自独立的规划管理,可以决定是否要在存储内存里缓存新的RDD,以及是否为新的任务分配执行内存,在一定程度上可以提升内存的利用率,减少异常的出现。

堆外内存

为了进一步优化内存的使用以及提高Shuffle时排序的效率,Spark引入了堆外(Off-heap)内存,使之可以直接在工作节点的系统内存中开辟空间,存储经过序列化的二进制数据。利用JDK Unsafe API(从Spark 2.0开始,在管理堆外的存储内存时不再基于Tachyon,而是与堆外的执行内存一样,基于JDK Unsafe API实现),Spark可以直接操作系统堆外内存,减少了不必要的内存开销,以及频繁的GC扫描和回收,提升了处理性能。堆外内存可以被精确地申请和释放,而且序列化的数据占用的空间可以被精确计算,所以相比堆内内存来说降低了管理的难度,也降低了误差。

在默认情况下堆外内存并不启用,可通过配置spark.memory.offHeap.enabled参数启用,并由spark.memory.offHeap.size参数设定堆外空间的大小。除了没有other空间,堆外内存与堆内内存的划分方式相同,所有运行中的并发任务共享存储内存和执行内存。

Spark 内存管理(堆内/堆外)详解