用户访问session分析Spark作业

接收用户创建的分析任务,用户可能指定的条件如下:

1、时间范围:起始日期~结束日期
2、性别:男或女
3、年龄范围
4、职业:多选
5、城市:多选
6、搜索词:多个搜索词,只要某个session中的任何一个action搜索过指定的关键词,那么session就符合条件
7、点击品类:多个品类,只要某个session中的任何一个action点击过某个品类,那么session就符合条件

我们的spark作业如何接受用户创建的任务?

J2EE平台在接收用户创建任务的请求之后,会将任务信息插入MySQL的task表中,任务参数以JSON格式封装在task_param 字段中

接着J2EE平台会执行我们的spark-submit shell脚本,并将taskid作为参数传递给spark-submit shell脚本
spark-submit shell脚本,在执行时,是可以接收参数的,并且会将接收的参数,传递给Spark作业的main函数
参数就封装在main函数的args数组中

这是spark本身提供的特性

UserVisitSessionAnalyzeSpark.java

		// 构建Spark上下文
		SparkConf conf = new SparkConf().setAppName(Constants.SPARK_APP_NAME_SESSION)
				// .set("spark.default.parallelism", "100")
				.set("spark.storage.memoryFraction", "0.5").set("spark.shuffle.consolidateFiles", "true")
				.set("spark.shuffle.file.buffer", "64").set("spark.shuffle.memoryFraction", "0.3")
				.set("spark.reducer.maxSizeInFlight", "24").set("spark.shuffle.io.maxRetries", "60")
				.set("spark.shuffle.io.retryWait", "60")
				.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
				.registerKryoClasses(new Class[] { CategorySortKey.class, IntList.class });
		SparkUtils.setMaster(conf);

		/**
		 * 比如,获取top10热门品类功能中,二次排序,自定义了一个Key
		 * 那个key是需要在进行shuffle的时候,进行网络传输的,因此也是要求实现序列化的
		 * 启用Kryo机制以后,就会用Kryo去序列化和反序列化CategorySortKey
		 * 所以这里要求,为了获取最佳性能,注册一下我们自定义的类
		 */

		JavaSparkContext sc = new JavaSparkContext(conf);
		// sc.checkpointFile("hdfs://");
		SQLContext sqlContext = getSQLContext(sc.sc());

		// 生成模拟测试数据
		SparkUtils.mockData(sc, sqlContext);

		// 创建需要使用的DAO组件
		ITaskDAO taskDAO = DAOFactory.getTaskDAO();

		// 首先得查询出来指定的任务,并获取任务的查询参数
		long taskid = ParamUtils.getTaskIdFromArgs(args, Constants.SPARK_LOCAL_TASKID_SESSION);
		Task task = taskDAO.findById(taskid);
		if (task == null) {
			System.out.println(new Date() + ": cannot find this task with id [" + taskid + "].");
			return;
		}

		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 = SparkUtils.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());
		// sessionid2actionRDD.checkpoint();

		// 首先,可以将行为数据,按照session_id进行groupByKey分组
		// 此时的数据的粒度就是session粒度了,然后呢,可以将session粒度的数据
		// 与用户信息数据,进行join
		// 然后就可以获取到session粒度的数据,同时呢,数据里面还包含了session对应的user的信息
		// 到这里为止,获取的数据是<sessionid,(sessionid,searchKeywords,clickCategoryIds,age,professional,city,sex)>
		// 改为:<sessionid,partAggrInfo(sessionid,searchKeywords,clickCategoryIds,visitLength,stepLength,startTime),age,professional,city,sex>
		JavaPairRDD<String, String> sessionid2AggrInfoRDD = aggregateBySession(sc, sqlContext, sessionid2actionRDD);

		// 接着,就要针对session粒度的聚合数据,按照使用者指定的筛选参数进行数据过滤
		// 相当于我们自己编写的算子,是要访问外面的任务参数对象的
		// 所以,大家记得我们之前说的,匿名内部类(算子函数),访问外部对象,是要给外部对象使用final修饰的

		// 重构,同时进行过滤和统计
		Accumulator<String> sessionAggrStatAccumulator = sc.accumulator("", new SessionAggrStatAccumulator());

		JavaPairRDD<String, String> filteredSessionid2AggrInfoRDD = filterSessionAndAggrStat(sessionid2AggrInfoRDD,
				taskParam, sessionAggrStatAccumulator);// AggrStat:聚合统计缩写
		filteredSessionid2AggrInfoRDD = filteredSessionid2AggrInfoRDD.persist(StorageLevel.MEMORY_ONLY());

		// 生成公共的RDD:通过筛选条件的session的访问明细数据

		/**
		 * 重构:sessionid2detailRDD,就是代表了通过筛选的session对应的访问明细数据
		 * 注释:RDD名称中,包含detail代表原始session数据Row,如果包含AggrInfo代表原始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(sc, task.getTaskid(), filteredSessionid2AggrInfoRDD, sessionid2detailRDD);

		/**
		 * 特别说明 我们知道,要将上一个功能的session聚合统计数据获取到,就必须是在一个action操作触发job之后
		 * 才能从Accumulator中获取数据,否则是获取不到数据的,因为没有job执行,Accumulator的值为空
		 * 所以,我们在这里,将随机抽取的功能的实现代码,放在session聚合统计功能的最终计算和写库之前
		 * 因为随机抽取功能中,有一个countByKey算子,是action操作,会触发job
		 */

		// 计算出各个范围的session占比,并写入MySQL(AggrStat:聚合统计缩写)
		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();

获取指定日期范围内的用户行为数据RDD

/**
	 * 获取指定日期范围内的用户行为数据RDD
	 * @param sqlContext
	 * @param taskParam
	 * @return
	 */
	public static JavaRDD<Row> getActionRDDByDateRange(
			SQLContext sqlContext, JSONObject taskParam) {
		String startDate = ParamUtils.getParam(taskParam, Constants.PARAM_START_DATE);
		String endDate = ParamUtils.getParam(taskParam, Constants.PARAM_END_DATE);
		
		String sql = 
				"select * "
				+ "from user_visit_action "
				+ "where date>='" + startDate + "' "
				+ "and date<='" + endDate + "'";  
//				+ "and session_id not in('','','')"
		
		DataFrame actionDF = sqlContext.sql(sql);
		
		/**
		 * 这里就很有可能发生上面说的问题
		 * 比如说,Spark SQl默认就给第一个stage设置了20个task,但是根据你的数据量以及算法的复杂度
		 * 实际上,你需要1000个task去并行执行
		 * 
		 * 所以说,在这里,就可以对Spark SQL刚刚查询出来的RDD执行repartition重分区操作
		 */
		
//		return actionDF.javaRDD().repartition(1000);
		
		return actionDF.javaRDD();
	}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值