目录
持久化的UserVisitSessionSpark.java
本篇文章记录用户访问session分析-性能调优之在实际项目中重构RDD架构以及RDD持久化。
什么是RDD持久化
Spark最重要的一个功能,就是在不同操作间,持久化(或缓存)一个数据集在内存中。当持久化一个RDD,每一个结点都将把它的计算分块结果保存在内存中,并在对此数据集(或者衍生出的数据集)进行的其它动作中复用。这将使得后续action操作变得更加迅速(通常快10倍)。
缓存是用Spark构建迭代算法的关键。RDD的缓存能够在第一次计算完成后,将计算结果保存到内存、本地文件系统或者Alluxio(,以前称为Tachyon,分布式内存文件系统)中。通过缓存,Spark避免了RDD上的重复计算,能够极大地提升计算速度。
为什么要进行RDD持久化
当第一次对RDD2执行算子,获取RDD3的时候,就会从RDD1开始计算,就是读取HDFS文件,然后对RDD1执行算子,获取
到RDD2,然后再计算,得到RDD3
默认情况下,多次对一个RDD执行算子,去获取不同的RDD,都会对这个RDD以及之前的父RDD,全部重新计算一次;读取HDFS->RDD1->RDD2-RDD4,这种情况,是一定要避免的,一旦出现一个RDD重复计算的情况,就会导致性能急剧降低。
比如,HDFS->RDD1-RDD2的时间是15分钟,那么此时就要走两遍,变成30分钟
另外一种情况,从一个RDD到几个不同的RDD,算子和计算逻辑其实是完全一样的,结果因为人为的疏忽,计算了多次,获取到了多个RDD。
RDD持久化的作用
第一,RDD架构重构与优化
尽量去复用RDD,差不多的RDD,可以抽取称为一个共同的RDD,供后面的RDD计算时,反复使用。
第二,公共RDD一定要实现持久化
对于要多次计算和使用的公共RDD,一定要进行持久化。持久化,也就是说,将RDD的数据缓存到内存中/磁盘中,(BlockManager),以后无论对这个RDD做多少次计算,那么都是直接取这个RDD的持久化的数据,比如从内存中或者磁盘中,直接提取一份数据。
第三,持久化,是可以进行序列化的
如果正常将数据持久化在内存中,那么可能会导致内存的占用过大,这样的话,也许,会导致OOM内存溢出。
当纯内存无法支撑公共RDD数据完全存放的时候,就优先考虑,使用序列化的方式在纯内存中存储。将RDD的每个partition的数据,序列化成一个大的字节数组,就一个对象;序列化后,大大减少内存的空间占用。
序列化的方式,唯一的缺点就是,在获取数据的时候,需要反序列化。
如果序列化纯内存方式,还是导致OOM,内存溢出;就只能考虑磁盘的方式,内存+磁盘的普通方式(无序列化)。
内存+磁盘,序列化
为了数据的高可靠性,而且内存充足,可以使用双副本机制,进行持久化,持久化的双副本机制,持久化后的一个副本,因为机器宕机了,副本丢了,就还是得重新计算一次,持久化的每个数据单元,存储一份副本,放在其他节点上面,从而进行容错,一个副本丢了,不用重新计算,还可以使用另外一份副本。这种方式,仅仅针对你的内存资源极度充足
持久化的级别
持久化的UserVisitSessionSpark.java
public static void main(String[] args) { Logger.getLogger("org").setLevel(Level.ERROR); args = new String[]{"1"}; // 构建Spark上下文 // 构建Spark上下文 SparkConf conf = new SparkConf() .setAppName(Constants.SPARK_APP_NAME_SESSION) .setMaster("local"); JavaSparkContext sc = new JavaSparkContext(conf); SQLContext sqlContext = getSQLContext(sc.sc()); // 生成模拟测试数据 mockData(sc, sqlContext); // 创建需要使用的DAO组件 ITaskDAO taskDAO = DAOFactory.getTaskDAO(); // 首先得查询出来指定的任务,并获取任务的查询参数 long taskid = ParamUtils.getTaskIdFromArgs(args); Task task = taskDAO.findById(taskid); JSONObject taskParam = JSONObject.parseObject(task.getTaskParam()); // 如果要进行session粒度的数据聚合 // 首先要从user_visit_action表中,查询出来指定日期范围内的行为数据 /** * actionRDD,就是一个公共RDD * 第一,要用ationRDD,获取到一个公共的sessionid为key的PairRDD * 第二,actionRDD,用在了session聚合环节里面 * * sessionid为key的PairRDD,是确定了,在后面要多次使用的 * 1、与通过筛选的sessionid进行join,获取通过筛选的session的明细数据 * 2、将这个RDD,直接传入aggregateBySession方法,进行session聚合统计 * * 重构完以后,actionRDD,就只在最开始,使用一次,用来生成以sessionid为key的RDD * */ JavaRDD<Row> actionRDD = getActionRDDByDateRange(sqlContext, taskParam); JavaPairRDD<String, Row> sessionid2actionRDD = getSessionid2ActionRDD(actionRDD); /** * 持久化,很简单,就是对RDD调用persist()方法,并传入一个持久化级别 * * 如果是persist(StorageLevel.MEMORY_ONLY()),纯内存,无序列化,那么就可以用cache()方法来替代 * StorageLevel.MEMORY_ONLY_SER(),第二选择 * StorageLevel.MEMORY_AND_DISK(),第三选择 * StorageLevel.MEMORY_AND_DISK_SER(),第四选择 * StorageLevel.DISK_ONLY(),第五选择 * * 如果内存充足,要使用双副本高可靠机制 * 选择后缀带_2的策略 * StorageLevel.MEMORY_ONLY_2() * */ sessionid2actionRDD = sessionid2actionRDD.persist(StorageLevel.MEMORY_ONLY()); // 首先,可以将行为数据,按照session_id进行groupByKey分组 // 此时的数据的粒度就是session粒度了,然后呢,可以将session粒度的数据 // 与用户信息数据,进行join // 然后就可以获取到session粒度的数据,同时呢,数据里面还包含了session对应的user的信息 // 到这里为止,获取的数据是<sessionid,(sessionid,searchKeywords,clickCategoryIds,age,professional,city,sex)> JavaPairRDD<String, String> sessionid2AggrInfoRDD = aggregateBySession(sqlContext, sessionid2actionRDD); // 接着,就要针对session粒度的聚合数据,按照使用者指定的筛选参数进行数据过滤 // 相当于我们自己编写的算子,是要访问外面的任务参数对象的 // 所以,大家记得我们之前说的,匿名内部类(算子函数),访问外部对象,是要给外部对象使用final修饰的 // 重构,同时进行过滤和统计 Accumulator<String> sessionAggrStatAccumulator = sc.accumulator( "", new SessionAggrStatAccumulator()); JavaPairRDD<String, String> filteredSessionid2AggrInfoRDD = filterSessionAndAggrStat( sessionid2AggrInfoRDD, taskParam, sessionAggrStatAccumulator); filteredSessionid2AggrInfoRDD = filteredSessionid2AggrInfoRDD.persist(StorageLevel.MEMORY_ONLY()); // 生成公共的RDD:通过筛选条件的session的访问明细数据 JavaPairRDD<String, Row> sessionid2detailRDD = getSessionid2detailRDD( filteredSessionid2AggrInfoRDD, sessionid2actionRDD); sessionid2detailRDD = sessionid2detailRDD.persist(StorageLevel.MEMORY_ONLY()); /** * 对于Accumulator这种分布式累加计算的变量的使用,有一个重要说明 * * 从Accumulator中,获取数据,插入数据库的时候,一定要,一定要,是在有某一个action操作以后 * 再进行。。。 * * 如果没有action的话,那么整个程序根本不会运行。。。 * * 是不是在calculateAndPersisitAggrStat方法之后,运行一个action操作,比如count、take * 不对!!! * * 必须把能够触发job执行的操作,放在最终写入MySQL方法之前 * * 计算出来的结果,在J2EE中,是怎么显示的,是用两张柱状图显示 */ randomExtractSession(task.getTaskId(), filteredSessionid2AggrInfoRDD, sessionid2detailRDD); /** * 特别说明 * 我们知道,要将上一个功能的session聚合统计数据获取到,就必须是在一个action操作触发job之后 * 才能从Accumulator中获取数据,否则是获取不到数据的,因为没有job执行,Accumulator的值为空 * 所以,我们在这里,将随机抽取的功能的实现代码,放在session聚合统计功能的最终计算和写库之前 * 因为随机抽取功能中,有一个countByKey算子,是action操作,会触发job */ // 计算出各个范围的session占比,并写入MySQL calculateAndPersistAggrStat(sessionAggrStatAccumulator.value(), task.getTaskId()); /** * session聚合统计(统计出访问时长和访问步长,各个区间的session数量占总session数量的比例) * * 如果不进行重构,直接来实现,思路: * 1、actionRDD,映射成<sessionid,Row>的格式 * 2、按sessionid聚合,计算出每个session的访问时长和访问步长,生成一个新的RDD * 3、遍历新生成的RDD,将每个session的访问时长和访问步长,去更新自定义Accumulator中的对应的值 * 4、使用自定义Accumulator中的统计值,去计算各个区间的比例 * 5、将最后计算出来的结果,写入MySQL对应的表中 * * 普通实现思路的问题: * 1、为什么还要用actionRDD,去映射?其实我们之前在session聚合的时候,映射已经做过了。多此一举 * 2、是不是一定要,为了session的聚合这个功能,单独去遍历一遍session?其实没有必要,已经有session数据 * 之前过滤session的时候,其实,就相当于,是在遍历session,那么这里就没有必要再过滤一遍了 * * 重构实现思路: * 1、不要去生成任何新的RDD(处理上亿的数据) * 2、不要去单独遍历一遍session的数据(处理上千万的数据) * 3、可以在进行session聚合的时候,就直接计算出来每个session的访问时长和访问步长 * 4、在进行过滤的时候,本来就要遍历所有的聚合session信息,此时,就可以在某个session通过筛选条件后 * 将其访问时长和访问步长,累加到自定义的Accumulator上面去 * 5、就是两种截然不同的思考方式,和实现方式,在面对上亿,上千万数据的时候,甚至可以节省时间长达 * 半个小时,或者数个小时 * * 开发Spark大型复杂项目的一些经验准则: * 1、尽量少生成RDD * 2、尽量少对RDD进行算子操作,如果有可能,尽量在一个算子里面,实现多个需要做的功能 * 3、尽量少对RDD进行shuffle算子操作,比如groupByKey、reduceByKey、sortByKey(map、mapToPair) * shuffle操作,会导致大量的磁盘读写,严重降低性能 * 有shuffle的算子,和没有shuffle的算子,甚至性能,会达到几十分钟,甚至数个小时的差别 * 有shfufle的算子,很容易导致数据倾斜,一旦数据倾斜,简直就是性能杀手(完整的解决方案) * 4、无论做什么功能,性能第一 * 在传统的J2EE或者.NET后者PHP,软件/系统/网站开发中,我认为是架构和可维护性,可扩展性的重要 * 程度,远远高于了性能,大量的分布式的架构,设计模式,代码的划分,类的划分(高并发网站除外) * * 在大数据项目中,比如MapReduce、Hive、Spark、Storm,我认为性能的重要程度,远远大于一些代码 * 的规范,和设计模式,代码的划分,类的划分;大数据,大数据,最重要的,就是性能 * 主要就是因为大数据以及大数据项目的特点,决定了,大数据的程序和项目的速度,都比较慢 * 如果不优先考虑性能的话,会导致一个大数据处理程序运行时间长度数个小时,甚至数十个小时 * 此时,对于用户体验,简直就是一场灾难 * * 所以,推荐大数据项目,在开发和代码的架构中,优先考虑性能;其次考虑功能代码的划分、解耦合 * * 我们如果采用第一种实现方案,那么其实就是代码划分(解耦合、可维护)优先,设计优先 * 如果采用第二种方案,那么其实就是性能优先 * * 讲了这么多,其实大家不要以为我是在岔开话题,大家不要觉得项目的课程,就是单纯的项目本身以及 * 代码coding最重要,其实项目,我觉得,最重要的,除了技术本身和项目经验以外;非常重要的一点,就是 * 积累了,处理各种问题的经验 * */ // 获取top10热门品类 List<Tuple2<CategorySortKey, String>> top10CategoryList = getTop10Category(task.getTaskId(), sessionid2detailRDD); // 获取top10活跃session getTop10Session(sc, task.getTaskId(), top10CategoryList, sessionid2detailRDD); // 关闭Spark上下文 sc.close(); }