Spark 原理与实践

89 阅读16分钟

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

Spark 原理与实践

概述

本课程主要有以下几个目标:

image.png

课前

大数据处理引擎Spark介绍

Spark生态组件:

  • Spark Core:Spark核心组件,它实现了Spark的基本功能,包含任务调度、内存管理、错误恢复、与存储系统交互等模块。
  • Spark SQL:用来操作结构化数据的核心组件,通过Spark SQL可以直接查询Hive、HBase等多种外部数据源中的数据。
  • Spark Structured Streaming:Spark提供的流式计算框架,支持高吞吐量、可容错处理的实时流式数据处理。
  • MLlib:Spark提供的关于机器学习功能的算法程序库,包括分类、回归、聚类、协同过滤算法等,还提供了模型评估、数据导入等额外的功能。
  • GraphX:Spark提供的分布式图处理框架,拥有对图计算和图挖掘算法的API接口以及丰富的功能和运算符。
  • 独立调度器、Yarn、Mesos、Kubernetes:Spark框架可以高效地在一个到数千个节点之间伸缩计算,集群管理器则主要负责各个节点的资源管理工作,为了实现这样的要求,同时获得最大灵活性,Spark支持在各种集群管理器(Cluster Manager)上运行。

Spark 运行架构和工作原理:

  • Application(应用):Spark上运行的应用。Application中包含了一个驱动器(Driver)进程和集群上的多个执行器(Executor)进程。
  • Driver Program(驱动器):运行main()方法并创建SparkContext的进程。
  • Cluster Manager(集群管理器):用于在集群上申请资源的外部服务(如:独立部署的集群管理器、Mesos或者Yarn)。
  • Worker Node(工作节点):集群上运行应用程序代码的任意一个节点。
  • Executor(执行器):在集群工作节点上为某个应用启动的工作进程,该进程负责运行计算任务,并为应用程序存储数据。
  • Task(任务):执行器的工作单元。
  • Job(作业):一个并行计算作业,由一组任务(Task)组成,并由Spark的行动(Action)算子(如:save、collect)触发启动。
  • Stage(阶段):每个Job可以划分为更小的Task集合,每组任务被称为Stage。

Spark目前支持几个集群管理器:

  • Standalone :Spark 附带的简单集群管理器,可以轻松设置集群。
  • Apache Mesos:通用集群管理器,也可以运行 Hadoop MapReduce 和服务应用程序。(已弃用)
  • Hadoop YARN: Hadoop 2 和 3 中的资源管理器。
  • Kubernetes:用于自动部署、扩展和管理容器化应用程序的开源系统。

SparkCore

RDD(Resilient Distributed Dataset):弹性分布式数据集,是一个容错的、并行的数据结构

RDD算子:对任何函数进行某一项操作都可以认为是一个算子,RDD算子是RDD的成员函数

Transform(转换)算子: 根据已有RDD创建新的RDD

Action(动作)算子: 将在数据集上运行计算后的数值返回到驱动程序,从而触发真正的计算

DAG(Directed Acyclic Graph): 有向无环图,Spark中的RDD通过一系列的转换算子操作和行动算子操作形成了一个DAG

DAGScheduler:将作业的DAG划分成不同的Stage,每个Stage都是TaskSet任务集合,并以TaskSet为单位提交给TaskScheduler。

TaskScheduler:通过TaskSetManager管理Task,并通过集群中的资源管理器(Standalone模式下是Master,Yarn模式下是ResourceManager)把Task发给集群中Worker的Executor

Shuffle:Spark中数据重分发的一种机制。

SparkSQL

DataFrame: 是一种以RDD为基础的分布式数据集, 被称为SchemaRDD

Catalyst:SparkSQL核心模块,主要是对执行过程中的执行计划进行处理和优化

DataSource:SparkSQL支持通过 DataFrame 接口对各种数据源进行操作。

Adaptive Query Execution:自适应查询执行

Runtime Filter:运行时过滤

Codegen:生成程序代码的技术或系统,可以在运行时环境中独立于生成器系统使用

SparkSql执行过程:

  • Unresolved Logical Plan:未解析的逻辑计划,仅仅是数据结构,不包含任何数据信息。
  • Logical Plan:解析后的逻辑计划,节点中绑定了各种优化信息。
  • Optimized Logical Plan:优化后的逻辑计划
  • Physical Plans:物理计划列表
  • Selected Physical Plan 从列表中按照一定的策略选取最优的物理计划

业界挑战与实践

向量化(vectorization):将循环转换为向量操作的编译器优化

代码生成(Codegen:Code generation):生成程序代码的技术或系统,可以在运行时环境中独立于生成器系统使用

spark集群搭建

安装配置基础Spark

【在test-cluster-hap-master-01虚拟主机上】

将已下载好的Spark压缩包(spark-3.1.1-bin-hadoop-3.2.2-lbx-jszt.tgz)通过工具【XFtp】拷贝到虚拟主机的opt目录下:

通过脚本挂起镜像

cd /opt/script/setup/spark

test-cluster-hap-master-01

 #!/bin/bash 
 #===========================================================================
 #编写作者:邵桐杰
 #创建时间:2022-07-28
 #程序说明:虚拟主机启动时自动执行脚本,固定容器IP、端口开放等。
 #---------------------------------------------------------------------------
 #程序版本修改历史
 #---------------------------------------------------------------------------
 #
 #
 #===========================================================================
 ​
 cname="test-cluster-spk-master-01"
 ​
 #port1="8080"
 #port2="7077"
 log="/opt/data/"${cname}
 images="10.249.0.137:80/base/jdk-1.8:20210202"
 ​
 mkdir -p ${log}
 mkdir ${log}/logs
 mkdir ${log}/work
 mkdir ${log}/data
 mkdir ${log}/jars
 ​
 #docker run -d --net=overlay-net --ip ${ip} -p ${port1}:${port1} -p ${port2}:${port2} --name ${cname} --hostname ${cname} --privileged=true --restart=always 
 docker run -d --net=host --name ${cname} --hostname ${cname} --privileged=true --restart=always \
 -v ${log}/logs:/usr/local/spark-3.1.1/logs \
 -v ${log}/work:/usr/local/spark-3.1.1/work \
 -v ${log}/jars:/usr/local/spark-3.1.1/jars \
 -v ${log}/data:/opt/data \
 ${images} \
 /usr/sbin/init

test-cluster-hap-master-02

 #!/bin/bash 
 #===========================================================================
 #编写作者:邵桐杰
 #创建时间:2022-07-28
 #程序说明:虚拟主机启动时自动执行脚本,固定容器IP、端口开放等。
 #---------------------------------------------------------------------------
 #程序版本修改历史
 #---------------------------------------------------------------------------
 #
 #
 #===========================================================================
 ​
 cname="test-cluster-spk-master-02"
 ​
 #port1="8080"
 #port2="7077"
 log="/opt/data/"${cname}
 images="10.249.0.137:80/base/jdk-1.8:20210202"
 ​
 mkdir -p ${log}
 mkdir ${log}/logs
 mkdir ${log}/work
 mkdir ${log}/data
 mkdir ${log}/jars
 ​
 #docker run -d --net=overlay-net --ip ${ip} -p ${port1}:${port1} -p ${port2}:${port2} --name ${cname} --hostname ${cname} --privileged=true --restart=always 
 docker run -d --net=host --name ${cname} --hostname ${cname} --privileged=true --restart=always \
 -v ${log}/logs:/usr/local/spark-3.1.1/logs \
 -v ${log}/work:/usr/local/spark-3.1.1/work \
 -v ${log}/jars:/usr/local/spark-3.1.1/jars \
 -v ${log}/data:/opt/data \
 ${images} \
 /usr/sbin/init

test-cluster-spk-slave-01

 #!/bin/bash 
 #===========================================================================
 #编写作者:邵桐杰
 #创建时间:2022-07-28
 #程序说明:虚拟主机启动时自动执行脚本,固定容器IP、端口开放等。
 #---------------------------------------------------------------------------
 #程序版本修改历史
 #---------------------------------------------------------------------------
 #
 #
 #===========================================================================
 ​
 cname="test-cluster-spk-slave-01"
 ​
 #port1="8080"
 #port2="7077"
 log="/opt/data/"${cname}
 images="10.249.0.137:80/base/jdk-1.8:20210202"
 ​
 mkdir -p ${log}
 mkdir ${log}/logs
 mkdir ${log}/work
 mkdir ${log}/data
 mkdir ${log}/jars
 ​
 #docker run -d --net=overlay-net --ip ${ip} -p ${port1}:${port1} -p ${port2}:${port2} --name ${cname} --hostname ${cname} --privileged=true --restart=always 
 docker run -d --net=host --name ${cname} --hostname ${cname} --privileged=true --restart=always \
 -v ${log}/logs:/usr/local/spark-3.1.1/logs \
 -v ${log}/work:/usr/local/spark-3.1.1/work \
 -v ${log}/jars:/usr/local/spark-3.1.1/jars \
 -v ${log}/data:/opt/data \
 ${images} \
 /usr/sbin/init
 [root@zookeeper-03-test spark]# ll
 总用量 4
 -rw-r--r--. 1 root root 1166 7月  28 17:44 install.sh
 [root@zookeeper-03-test spark]# chmod +x install.sh 
 [root@zookeeper-03-test spark]# ll
 总用量 4
 -rwxr-xr-x. 1 root root 1166 7月  28 17:44 install.sh
 [root@zookeeper-03-test spark]#

在容器映射目录下 :/opt/data/test-cluster-spk-slave-01/data

 [root@hadoop-01 data]# pwd
 /opt/data

用Xshell上传包

这里需要上传两个,使用的是spark-3.1.1-bin-without-hadoop.tgz

但是需要将spark-3.1.1-bin-hadoop-3.2.2-lbx-jszt下的jars包移到/usr/local/spark-3.1.1/jars下

(1)解压安装包

 mkdir -p /usr/local/spark-3.1.1
 cd /opt/data
 tar -zxvf spark-3.1.1-bin-without-hadoop.tgz -C /usr/local/spark-3.1.1


(2)编辑全局变量

 vim /etc/profile

增加以下全局变量

 export SPARK_HOME=/usr/local/spark-3.1.1            
 export PATH=$PATH:$SPARK_HOME/bin

#即时生效

 source /etc/profile

(3)配置spark-env.sh

 cd /usr/local/spark-3.1.1/conf
 cp spark-env.sh.template spark-env.sh
 vim spark-env.sh
 export SPARK_MASTER_IP=test-cluster-spk-master-01
 export SPARK_WORKER_CORES=1
 export SPARK_WORKER_MEMORY=800m
 #export SPARK_DRIVER_MEMORY=4g
 export SPARK_EXECUTOR_INSTANCES=2
 export HADOOP_CONF_DIR=/usr/local/hadoop/etc/hadoop
 export SPARK_LOCAL_DIRS=/home/hadoop/tmp/spark/tmp
 ​
 #定时清理worker文件 一天一次
 export SPARK_WORKER_OPTS="  
 -Dspark.worker.cleanup.enabled=true  
 -Dspark.worker.cleanup.interval=86400 
 -Dspark.worker.cleanup.appDataTtl=86400"
 ​
 export JAVA_HOME=/usr/local/jdk1.8
 export HADOOP_HOME=/usr/local/hadoop
 export HADOOP_CONF_DIR=$HADOOP_HOME/etc/hadoop
 export SCALA_HOME=/usr/local/scala
 export PATH=${SCALA_HOME}/bin:$PATH
 export SPARK_DAEMON_JAVA_OPTS="-Dspark.deploy.recoveryMode=ZOOKEEPER -Dspark.deploy.zookeeper.url=zookeeper-01-test:2181,zookeeper-02-test:2181,zookeeper-03-test:2181 -Dspark.deploy.zookeeper.dir=/usr/local/spark"
 ​

(4)配置workers

 cp workers.template workers
 vim workers
 # 添加
 test-cluster-spk-slave-001

(5)配置log4j.properties

 cp log4j.properties.template log4j.properties
 vim log4j.properties
 ​
 log4j.rootCategory=WARN, console

(宿主机上)复制拷贝Spark到其他Slaves节点:

 scp -r /usr/local/spark/spark-2.1.0-bin-hadoop2.7 root@slave-001-spark-dev:/usr/local/spark/
 ​
 scp -r /usr/local/spark/spark-2.1.0-bin-hadoop2.7 root@slave-002-spark-dev:/usr/local/spark/
 ​
 scp -r /usr/local/spark/spark-2.1.0-bin-hadoop2.7 root@slave-003-spark-dev:/usr/local/spark/

如执行命令出现出现问题时,请现在相应的Slave节点执行mkdir -p /usr/local/spark

复制到master-02时,使用start-mater.sh启动master-02

启动

  1. 先启动两个master,然后启动slave节点

    1. 主节点1启动完成
     [root@test-cluster-spk-master-01 sbin]# ./start-master.sh 
     starting org.apache.spark.deploy.master.Master, logging to /usr/local/spark-3.1.1/logs/spark-root-org.apache.spark.deploy.master.Master-1-test-cluster-spk-master-01.out
     [root@test-cluster-spk-master-01 sbin]# jps
     548 Jps
     492 Master
     [root@test-cluster-spk-master-01 sbin]# pwd
     /usr/local/spark-3.1.1/sbin
     [root@test-cluster-spk-master-01 sbin]#
    

    1. 主节点2启动完成

       [root@test-cluster-spk-master-02 sbin]# ./start-master.sh 
       starting org.apache.spark.deploy.master.Master, logging to /usr/local/spark-3.1.1/logs/spark-root-org.apache.spark.deploy.master.Master-1-test-cluster-spk-master-02.out
       [root@test-cluster-spk-master-02 sbin]# pwd
       /usr/local/spark-3.1.1/sbin
       [root@test-cluster-spk-master-02 sbin]# jps
       274 Jps
       218 Master
       [root@test-cluster-spk-master-02 sbin]#
      

    2. 从节点启动完成

       /usr/local/spark-3.1.1/sbin/start-slave.sh test-cluster-hap-slave-001 test-cluster-hap-master-02:7077,test-cluster-hap-master-02:7077
      

验证

原本是访问http://10.8.46.35:8080 就可,但是我这里在配置镜像的时候,多了8080,导致这里访问不了。看日志可以知道,已经走向8081

所以http://10.8.46.35:8081/即可

主节点1主节点2从节点1
停掉主节点从节点成为ALIVE从节点1

这里遇到了许多问题,第一个是包不兼容,导致搭建两次失败

然后换了官方的包spark-3.1.1-bin-without-hadoop,启动还是有问题。

最后通过替换jars才成功。(使用spark-3.1.1-bin-hadoop-3.2.2-lbx-jszt下的jars)

Spark介绍

目标:了解常见的大数据处理pipeline。了解spark技术栈。通过提交一个基本的Spark程序开始Spark学习之路。

Spark运行架构和工作原理

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节点比较近的机器上。

SparkCore:

目标:认识spark的核心概念RDD,RDD两种算子处理过程,理解RDD依赖,学习RDD在Spark中的执行流程。了解spark中调度、内存管理机制、shuffle机制。

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采用统一内存管理机制。重点在于动态占用机制。

  • 设定基本的存储内存(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 扫描和回收,提升了处理性能。

SparkSQL:

目标:了解SQL执行链路。重点学习核心模块calalyst优化器以及SparkSQL三大重点特性(Codegen/AQE/RuntimeFilter)

SparkSQL执行过程

  • SQL Parse: 将SparkSQL字符串或DataFrame解析为一个抽象语法树/AST,即Unresolved Logical Plan
  • Analysis:遍历整个AST,并对AST上的每个节点进行数据类型的绑定以及函数绑定,然后根据元数据信息Catalog对数据表中的字段进行解析。 利用Catalog信息将Unresolved Logical Plan解析成Analyzed Logical plan
  • Logical Optimization:该模块是Catalyst的核心,主要分为RBO和CBO两种优化策略,其中RBO是基于规则优化,CBO是基于代价优化。 利用一些规则将Analyzed Logical plan解析成Optimized Logic plan
  • Physical Planning: Logical plan是不能被spark执行的,这个过程是把Logic plan转换为多个Physical plans
  • CostModel: 主要根据过去的性能统计数据,选择最佳的物理执行计划(Selected Physical Plan)。
  • Code Generation: sql逻辑生成Java字节码

影响SparkSQL性能两大技术:

  1. Optimizer:执行计划的优化,目标是找出最优的执行计划
  1. Runtime:运行时优化,目标是在既定的执行计划下尽可能快的执行完毕。

Catalyst优化

  1. Rule Based Optimizer(RBO): 基于规则优化,对语法树进行一次遍历,模式匹配能够满足特定规则的节点,再进行相应的等价转换。
  1. Cost Based Optimizer(CBO): 基于代价优化,根据优化规则对关系表达式进行转换,生成多个执行计划,然后CBO会通过根据统计信息(Statistics)和代价模型(Cost Model)计算各种可能执行计划的代价,从中选用COST最低的执行方案,作为实际运行方案。CBO依赖数据库对象的统计信息,统计信息的准确与否会影响CBO做出最优的选择。

AQE

AQE对于整体的Spark SQL的执行过程做了相应的调整和优化,它最大的亮点是可以根据已经完成的计划结点真实且精确的执行统计结果来不停的反馈并重新优化剩下的执行计划。

AQE框架三种优化场景:

  • 动态合并shuffle分区(Dynamically coalescing shuffle partitions)
  • 动态调整Join策略(Dynamically switching join strategies)
  • 动态优化数据倾斜Join(Dynamically optimizing skew joins)

RuntimeFilter

实现在Catalyst中。动态获取Filter内容做相关优化,当我们将一张大表和一张小表等值连接时,我们可以从小表侧收集一些统计信息,并在执行join前将其用于大表的扫描,进行分区修剪或数据过滤。可以大大提高性能

Runtime优化分两类:

  1. 全局优化:从提升全局资源利用率、消除数据倾斜、降低IO等角度做优化。包括AQE。
  1. 局部优化:提高某个task的执行效率,主要从提高CPU与内存利用率的角度进行优化。依赖Codegen技术。

Codegen

从提高cpu的利用率的角度来进行runtime优化。

  1. Expression级别

表达式常规递归求值语法树。需要做很多类型匹配、虚函数调用、对象创建等额外逻辑,这些overhead远超对表达式求值本身,为了消除这些overhead,Spark Codegen直接拼成求值表达式的java代码并进行即时编译

  1. WholeStage级别

传统的火山模型:SQL经过解析会生成一颗查询树,查询树的每个节点为Operator,火山模型把operator看成迭代器,每个迭代器提供一个next()接口。通过自顶向下的调用 next 接口,数据则自底向上的被拉取处理,火山模型的这种处理方式也称为拉取执行模型,每个Operator 只要关心自己的处理逻辑即可,耦合性低。

火山模型问题:数据以行为单位进行处理,不利于CPU cache 发挥作用;每处理一行需要调用多次next() 函数,而next()为虚函数调用。会有大量类型转换和虚函数调用。虚函数调用会导致CPU分支预测失败,从而导致严重的性能回退

Spark WholestageCodegen:为了消除这些overhead,会为物理计划生成类型确定的java代码。并进行即时编译和执行。

Codegen打破了Stage内部算子间的界限,拼出来跟原来的逻辑保持一致的裸的代码(通常是一个大循环)然后把拼成的代码编译成可执行文件。

链接:juejin.cn/post/712390…