Spark应用依据计划执行时, 非常容易编写, 也非常容易懂. 然而, 当spark应用执行非常慢或失败时就变得非常难了. 有时一个好的调优可能因为数据变化或者数据布局变化或而失败, 有时应用程序在一开始时运行良好,但由于资源不足而变差. 有非常多这样的例子.
不仅了解Spark应用非常重要, 也需要了解底层运行时组件, 像磁盘利用率, 网络利用率,相互之前的竞争等等. 当应用出现不好的情况下,需要做出明智的决定.
在一系列文章, 我专注于最通用的原因,为什么Spark应用失败或执行的非常慢. 首要和最常见提内存管理问题.
如果进行Spark开发者投票,内存溢出将要是每个人要面对的问题. 由于Spark架构是心内存为中心,也这就不用惊讶. 常见引起内存溢出的几种情况:
- 没有正确使用Spark
- 高并发
- 低效查询
- 错误参数配置
为避免这些问题,需要对spark 和操作的数据有基础了解. 有些操作十分清晰要么阻止内存溢出要么调整程序是由于内存溢出而导致的失败. 对Spark应用来说默认配置有可能是不充足的或是不够精确. 有时经过优化后的应用也会因为内存溢出而失败,是因为底层的数据变化了.
内存溢出问题是可以观察出来的,可以通过driver节点,执行节点, 并且甚至可以通过Node 管理节点.让我位看看不同的案例吧.
内存溢出在Driver级别
Driver在spark应用中是一个JVM,它控制流运行, 通常driver因为内存溢出失败是因为错误的使用Spark. Spark是一个分布式处理引擎. driver应该只被考虑用来做指挥. 在典型的生产环境, driver被分配内存少于执行任务节点. 我们应该小心了解到driver上做了什么.
通常在driver上引起内存溢出的有:
- rdd.collect
- spark.broadcast
- driver的内存配置的低
- 错误配置了spark.sql.autoBroadcastJoinThreshold. Spark用这个配置去限制广播一个关系到所有节点,一旦有join操作. 在第一次使用,所有的关系是物化在driver节点上.有时多表操作也作为一部分查询语句会广播.
用这种方式写你的程序可以避免结果集合在Driver端. 你可以非常好的把任务委托到某个执行节点上. 例如, 你可以保存结果到某个文件上,也可以在driver端收集,或是分配到执行节点.
// Inefficient code
val result = dataFrame.collect() // Will cause driver to collect the results
saveToFile(result)
// Better code
dataFrame.repartition(1).write.csv("/file/path") // Will assign an executor to collect the result. Assuming executors are better provisioned.
如果你在用Spark Sql并且driver内存溢出是因为广播关系,你可以增加driver内存或是减少spark.sql.autoBroadcastJoinThreshold的值,这样你可以使join操作更友好的内存排序合并.
在执行节点内存溢出
在Spark应用中是非常常见的,也是由于多种原因导致的. 常见的原因是高并发,低效查询和错误的配置.每个都依次看下:
高并发
在明白为什么高并行引起内存溢出之前,需要先知道Spark怎么执行一个查询或是Job,哪个组件是引起内存消耗的原因.
Spark job或是任务分解成多个阶段, 每个阶段又近一步拆分成任务. 大量任务依赖不同因素, 像那个阶段正在执行,数据在被读等等. 如果是一个map阶段操作(在SQL是扫描阶段),通常要考虑数据源分区.
例如, Hive orc格式的表有2000个分区, 在map阶段会创建2000个任务来读表,假如partition数量没有被修改.如果reduce阶段(shuffle 阶段),Spark需要使用spark.default.parallelism的默认值来处理RDD.或者用spark.sql.shuffle.partitions来决定数据集任务的个数.多少任务并行在不同的执行节点是依赖spark.executor.cores属性. 如果这个值设置的高而没有考虑内存实际需求,执行节点可能会内存溢出. 现在来看看当任务出现了内存溢出发生了什么.
我们来看看读HDFS文件或是Parquet/ORC表执行map任务或是SparkSql的扫描阶段. 对于HDFS文件,每个Spark任务将要读一个块大小的数据.所以如果10个并行任务在运行,内存需要至少块大小*10 -- 并且这仅仅是分区中排序数据.这里也忽略数据的压缩可能引起的明显的变大,这是依赖压缩算法.
Spark以失量化格式来读取Parquet文件.简单来说,每个任务读Parquet文件是一批一批的. 由于Parquet是列式存储的,每批是以每列构造的. 在列上执行的任何操作之前,就会在内存中积累大量列数据.也就意味着Spark需要一些数据结构并且记录存储很多数据. 同时,像有一些在内存有状态的字典编码技术. 他们都需要内存.
图:当执行表扫描时Spark任务和内存组件
所以用更多的并行会增加负载.并且如果有广播join参与,这个广播变量也会占用一些内存空间.上图展示一个简单的例子, 每个执行节点有两个执行两个并行的任务.
低效查询
当Spark Catalyst引擎试着尽可能优化查询时,如果查询本身写的差那是无效的.例如,选择所有列在Parquet或是ORC表中.在上一节中,每个列都需要一些内存中列的批处理状态. 如果更多万被选择,开销就会更多.
尽可能的少读列.多用过滤,这样执行节点会获取少量数据.一些数据源支持分区修改.如果你的查询可以转换成分区列, 这样会减少数据的大范围移动.
错误的配置
内存或是caching配置的错误配置可以导致任务失败或是Spark应用的程序停止.看看这些例子.
执行节点和driver内存
不同的应用需要不同的内存.不同的需求每个应用可以有不同的配置.你应该确保spark.executor.memory
或 spark.driver.memory值是否正确,是由工作负载决定的. 这是显尔意见的,但先对也是最难的.我们需要工具来监控实际应用使用的内存.
内存开销
有时不同执行节点内存引起的内存溢出而是Yarn容器引起的或是节点被yarn杀了.“YARN kill”的消息一般是这样的:
[pid=<pid>,containerID=[contained_ID] isrunningbeyondphysicalmemorylimits. Currentusage: 1.5 GBof 1.5 GBphysicalmemoryused; 4.6 GBof 3.1 GBvirtualmemoryused. Killingcontainer
Yarn 容器中运行Spark组件,像执行节点和driver. 内存消耗有JVM的堆外内存和实际字符串,JVM的元数据.在这个例子中,你需要配置spark.yarn.executor.memoryOverhead一个全程的值.一般用总内存的10%被用来分配开销.
缓存内存
如果你的应用有Spark缓存来存储一些数据集,需要考虑Spark内存管理的配置.Spark内存管理是以一种能用的方式来写的,来满足所有负载.
Spark定义内存需要有两种类型:执行和存储. 存储内存是以缓存为目的,执行内存需要获得临时的结构,像hash表用来做聚合,join,等.
执行和存储内存可能通过配置所有的堆内存分数来获得.设置项是spark.memory.fraction.默认是60%. 除了这个默认是50%被分配存储(spark.memory.storageFraction),剩下的分配给运行.
一些情况,每个上边的内存池,也就是执行和存储,如果池是空闲的可以彼此相互借用.如果借用执行内存,当执行时内存存储内存可以被驱逐. 尽管这样,不需要引入更复杂,我们配置程序,像内存数据合适的数据存储,执行时应该不会引起问题.
如果你不想要我们所有缓存数据都存在内存中,可以配置spark.memory.storageFraction为较小值,这样额外的数据应该被释放,这样执行不会面对内存压力.
管理节点内存溢出
Spark应用做数据重组在group by或者join这样操作,增加了非常大的负载. 正常数据重组处理完成通过执行器处理.
如果执行器是忙或者高GC,则不能满足数据重组请求.这个问题可能被减轻通过使用额外的数组重组服务.
额外的数据重组服务运行在每个工作节点上,并且从执行节点处理重组请求.执行器读重组文件从这个服务中,而还是从其它节点. 这样请求器去读重组问题, 即使生产执行器是被kill或者慢. 并且当动态分配开启, 会强制开始额外的重组服务.
当Spark配置Yarn做为扩展重组服务,管理节点启动一个辅助服务来扮演扩展重组服务提供者.默认,管理节点内存大约是1G. 既然这样,应用做大量数组重组可能失败是由于管理节点的内存溢出. 这是非常重要的配置管理节点,如果你的程序是以下类型.
多亏内存
Spark内存处理是他处理的关键点. 因此,Spark应用和数据管道高效的内存管理是获得高性能,可扩展,稳定的核心因素. 无效如何,Spark默认配置通常是不够的. 依赖应用或环境或十分确定的配置一定要设置正确来满足性能目标. 了解一些基本概念并且对整体应用的影响是有用的.