58.Spark大型电商项目-用户访问session分析-性能调优之在实际项目中重构RDD架构以及RDD持久化

目录

什么是RDD持久化

为什么要进行RDD持久化 

RDD持久化的作用

第一,RDD架构重构与优化

第二,公共RDD一定要实现持久化

第三,持久化,是可以进行序列化的

持久化的级别

​ 持久化的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();
    }

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值