Data Serialization

对spark程序来说,可能会产生的瓶颈包括:cpu,网络带宽,内存

在任何分布式应用中数据序列化都非常重要,数据序列化带来的作用是什么?第一减少内存占用,第二减小网络传输带宽消耗。spark提供了两种序列化方式:

1.Java serialization

默认情况下,spark序列化对象使用java的ObjectOutputStream框架,只需要我们在创建类的时候实现 java.io.Serializable

你也可以通过继承java.io.Externalizable来实现更好的性能,java序列化比较灵活但是相对较慢,性能不够好。

2.Kryo serialization

第二种方式就是目前应用比较多的Kryo序列化,
Kryo比Java序列化快很多,并且序列化之后的结果也更小(基本上是java序列的10倍左右),但是不支持所有的序列化类型,需要在程序中对要使用的类提前注册,以获得最佳性能。

使用kryo序列化只需要在spark程序中设置sparkConf,conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer"),这样一来,shuffle时候,在各个节点进行数据传输时就会生效。同理,将RDD序列化到磁盘时候也一样。spark之所以没有把kryo序列化作为默认的序列化方式主要原因就是需要自定义的注册。但是还是墙裂推荐使用kryo序列化的方式,尤其那些网络密集型的应用。从spark2.0.0开始,当shuffle那些基本数据类型,数组或者string类型的RDD时候,已经默认使用kryo序列化了。

对于自定义的类使用kryo序列化:

val conf = new SparkConf().setMaster(...).setAppName(...)
conf.registerKryoClasses(Array(classOf[MyClass1], classOf[MyClass2]))
val sc = new SparkContext(conf)

java版

conf.registerKryoClasses(new Class[]{Class1.class,Class2.class});

另外,如果你要序列化的对象比较大的话,可能还需要增加spark.kryoserializer.buffer参数值。

如果没有注册自定义类,Kryo也可以使用,但是在序列化时候必须将完整的类名保存在每个对象中,这是一个比较大的开销。

序列化也同样可以应用在缓存数据时候(persist级别),比如MEMORY_ONLY_SER 但是如果内存足够,也可以不使用序列化存储,因为序列化虽然在内存占用上减少了,但是在访问该数据时候,需要进行反序列化。


Memory Tuning

谈内存优化之前先了解一下java对象在内存中占用的情况以及spark的内存管理模型

1.java对象占用内存大小

在保存java对象到内存中时候,一般会比对象中原始的字段多占用3-5倍的空间,原因如下:

  • 1.每个不同的对象都会有一个大约16字节的object header,这个大小有可能会比该对象保存的数据都要大。
  • 2.Java String字符串在原始数据的基础上额外有将近40个字节的开销(String底层是以char类型的数组存储,并且会保存额外的数据,比如其长度)由于字符串内部使用UTF-16编码,所以将每个字符存储为两个字节。因此,一个10个字符的字符串可以轻松地消耗60个字节。
  • 3.公共集合类,如HashMap和LinkedList,使用链表数据结构,其中每个entry都有一个“包装”对象(例如Map.Entry)。这个对象不仅有一个object header,而且还有指向列表中下一个对象的指针(每个指针一般占8个字节)。
  • 4.原始类型的集合通常将元素以装箱的形式存储,比如java.lang.Integer。

2.Spark内存管理模型

以下内容是基于spark2.3版本

默认情况下,spark只使用堆内内存,如下:

executor 端的堆内内存大致可以分为以下四大块:

● Execution 内存:主要用于存放 Shuffle、Join、Sort、Aggregation 等计算过程中的临时数据
● Storage 内存:主要用于存储spark的cache数据,例如RDD的缓存、unroll数据;
● 用户内存(User Memory):主要用于存储 RDD 转换操作所需要的数据,例如 RDD 依赖等信息。
● 预留内存(Reserved Memory):系统预留内存,会用来存储Spark内部对象。

如下图:

systemMemry也就是通过--executor-memory 配置的,Reserverd Memory是写死的300M(可以参考官网对该处的描述),usable Memory等于systemMemry减去Reserverd Memory。

usable Memory乘于spark.memory.fraction比例才等于execution和storage可用的内存,该版本比例为0.6。

源代码:

  /**
* Return the total amount of memory shared between execution and storage, in bytes.
*/
private def getMaxMemory(conf: SparkConf): Long = {
val systemMemory = conf.getLong("spark.testing.memory", Runtime.getRuntime.maxMemory)
val reservedMemory = conf.getLong("spark.testing.reservedMemory",
if (conf.contains("spark.testing")) 0 else RESERVED_SYSTEM_MEMORY_BYTES)
val minSystemMemory = (reservedMemory * 1.5).ceil.toLong
if (systemMemory < minSystemMemory) {
throw new IllegalArgumentException(s"System memory $systemMemory must " +
s"be at least $minSystemMemory. Please increase heap size using the --driver-memory " +
s"option or spark.driver.memory in Spark configuration.")
}
// SPARK-12759 Check executor memory to fail fast if memory is insufficient
if (conf.contains("spark.executor.memory")) {
val executorMemory = conf.getSizeAsBytes("spark.executor.memory")
if (executorMemory < minSystemMemory) {
throw new IllegalArgumentException(s"Executor memory $executorMemory must be at least " +
s"$minSystemMemory. Please increase executor memory using the " +
s"--executor-memory option or spark.executor.memory in Spark configuration.")
}
}
val usableMemory = systemMemory - reservedMemory
val memoryFraction = conf.getDouble("spark.memory.fraction", 0.6)
(usableMemory * memoryFraction).toLong
}

举个例子:

启动一个任务,executor内存分配为8G

[etluser@master01 kong]$ spark2-shell --master yarn --executor-memory 8g --num-executors 

在spark web ui界面查看executor内存:

在stroage memory那一项4.4G,这个值指的是execution和storage内存之和。根据上面描述计算如下:

Memory = (8 *1024-300)*0.6 = 4735M = 4.62G,为什么比界面显示的4.4G大??

虽然我们设置的--executor-memory为 8g,但是 Spark 的 Executor 端通过 Runtime.getRuntime.maxMemory 拿到的内存其实没这么大,其值会比实际配置的executor内存的值小。
这是因为内存分配池的堆部分分为 Eden,Survivor 和 Tenured 三部分空间,而这里面一共包含了两个 Survivor 区域,而这两个 Survivor 区域在任何时候我们只能用到其中一个,
所以我们可以使用下面的公式进行描述:
ExecutorMemory = Eden + 2 * Survivor + Tenured
Runtime.getRuntime.maxMemory = Eden + Survivor + Tenured

另外execution和storage之间的内存是可以互相动态调整的,(在spark1.5之前,两者的内存大小占比是定值),如下图:

为了更好地使用使用内存,Executor 内运行的 Task 之间共享着 Execution 内存。具体的,Spark 内部维护了一个 HashMap 用于记录每个 Task 占用的内存。当 Task 需要在 Execution 内存区域申请 numBytes 内存,其先判断 HashMap 里面是否维护着这个 Task 的内存使用情况,如果没有,则将这个 Task 内存使用置为0,并且以 TaskId 为 key,内存使用为 value 加入到 HashMap 里面。之后为这个 Task 申请 numBytes 内存,如果 Execution 内存区域正好有大于 numBytes 的空闲内存,则在 HashMap 里面将当前 Task 使用的内存加上 numBytes,然后返回;如果当前 Execution 内存区域无法申请到每个 Task 最小可申请的内存,则当前 Task 被阻塞,直到有其他任务释放了足够的执行内存,该任务才可以被唤醒。每个 Task 可以使用 Execution 内存大小范围为 1/2N ~ 1/N,其中 N 为当前 Executor 内正在运行的 Task 个数。一个 Task 能够运行必须申请到最小内存为 (1/2N * Execution 内存);当 N = 1 的时候,Task 可以使用全部的 Execution 内存。
比如如果 Execution 内存大小为 10GB,当前 Executor 内正在运行的 Task 个数为5,则该 Task 可以申请的内存范围为 10 / (2 * 5) ~ 10 / 5,也就是 1GB ~ 2GB的范围。
 

对于java对象占用内存的情况,spark关于优化数据结构的建议:

  • 避免使用java/scala的集合类(比如hashMap)以及创建各种小对象,尽量使用数组和原始数据类型来代替。
  • 使用fastutil优化数据结果(fastutil githup),fastutil扩展了Java标准集合框架(Map、List、Set;HashMap、ArrayList、HashSet)的类库,提供了特殊类型的map、set、list和queue;优点就是更小的内存占用,更快的存取速度,所以可以使用fastutil提供的集合类,来替代自己平时使用的JDK的原生的Map、List、Set,
  • 如果算子函数使用了外部变量;第一你可以使用Broadcast广播变量优化;第二,可以使用Kryo序列化类库,提升序列化性能和效率;第三,如果外部变量是某种比较大的集合,那么可以考虑使用fastutil改写外部变量,从源头上就减少内存的占用,通过广播变量进一步减少内存占用,再通过Kryo序列化类库进一步减少内存占用
  • 在你的算子函数里,也就是task要执行的计算逻辑里面,如果要创建比较大的Map、List等集合,可能会占用较大的内存空间,而且可能涉及到消耗性能的遍历、存取等集合操作;那么此时,可以考虑将这些集合类型使用fastutil类库重写,使用了fastutil集合类以后,就可以在一定程度上,减少task创建出来的集合类型的内存占用。避免executor内存频繁占满,频繁唤起GC,导致性能下降。
  • 对于用的一些key值,尽量使用数值类型而不要使用string类型。

对于大的RDD,可以考虑使用序列化的方式存储:

  • 以序列化形式存储数据,可以减少空间占用。
  • 以序列化形式存储数据,会将每个RDD分区保存成一份大的字节数组。
  • 以序列化形式存储数据,唯一的缺点是访问速度较慢,需要反序列化。
  • 推荐使用上述kryo序列化方式
  • 存储级别方式,比如MEMORY_ONLY_SER,详细可以参考官网RDD Persistence

GC优化:

  • 推荐使用G1 GC,具体的可以参考前面的G1 GC文章

任务并行度:

  • 并行度过低,集群资源利用率低,应用性能低;并行度过高,增加任务调度开销,性能不一定会相对提升反而可能会下降。
  • 对于map端的并行度,spark会根据数据量的大小自动计算map的数量(通过一个简单的公式),而对于reduce的操作,比如groupByKey, reduceByKey等,它使用其父RDD的最大分区数(对这里的进一步优化可以参考spark自适应执行文章)
  • 调整并行度可以在一些算子中传入并行度的参数,或者通过spark.default.parallelism指定(sparksql也有其对应的参数值spark.sql.shuffle.partitions)
  • 一般情况下,建议并行度设置为executor配置的cpu总数量的2-3倍

广播变量:

  • 对于read-only数据可以考虑广播,广播的好处在于:不执行广播每个task拥有一个变量,广播之后每个executor共享一个变量。减少了网络传输以及内存占用。
  • 创建广播变量代码:
Broadcast<int[]> broadcastVar = sc.broadcast(new int[] {1, 2, 3});

broadcastVar.value();
// returns [1, 2, 3]
  • spark会在日志中打印出每个task序列化之后的大小,一般情况下,如果task大小超过20KB便可以考虑是否可以使用广播方式优化。
  • 对于spark sql的自动广播小表,需要在知道小表的元数据前提下且该小表在生成元数据时候执行了ANALYZE命令。此外部分transform得到的小表不一定可以广播,可以将小表落地或者使用sql brocast hint

数据本地性:

数据本地性对spark任务的性能有很大的影响的,如果数据和代码(task)在一块计算基本上是最快的。如果数据和代码(task)不在一块,那么必须有一项需要移动的,并且往往移动的是代码(task),因为大部分情况下它是相对较小的,移动它相对更快开销更小,

数据本地性分为几个级别:

PROCESS_LOCAL data is in the same JVM as the running code. This is the best locality possible
NODE_LOCAL data is on the same node. Examples might be in HDFS on the same node, or in another executor on the same node. This is a little slower than PROCESS_LOCAL because the data has to travel between processes
NO_PREF data is accessed equally quickly from anywhere and has no locality preference
RACK_LOCAL data is on the same rack of servers. Data is on a different server on the same rack so needs to be sent over the network, typically through a single switch
ANY data is elsewhere on the network and not in the same rack

PROCESS_LOCAL是最好的级别,也就是数据和代码是在同一JVM,但是并不是所有的task都可以达到这个级别。对于怎么对每个task都达到一个最理想的级别,spark有两种策略:

1.等,等到可以达到理想的数据本地性级别的cpu闲下来,然后自己顶上去。等待时间有个默认值。(这里也有一定的坑,比如数据不大或者执行逻辑简单,在刚达到或者还未达到该时间的时候,task就已执行完毕,这种情况下就会发现task扎堆执行,给人一种数据倾斜的错觉)

2.不管一切就是起任务,立即在非理想级别状态下执行。

Tunning spark的更多相关文章

  1. Spark踩坑记——Spark Streaming+Kafka

    [TOC] 前言 在WeTest舆情项目中,需要对每天千万级的游戏评论信息进行词频统计,在生产者一端,我们将数据按照每天的拉取时间存入了Kafka当中,而在消费者一端,我们利用了spark strea ...

  2. Spark RDD 核心总结

    摘要: 1.RDD的五大属性 1.1 partitions(分区) 1.2 partitioner(分区方法) 1.3 dependencies(依赖关系) 1.4 compute(获取分区迭代列表) ...

  3. spark处理大规模语料库统计词汇

    最近迷上了spark,写一个专门处理语料库生成词库的项目拿来练练手, github地址:https://github.com/LiuRoy/spark_splitter.代码实现参考wordmaker ...

  4. Hive on Spark安装配置详解(都是坑啊)

    个人主页:http://www.linbingdong.com 简书地址:http://www.jianshu.com/p/a7f75b868568 简介 本文主要记录如何安装配置Hive on Sp ...

  5. Spark踩坑记——数据库(Hbase+Mysql)

    [TOC] 前言 在使用Spark Streaming的过程中对于计算产生结果的进行持久化时,我们往往需要操作数据库,去统计或者改变一些值.最近一个实时消费者处理任务,在使用spark streami ...

  6. Spark踩坑记——初试

    [TOC] Spark简介 整体认识 Apache Spark是一个围绕速度.易用性和复杂分析构建的大数据处理框架.最初在2009年由加州大学伯克利分校的AMPLab开发,并于2010年成为Apach ...

  7. Spark读写Hbase的二种方式对比

    作者:Syn良子 出处:http://www.cnblogs.com/cssdongl 转载请注明出处 一.传统方式 这种方式就是常用的TableInputFormat和TableOutputForm ...

  8. (资源整理)带你入门Spark

    一.Spark简介: 以下是百度百科对Spark的介绍: Spark 是一种与 Hadoop 相似的开源集群计算环境,但是两者之间还存在一些不同之处,这些有用的不同之处使 Spark 在某些工作负载方 ...

  9. Spark的StandAlone模式原理和安装、Spark-on-YARN的理解

    Spark是一个内存迭代式运算框架,通过RDD来描述数据从哪里来,数据用那个算子计算,计算完的数据保存到哪里,RDD之间的依赖关系.他只是一个运算框架,和storm一样只做运算,不做存储. Spark ...

随机推荐

  1. 吴裕雄 Bootstrap 前端框架开发——Bootstrap 辅助类:元素浮动到右边

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title> ...

  2. 页面的五种布局以及嵌套『Android系列八』

    转自:http://blog.csdn.net/dazlly/article/details/7860125 因为学习比较晚,我用的相关版本为SDK4.1.eclipse4.2,而自己看的教材都是低版 ...

  3. Numpy与List之间的转换

    说明:在做NLP的时候,经常需要查看当前数组数据的维度,也就是data.shape,而List是没有这个属性的,因此需要先将其转换成Numpy,以下为两者户想转换的方法 List转Numpy:nump ...

  4. hibernate中简单的增删改查

    项目的整体结构如下 1.配置文件 hibernate.cfg.xml <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hi ...

  5. C语言常用函数

    一.数学函数 调用数学函数时,要求在源文件中包下以下命令行: #include <math.h> 函数原型说明 功能 返回值 说明 int abs( int x) 求整数x的绝对值 计算结 ...

  6. PAT (Advanced Level) 1128~1131:1128N皇后 1129 模拟推荐系统(set<Node>优化) 1130 中缀表达式

    1128 N Queens Puzzle(20 分) 题意:N皇后问题.按列依次给定N个皇后的行号,问N个皇后是否能同时不存在行冲突.列冲突和主副对角线冲突. 分析: 1.根据题意一定不存在列冲突,所 ...

  7. bootstrap点击下拉菜单没反应

    出现这个问题一般就涉及 网页脚本的问题 好好看看自己网页 scripts 编写是否正确 也可以通过浏览器的 F12 进入console 控制台看看是什么问题 总的来说 该错误要从网页脚本编写的问题出发 ...

  8. Java8 使用LocalDate计算两个日期间隔多少年,多少月,多少天

    最近项目遇到一个需要计算两个日期间隔的期限,需要计算出,整年整月整日这样符合日常习惯的说法,利用之前的Date和Calendar类会有点复杂,刚好项目使用了JDK8,那就利用起来这个新特性,上代码: ...

  9. springboot学习3事务控制

    springboot学习3事务控制 spring的事务控制本质上是通过aop实现的. 在springboot中使用时,可以通过注解@Transactional进行类或者方法级别的事务控制,也可以自己通 ...

  10. Flask—核心对象app初步理解

    前言 flask的核心对象是Flask,它定义了flask框架对于http请求的整个处理逻辑.随着服务器被启动,app被创建并初始化,那么具体的过程是这样的呢? flask的核心程序就两个: 1.we ...