一、spark的运行架构包括集群资源管理器Cluster Manager(standalone:spark自带的、Yarn、Messos)、运行作业的工作节点(Worker Node)、每个应用的任务控制节点(Driver Program简称Driver)、每个工作节点上负责具体任务的执行进程(Executor)。spark采用的是主从架构,包含一个主节点Master(即Driver)和若干个从节点Worker。
1、spark所采用的executor有两个优点:第一是利用多线程执行具体任务,减少任务的启动开销,第二是Executor中有一个BlockManager存储模块,会将内存和磁盘共同作为存储设备(默认使用内存,当内存不够时,会写到磁盘)
2、在spark中,一个应用由一个任务控制节点(Driver)和若干的作业(job)构成,一个作业由多个阶段(stage)构成,一个阶段由多个任务(Task)组成。
3、当执行一个应用时,任务控制节点Driver会向集群管理器(Cluster Manager)申请资源,然后在Worker节点上启动Executor并向Executor发送应用程序代码和文件,然后在Executor上执行任务,运行结束后,执行结果会返回给任务控制节点Driver,最后写道HDFS或者其他数据库中。
4、RDD叫做弹性分布式数据集,它分为两种数据运算:转换(Transformation)和行动(Action)两种类型,Transformation制定RDD之间的相互依赖关系,action用于执行计算并制定输出形式。这两种类型的主要区别是:Transformation包含map、filter、groupBy、join等操作接受RDD并返回RDD,而Action包含count、collect等操作,接受RDD不返回RDD(只是一个值或输出结果),因此RDD比较适合对于数据集中元素执行相同操作的批处理式应用,而不适合用于需要异步、细粒度状态的应用,比如Web应用系统、增量式的网页爬虫等,
5、RDD真正计算是发生在Action操作,而转换只是记录了各个RDD的对应关系(即DAG图)。
6、RDD特性,Spark采用RDD以后能够实现高效计算的主要原因有:
(1)高效的容错性:比如当有一个节点或分区出现问题,或者需要修改数据时,不需要回滚整个系统,只需要重新计算出现问题的节点或分区即可,从而提高了容错性。
(2)中间结果持久化到内存:数据在内存中的多个RDD操作之间传递,不需要“落地”到磁盘上,避免了不比较的读写磁盘开销。
(3)存放的数据可以是java对象,避免了不必要的对象序列化和反序列化开销。
7、RDD中的依赖关系可以分为窄依赖和宽依赖,二者的区别在于是否包含Shuffle。
8、窄依赖是两个RDD中的分区之间的关系是一对一的关系,它包括map、filter、union等不会包含shuffle,宽依赖则是两个RDD中的分区之间的关系是一对多的关系,它包含groupByKey、sortByKey等,通常会包含shuffle,对于join操作,可以分为两种情况:
(1)对输入进行协同划分属于窄依赖,所谓协同划分是指多个父RDD的某一个分区的所有key落在ziRDD的同一个分区内,不会产生同一个父RDD的某一分区落在子RDD的两个分区的情况。
(2)对输入做非协同划分属于宽依赖
9、shuffle操作会产生大量的磁盘I/O和网络开销,比如计算workcount操作,在map阶段会将key相同的分发到同一台机器进行reduce,从而导致了网络开销。Shuffle是对Map输出结果进行分区、排序、合并等处理交给Reduce的过程。
一、避免创建重复的RDD
1、对同一份数据,只创建一个RDD,不能创建多个RDD来代表同一份数据,否则会极大的浪费内存。
二、尽可能的复用同一个RDD,
1、比如:一个RDD数据格式是key-value,另一个是单独的value类型,这两个RDD的value部分完全一样,这样可以复用达到减少算子执行次数。
三、对多次使用的RDD进行持久化处理
1、每次对一个RDD执行一个算子操作时,都会重新从源头处理计算一遍,计算出那个RDD出来然后进一步操作,这种方式性能很差,可以通过对多次使用的RDD进行持久化,将RDD的数据保存在内存或磁盘中,避免重新计算,可以借助cache()和persist()方法,如下参数
spark持久化:cache、persist
持久化级别 | 含义 |
MEMORY_ONLY | 使用未序列化的java对象格式,将数据保存到内存中,如果内存不够存放所有数据,则数据可能就不会进行持久化,那么下次对这个RDD执行算子操作时,那些没有被持久化的数据,需要从源头处重新计算一遍,这是默认的持久化策略,使用cache()方法时,实际就是使用的这种持久化策略。 |
MEMORY_AND_DISK | 使用未序列化的java对象格式,优先尝试将数据保存在内存中,如果内存不够存放所有数据,会将数据写入磁盘文件中,下次对这个RDD执行算子时,持久化在磁盘文件中的数据会被读取出来使用 |
MEMORY_ONLY_SER | 基本含义同MEMORY_ONLY。唯一的区别就是会将RDD中的数据进行序列化,RDD的每个partition会被序列化成一个字节数组。这种方式更加节省内存,从而可以避免持久化的数据占用过多内存导致频繁GC。 |
MEMORY_AND_DISK_SER | 基本含义同MEMORY_AND_DISK,唯一的区别是会将RDD中的数据进行序列化,RDD的每个partition会被序列化成一个字节数组,这种方式更加节省内存,从而可以避免持久化的数据占用过多内存导致频繁GC。 |
DISK_ONLY | 使用未序列化的java对象格式,将数据全部写入磁盘文件中。 |
MEMORY_ONLY_2,MEMORY_AND_DISK_2 | 对于上述任一一种持久化策略,如果加上后缀_2,代表的是将每个持久化数据都复制一份副本,并将副本保存到其他节点上,这种基于副本的持久化机制主要用于进行容错,加入某个节点挂掉,节点的内存或磁盘中的持久化数据丢失了,那么后续对RDD计算时还可以使用该数据在其他节点上的副本,如果没有副本的话,就只能将这些数据从源头处重新计算一遍。 |
持久化级别可以通过persist(StorageLevel.MEMORY_AND_DISK_SER)设置,cache只放内存相当于persist(StorageLevel.MEMORY_ONLY).
四、避免使用shuffle类算子
1、在spark作业运行过程中,最消耗性能的地方就是shuffle过程
2、将分布在集群中多个节点上的同一个key,拉取到同一个节点上,进行聚合和join处理,比如:groupByKey、reduceByKey、join等算子,都会触发shuffle
比如一个join操作可以通过Broadcast+map的join操作,这样不会触发shuffle操作,但前提是只适合数据量较少时使用。
五、使用map-side预聚合的shuffle操作
1、一定要是用shuffle的且无法用map类算子替代的,那么尽量使用map-site预聚合算子,思想类似MapReduce的Combiner
2、可能的情况下尽量使用reduceByKey或aggregateByKey算子替代groupByKey算子,因为reduceByKey或aggregateByKey算子会使用用户自定义的函数对每个节点本地相同的key进行。
六、使用Kryo优化序列性能
1、Kryo是一个序列化类库,来优化序列化和反序列化性能
2、Spark默认使用java序列化机制(ObjectOutputStream/ObjectInputStream API)进行序列化和反序列化。
3、Spark支持使用Kryo序列化库,性能比java序列化库高很多,10倍左右。
可以通过conf.set("spark.serializer","org.apache.spark.serializer.KryoSerializer")来设置Kryo序列化,然后在注册:
conf.registerKryoClasses(Array(classOf[MyClass1],classOf[MyClass2]))。
七、调优参数
1、--num-executors:该作业总共需要多少executor进程执行,建议:每个作业运行一般设置50~100个左右合适。
2、--executor-memory:设置每个executor进程的内存,num-executors*executor-memory代表作业申请的总内存量(尽量不要超过最大总内存的1/3或1/2),建议:设置4G~8G较合适。
3、executor-cores:每个executor进程的CPU Core数量,该参数决定每个executor进程并行执行task线程的能力,num-executors*executor-cores代表作业申请总CPU core数(不要超过总CPU core的1/3~1/2) ,建议:设置2~4较合适。
4、driver-memory:设置Driver进程的内存,建议:通常不用设置,一般1G就够了,若出现使用collect算子将RDD数据全部拉取到Driver上处理,就必须确保该值足够大,否则OOM内存溢出。
5、spark.default.parallelism:分区,每个stage的默认task数量,建议:设置500~1000较合适,默认一个HDFS的block对应一个task,spark默认值偏少,这样导致不能充分利用资源。
6、spark.storage.memoryFraction:设置RDD持久化数据在executor内存中能占得比例,默认0.6,即默认executor60……的内存可以保存持久化RDD数据,建议:若有较多的持久化操作,可以设置高些,超出内存的会频繁gc导致运行缓慢。
7、spark.shuffle.memoryFraction:聚合操作占executor内存的比例,默认0.2,建议:若持久化操作较少,但shuffle较多时,可以降低持久化内存占比,提高shuffle操作内存占比。
命令:spark-submit --master yarn-cluster --num-executors 100 --executor-memory 6G --executor-cores 4 --driver-memery 1G --conf spark.default.parallelism=1000 --conf spark.storage.memoryFraction=0.5 --conf spark.shuffle.memoryFraction=0.3
八、spark的分区可以大大的减少了网络传输开销(将大的数据分区,只传输小数据集),其分区原则为:
RDD分区的一个 原则是使分区的个数尽量等于集群中的CPU核心(Core)数据,对于不同的Spark部署模式而言(Local模式、Standalone模式、YARN模式、Messos模式),都可以通过设置spark.default.parallelism这个参数值来配置默认的分区数目,一般而言,各种模式下的默认分区数据如下:
1、Local模式:默认为本地机器的CPU数目,若设置了local[N],则默认为N
2、Standalone或Yarn模式:在“集群中所有CPU核心数目”和“2”这二者取较大值作为默认值。
3、Mesos模式:默认的分区数为8.
4、可以通过代码手动设置,如:sc.textFile("filename",2),sc.parallelize(array,2),2表示分区数,也可以通过reparition重新设置分区个数,如:data.repartition(3),可以通过data.partitions.size查看分区数目,其中data是一个rdd
注意:spark的RDD分区只有(key,value)类型时才能分区,对于一些非(key,value)类型的RDD进行自定义分区时,需要先将RDD元素转换成(key,value)类型。
5、spark使用yarn集群的两种模式:yarn-cluster模式和Yarn-client模式,而在使用spark-shell和pyspark时,用yarn模式必须是yarn-client模式,其中对于spark中使用yarn-cluster模式和Yarn-client模式本质的区别在于:AM进程的区别,cluster模式下,driver运行在AM中,负责向Yarn申请资源,并监督作业运行状况,当用户提交完作业后,就关掉Client,作业会继续在yarn上运行,但cluster模式不适合交互类型的作业。而client模式时AM仅向yarn请求executor,client会和请求的container通信来调度任务,即client不能离开
九、在调试程序时,如果想让spark生成的文件保存到本地上,那么必须使用local模式,如:
spark-submit --master local[2] spark_partition-1.0-SNAPSHOT-jar-with-dependencies.jar