常用Actoin算子 与 内存管理 、共享变量、内存机制
一、常用Actoin算子 (reduce 、collect 、count 、take 、saveAsTextFile 、 countByKey 、foreach )
collect:从集群中将所有的计算结果获取到本地内存,然后展示
take:从集群中将一部分的计算结果获取到本地内存,然后展示
rdd.collect
rdd.take(n)
二、内存管理
1、RDD内存持久化
Spark非常重要的一个功能特性就是可以将RDD持久化在内存中。
当对RDD执行持久化操作时, 每个节点都会将自己操作的RDD中的partition持久化到内存中, 并且在之后对该RDD的反复使用中, 直接使用内存中缓存的partition数据。
这样的话, 对于针对一个RDD反复执行多个操作的场景, 就只要对RDD计算一次即可, 后面 直接使用该RDD, 而不需要反复计算多次该RDD。
巧妙使用RDD持久化, 甚至在某些场景下, 可以将Spark应用程序的性能提升10倍。 对于迭代式算法和快速交互式应用来说, RDD持久化, 是非常重要的。
2、RDD持久化原理
要持久化一个RDD, 只要调用其cache()或者persist()方法即可。
在该RDD第一次被计算出来时, 就会直接缓存在每个节点中,而且Spark的持久化机制还是自 动容错的。 如果持久化的RDD中的任何partition丢失了, 那么Spark会自动通过其源RDD, 使
用transformation操作重新计算该partition。
cache()和persist()的区别在于:
cache()是persist()的一种简化方式, cache()的底层就是调用的persist()的无参版本, 同时就是调用 persist(MEMORY_ONLY), 将数据持久化到内存中。 如果需要从内存中清除缓存,
那么可以使用 unpersist()方法。
Spark自己也会在shuffle操作时, 进行数据的持久化,比如写入磁盘, 主要是为了在节点失败时, 避免需要重新计算整个过程。
3、RDD持久化策略
RDD持久化是可以手动选择不同的策略的。
比如可以将RDD持久化在内存中、 持久化到磁盘上、 使用序列化的方式持久化, 多持久化的数据进 行多路复用。 只要在调用persist()时传入对应的StorageLevel即可。
以序列化方式优点:节省内存 缺点:读取时有一个反序列化,会加大CPU的开销
2代表副本数
4、如何选择RDD持久化策略
Spark提供的多种持久化级别, 主要是为了在CPU和内存消耗之间进行取舍。
下面是一些通用的持久化级别的选择建议:
优先使用MEMORY_ONLY, 如果可以缓存所有数据的话, 那么就使用这种策略。 因为纯内 存速度最快, 而且没有序列化, 不需要消耗CPU进行反序列化操作。
如果MEMORY_ONLY策略, 无法存储的下所有数据的话, 那么使用MEMORY_ONLY_SER, 将数据进行序列化进行存储, 此操作会消耗CPU进行反序列化。
如果需要进行快速的失败恢复, 那么就选择带后缀为_2的策略, 进行数据的备份, 这样在 失败时, 就不需要重新计算了。
能不使用DISK相关的策略, 就不用使用, 有的时候从磁盘读取数据, 还不如重新计算一次。
三、共享变量
1、共享变量
Spark一个非常重要的特性就是共享变量。
默认情况下, 如果在一个算子的函数中使用到了某个外部的变量, 那么这个 变量的值会被拷贝到每个task中。
此时每个task只能操作自己的那份变量副本。 如果多个task想要共享某个变 量, 那么这种方式是做不到的。
Spark为此提供了两种共享变量:
一种是Broadcast Variable( 广播变量) 另一种是Accumulator( 累加器)
Broadcast Variable会将使用到的变量, 仅仅为每个节点拷贝一份, 更大的 用处是优化性能, 减少网络传输以及内存消耗。
Accumulator则可以让多个task共同操作一份变量, 主要可以进行累加操作。
2、Broadcast Variable广播变量
Spark提供的Broadcast Variable, 是只读的, 并且在每个节点上只会有一份副本, 而不 会为每个task都拷贝一份副本。
因此其最大作用, 就是减少变量到各个节点的网络传输消耗, 以及在各个节点上的内存 消耗。 此外, spark自己内部也使用了高效的广播算法来减少网络消耗。
通过调用SparkContext的broadcast()方法, 来针对某个变量创建广播变量, 然后在算子 的函数内使用广播变量时, 每个节点只会拷贝一份副本了 。
每个节点可以使用广播变量的value()方法获取值。 记住, 广播变量是只读的。
val factor = 3
val factorBroadcast = sc.broadcast(factor)
val arr = Array(1, 2, 3, 4, 5)
val rdd = sc.parallelize(arr)
val multipleRdd = rdd.map(num => num * factorBroadcast.value())
multipleRdd.foreach(num => println(num))
3、Accumulator 累加器
Spark提供的Accumulator, 主要用于多个节点对一个变量进行共享性的操作。
Accumulator只提供了累加的功能, 但是却给我们提供了多个task对一个变量并 行操作的功能。
task只能对Accumulator进行累加操作, 不能读取它的值, 只有Driver程序可以 读取Accumulator的值。
val sumAccumulator = sc.accumulator(0)
val arr = Array(1, 2, 3, 4, 5)
val rdd = sc.parallelize(arr)
rdd.foreach(num => sumAccumulator += num)
println(sumAccumulator.value)
四、容错机制
1、容错机制
分布式数据集的容错性有两种方式: 数据检查点和记录数据的更新。
数据检查点(Check Point), 即将某个时机的中间数据写到存储(通常是HDFS)中。
面向大规模数据分析, 数据检查点操作成本很高, 需要通过数据中心的网络连接在机器之间 复制庞大的数据集, 而网络带宽往往比内存带宽低得多, 同时还需要消耗更多的存储资源。
Spark选择记录更新的方式。
RDD是一个有向无环图(DAG), 每一个RDD都会记住创建该数据集需要哪些操作, 跟踪记 录RDD的继承关系, 这个关系在Spark中称为LineAge(血统)。
由于创建RDD的操作是相对粗粒度的变换, 比如map、 filter、 join等, 即单一的操作应用于 许多数据元素, 而不需要存储真正的数据, 比通过网络复制数据更高效。
当一个RDD的某个分区丢失时, RDD有足够的信息记录其如何通过其他RDD进行计算的, 只需要通过其他RDD重新计算即可。
RDD 血统的依赖关系分为两种: 宽依赖、 窄依赖。
根据父RDD分区是对应一个还是多个子RDD分区来判断。
窄依赖, 一个父RDD分区对应一个子RDD分区;
宽依赖, 一个父RDD分区对应多个子RDD分区;
对于窄依赖, 只需通过重新计算丢失的那一块数据来恢复, 容错成本
对于宽依赖, 当容错重算分区时, 因为父分区数据只有一部分是需要重算子分区的, 其余数 据重算就造成了冗余计算。
所以, 不同的应用有时候也需要在适当的时机设置数据检查点。 由于RDD的只读特性使得 它比常用的共享内存更容易做检查点, 具体可以使用doCheckPoint方法。
检查点( 本质是通过将RDD写入Disk做检查点) 是为了通过lineage做容错的辅助, lineage 过长会造成容错成本过高, 这样就不如在中间阶段做检查点容错, 如果之后有节点出现问题
而丢失分区, 从做检查点的RDD开始重做Lineage, 就会减少开销。
(血统是系统生成的,checkpoint是辅助血统的,需要手动添加)
(A-B-C-D如果把c缓存到内存当中,ABC的依赖关系依然存在,如果把D放到HDFS上,则ABC的依赖关系就没有了)
案例:移动互联网数据分析: (1)对APP字段访问次数进行统计,并进行排序 (2)统计移动互联网上日活跃用户(DAU)和月活跃用户(MAU)
(3)统计在不同应用中的上下行流量
package SparkCore.day1 import org.apache.spark.{SparkConf, SparkContext} import org.apache.spark.rdd.RDD /** * Created by tg on 3/23/17. */ object MobileDemo { def main(args: Array[String]): Unit = { upDownAmount // mauCount // dauCount // appCount } //返回创建的初始RDD def creatFirstRDD:RDD[Array[String]]={ val conf=new SparkConf().setAppName("MobileDemo") .setMaster("local") val sc=new SparkContext(conf) //创建初始RDD val lines=sc.textFile("file:///home/tg/datas/mobile") .map(x=>x.split(",")) lines } /** * 对APP字段访问次数进行统计,并进行排序 */ def appCount: Unit ={ val lines=creatFirstRDD lines.filter(x=>x.length==7 && x(3)!=null) .map(x=>(x(3).trim,1)) .reduceByKey(_+_) .sortBy(_._2,false) .foreach(x=>{ println(x._1+"这款APP访问的次数是:"+x._2) }) } /** * 统计日活跃用户的数量 * imei是移动设备的唯一标识 * imei+time,可以确定日活跃用户,但是要注意数据去重 */ def dauCount: Unit ={ val lines=creatFirstRDD lines.filter(x=>x.length==7 && x(2)!=null && x(4)!=null) .map(x=>{ val imei=x(2).trim val time=x(4).trim imei+":"+time //IMEI:Time }).distinct() //进行数据去重 .map(x=>{ val time=x.split(":")(1).trim (time,1) //形成键值对(Time,1) }).reduceByKey(_+_) .foreach(item=>{ println(item._1+"活跃用户的数量:"+item._2) }) } /** * 统计月活跃用户 */ def mauCount: Unit ={ val lines=creatFirstRDD lines.filter(x=>x.length==7 && x(2)!=null && x(4)!=null) .map(x=>{ val imei=x(2).trim val time=x(4).trim //把时间截取到月份 val month=time.substring(0,time.lastIndexOf("-")) imei+":"+month }).distinct() .map(x=>{ val month=x.split(":")(1).trim (month,1) }).reduceByKey(_+_) .foreach(x=>{ println(x._1+"活跃用户数量:"+x._2) }) } /** * 统计在不同应用中的上下行总流量 */ def upDownAmount: Unit ={ val lines=creatFirstRDD lines.filter(x=>x.length==7 && x(3)!=null&&x(5)!=null&&x(6)!=null) .map(x=>{ val app=x(3).trim //APP val up=x(5).trim.toInt //上行流量,注意转换成Int val down=x(6).trim.toInt //下行流量 (app,(up,down)) }).reduceByKey((x,y)=>{ /** * x._1+y._1将相近的两个上行流量相加 * x._2+y._2将相近的两个下行流量相加 */ (x._1+y._1,x._2+y._2) }).foreach(x=>{ println(x._1+"这款应用的上下行流量总和分别是:") println("上行流量总和:"+x._2._1) println("下行流量总和:"+x._2._2) }) } }
补充:
1、大数据开发的核心理念:
(高)并发操作、 数据不动,计算动
2、HA高可靠:通过两个namenode,其中一个处于active状态。对外提供服务,另一个处于standby状态,对外不提供服务。当处于active的状态的namenode发生故障时,处于standby状态的namenode会切换为active状态并且对外提供服务,从而保证集群的正常运行。
faderation联盟:
hadoop集群在启动时,namenode需要将全部的元数据信息都加载到内存中,从而保证hadoop集群的正常启动。如果namenode中的元数据信息体量太大,内存容量又太小,那么会造成元数据信息无法全部都加载到内存中,这时hadoop集群就无法正常启动了。
为了解决这个问题,提出了faderation联盟,让原先的一个namenode变成了多个namenode,同时对外提供可服务,并且每个namenode保存元数据的一部分。在hadoop集群启动时,每个namenode将各自保存的那一部分元数据信息加载到内存中,保证hadoop集群的正常启动,这样也减轻namenode所在节点内存的压力。
但是faderationn联盟也是有缺陷的,当其中一个namenode发生故障会造成元数据信息的丢失,所以faderation无法从根本上解决单点故障。