Spark权威指南读书笔记(七) Spark生产与应用
Spark权威指南读书笔记(七) Spark生产与应用
一、Spark运行
Spark应用程序体系
Spark驱动器
Spark驱动器是控制你应用程序的进程。它负责控制整个Spark引用程序的执行并且维护Spark集群状态,即执行器任务和状态,它必须与集群管理器交互才能获得物理资源并启动执行器。他只是一个物理机器上的一个进程,负责维护集群上运行的应用程序状态。
Spark执行器
Spark执行器是一个进程,它负责执行由Spark驱动器分配的任务。其核心功能为完成驱动器分配的任务,执行并报告其状态和执行结果。每个Spark应用程序都有自己的执行器进程。
集群管理器
集群管理器负责维护一组运行Spark应用程序的机器。集群管理器也拥有master和worker抽象,核心区别在于集群管理器管理的是物理机器,而不是进程。在实际运行Spark应用程序时,会首先从集群管理器请求资源。在这个执行过程中,集群管理器负责管理执行应用程序的底层机器。
执行模式
Spark 提供三种执行模式:
- 集群模式
- 客户端模式
- 本地模式
集群模式
常见方式。在集群模式下,用户将预编译的JAR包、Python脚本或R语言脚本提交给集群管理器。除执行器进程外,集群管理器还会在集群内的某个工作节点上启动驱动器进程,这意味集群管理器负责维护所有与Spark应用进程相关的进程。
客户端模式
与集群模式几乎相同,Spark驱动器保留在提交应用程序的客户端机器上,这意味着客户端机器负责维护Spark驱动器进程,并且集群管理器维护执行器进程。若使用集群外的一台机器运行Spark应用程序,这些机器通常被称为网关机器或边缘节点。
本地模式
在一台机器上运行整个Spark应用程序,通过单机上的线程实现并行性。
Spark应用程序生命周期(以集群模式为例)
外部角度
客户请求
提交一个应用程序(编译好的JAR包或者是库),并向集群管理器驱动节点发出请求,为Spark驱动器显示申请资源。若集群管理器接受请求并将驱动器放置到集群中的一个物理节点上,之后提交原始作业的客户端进程退出,应用程序开始在集群上运行。
启动
此时驱动器进程已放置与集群中,它开始执行用户代码。此代码必须包括一个初始化Spark集群的SparkSession。SparkSession随后与集群管理器驱动节点通信,要求它在集群上启动Spark执行器,集群管理器随后在集群工作节点上启动执行器,执行器数量及其相关配置由用户于Spark submit调用中的命令行提交。一切顺利的化,集群管理器会启动Spark执行器,并将程序执行位置等相关信息发送给Spark驱动器。此时,成功构建一个“Spark集群”。
执行
集群的驱动节点和工作节点相互通信,执行代码和移动数据,驱动节点将任务安排到每个工作节点上,每个工作节点回应给驱动节点这些任务的执行状态,也可能恢复启动成功或失败等。
完成
Spark应用程序完成后,Spark驱动器会以成功或失败状态退出,然后集群管理器会为该驱动器关闭集群中的执行器。此时,还可以向集群管理器询问获知Spark应用程序退出状态。
内部角度
SparkSession
任何Spark应用程序的第一步都是创建一个 Spark Session。为了统一早期版本繁杂的Context,2.X版本后引入SparkSession,整合SQL Context、HiveContext及SparkContext。SparkSession可以更稳定地实例化Spark和SQLContext,并确保没有多线程切换导致地上下文冲突。
SparkContext
SparkSession中的SparkContext对象代表与Spark集群连接,可以通过它与一些低级API进行通信。在早期版本中,它通常以变量名sc存储,通过其可以创建RDD、累加器和广播变量,并且可以在集群上运行代码。
逻辑指令到物理执行
Spark作业
阶段
Spark中的阶段代表可以一起执行的任务组,用以在多台机器上执行相同的操作。一次Shuffle操作意味着一次对数据的物理重分区,Spark在每次shuffle之后开始一个新阶段,并按照顺序执行各阶段以计算最终结果。
对于分区数的设置,一个经验法则是分区数量应该大于集群上执行器数量,这可能取决于工作负载相关的多个因素。
注:使用range创建DF默认使用8个分区,shuffle默认使用200个分区,可通过spark.sql.shuffle.partitions设置.
任务
spark中阶段由若干个任务组成,每个任务都对应于一组数据和一组将在单个执行器运行的转换操作。任务是应用于每个数据单元(分区)的计算单位,将数据划分为更多分区意味着可以并行执行更多分区。
执行细节
流水线执行
Spark执行的关键优化之一是流水线,它在RDD级别或其以下级别执行。通过流水线技术,在不需要任何跨节点的数据移动的情况下,可以将一系列操作合并为一个单独的任务阶段。
实际上,spark流水线优化对你是透明的。Spark引擎会自动完成。你可以通过SparkUI或其日志文件检查你的应用程序。
shuffle数据持久化
spark执行shuffle操作时,总是首先让前一段的“源”任务(发送数据的那些任务)将要发送的数据写入到本地自盘的shuffle文件上,然后下一阶段执行按键分组和约减的任务将从每个shuffle文件中获取相应的记录并执行某些计算任务。将shuffle任务持久化到磁盘上允许Spark稍晚些执行reduce阶段某些任务(如没有足够多执行器同时执行分配任务,由于数据已经持久化到磁盘上,便可以晚些执行某些任务)。另外在发生错误时,也允许计算引擎仅重新执行reduce任务而不必重新启动所有输入任务。
持久化的副作用,在已shuffle操作的数据上运行新的作业并不会重新运行shuffle操作,因为Spark知道可以直接使用已经生成好的shuffle文件来运行作业的后一阶段。在SparkUI和日志中,将看到标记位“skipped”的预shuffle阶段。
二、Spark应用程序开发
sbt学习(待完成)
在 build.sbt 需要设置的关键信息:
- 项目元数据(包名称、 包版本信息等)
- 在哪里解决依赖关系
- 构件库所需要的包依赖关系
测试Spark应用程序
战略原则
- 对输入数据的适应性
- 对业务逻辑的适应性和演变
- 对输出模式和原子性的适应性
战术指导
- 管理SparkSession 使用JUnit或ScalaTest等单元测试框架测试Spark代码相对容易。
- 高级SparkAPI 还是 低级API 使用高级API需确保安全性,使用低级API需要注意优化 一般建议高级API
启用应用程序
指令
./bin/spark-submit \ --class <main-class> \ --master <master-url> \ --deploy-mode <deploy-mode> \ --conf <key>=<value> \ ... #其他选项 <application-jar-or-script> \ [application-arguments]
Parameter description –master MASTER_URL 指定master节点URL –deploy-mode DEPOLY-MODE 配置是在本地以客户端模式client还是一台集群中节点以集群模式cluster运行应用程序(默认使用客户端模式) –class CLASS_NAME 配置应用程序的入口类(main函数所在的类) –name NAME 配置应用程序的名字 –jars JARS 配置驱动器或者执行器路径上包括的jar包,用逗号隔开 –packages 配置驱动器或者执行器路径上包括的Maven依赖包,用逗号隔开。将会首先搜索本地Maven版本库(repo),然后搜索Maven central及远程版本库(远程repo通过–repositories选项指定)依赖软件包格式为groupId:artifactId: version –exclude-packages 为了避免依赖冲突,配置排除在–package选项中指定依赖包,通过逗号隔开, 格式为artifactId –repositories 配置出了通过–packages指定的,其他Maven远程依赖库,通过逗号隔开 –py-files PY_FILES 配置Python应用程序需要的.zip、.egg或者py文件,用逗号隔开 –files FILES 配置在每个执行器工作目录路径下的文件,用逗号隔开 –conf PROG=VALUE 配置Spark属性 –properties-file FILE 配置需要从哪个文件加载额外的属性 默认是conf/spark-default.conf —driver-memory MEM 配置需要从哪个文件加载额外的属性(默认:1024M) –driver-Java-options 配置驱动器Java参数 –driver-library-path 配置驱动器的library path –driven-class-path 配置驱动器的classpath。注意,通过–jars添加的JAR包已经自动包含在classpath里 –executor-memory MEM 配置执行器内存大小 默认1GB –proxy-user NAME 配置提交应用程序时的代理用户,在配置了–principal/--keytab选项时,这个配置不生效 –help, -h 显示帮助信息并退出 –verbose, -v 打印额外的debug信息 –version 打印Spark版本号 部署相关配置
Cluster Managers modes conf Description Standalone Cluster –driver-cores NUM 驱动器核心数量(默认为1) Standalone/Mesos Cluster –supervise 失败后重新启动驱动器 Standalone/Mesos Cluster –kill SUBMISSION_ID 杀死指定驱动器进程 Standalone/Mesos Cluster –status SUBMISSION_ID 获取指定驱动器的状态 Standalone/Mesos Either –total-executor-cores NUM 所有执行器的总核心数 Standalone/YARN Either –executor-cores NUM1 每个执行器核心数(默认YARN模式下为1, 或在standalone模式下worker节点所有可用核心数) YARN Either –driver-cores NUM 集群模式下驱动器的核心数(默认为1) YARN Either queue QUEUE_NAME 提交到YARN的队列名(默认“default”) YARN Either –num-executors NUM 启动执行器(默认2)如果哦欸值了动态分配,那么初始的执行器数量最少为NUM YARN Either –archives ARCHIVES 需要提取到每个执行器工作目录下的archive, 用逗号分隔 YARN Either –principal PRINCIPAL 当运行安全HDFS时,登陆导KDC用到的原则 YARN Either –keytab KEYTAB 包含上面指定principal的keytab的完整路径,keytab将会通过Distributed Cache被复制到执行应用程序的master节点上,为定期更新登录口令使用 应用程序启动实例
./bin/spark-submit \ --class org.apache.spark.examples.SparkPi \ --master spark://207.184.161.138:7077 \ --executor-memory 20G \ --total-executor-cores 100 \ replace/with/path/to/examples.jar \
SparkConf
SparkConf管理应用配置,首先需要使用import语句并创建一个SparkConf对象
import org.apache.spark.SparkConf val conf = new SparkConf().setMaster("local[2]").setAppName("DefinitiveGuide").set("some.conf","to.some.value")
Property name default meaning spark.app.name none 应用程序名称,会出现在用户界面和日志 spark.driver.cores 1 驱动器进程使用的核心数(仅在集群模式下有效) spark.driver.maxResultSize 1g 每个Spark动作所允许产生的中间结果大小限制,最小限制为1MB,0代表无限制。若产生中间结果超过限制,作业将会被强制终止。如果这个限制设置太小可能会导致OutOfMemory错误。 spark.driver.memory 1g 驱动器进程所允许使用的内存大小。注意,客户端模式下,这个参数不能在应用程序中通过SparkConf'设置,因为执行驱动器进程的JVM此时已经启动,必须通过命令行选项 –driver-memory 或者配置文件中设置该值 spark.executor.memory 1g 驱动器进程所允许使用的内存大小 spark.extraListeners none 指定SparkListener实现类,通过逗号分隔。在初始化SparkContext对象时,这些实现类将会被实例化注册。若SparkListener实现类某个构造器函数需要一个SparkConf参数,该构造器会被默认调用,否则会调用空参数构造器。若找不到以上构造器函数,SparkContext对象创建会失败并抛出异常。 spark.logConf FALSE 当SparkContext启动后,在日治中记录有效的SparkConf信息,并用INFO标识 spark.master none 设置集群管理器URL spark.submit.deployMode none Spark驱动器的部署模式,即客户端模式或集群模式, 客户端模式代表在本地执行,集群模式代表在集群中某个节点远程执行 spark.log.callerContext none 当运行Yarn/HDFS时,设置应用程序信息写入Yarn的RM日志中或HDFS监听日志中,应用程序信息长度取决于Hadoop参数hadoop.caller.context.max.size,而且应该精确设置且小于50字符。 spark.driver.supervise FALSE 若设置为真,驱动进程将会在遇到一个意外退出后重新启动,仅在Spark的standalone模式和Mesos集群配置模式下有效 环境变量
默认位置是在Spark根目录下的conf/spark-env.sh脚本中读取。由conf/spark-env.sh.template复制创建 ,并且赋予该文件可执行权限。
可在spark-env.sh设置以下变量:
- JAVA_HOME: 安装Java的位置
- PYSPARK_PYTHON: 在驱动和工作节点上pyspark使用python。如果设置属性spark.pyspark.python,则优先使用
- PYSPARK_DRIVEN_PYTHON: 在驱动节点上Pyspark使用python(默认为PYSPARK_PYTHON)。若设置spark.pyspark.driver.python, 则优先使用
- SPARKR_DRIVEN_PYTHON: SparkR shell使用的R(默认为R)。若设置了属性spark.r.shell.command,则优先使用
- SPARK_LOCAL_IP: 节点绑定IP地址
- SPARK_PUBLIC_DNS: 用于与其他节点通信的主机名
在集群模式下在YARN上运行Spark时,需要使用conf/spark-defualts.conf文件中的spark.yarn.appMasterEnv.[EnvironmentVariableName]属性来设置环境变量。在spark-env.sh中设置的环境变量不会反映在集群模式下YARN应用程序master进程中。
应用程序之内的作业调度
Spark作业是指一个Spark动作以及执行该动作所需要启动的任务。Spark调度是完全线程安全的,使得应用程序支持多个请求。
默认情况下,Spark遵循FIFO方式调度如果位于队列头的作业不需要使用整个集群资源,后面的作业可以立即开始,但如果队列头的作业工作量很大,位于队列后部作业可能会被延迟启动。
可以配置作业之间公平共享集群资源。Spark以轮询方式调度各作业并分配任务,以便所有作业获得大致相等的集群资源份额,这意味当一个大作业正在运行并占用集群资源时,新提交的小作业可以立即获得计算资源并被启动,无需等待大作业的结束再开始,所以可以获得较短的响应时间。此模式适用于多用户情况。若要启动公平调度,需要在配置SparkContext时将spark.scheduler.mode属性设置为FAIR。
公平调度程序还支持将作业分组到作业池中,并为每个池设置不同调度策略或优先级。可以为更重要的作业创建高优先级作业池,也可以将每个用户的工作组合在一起配置一个作业池,并为每个用户分配相同的调度资源,而不管他们的并发量。
默认情况下,新提交的作业会进入默认池,也可以通过SparkContext设置作业要提交到的作业池,即设置spark.scheduler.pool属性。
//举例 sc.setLocalProperty("spark.scheduler.pool", "pool1") //设置本地属性后,此线程提交的所有作业将使用此作业池名称。该设置是按线程进行配置的,以便让代表统一用户线程可以配置多个作业。若清楚该线程关联作业池,可将该属性设置为null。
三、spark部署
本地部署 or 公有云
本地部署 集群
对于由自己的数据中心的某些大企业或组织,应该将Spark部署到本地数据中心。本地集群允许你完全控制控制所用硬件设备,可以针对特定工作负载来优化性能。
缺陷:
首先采用本地部署,集群大小往往是固定的,数据分析作业资源需要往往是弹性的。如果集群较小,很难执行任务量异常繁重的工作,若集群规模过大,将会由资源闲置和资源浪费情况。其次,本地集群需要相应地维护自己的存储系统及完整的容灾方案。
本地部署解决资源利用问题最佳方法为使用集群管理器。处理资源共享问题可能是使用Spark本地部署与云部署最大区别,集群管理器支持在集群上运行多个Spark应用程序并在他们之间动态重分配资源,甚至支持非Spark应用程序共享集群资源。在公有云中,可以为每个应用程序构建合适的Spark集群大小,并为应用程序的生命周期内为其动态提供所需的集群资源。
公有云部署
优势:
首先可以弹性地申请和释放计算资源。其次,公有云支持低成本地跨地域备份存储,可以更便捷地管理这些大数据。
对于云部署,建议使用与计算集群分离的存储系统,并为每个Spark作业动态绑定拓展存储系统,通过分离计算与存储,实现动态拓展,并混合使用不同硬件类型。
集群管理器
Standalone集群管理器
Standalone集群管理器专门为ApacheSpark工作负载构建的轻量级平台。Standalone集群管理器允许你在同一个物理集群上运行多个Spark应用程序,提供了简单的操作界面,可以拓展到大规模Spark工作负载。其主要缺点为功能相对其他集群管理器比较有限,只能运行Spark作业。
手动启动Standalone集群
首先需要操控集群包含的物理节点,确保它们可以通过网络互相通信。使用以下命令在某台机器上启动主进程:
$SPARK_HOME/sbin/start-master.sh
运行这个命令时,集群管理器主进程将在该机器上启动。一旦启动成功,master主机会在命令行打印出一个URL,即spark://HOST:PORT。可以在应用程序初始化将此URL用作SparkSession的主参数,还可以在master节点WEB用户界面上找到URL,默认情况下WEB用户界面地址为http://master-ip-address:8080.并且可以登陆到每台计算节点使用该URL运行以下脚本来启动worker节点,注意master节点必须在网络上可访问,并且master节点指定的端口也必须打开:
$SPARK_HOME/sbin/start-slave.sh <master-spark-URL>
集群启动脚本
需要在Spark目录中创建一个名为conf/slaves的文件,该文件需要包含启动worker进程的所有计算机主机名,每行一个。如果这个文件不存在,那么集群将会以本地模式启动。实际启动集群时,master节点将通过SSH访问每个worker节点。默认情况下,SSH并行运行,并要求配置无密码(使用私钥)访问环境。如果你没有无密码设置,则需要设置环境变量SPARK_SSH_FOREGROUND顺序地位每个worker节点提供访问密码。
#在执行脚本地机器上启动master实例 $SPARK_HOME/sbin/start-master.sh #在conf/slaves文件中指定的每台机器启动一个slave实例 $SPARK_HOME/sbin/start-slaves.sh #在执行脚本机器启动一个slave实例 $SPARK_HOME/sbin/start-slave.sh #按照配置文件,在指定机器上启动一个master实例和在指定的多台机器上启动多个slave实例 $SPARK_HOME/sbin/start-all.sh #停止通过bin/start-master.sh脚本启动的master实例 $SPARK_HOME/sbin/stop-master.sh #停止conf/slaves文件中指定的机器上的slave实例 $SPARK_HOME/sbin/stop-slaves.sh #停止配置文件指定机器上的master实例和slave实例 $SPARK_HOME/sbin/stop-all.sh
YARN集群管理器
提交应用程序
将应用程序提交到YARN时,与其他部署方式核心区别在于,–master需要指定的是yarn而非master节点IP,Spark使用环境变量HADOOP_CONF_DIR或YARN_CONF_DIR查找yarn配置文件。
有两种部署模式可用于在Yarn上启动Spark。集群模式将spark驱动器作为由YARN集群管理的进程,客户端在创建应用程序后退出。在客户端模式下, 驱动器将运行在客户端进程中,YARN只负责将执行器的资源授予应用程序,而不是master节点。
Hadoop配置
若使用Spark从HDFS进行读写,需要在Spark的classpath中包含两个Hadoop配置文件:hdfs-site.xml(配置hdfs客户端)和core-site.xml,常见位置在/etc/hadoop/conf。为了让这些文件对Spark可见,需要将$SPARK_HOME/spark-env.sh中的HADOOP_CONF_DIR设置为包含配置Hadoop文件的位置,或者启动应用程序时将其设置为环境变量。
Mesos集群管理器
Mesos目前仅支持粗粒度模式。粗粒度模式意味着每个Spark执行作业作为单个Mesos任务运行,Spark执行器大小根据以下应用程序属性确定:
- spark.execotor.memory
- spark.executor.cores
- spark.cores.max/spark.executor.cores
提交应用程序
在spark-env.sh中设置一下环境变量
export MESOS_NATIVE_JAVA_LIBRARY=<path to libmesos.so>
此路径通常为<prefix>/lib/libmesos.so,默认情况下前缀为/usr/local。将Spark应用程序属性spark.executor.uri设置为
。 应用程序调度
每个Spark应用程序都会启动一组执行器进程,集群管理器对这些进程进行调度。其次,在一个Spark应用程序中,若多个作业由不同的线程提交,则可能会并发执行多个作业。
最简单调度方式是资源竞态划分。通过这种方法,每个应用程序被分配到一定量的资源,并在整个程序运行期间一直占用这些资源。
动态分配
如果在同一个集群上同时运行多个Spark应用程序,Spark提供了一种可以根据工作负载动态调整应用程序占用资源的机制。在应用程序不再使用资源时将资源返回给集群,并在集群需要时再次请求使用。
此功能默认处于禁用状态,但粗粒度集群管理器上都支持。使用此功能有两个要求,首先,应用程序必须将spark.dynamicAllocation.enabled属性设置为true,其次,你要在每个工作节点上设置外部shuffle服务,并在应用程序中将spark.shuffle.service.enabled属性设置为true。外部shuffle服务的目的是为了允许终止执行进程而不删除由它们输出的shuffle文件。
选择部署方式的其他注意事项
需要考虑清楚应用程序的数量与类型。比如,YARN不太适合在云环境下运行,它需要从HDFS中获取信息,计算和存储很大程度上耦合在一起。这意味着当需要拓展存储和计算中的某一种资源时,需要同时拓展存储与计算。
此外需要注意管理不同版本的应用程序及设置日志记录,维护一个元数据存储及维护关于数据集的元数据。
根据工作负载,可能要考虑使用Spark的外部shuffle服务。Spark会在某节点的本地磁盘上存储shuffle块。外部shuffle服务存储这些shuffle块,以便它们可供所有进程使用,即可以终止任意执行进程,而其他进程仍然可以访问该被终止进程产生的shuffle输出。
三、监控与调试
监控级别
spark应用程序和作业 。无论是为了调试还是更好地理解应用程序在集群上地执行过程,通过spark UI和Spark日志是最方便获取监控报告的方式。
JVM Spark在JVM上运行执行器。JVM提供一些监视工具,如用于跟踪堆栈的jstack,用于创建堆存储的jmap,用于报告时序统计信息的jstat及用于JVM可视化属性的jconsole。其中一部分JVM监视信息已在Spark UI提供。
操作系统/主机 监控内容包括CPU、网络、IO等
集群 监视运行Spark应用程序的集群,一些流行的集群级监控工具包括Ganglia和Prometheus。
监控内容
驱动器和执行器进程
Spark提供一个基于Dropwizard Metrics Library的可配置指标监控系统,它配置文件一般在$SPARK_HOME/conf/metrics.properties中指定,可以通过更改spark.metrics.conf配置属性来自定义配置文件位置,这些监控指标可以输出到包括Ganglia等多种不同监控系统。
查询、作业、阶段与任务(Spark日志 SparkUI)
对于Spark日志,要更改spark日志级别,只需执行以下命令:
spark.sparkContext.setLogLevel("INFO")
在运行本地模式时,日志将会被打印到标准错误输出流,若在集群上运行Spark时,日志会由集群管理器保存到文件。
对于SparkUI,其提供了一种可视化方式在Spark和JVM级别来监视运行中的应用程序及Spark工作负载的性能指标,每个运行的SparkContext都将启动一个WebUI,默认端口4040.
Spark UI历史记录服务器
Spark提供了一个名为Spark历史记录服务器的工具,它允许你重建SparkUI和REST API,前提为应用程序配置为保存事件日志。
使用历史记录服务器首先需要配置应用程序将事件日志存储到特定位置,需要启用spark.eventLog.enabled和配置spark.eventLog.dir来指定事件日志存放位置。一旦存储了事件,就可以将历史记录服务器作为独立应用程序运行,并且会根据这些日志自动重建WebUI。
Spark调试与抢救方案
Spark作业未启动
表现形式:
- Spark作业无法启动
- 除驱动器节点,SparkUI不显示集群中其他执行器节点信息
- Spark报告疑似不正确信息
应对措施
通常是由于集群或应用程序资源需求配置不正确,错误配置导致驱动器节点无法与执行器节点进行通信。另一种可能性为应用程序为每个执行器进程请求了过多资源,以至于大于集群管理器当前的空闲资源,在这种情况下,驱动器进程将永远等待执行器进程启动。
- 确保节点可以在指定端口相互通信,最好打开工作节点的所有端口,除非有严格的安全限制。
- 确保Spark资源配置正确,并且确保集群管理器已针对Spark进行正确配置。一个常见问题是每个执行器进程配置了太大的内存,已超过集群管理器内存资源配额。请先检查内存资源是否足够,并且接茬spark-submit的内存配置。
执行前错误
表现形式:
- 命令不执行,并输出大量错误消息
- 通过SparkUI看不到作业、阶段或任务的执行
应对措施
- 查看Spark返回的错误,并确认不是代码的问题。
- 反复仔细检查以确认集群网络连接正常,检查驱动器节点、执行器节点,以及存储系统之间的网络连接情况。
- 可能是库或classpath路径配置问题,导致加载了外部库错误版本。需试着一步步删减代码已缩小错误的范围和定位错误,知道最后找到一个能够重视问题的较小代码片段
执行期间错误
表现形式:
- 一个Spark作业在集群中成功运行,但下一个作业失败
- 多步骤查询中的某个步骤失败
- 一个已经成功运行过的程序在第二次运行时失败了
- 难以解析错误信息
应对措施
- 检查数据是否存在和输入数据格式是否正确,输入数据可能随时间而改变。
- 若在运行查询时就弹出错误,则在建立查询计划时可能出现了分析错误 。这可能是因为查询中应用的列名称乒协错误,或是引用的列,视图或表不存在。
- 仔细读stack trace错误跟踪日志,以尝试找到某些组件错误的线索
- 用确保格式正确的输入数据集来隔离问题,排除数据问题后,可以尝试删除部分逻辑代码,逐步缩减代码,直到找到一个可以产生错误的较小代码版本定位问题
- 若作业执行一段时间后失败,可能是由于输入数据本身存在问题 ,可能特定行数据模式不正确,
- 代码在处理数据是可能会发生崩溃,在这种情况下,Spark会显示代码抛出的异常,在SparkUI中标记为“失败”任务,且可以通过日志来了解失败任务,可尝试在代码中添加更多日志以确定哪个正在处理数据记录有问题。
缓慢任务或落后者
较为常见,可能是由于工作负载没有被均匀分布在集群各节点上导致负载倾斜。
表现形式:
- Spark阶段中只能下少数任务未完成,这些任务运行了很长时间。
- 在Spark UI中可以观察到这些缓慢任务始终在相同的数据集上发生。
- 各阶段都有这些缓慢任务。
- 扩大Spark集群规模并没有太大效果,有些任务仍然比其他任务耗时更长
- 在Spark指标中,某些执行器进程读取和写入数据量比其他执行器进程大得多。
应对措施
缓慢任务称为“落后者”,最常见原因是数据不均匀分布DataFrame或RDD分区。
- 尝试增加分区数以减少每个分区被分配到数据量
- 尝试通过另一种列组合来重新分区
- 尽可能分配给执行器进程更多内存
- 监视有缓慢任务的执行器节点,并确定该执行器节点在其他作业上也总是执行缓慢任务,这说明集群中可能存在一个不健康执行器节点。
- 如果在执行连接操作或聚合操作时产生缓慢任务
- 检查用户定义函数是否在其对象分配或业务逻辑中有资源浪费情况。可能的话,尝试将它们转换为DF代码
- 确保UDF或UDAF在足够小的数据上运行,通常情况下,聚合操作要将大量数据存入内存以处理对某个key聚合操作,从而导致该执行器比其他执行器要完成更多的工作。
- 打开推测执行功能,将为缓慢任务在另外一台节点重新运行一个任务副本。如果缓慢问题是由于硬件节点的原因,推测执行有所帮助。因为任务会被迁移到更快的节点上运行。然而,推测执行会付出代价,一是会消耗额外资源,另外,对于一些使用最终一致性存储系统莫若写操作不是幂等的,则可能会产生重复冗余的输出数据。
- 使用Dataset时可能会出现另一种常见问题。由于Dataset执行大量对象实例化并将记录转换为用户定义函数中的Java对象,可能会导致大量垃圾回收。若使用Dataset,需查看SparkUI垃圾回收指标,确定其是否是导致缓慢任务的原因。
缓慢的聚合操作
表现形式:
- 在执行groupby操作时产生缓慢任务
- 聚合操作之后的作业也执行非常缓慢
应对措施
在聚合操作之前增加分区数量可能有利于减少每个任务中处理的不同key的数量
增加执行器进程的内存配额也可以帮助缓解此问题。若一个key拥有大量数据,将允许其执行器进程更少地与磁盘交互数据,尽管可能仍然比处理其他key地执行器进程要慢很多。
若聚合之后任务也很慢,意味着数据集在群和操作后可能仍然不均衡,需尝试调用repartition对数据进行随机重新分区。
确保涉及所有过滤操作和select操作在聚合操作之前完成,保证需要执行聚合操作的数据进行处理,避免处理无关数据。Spark查询优化器将自动为结构化API执行此操作。
确保空值被正确标识(建议使用Spark的null关键字),不要用“ ” 或 “ENPTY”之类控制表示。Spark优化器通常会在作业执行初期来跳过对null空值的处理,但无法为自定义的空值形式进行优化。
一般聚合操作本身比其他聚合操作慢。如,collect_list 和 collect_set 比较慢,因为他们必须将所有匹配对象返回给驱动器进程,在代码中应该尽量避免使用这些聚合操作。
缓慢连接操作
表现形式
- 连接操作阶段需要很长时间。可能是一项任务也可能是许多任务
- 连接操作之前阶段和之后阶段似乎都很正常
应对措施
- 许多连接操作类型可以被(手动或自动)转化为其他连接操作类型
- 实验不同的连接顺序,若某些连接操作可能过滤大量数据,应该优先执行
- 在连接操作前对数据集进行分区,对于减少集群之间的数据移动非常有效。特别是在多个连接操作中使用相同的数据集,尝试不同的数据分区方法对提高连接操作性能值得一试。需要注意的是数据分区的代价并不是免费,而是以数据shuffle操作为代价。
- 数据倾斜可能导致连接操作速度变慢。对于数据倾斜并没好方法,但是增加执行器节点数量有一定效果。
- 确保涉及所有过滤操作和select操作在连接操作前完成,可以保证只对需要执行聚合操作的数据进行处理。
- 与聚合操作一样,需要使用null标识空值
- 若Spark不知道有关输入DF或数据表的统计信息,则无法选择合适的连接操作查询计划。若执行连接操作的数据表很小或使用Spark提供数据统计能获得表的大小,可以强制采用广播这个小数数据表方法实现连接操作。
缓慢读写操作
表现形式
- 从分布式文件系统或外部存储系统读取数据缓慢
- 往网络文件系统或bolb存储上写入数据缓慢
应对措施
- 开启推测执行(将spark.speculation设置为true)有助于解决缓慢读写问题。推测执行功能启动执行相同操作的任务副本。推测执行是一个强大工具,与支持数据一致性的文件系统兼容良好。但对于支持最终一致性的云存储系统,可能会导致重复的数据写入,使用前需检查使用的存储系统连接器是否支持。
- 确保网络连接状态良好
- 若相同节点上运行Spark和HDFS等分布式文件系统,确保Spark与文件系统的节点主机名相同。spark将考虑数据局部性进行优化进行调度,可以在SparkUI的locality查看调度情况
驱动器OutOfMemoryError错误或者驱动器无响应
表现形式
- Spark应用程序无响应或崩溃
- 在驱动器进程错误日志中发现OutOfMemoryError错误或垃圾回收消息
- 命令需要很长时间才能完成或者根本不运行
- 交互性非常低或者根本不存在
- 驱动器程序JVM内存使用率很高
应对措施
- 代码中可能使用如collect之类操作过大数据集收集到驱动器节点
- 使用了广播连接,但广播数据太大,设置Spark最大广播连接数以更好地控制广播消息地大小
- 应用程序长时间运行导致驱动器进程生成大量对象,且无法释放。Java中jmap工具可以打印堆内存维护对象数量直方图,这样可以查看哪些对象正在占用驱动器进程JVM内存。注意,运行jmap将需要暂停该JVM。
- 可能的话,可以增加驱动器进程地内存分配
- 由于两者之间数据转换需要占用JVM大量内存,若使用其他语言绑定,JVM可能会出现内存不足。具体取决于语言的选择,将更少的数据带回驱动器节点或者将其写入文件而不是将其作为内存中对象返回。
- 若与其他用户共享SparkContext,请不要在其他用户在驱动器中执行可能导致分配大量内存的操作。
执行器的OutOfMemoryError错误或执行器
表现形式:
- 在执行器错误日志中发现OutOfMemoryError错误或垃圾回收消息
- 执行器崩溃或无响应
- 某些节点上的缓慢任务始终无法恢复
应用措施
- 尝试增加执行器进程的可用内存和执行器节点数量
- 尝试通过相关Python配置来增加PySpark工作节点大小
- 在执行器日志中查找垃圾回收的错误消息。一些任务,特别是UDF任务,可能会创建大量需要垃圾回收的对象,对数据重新分区以增加并行性,减少每个任务处理的记录数量,并确保所有执行者获得大体相同的工作负载。
- 使用RDD或Dataset时,由于对象实例化可能会导致尝试执行器内存溢出错误。尽可能避免使用用户定义函数UDF,而尽可能使用Spark结构化操作。
- 使用Java监视工作获取执行器对象占用内存情况的直方图,查看哪类对象占用空间最多
- 若执行器进程被放置在同时有其他工作负载运行的物理节点上,尝试将Spark作业与其他工作负载隔离
结果返回意外的空值
表现形式
- 转换操作后,意外得到空值
- 以前可以运行的生产作业不再正确运行,或不再产生正确的结果
应对措施
- 在处理过程中的数据格式可能已经更改,但业务逻辑并没有及时调整。
- 使用累加器对记录或某些类型记录进行计数,以及在跳过记录时解析或处理错误数。大多数情况下,用户在将原始数据解析为某种格式时,会将累加器放在其UDF中,并在那里执行计数,使得可以计算有效和无效记录,并基于此进行调试
- 确保你的转换操作解析成了正确得查询计划。SparkSQL有时会执行隐式强制类型转换,可能导致错误结果。
磁盘空间不足错误
表现形式
作业失败并返回“no space left on disk”
应对措施
- 增加更多磁盘空间或在云上,扩大集群规模挂载额外存储
- 若集群存储空间有限,由于数据倾斜,某些节点存储可能会首先耗尽,这种情况下可以考虑对数据重新分区
- 可以尝试调整存储配置
- 尝试在相关机器上手动删除一些有问题的旧日志文件或旧shuffle文件
序列化错误
表现形式
作业失败并显示序列化错误
应对措施
- 使用结构化API较为罕见。当使用UDF或RDD对执行器执行一些自定义逻辑或序列化到执行器或共享数据无法序列化时,会遇到序列化错误。
- 使用Java或Scala类中创建UDF时,不要在UDF中引用封闭对象的任何字段。因为折会导致Spark序列化整个封闭对象,可能会导致错误。正确做法为将相关字段复制到与封闭对象相同的作用域内的局部变量,再使用。
四、性能优化
整体而言,有两种方法可以指定Spark作业之外的执行特性:第一、可以通过合理配置和改变运行环境来间接优化程序,这种方法会对刺激群上的其他Spark应用程序或作业一起产生作用;第二,可以尝试直接指定某个Spark作业、某个阶段、某个任务的执行特性,这些设置非常有针对性,不会对整体有什么影响。
间接性能优化
语言选择 (建议使用Scala) DF、SQL、Dataset、RDD选择(建议使用结构化API, RDD使用Scala或Java编写)
RDD对象序列化
可以通过将spark.serializer设置为org.apache.spark.serializer.KryoSerializer使用Kryo序列化,可通过过spark.kryo.classesToRegister显示注册Kryo序列化器的类。注册类的方法:
conf.registerKryoClasses(Array(classOf[MyClass1], classOf[MyClass2]))
集群配置
集群/应用程序的规模和共享
动态分配
Spark提供了一种机制,可根据工作负载动态调整应用程序占用资源。意味着你的应用程序可以在资源不在使用时将资源返回给集群,并在有需求时再次请求资源使用。此功能默认情况下处于禁用状态,并在粗粒度集群管理器上可用。将Spark.dynamicAllocation.enabled设置为true。
调度
你可以利用调度池来优化Spark作业并行度,或利用动态分配或设置max-executor-cores来优化Spark应用程序并行度。(spark.scheduler.mode设置为FAIR),也可以设置–max-executor-cores,指定应用程序最大执行核心数量,指定该值可确保应用程序不会占满集群上的所有资源。可以根据集群管理器更改默认值,设置spark.cores.max参数。
静息数据
当你保存数据时,你的组织中的其他人可以访问相同的数据集以进行不同的分析任务,因此它会被多次读取,确保你的数据以高性能读取方式存储。
基于文件的长期数据存储
优化Spark作业最简单方法之一,选择最高效的存储格式。一般而言,应始终支持结构化二进制类型来存储数据。
可拆分文件类型和压缩
确保文件是“可分割”的。文件的可分割能力主要取决于压缩格式。ZIP文件或TAR归档文件不能被拆分(多核心多文件只有单个核心读取该数据)。相比之下由Hadoop或Spark并行处理框架输出的gzip、bzip2或lz4压缩文件,它们通常是可拆分的。
表分区
表分区是指文件基于关键字存储在分开的目录中,正确对数据分区,允许Spark在查询特定范围数据时跳过不想管文件。分区主要缺点,如果分割粒度太细,可能会导致很多小文件,当列出存储系统中所有文件可能会产生巨大开销。
分桶
分桶允许Spark根据可能执行的连接操作或聚合操作对数据进行“预先分区”,这可以提高性能和稳定性,因为分桶可以帮助数据在分区间持续分布,而不是倾斜到一两个分区(若读取后频繁根据某一列进行连接操作,则可以使用分桶确保数据根据这一列的值进行良好分区)。这可以减少在连接操作之前的shuffle操作,有助于加快数据访问。分桶通常与分区协同工作,他是物理分割数据的第二种方式。
文件数量
需要考虑文件数量和存储文件的大小。拥有大量小文件将使调度程序更难以找数据并启动读取任务,这可能会增加作业的网络开销和调度开销。减少文件数量,使每个文件更大,则可以减轻调度程序的开销,但会使任务运行更长时间。在这种情况下,可以启动比输入文件个数更多任务增加并行性,Spark会将每个文件分割并分配多个任务,前提是你使用的使更拆分文件。经验而言,建议调整文件大小使每个文件至少包含几十兆字节数据。控制每个文件有多少条记录,可以为写入操作指定maxRecordPerFile选项。
数据局部性
数据局部性指定了某些节点的存储偏好,哪些节点应该存储哪些数据,进而不必通过网络交换数据。若在运行Spark相同集群机器上运行存储系统,并且系统支持局部性题使,则Spark将尝试调度与每个输入数据块在物理上更近的任务。
统计信息收集
Spark包含一个基于成本的查询优化器,它使用结构化API根据输入数据属性来计划查询。统计信息包含两类,数据表级的和列级的统计信息。统计信息手机仅适用于命名表,不适用于任意DF或RDD。
#收集表级统计信息 ANALYZE TABLE table_name COMPUTE STATISTICS #收集列级统计信息(较慢) ANALYZE TABLE table_name COMPUTE STATISTICS FOR COLUMNS column_name1, column_name2, ...
shuffle设置
对于基于RDD作业,序列化格式对shuffle性能有较大影响。Kryo序列化通常比Java快。Shuffle分区数量很关键,较少的话则只有少量节点在工作易导致数据倾斜,过多分区的话启动每一个任务需要的开销可能占据所有资源。为了平衡,可为shuffle每个输出分区设置至少几十兆字节数据。
内存压力和垃圾回收
尽可能使用结构化API,不仅会提高执行Spark作业效率,而且还会大大降低内存压力,因为结构化API不会生成JVM对象,SparkSQL只是在其内部格式执行计算。
评估垃圾回收影响
垃圾回收调优第一步使统计垃圾回收发生的频率和时间。你可以使用spark.executor.extraJavaOptions配置参数添加-verbose:gc -XX:+ PrintGCDetails -XX:+PrintGCTimeStamps到Spark的JVM选项来完成。下次运行Spark作业时,每次发生垃圾回收时都会在工作节点日志打印相关信息,这些日志位于集群工作节点上(其工作目录的stdout文件中),而非驱动器中。
垃圾收集调优
基本信息
- Java堆空间分为两个新区域,Young和Old。Young用于保存短寿命对象,Old用于保存寿命长对象。
- 新生代进一步被划分为三个区域:Eden, Survive1, Survive2
简单描述
- 当Eden区域满了时,在Eden运行一个小型垃圾回收系统,Eden和Survivor1区域中活跃对象被复制到Survivor2区域中
- survivor1区域和Survivor2区域交换
- 若一个对象足够旧,或Survivor区域已满,则将该对象移至Old区域。
- 最后,当老年代区域装满时,将调用完整的垃圾回收,涉及遍历堆中的所有对象,删除未引用的对象,以及移动 其他对象以填充未使用空间。
Spark中垃圾回收调优目的时确保只有长寿命数据对象存储在Old区域,且Young区域大小足以存储所有短期对象,将有助于避免完整垃圾回收处理在任务执行过程中创建临时对象。
收集垃圾回收统计信息以确定其执行是否过于频繁。若任务完成前多次调用完整的垃圾回收,则意味着没有足够内存可用于执行任务,则应减少Spark用于缓存的内存大小(spark.Memory.fraction)。
垃圾回收调优效果取决于应用程序和可用内存量。在较高层次上,设置完整垃圾回收频率可帮助减少开销。可通过作业配置中设置spark.executor.extraJavaOptions来指定执行器垃圾回收选项。
直接性能调优
并行度
增加并行度,建议分配每个CPU核心至少有两三个任务,可通过spark.default.parallelism属性设置它,同时根据集群中核心数量来调整spark.sql.shuffle.partitions属性。
过滤优化
过滤器尽可能移动到Spark任务最开始。启动分区和分桶有助于实现此目的,尽可能早的过滤大量数据。
重分区和合并
重新分区调用可能导致shuffle操作。但用于平衡集群中数据优化是值得的。一般来说,应该试着shuffle尽可能少的数据,因此,若减少DF或RDD中整个分区数量,可先尝试coalesce方法,该方法不执行数据shuffle,而是将同一个节点上多个分区合并到一个分区中,相对较慢的repartition方法会跨网络shuffle数据以实现负载均衡,重新分区执行连接操作前或调用cache前使用有非常好效果。
自定义分区
UDF
一般来说,避免使用UDF是一个很好策略。UDF强制将数据表始终为JVM对象,尽可能使用结构化API。
临时数据存储
最有用的优化之一是缓存。缓存将DF,数据表,或RDD放入集群中执行器的临时存储区(内存或磁盘),是后续读取更快。
缓存是一种惰性操作,这意味着只有在访问它们时才会对其进行缓存。RDD API和结构化API在实际执行缓存有所不同,缓存RDD时,缓存实际物理数据(即比特位)。在结构化API中,缓存是基于物理计划完成的。这意味着我们高效地将物理计划存储为键并在执行结构化作业之前进行拆线呢,可能会导致混乱(缓存访问冲突)。
存储级别 意义 MEMORY_ONLY 将RDD存储为JVM中反序列化后的Java对象。若内存装不上RDD,则某些分区将不会被缓存。在每次需要时重新计算这些部分。默认存储级别。 MEMORY_AND_DISK 将RDD存储为JVM中反序列化后的Java对象。若内存装不下RDD,则再微盘上存储内存装不下的分区,并在需要时从磁盘读取。 MEMORY_ONLY_SER(Java 或 Scala) 将RDD存储为JVM中序列化后的Java对象。通过比反序列化更节省空间,但CPU开销更大。 MEMORY_AND_DISK_SER(Java | Scala) 与MEMORY_ONLY_SER相似,将不合适内存的分区存储在磁盘,而不是每层需要时动态重新计算 DISK_ONLY 仅将RDD分区存储在磁盘上 MEMORY_ONLY_2,MEMORY_AND_DISK_2等 与上一级别相同,但每个分区被复制到两个集群节点上 OFF_HEAP(experimental) 与MEMORY_ONLY_SER类似,但将数据存储在堆外内存中,这需要启动堆外内存。 Spark中cache命令默认将数据置于内存中,若内存已满,则只缓存数据集的一部分。对于更多控制,可使用persist方法。它通过StorageLevel对象指定缓存数据位置,在内存,磁盘或两者混合。
连接操作
等价连接最容易优化,尽可能使用等值连接。使用广播连接提示可帮助Spark在创建查询计划时做出只能计算决策。避免笛卡尔连接或完全外连接通常是提高稳定性和性能优化简单方法
聚合操作
若使用RDD,则精确控制聚合执行方式很有用(如使用reduceByKey而非groupByKey)
广播变量
基本思路是,如果某一大块数据被程序中的多个UDF访问,则可以将其广播让每个节点上存储一个只读副本,并避免在每项作业中重新发送此数据。