一、为什么需要调优
一般情况在实际的生产环境中编写代码会有各种各样的事情发生,比如说赶项目,所以留给程序猿的时间并不是很多,有时候还要面临需求方催进度或给领导进行汇报等等原因,造成在开发初期的时候一味的只是追求代码的功能实现,所以在易用性和性能上会比较差一些,所以在后期会对原有的代码进行性能调优和代码进行维护升级、优化、重构等等(ps:要不然性能低下的话真的是把人给“搞死”了,尤其是在大数据领域)。
二、开发调优
开发调优就是在代码层级来对程序进行一些调试和优化!
2.1避免创建重复的RDD
通常来说,我们在开发一个Spark作业时,首先是基于某个数据源(比如Hive表或HDFS文件)创建一个初始的RDD;接着对这个RDD执行某个算子操作,然后得到下一个RDD;以此类推,循环往复,直到计算出最终我们需要的结果。在这个过程中,多个RDD会通过不同的算子操作(比如map、reduce等)串起来,这个“RDD串”,就是RDD lineage,也就是“RDD的血缘关系链”。
我们在开发过程中要注意:对于同一份数据,只应该创建一个RDD,不能创建多个RDD来代表同一份数据。
2.2尽可能复用同一个RDD
1、我们除了要避免在开发过程中对一份完全相同的数据创建多个RDD之外,在对不同的数据执行算子操作时还要尽可能地复用一个RDD。
2、比如说,有一个RDD的数据格式是key-value类型的,另一个是单value类型的,这两个RDD的value数据是完全一样的。那么此时我们可以只使用key-value类型的那个RDD,因为其中已经包含了另一个的数据。对于类似这种多个RDD的数据有重叠或者包含的情况,我们应该尽量复用一个RDD,这样可以尽可能地减少RDD的数量,从而尽可能减少算子执行的次数。
2.3对多次使用的RDD进行持久化
1、当我们在Spark代码中多次对一个RDD做了算子操作后,恭喜,你已经实现Spark作业第一步的优化了,也就是尽可能复用RDD。此时就该在这个基础之上,进行第二步优化了,也就是要保证对一个RDD执行多次算子操作时,这个RDD本身仅仅被计算一次。
2、Spark中对于一个RDD执行多次算子的默认原理是这样的:每次你对一个RDD执行一个算子操作时,都会重新从源头处计算一遍,计算出那个RDD来,然后再对这个RDD执行你的算子操作。这种方式的性能是很差的。
3、因此对于这种情况,建议是:对多次使用的RDD进行持久化。此时Spark就会根据你的持久化策略,将RDD中的数据保存到内存或者磁盘中。以后每次对这个RDD进行算子操作时,都会直接从内存或磁盘中提取持久化的RDD数据,然后执行算子,而不会从源头处重新计算一遍这个RDD,再执行算子操作。
2.3.1持久化策略
祥参:
2.3.2选择合适的持久化策略
1、默认情况下,性能最高的当然是MEMORY_ONLY,但前提是你的内存必须足够足够大,可以绰绰有余地存放下整个RDD的所有数据。因为不进行序列化与反序列化操作,所以就避免了这部分的性能开销;对这个RDD的后续算子操作,都是基于纯内存中的数据的操作,不需要从磁盘文件中读取数据,性能也很高;而且不需要复制一份数据副本,并远程传送到其他节点上。但是这里必须要注意的是,在实际的生产环境中,恐怕能够直接用这种策略的场景还是有限的,如果RDD中数据比较多时(比如几十亿),直接用这种持久化级别,会导致JVM的OOM内存溢出异常。
2、如果使用MEMORY_ONLY级别时发生了内存溢出,那么建议尝试使用MEMORY_ONLY_SER级别。该级别会将RDD数据序列化后再保存在内存中,此时每个partition仅仅是一个字节数组而已,大大减少了对象数量,并降低了内存占用。这种级别比MEMORY_ONLY多出来的性能开销,主要就是序列化与反序列化的开销。但是后续算子可以基于纯内存进行操作,因此性能总体还是比较高的。此外,可能发生的问题同上,如果RDD中的数据量过多的话,还是可能会导致OOM内存溢出的异常。
3、如果纯内存的级别都无法使用,那么建议使用MEMORY_AND_DISK_SER策略,而不是MEMORY_AND_DISK策略。因为既然到了这一步,就说明RDD的数据量很大,内存无法完全放下。序列化后的数据比较少,可以节省内存和磁盘的空间开销。同时该策略会优先尽量尝试将数据缓存在内存中,内存缓存不下才会写入磁盘。
4、通常不建议使用DISK_ONLY和后缀为_2的级别:因为完全基于磁盘文件进行数据的读写,会导致性能急剧降低,有时还不如重新计算一次所有RDD。后缀为_2的级别,必须将所有数据都复制一份副本,并发送到其他节点上,数据复制以及网络传输会导致较大的性能开销,除非是要求作业的高可用性,否则不建议使用。
2.4尽量避免使用shuffle类算子
因为一旦有shuffle操作,那么必然就会有数据的网络传输,这也是分布式计算最消耗性能的地方,所以我们能避免shuffle则尽量避免shuffle。
shuffle过程中,各个节点上的相同key都会先写入本地磁盘文件中,然后其他节点需要通过网络传输拉取各个节点上的磁盘文件中的相同key。而且相同key都拉取到同一个节点进行聚合操作时,还有可能会因为一个节点上处理的key过多,导致内存不够存放,进而溢写到磁盘文件中。因此在shuffle过程中,可能会发生大量的磁盘文件读写的IO操作,以及数据的网络传输操作。磁盘IO和网络数据传输也是shuffle性能较差的主要原因。
最经典的案例,莫过于使用广播变量+map或者flatMap算子代替join操作。
?:
import org.apache.spark.broadcast.Broadcast
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkContext, SparkConf}
object BroadCast {
def main(args: Array[String]) {
val conf =new SparkConf()
.setAppName("BroadCast")
.setMaster("local[*]")
val sc = new SparkContext(conf)
//小表,学生表
val stuMap = List(
"1 zhangsan 22 bd-bj",
"2 lisi 25 bd-bj",
"3 zhangyaoshi 24 bd-sz",
"4 hudada 18 bd-wh",
"5 chenpingan 25 bd-bj"
).map(line => {
val sid = line.substring(0,1)
val info = line.substring(1).trim()
(sid,info)
}).toMap
//创建广播变量
val stuBC:Broadcast[Map[String,String]] =sc.broadcast(stuMap)
//大表,成绩表
val scores = List(
"1 1 math 82",
"2 1 english 0",
"3 2 chinese 85.5",
"4 3 PE 99",
"5 5 math 99"
)
val scoreRDD:RDD[String] = sc.parallelize(scores)
scoreRDD.map(scoreLine =>{
val filds = scoreLine.split("\\s+")
val sid = filds(1)
//取出广播变量的值
val bcMap = stuBC.value
val info = bcMap.getOrElse(sid,null)
s"${sid}\t${info}\t${filds(2)}\t${filds(3)}"
}).foreach(println)
}
}
2.5使用map-side预聚合的shuffle操作
使用reduceByKey,而尽量避免使用groupByKey这些没有本地预聚合map-side聚合操作的算子。如果因为业务需要,一定要使用shuffle操作,无法用map类的算子来替代,那么尽量使用可以map-side预聚合的算子。
所谓的map-side预聚合,说的是在
每个节点本地对相同的key进行一次聚合操作
,类似于MapReduce中的本地combiner。map-side预聚合之后,每个节点本地就只会有一条相同的key,因为多条相同的key都被聚合起来了。其他节点在拉取所有节点上的相同key时,就会大大减少需要拉取的数据数量,从而也就减少了磁盘IO以及网络传输开销。通常来说,在可能的情况下,建议使用reduceByKey或者aggregateByKey算子来替代掉groupByKey算子。因为reduceByKey和aggregateByKey算子都会使用用户自定义的函数对每个节点本地的相同key进行预聚合。而groupByKey算子是不会进行预聚合的,全量的数据会在集群的各个节点之间分发和传输,性能相对来说比较差。
2.6 使用高性能的算子
2.6.1. 建议使用mapPartitions代替map
mapPartitions(func)的作用其实mapPartitions作和map一模一样,只不过,map(func)算子是partition中的每一条数据都会调用一次map函数;而mapPartitions,就是一个分区中的所有数据只调用一次函数。所以我们可以将mapPartitions理解为map操作的批量处理版本。但是也需要慎重,因为一次性加载一个分区中的所有数据进行处理,需面临gc的风险。
/*
spark中Partitioner有两种
RangePartitioner--->加载数据的时候进行分区
HashPartitioner--->shuffle操作的时候按照key和partition的个数hash取模分区
*/
import org.apache.spark.{SparkContext, SparkConf}
object MapPartitions {
def main(args: Array[String]) {
val conf=new SparkConf()
.setAppName("MapPartitions")
.setMaster("local[4]")
val sc = new SparkContext(conf)
val list = 1 to 500
val numbersRDD=sc.parallelize(list)
val retRDD=numbersRDD.mapPartitions(partition =>{
partition.map(_*5)
})
retRDD.saveAsTextFile("file:///Users/XXX/Desktop/文档/sparkOptimize")
sc.stop()
}
}
运行结果(一个分区对应一个文件):
2.6.2建议使用foreachPartitions代替foreach
foreach操作一般都是将partition中的每一条数据遍历处理,很常见的一个操作就是将结果写入到数据库。
2.6.2.1有问题的代码
?:
/**
* 代码有问题,jdbs的相关对象都是在driver中创建的,而在网络中传输是需要进行序列化的,所以这里会报错!!!rbkRDD.foreach是在executor中执行的~
* @param rbkRDD
*/
private def foreachOps1(rbkRDD: RDD[(String, Int)]) = {
/*
将统计结果输出到mysql数据
jdbc: 6个步骤
1、加载驱动
2、创建连接Connection
3、创建执行器statement
4、执行sql语句
5、如果有结果集,封装结果集
6、释放资源
*/
// Class.forName("com.mysql.jdbc.Driver")
//step 1、
classOf[Driver]
//step 2、connection
val url = "jdbc:mysql://localhost:3306/test"
val connection = DriverManager.getConnection(url, "数据库用户名", "数据库密码")
// connection.createStatement()//会有sql注入的风险
val sql = "insert into wordcount(word, `count`) values(?, ?)"
//step 3
val ps = connection.prepareStatement(sql)
rbkRDD.foreach { case (word, count) => {
ps.setString(1, word)
ps.setInt(2, count)
//step 4
ps.execute()
}
}
//step 6
ps.close()
connection.close()
}
2.6.2.2上述问题代码修正
只需要将connection等构建在partition侧,自然就是分区内部在进行操作,也就不会有网络的数据的传递,进而就避免了序列化问题。
?:
private def foreachOps2(rbkRDD: RDD[(String, Int)]) = {
rbkRDD.foreach { case (word, count) => {
classOf[Driver]
//step 2、connection
val url = "jdbc:mysql://localhost:3306/Test"
val connection = DriverManager.getConnection(url, "数据库用户名", "数据库密码")
// connection.createStatement()//会有sql注入的风险
val sql = "insert into wordcount(word, `count`) values(?, ?)"
//step 3
val ps = connection.prepareStatement(sql)
ps.setString(1, word)
ps.setInt(2, count)
//step 4
ps.execute()
ps.close()
connection.close()
}}
}
2.6.2.3优化——使用foreachPartition代替foreach
上述2.6.1.2中的foreach操作是每一条记录都会创建一个Connection,性能非常差,如果rdd中有100w数据,那么就会创建100w次的Connection,这样的话mysql扛不住。所以我们使用foreachPartition去代替foreach操作,可以做到每一个分区只创建一个Connection连接,大大的提升写入mysql的性能。
?:
//使用foreachPartition替换foreach
def foreachOps3(rbkRDD: RDD[(String, Int)]): Unit ={
rbkRDD.foreachPartition(partition =>{
classOf[Driver]
val url = "jdbc:mysql://localhost:3306/Test"
val connection = DriverManager.getConnection(url,"数据库用户名", "数据库密码")
val sql = "insert into wordcount(word, `count`) values(?, ?)"
val ps = connection.prepareStatement(sql)
partition.foreach{case (word,count) =>
ps.setString(1, word)
ps.setInt(2, count)
ps.execute()
ps.close()
}
connection.close()
})
}
2.6.2.4优化——使用foreachPartition优化
2.6.1.3中的操作,虽然使用了foreachPartition,但是在执行的时候,还是一条sql语句执行一次数据库交互,而这些sql执行的动作都一样,所以非常适合做批量提交。
?:
def foreachOps4(rbkRDD: RDD[(String, Int)]): Unit ={
rbkRDD.foreachPartition(partition =>{
classOf[Driver]
val url = "jdbc:mysql://localhost:3306/Test"
val connection = DriverManager.getConnection(url, "数据库用户名", "数据库密码")
val sql = "insert into wordcount(word, `count`) values(?, ?)"
val ps = connection.prepareStatement(sql)
partition.foreach{case (word,count) =>
ps.setString(1, word)
ps.setInt(2, count)
ps.addBatch()//批量处理
}
ps.executeBatch()//批量提交
ps.close()
connection.close()
})
}
2.6.2.5优化——使用数据库连接池优化Connection创建
?:
自定义数据库连接池(java)
public class ConnectionPool {
private static LinkedList<Connection> pool = new LinkedList<Connection>();
private ConnectionPool(){}
static {//初始化工作
//注册驱动
try {
Properties properties = new Properties();
properties.load(ConnectionPool.class.getClassLoader()
.getResourceAsStream("db.properties"));
Class.forName(properties.getProperty(Constants.JDBC_DRIVER));
int MAX_ACTIVE = Integer.parseInt(properties.getProperty(Constants.JDBC_MAX_ACTIVE));
String url = properties.getProperty(Constants.JDBC_URL);
String user = properties.getProperty(Constants.JDBC_USER);
String password = properties.getProperty(Constants.JDBC_PASSWORD);
for(int i =0;i<MAX_ACTIVE;i++){
pool.push(DriverManager.getConnection(url,user,password));
}
} catch (Exception e) {
throw new ExceptionInInitializerError("初始化异常");
}
}
public static Connection getConnection(){
while(pool.isEmpty()){
try {
System.out.println("线程池为空,请稍后再试!");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return pool.poll();
}
public static void release(Connection connection){
pool.push(connection);
}
}
读取properties文件的内容的接口(java):
public interface Constants {
String JDBC_DRIVER = "jdbc.driver";
String JDBC_URL = "jdbc.url";
String JDBC_USER = "jdbc.user";
String JDBC_PASSWORD = "jdbc.password";
String JDBC_MAX_ACTIVE = "jdbc.max.active";
}
db.properties文件:
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/Test
jdbc.user=数据库用户名
jdbc.password=数据库密码
jdbc.max.active=10
调用类(scala):
object ForeachOps {
def main(args: Array[String]) {
val conf = new SparkConf()
.setAppName("ForeachPartitionsOps")
.setMaster("local[2]")
val sc = new SparkContext(conf)
val lines = sc.textFile("file:///Users/wangjinqi/Desktop/文档/Spark-调优-day01/代码/spark-study-1901/data/1行数据.txt")
val rbkRDD: RDD[(String, Int)] = lines.flatMap(_.split("\\s+")).map((_,1)).reduceByKey(_ + _)
foreach5(rbkRDD)
sc.stop()
}
def foreach5(rbkRDD: RDD[(String, Int)]): Unit = {
rbkRDD.foreachPartition(partition => {
val connection: Connection = ConnectionPool.getConnection()
val sql = "insert into wordcount(word, `count`) values(?, ?)"
val ps = connection.prepareStatement(sql)
partition.foreach { case (word, count) => {
ps.setString(1, word)
ps.setInt(2, count)
ps.addBatch()
}
}
ps.executeBatch()
ps.close()
connection.close()
})
}
2.6.2.6优化分批次批量写入
大家思考一下一个分区中的数据很多,假如有100w行的数据,不可能使用批量操作,把这100w的数据一次性的写入,会对数据库性能造成极大的考验,此时我们一般都是分批次批量的写入,比如一批写入5w,分20次写入。
提示:需要一个计数器,记录添加记录的条数,当达到提交之后执行写入。
?:
object ForeachOps {
def main(args: Array[String]) {
val conf = new SparkConf()
.setAppName("ForeachPartitionsOps")
.setMaster("local[2]")
val sc = new SparkContext(conf)
val lines = sc.textFile("file:///Users/wangjinqi/Desktop/文档/Spark-调优-day01/代码/spark-study-1901/data/1行数据.txt")
val rbkRDD: RDD[(String, Int)] = lines.flatMap(_.split("\\s+")).map((_, 1)).reduceByKey(_ + _)
foreach5(rbkRDD)
sc.stop()
}
def foreach5(rbkRDD: RDD[(String, Int)]): Unit = {
//计数器
var sum = 0
//该参数决定每次写入的数据量
val counter = 10
rbkRDD.foreachPartition(partition => {
val connection: Connection = ConnectionPool.getConnection()
val sql = "insert into wordcount(word, `count`) values(?, ?)"
val ps = connection.prepareStatement(sql)
partition.foreach { case (word, count) => {
sum += 1
ps.setString(1, word)
ps.setInt(2, count)
ps.addBatch()
if(sum%counter == 0){
ps.executeBatch()
}
}
}
ps.executeBatch()
ps.close()
connection.close()
})
}
2.6.3建议在filter之后执行coalesce
filter是过滤的意思,coalesce是合并的意思。也就是当一个rdd在执行filter之后,进行合并分区。比如说原先分区有100个,经过filter之后有30%被过滤掉了,平均每个partition的数据量就比原来少了30%,那么partition不饱和,此时为了提交计算的效率,我们可以执行coalesce算子合并,将其中多个partition的数据合并到一个partition中。
val conf = new SparkConf()
.setAppName("RepartitionOps")
.setMaster("local[2]")
val sc = new SparkContext(conf)
val list = 1 to 10000
val listRDD = sc.parallelize(list, 10)
println("partitions num: " + listRDD.getNumPartitions)
//过滤其中的偶数
val oddRDD = listRDD.filter( _ % 2 != 0)
//val coalRDD = oddRDD.coalesce(5, shuffle = true)
//coalesce合并分区增大有一个简写方式:repartition
val coalRDD = oddRDD.repartition(5)
println("合并之后的rdd的分区num: " + coalRDD.getNumPartitions)
println("coalRDD中的记录数: " + coalRDD.count())
sc.stop()
默认情况下coalesce算子是一个窄依赖算子,repartition是coalesce算子的宽依赖操作的版本。
要想减少分区使用默认的coalesce、增大分区使用repartition。
2.6.4使用repartitionAndSortWithinPartitions替代repartition与sort类操作
repartitionAndSortWithinPartitions是Spark官网推荐的一个算子,官方建议,如果需要在repartition重分区之后,还要进行排序,建议直接使用repartitionAndSortWithinPartitions算子。因为该算子可以一边进行重分区的shuffle操作,一边进行排序。shuffle与sort两个操作同时进行,比先shuffle再sort来说,性能可能是要高的。
2.7 广播大变量
广播变量在使用过程中,需要注意:第一,体积比较大的变量;第二:避免频繁的更新
祥参:
2.8 使用Kryo优化序列化性能
在spark中使用的默认的序列化是java中的序列化。
2.8.1什么是序列化
1、在任何一个分布式系统中,序列化都扮演着一个重要的角色。如果使用序列化技术,在执行序列化操作的时候很慢,或者是序列化后的数据还是很大,那么会让分布式应用程序的性能下降很多。所以,进行Spark性能优化的第一步,就是进行序列化的性能优化。
2、Spark自身默认就会在一些地方对数据进行序列化,比如Shuffle。还有就是,如果我们的算子函数使用到了外部的数据(比如Java内置类型,或者自定义类型),那么也需要让其可序列化。
3、而Spark自身对于序列化的便捷性和性能进行了一个取舍和权衡。默认,Spark倾向于序列化的便捷性,使用了Java自身提供的序列化机制——基于ObjectInputStream和ObjectOutputStream的序列化机制。因为这种方式是Java原生提供的,很方便使用。
4、但是问题是,Java序列化机制的性能并不高。序列化的速度相对较慢,而且序列化以后的数据,还是相对来说比较大,还是比较占用内存空间。因此,如果你的Spark应用程序对内存很敏感,那么,实际上默认的Java序列化机制并不是最好的选择。
5、我们有时候会根据我的应用场景来进行取舍,稳定性 OR 性能?
2.8.2 spark的序列化机制
Spark实际上提供了两种序列化机制,它默认的是使用Java的序列化机制
1、Java序列化机制:默认情况下,Spark使用Java自身的ObjectInputStream和ObjectOutputStream机制进行对象的序列化。只要你的类实现了Serializable接口,那么都是可以序列化的。而且Java序列化机制是提供了自定义序列化支持的,只要你实现Externalizable接口即可实现自己的更高性能的序列化算法。Java序列化机制的速度比较慢,而且序列化后的数据占用的内存空间比较大。
2、Kryo序列化机制:Spark也支持使用Kryo类库来进行序列化。Kryo序列化机制比Java序列化机制更快,而且序列化后的数据占用的空间更小,通常比Java序列化的数据占用的空间要小10倍。Kryo序列化机制之所以不是默认序列化机制的原因是,有些类型虽然实现了Seriralizable接口,但是它也不一定能够进行序列化;此外,如果你要得到最佳的性能,Kryo还要求你在Spark应用程序中,对所有你需要序列化的类型都进行注册。
2.8.3 涉及到序列化的地方
1、在算子函数中使用到外部变量时,该变量会被序列化后进行网络传输。
2、将自定义的类型作为RDD的泛型类型时(比如JavaRDD,Student是自定义类型),所有自定义类型对象,都会进行序列化。因此这种情况下,也要求自定义的类必须实现Serializable接口。
如果说你实现了一个自定义的这种类型,那么必须注册让kryo知道,你要进行此类的一个序列化类
3、使用可序列化的持久化策略时(比如MEMORY_ONLY_SER),Spark会将RDD中的每个partition都序列化成一个大的字节数组。
2.8.4 如何使用kryo序列化机制
- 开启kryo序列化策略
set("spark.serializer", classOf[KryoSerializer].getName)
或者
set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
- spark-submit脚本时需要进行开启
--conf spark.serializer=org.apache.spark.serializer.KryoSerializer
- 注册要进行序列化的类型
conf.registerKryoClasses(Array(classOf[MySecondSort]))
2.8.5kryo本身参数优化说明
sparkConf官方网址:http://spark.apache.org/docs/2.2.2/configuration.html
1、优化缓存大小
如果注册的要序列化的自定义的类型,本身特别大,比如包含了超过100个field。那么就会导致要序列化的对象过大。此时就需要对Kryo本身进行优化。因为Kryo内部的缓存可能不够存放那么大的class对象。此时就需要调用SparkConf.set()方法,设置spark.kryoserializer.buffer.mb参数的值,将其调大。
默认情况下它的值是2,就是说最大能缓存2M的对象,然后进行序列化。可以在必要时将其调大。比如设置为10。
2、预先注册自定义类型
虽然不注册自定义类型,Kryo类库也能正常工作,但是那样的话,对于它要序列化的每个对象,都会保存一份它的全限定类名。此时反而会耗费大量内存。因此通常都建议预先注册好要序列化的自定义的类。
2.9优化数据结构
1、要减少内存的消耗,除了使用高效的序列化类库以外,还有一个很重要的方法,就是优化数据结构。从而避免Java语法特性中所导致的额外内存的开销,比如基于指针的Java数据结构,以及包装类型。
2、有一个关键的问题,就是优化什么数据结构?其实主要就是优化你的算子函数,内部使用到的局部数据,或者是算子函数外部的数据。都可以进行数据结构的优化。优化之后,都会减少其对内存的消耗和占用可能以及合适的情况下,使用占用内存较少的数据结构,但是前提是要保证代码的可维护性
Java中,有三种类型比较耗费内存:
①对象,每个Java对象都有对象头、引用等额外的信息,因此比较占用内存空间。
②字符串,每个字符串内部都有一个字符数组以及长度等额外信息。
③集合类型,比如HashMap、LinkedList等,因为集合类型内部通常会使用一些内部类来封装集合元素,比如Map.Entry。
因此Spark官方建议,在Spark编码实现中,特别是对于算子函数中的代码,尽量不要使用上述三种数据结构,尽量使用字符串替代对象,使用原始类型(比如Int、Long)替代字符串,使用数组替代集合类型,这样尽可能地减少内存占用,从而降低GC频率,提升性能。
总结:
想要做到这些其实很难。因为我们同时要考虑到代码的可维护性,如果一个代码中,完全没有任何对象抽象,全部是字符串拼接的方式,那么对于后续的代码维护和修改,无疑是一场巨大的灾难。同理,如果所有操作都基于数组实现,而不使用HashMap、LinkedList等集合类型,那么对于我们的编码难度以及代码可维护性,也是一个极大的挑战。因此建议,在可能以及合适的情况下,使用占用内存较少的数据结构,但是前提是要保证代码的可维护性
2.10提高并行度
2.10.1什么是并行度
并行度就是说同时又多少个线程来运行task作业,运行一个task自然需要一个线程。所以有时候我们也可以粗略的将并行度理解为同时能够运行的线程个数。
2.10.2为什么要设置并行度
在我们提交我们的Spark程序时,Spark集群的资源并不一定会被充分使用,所以要设置合适的并行度,来充分利用我们集群的资源。
比如,Spark在读取HDFS文件时,默认情况下会根据每个block创建一个partition,也依据这个设置并行度。
2.10.3并行度设置方式
两种方式:
1、手动使用textFile()、parallelize()等方法的第二个参数来设置并行度;
2、在sparkConf或者Spark-submit中指定使用spark.default.parallelism参数,来设置统一的并行度。
注意: Spark官方的推荐是,给集群中的每个cpu core设置2~3倍个task。
比如说,spark-submit设置了executor数量是100个,每个executor要求分配5个core,那么我们的这个application总共会有500个core。此时可以设置new
SparkConf().set(“spark.default.parallelism”,“1200”)来设置合理的并行度,从而充分利用资源。
默认值:
1、如果是reduceByKey或者join等shuffle操作,对应的并行度就是这些操作的父rdd中最大的分区数
2、如果是textFile或parallelize等输入算子操作,就要根据clustermanager设置来决定。
1)本地模式就是local[N]中的N
2)mesos的细粒度模式,就是8
3)其它情况Math.max(2, executor-num * executor-core)
2.11数据本地化
2.11.1什么是数据本地化
数据本地化指的就是数据和计算它的代码之间的距离。
2.11.2数据本地化级别
1、PROCESS_LOCAL:数据和计算它的代码在同一个JVM进程中。
2、NODE_LOCAL:数据和计算它的代码在一个节点上,但是不在一个进程中,比如在不同的executor进程中,或者是数据在HDFS文件的block中。
3、NO_PREF:数据从哪里过来,性能都是一样的。
4、RACK_LOCAL:数据和计算它的代码在一个机架上。
5、ANY:数据可能在任意地方,比如其他网络环境内,或者其他机架上。
2.11.3数据本地化的参数配置
Spark倾向于使用最好的本地化级别来调度task,但是这是不可能的。如果没有任何未处理的数据在空闲的executor上,那么Spark就会放低本地化级别。这时有两个选择:
第一,等待,直到executor上的cpu释放出来,那么就分配task过去;
第二,立即在任意一个executor上启动一个task。
Spark默认会等待一会儿,来期望task要处理的数据所在的节点上的executor空闲出一个cpu,从而将task分配过去。只要超过了时间,那么Spark就会将task分配到其他任意一个空闲的executor上。
可以设置参数,spark.locality系列参数,来调节Spark等待task可以进行数据本地化的时间。
spark.locality.wait(3000毫秒)、
spark.locality.wait.node、
spark.locality.wait.process、
spark.locality.wait.rack。
2.11.4如何选择数据本地化
Spark中任务的处理需要考虑所涉及的数据的本地性的场合基本就两种:
一是数据的来源是HadoopRDD;
二是RDD的数据来源来自于RDD Cache(即由CacheManager从BlockManager中读取,或者Streaming数据源RDD)。
其它情况下,如果不涉及shuffle操作的RDD,不构成划分Stage和Task的基准,不存在判断Locality本地性的问题,而如果是ShuffleRDD,其本地性始终为No Prefer,因此其实也无所谓Locality。在理想的情况下,任务当然是分配在可以从本地读取数据的节点上时(同一个JVM内部或同一台物理机器内部)的运行时性能最佳。但是每个任务的执行速度无法准确估计,所以很难在事先获得全局最优的执行策略,当Spark应用得到一个计算资源的时候,如果没有可以满足最佳本地性需求的任务可以运行时,是退而求其次,运行一个本地性条件稍差一点的任务呢,还是继续等待下一个可用的计算资源已期望它能更好的匹配任务的本地性呢?
这几个参数一起决定了Spark任务调度在得到分配任务时,选择暂时不分配任务,而是等待获得满足进程内部/节点内部/机架内部这样的不同层次的本地性资源的最长等待时间。默认都是3000毫秒。