离线日志采集流程
互联网:网站,app,系统,传统行业:电信,打电话,上网,发短信,数据源:网站,app 都要往后台发送请求,获取数据,执行业务逻辑------->网站/app会发送请求到后台服务器,通常会由Nginx接受请求,并进行转发------>后台服务器,比如Tomcat、Jetty;但是,其实在面向大量用户,高并发(每秒访问量过万)的情况下,通常都不会直接是用Tomcat来接收请求。这种时候,通常,都是用Nginx来接收请求,并且后端接入Tomcat集群/Jetty集群,来进行高并发访问下的负载均衡。 比如说,Nginx,或者是Tomcat,你进行适当配置之后,所有请求的数据都会作为log存储起来;接收请求的后台系统(J2EE、PHP、Ruby On Rails),也可以按照你的规范,每接收一个请求,或者每执行一个业务逻辑,就往日志文件里面打一条log。------>日志文件(通常由我们预先设定的特殊的格式)通常每天一份。此时呢,由于可能有多份日志文件,因为有多个web服务器。------>一个日志转移的工具,比如自己用linux的crontab定时调度一个shell脚本/python脚本;或者自己用java开发一个后台服务,用quartz这样的框架进行定时调度。这个工具,负责将当天的所有日志的数据,都给采集起来,进行合并和处理,等操作;然后作为一份日志文件,给转移到flume agent正在监控的目录中。------>flume,按照我们上节课所讲的;flume agent启动起来以后,可以实时的监控linux系统上面的某一个目录,看其中是否有新的文件进来。只要发现有新的日志文件进来,那么flume就会走后续的channel和sink。通常来说,sink都会配置为HDFS。------>flume负责将每天的一份log文件,传输到HDFS上--------------->HDFS,Hadoop Distributed File System。Hadoop分布式文件系统。用来存储每天的log数据。为什么用hadoop进行存储呢。因为Hadoop可以存储大数据,大量数据。比如说,每天的日志,数据文件是一个T,那么,也许一天的日志文件,是可以存储在某个Linux系统上面,但是问题是,1个月的呢,1年的呢。当积累了大量数据以后,就不可能存储在单机上,只能存储在Hadoop大数据分布式存储系统中。------->Hadoop HDFS中的原始的日志数据,会经过数据清洗。为什么要进行数据清洗?因为我们的数据中可能有很多是不符合预期的脏数据。 HDFS:存储一份经过数据清洗的日志文件。------>Hive,底层也是基于HDFS,作为一个大数据的数据仓库。数据仓库内部,再往后,其实就是一些数据仓库建模的ETL。ETL会将原始日志所在的一个表,给转换成几十张,甚至上百张表。这几十,甚至上百张表,就是我们的数据仓库。然后呢,公司的统计分析人员,就会针对数据仓库中的表,执行临时的,或者每天定时调度的Hive SQL ETL作业。来进行大数据的统计和分析。-------->我们的Spark大型大数据平台/系统(比如我们这套课程要讲解的这个),其实,通常来说,都会针对Hive中的数据来进行开发。也就是说,我们的Spark大数据系统,数据来源都是Hive中的某些表。这些表,可能都是经过大量的Hive ETL以后建立起来的数据仓库中的某些表。然后来开发特殊的,符合业务需求的大数据平台。通过大数据平台来给公司里的用户进行使用,来提供大数据的支持,推动公司的发展。
实时数据采集流程
数据来源:网站或者app。就是埋点。在网站/app的哪个页面的哪些操作发生时,前端的代码,就通过网络请求,(Ajax;socket),向后端的服务器发送指定格式的日志数据。------>Nginx,后台Web服务器(Tomcat、Jetty),后台系统(J2EE、PHP)。到这一步为止,其实还是可以跟我们之前的离线日志收集流程一样。--------->实时数据,通常都是从分布式消息队列集群中读取的,比如Kafka;实时数据,实时的log,实时的写入到消息队列中,比如Kafka;然后呢,再由我们后端的实时数据处理程序(Storm、Spark Streaming),实时从Kafka中读取数据,log日志。然后进行实时的计算和处理。
需求分析
1、按条件筛选session
按条件筛选session 搜索过某些关键词的用户、访问时间在某个时间段内的用户、年龄在某个范围内的用户、职业在某个范围内的用户、所在某个城市的用户,发起的session。方便后续对特定人群进行分析。
获取指定日期范围内的用户访问行为数据
getActionRDDByDateRange:通过paramutils.getParam来截取startDate和endDate → sql语句"select * "+ "from user_visit_action "+ "where date>='" + startDate + "' "+ "and date<='" + endDate + "'" 来生成所需要的DataFrame,将DF转为javaRDD( ),得到第一个actionRDD。
获取sessionid2到访问行为数据的映射的RDD
对行为数据按session粒度进行聚合,利用groupbykey对session粒度进行分组,将session中所有的搜索词和点击品类都聚合起来(对groupby之后的sessinoid2actionRDD进行maptopair,通过iterator.hasnext对访问行为进行遍历,计算开始和结束的时间,访问步长,同时获取searchkeyword和clickCategoryId两个字段,而且两个字段不一定同时出现,要判空,在写进两个stringbuffer里面。拼接成聚合的字段partAggrinfo,包含sessionID,searchkeyword,clickCategoryIds,visitLength,stepLength,starttime,返回Tuple2<Long, String>(userid, partAggrInfo);)
查询用户数据并且映射成<userid,Row>的格式
将session粒度聚合数据,与用户信息进行join
对join起来的数据进行拼接,并且返回<sessionid,fullAggrInfo>格式的数据
sessionid2FullAggrInfoRDD : 从本来的partAggrInfo里抽出sessionID。 对之前聚合userInfo当中的age,professional,sex,city等信息与partAggrInfo进行拼接,组成fullAgggrInfo。 返回Tuple2<String, String>(sessionid, fullAggrInfo);
截取 sessionid2AggrInfoRDD当中的日期,年纪,职业,性别,搜索关键词,商品品类等字段,拼接成paramter
根据筛选参数进行过滤
先从tuple中截取聚合数据,接着依据年龄,职业,城市,搜索词等进行筛选划分,在经过多个过滤条件之后,说明这个session就是需要技术的session,用自定义的Accumulator进行计数,同时利用它来计算步长和时长。 最后返回 filteredSessionid2AggrInfoRDD
获取通过筛选条件的session的访问明细数据RDD
JavaPairRDD<String, Row> sessionid2detailRDD = sessionid2aggrInfoRDD
.join(sessionid2actionRDD)
.mapToPair
返回sessionid2detailRDD2、统计出符合条件的session中,访问时长在1s~3s、4s~6s、7s~9s、10s~30s、30s~60s、1m~3m、3m~10m、10m~30m、30m以上各个范围内的session占比;访问步长在1~3、4~6、7~9、10~30、30~60、60以上各个范围内的session占比
直接拿出累加器中统计好的值,进行除法运算即可
3、在符合条件的session中,按照时间比例随机抽取1000个session
计算出每天每小时的session数量
获取<yyyy-MM-dd_HH,aggrInfo>格式的RDD
JavaPairRDD<String, String> time2sessionidRDD = sessionid2AggrInfoRDD.mapToPair
利用DateUtils.getDateHour将aggrInfo当中的start转换成yyyy-MM-dd_HH的形式作为dateHour,返回Tuple2<String, String>(dateHour, aggrInfo)
得到每天每小时的session数量
我们就用这个countByKey操作,新建map对过滤的RDD进行基于日期时间的count分组,Map<String, Object> countMap = time2sessionidRDD.countByKey();
使用按时间比例随机抽取算法,计算出每天每小时要抽取session的索引
将<yyyy-MM-dd_HH,count>格式的map,转换成<yyyy-MM-dd,<HH,count>>的格式
实现随机抽取算法;
同样新建一个hashmap,对之前countmap进行迭代,切片和获取date和hour,再分别放入hourcountMap和dateHourcountMap
开始实现我们的按时间比例随机抽取算法
总共要抽取100个session,先按照天数,进行平分
session随机抽取功能
创建Map<String, Map<String, List<Integer>>> dateHourExtractMap =
new HashMap<String, Map<String, List<Integer>>>(); →通过dateHourCountMap
循环求得一天的session总数→遍历每个小时获取时间和session数→计算每个小时session数量占据当天session数量的比例(如果要抽取的数量大于每小时session数量,则让抽取数量等于每小时数量)→获取当前小时的存放随机数的list→生成根据所需要抽取数量范围内的随机数并进行判重,再将随机数写入list当中
遍历每天每小时的session,然后根据随机索引进行抽取
执行groupByKey算子,得到<dateHour,(session aggrInfo)>
JavaPairRDD<String, Iterable<String>> time2sessionsRDD = time2sessionidRDD.groupByKey(); → 用flatMap算子,遍历所有的<dateHour,(session aggrInfo)>格式的数据→会遍历每天每小时的session→判断当前session的index是否在随机抽取的list当中→将需要抽取的sessionID从聚合信息里拿出来→返回extractSessionids这个RDD包含需要抽取sessionID
获取抽取出来的session的明细数据
用抽取出来的sessionID去join sessionID对应的明细操作
JavaPairRDD<String, Tuple2<String, Row>> extractSessionDetailRDD =
extractSessionidsRDD.join(sessionid2actionRDD);
再用foreachPartition对聚合后的数据进行封装写入到mysql当中4、在符合条件的session中,获取点击、下单和支付数量排名前10的品类
获取符合条件的session的访问明细
获取符合条件的session访问过的所有品类// 获取session访问过的所有品类id
// 访问过:指的是,点击过、下单过、支付过的品类JavaPairRDD<Long, Long> categoryidRDD = sessionid2detailRDD.flatMapToPair(
切割聚合信息里的clickCategoryId,orderCategoryIds,payCategoryIds以pairRDD的形式写进list当中。
去重,对categoryID进行去重,如果不去重,排序会对重复的categoryid已经countInfo进行排序,最后可能拿到重复的数据。
计算各品类的点击、下单和支付的次数
先对是否有进行点击行为进行过滤,再封装成 Tuple2<Long, Long>(Long.valueOf(payCategoryId), 1L)形式的RDD进行reducebykey的计算
join各品类与它的点击、下单和支付的次数
通过leftouterjoin来实现每个categoryID对应的点击数、订单数和付款数拼接到一起。
封装你要进行排序算法需要的几个字段:点击次数、下单次数和支付次数
将数据映射成<CategorySortKey,info>格式的RDD,然后进行二次排序(降序)
将需要排序的点击数和订单数CategorySortKey sortKey = new CategorySortKey(clickCount,orderCount, payCount); 封装排序key,再倒序排序取出top10
取出前10个写入mysql(take10)
5、对于排名前10的品类,分别获取其点击次数排名前10的session
将top10热门品类的id,生成一份RDD
计算top10品类被各session点击的次数
JavaPairRDD<String, Iterable<Row>> sessionid2detailsRDD =
sessionid2detailRDD.groupByKey();计算出该session,对每个品类的点击次数
返回结果,<categoryid,sessionid,count>格式
list.add(new Tuple2<Long, String>(categoryid, value))
获取到to10热门品类,被各个session点击的次数
JavaPairRDD<Long, String> top10CategorySessionCountRDD = top10CategoryIdRDD
.join(categoryid2sessionCountRDD)分组取TopN算法实现,获取每个品类的top10活跃用户
JavaPairRDD<Long, Iterable<String>> top10CategorySessionCountsRDD =
top10CategorySessionCountRDD.groupByKey();定义top10排序数组,通过冒泡排序比较写入到mysql当中
获取top10活跃session的明细数据,并写入MySQL
JavaPairRDD<String, Tuple2<String, Row>> sessionDetailRDD =
top10SessionRDD.join(sessionid2detailRDD);
实时广告黑名单流量计算:
动态黑名单生成:
获取原始数据:
// 一条一条的实时日志
// timestamp province city userid adid
// 某个时间点 某个省份 某个城市 某个用户 某个广告
// 计算出每5个秒内的数据中,每天每个用户每个广告的点击量
// 通过对原始实时日志的处理
// 将日志的格式处理成<yyyyMMdd_userid_adid, 1L>格式从原始日志中,截取日期(timestamp)和userid、aid拼接成key
// 针对处理后的日志格式,执行reduceByKey算子即可
// (每个batch中)每天每个用户对每个广告的点击量分区写入MYSQL当中
每个5s的batch中,当天每个用户对每支广告的点击次数 写入数据库
对大于100次点击的用户进行拉黑
现在我们在mysql里面,已经有了累计的每天各用户对各广告的点击量
// 遍历每个batch中的所有记录,对每条记录都要去查询一下,这一天这个用户对这个广告的累计点击量是多少
// 从mysql中查询
// 查询出来的结果,点击量如果是100,如果你发现某个用户某天对某个广告的点击量已经大于等于100了
// 那么就判定这个用户就是黑名单用户,就写入mysql的表中,持久化对过滤了黑名单用户的blacklistDStream,需要进行一次全局去重(DS→取出userid的RDD.Distinct)
// 里面的每个batch,其实就是都是过滤出来的已经在某天对某个广告点击量超过100的用户
// 遍历这个dstream中的每个rdd,然后将黑名单用户增加到mysql中根据动态黑名单对数据进行过滤
// 刚刚接受到原始的用户点击行为日志之后
// 根据mysql中的动态黑名单,进行实时的黑名单过滤(黑名单用户的点击行为,直接过滤掉,不要了)
// 使用transform算子(将dstream中的每个batch RDD进行处理,转换为任意的其他RDD,功能很强大)// 首先,从mysql中查询所有黑名单用户,将其转换为一个rdd
// 将原始日志数据rdd,与黑名单rdd,进行左外连接
// 如果说原始日志的userid,没有在对应的黑名单中,join不到,左外连接
// 用inner join,内连接,会导致数据丢失// 如果这个值存在,那么说明原始日志中的userid,join到了某个黑名单用户
if(optional.isPresent() && optional.get()) {
return false; // false表示过滤
}
return true; //表示非黑名单用户忽略再返回结果RDD
计算广告点击流量实时统计
获取黑名单过滤后的数据
封装key和tuple
String key = datekey + "_" + province + "_" + city + "_" + adid;
return new Tuple2<String, Long>(key, 1L);用updatebykey算子更新状态
同步状态判断
首先根据optional判断,之前这个key,是否有对应的状态
long clickCount = 0L;
如果说,之前是存在这个状态的,那么就以之前的状态作为起点,进行值的累加
if(optional.isPresent()) {
clickCount = optional.get();
}
values,代表了,batch rdd中,每个key对应的所有的值
for(Long value : values) {
clickCount += value;
}
return Optional.of(clickCount); 计算的结果分区写到MYSQL当中去
在实际项目中分配更多资源
分配哪些资源?
executor、cpu per executor、memory per executor、driver memory
在哪里分配这些资源?
在我们在生产环境中,提交spark作业时,用的spark-submit shell脚本,里面调整对应的参数
/usr/local/spark/bin/spark-submit \
--class cn.spark.sparktest.core.WordCountCluster \
--num-executors 3 \ 配置executor的数量
--driver-memory 100m \ 配置driver的内存(影响不大)
--executor-memory 100m \ 配置每个executor的内存大小
--executor-cores 3 \ 配置每个executor的cpu core数量
/usr/local/SparkTest-0.0.1-SNAPSHOT-jar-with-dependencies.jar \
调节到多大,算是最大呢?
第一种,Spark Standalone,公司集群上,搭建了一套Spark集群,你心里应该清楚每台机器还能够给你使用的,大概有多少内存,多少cpu core;那么,设置的时候,就根据这个实际的情况,去调节每个spark作业的资源分配。比如说你的每台机器能够给你使用4G内存,2个cpu core;20台机器;executor,20;4G内存,2个cpu core,平均每个executor。
第二种,Yarn。资源队列。资源调度。应该去查看,你的spark作业,要提交到的资源队列,大概有多少资源?500G内存,100个cpu core;executor,50;10G内存,2个cpu core,平均每个executor。 一个原则,你能使用的资源有多大,就尽量去调节到最大的大小(executor的数量,几十个到上百个不等;executor内存;executor cpu core)
4、为什么调节了资源以后,性能可以提升?
增加executor: 如果executor数量比较少,那么,能够并行执行的task数量就比较少,就意味着,我们的Application的并行执行的能力就很弱。 比如有3个executor,每个executor有2个cpu core,那么同时能够并行执行的task,就是6个。6个执行完以后,再换下一批6个task。
增加每个executor的cpu core,也是增加了执行的并行能力。原本20个executor,每个才2个cpu core。能够并行执行的task数量,就是40个task。 现在每个executor的cpu core,增加到了5个。能够并行执行的task数量,就是100个task。
增加每个executor的内存量。增加了内存量以后,对性能的提升,有两点:
1、如果需要对RDD进行cache,那么更多的内存,就可以缓存更多的数据,将更少的数据写入磁盘,甚至不写入磁盘。减少了磁盘IO。
2、对于shuffle操作,reduce端,会需要内存来存放拉取的数据并进行聚合。如果内存不够,也会写入磁盘。如果给executor分配更多内存以后,就有更少的数据,需要写入磁盘,甚至不需要写入磁盘。减少了磁盘IO,提升了性能。
3、对于task的执行,可能会创建很多对象。如果内存比较小,可能会频繁导致JVM堆内存满了,然后频繁GC,垃圾回收,minor GC和full GC。(速度很慢)。内存加大以后,带来更少的GC,垃圾回收,避免了速度变慢,速度变快了。
fastUtil优化数据格式
fastutil介绍: fastutil是扩展了Java标准集合框架(Map、List、Set;HashMap、ArrayList、HashSet)的类库,提供了特殊类型的map、set、list和queue; fastutil能够提供更小的内存占用,更快的存取速度;我们使用fastutil提供的集合类,来替代自己平时使用的JDK的原生的Map、List、Set,好处在于,fastutil集合类,可以减小内存的占用,并且在进行集合的遍历、根据索引(或者key)获取元素的值和设置元素的值的时候,提供更快的存取速度; fastutil也提供了64位的array、set和list,以及高性能快速的,以及实用的IO类,来处理二进制和文本类型的文件; fastutil最新版本要求Java 7以及以上版本; fastutil的每一种集合类型,都实现了对应的Java中的标准接口(比如fastutil的map,实现了Java的Map接口),因此可以直接放入已有系统的任何代码中。 fastutil还提供了一些JDK标准类库中没有的额外功能(比如双向迭代器)。 fastutil除了对象和原始类型为元素的集合,fastutil也提供引用类型的支持,但是对引用类型是使用等于号(=)进行比较的,而不是equals()方法。 fastutil尽量提供了在任何场景下都是速度最快的集合类库。 Spark中应用fastutil的场景: 1、如果算子函数使用了外部变量;那么第一,你可以使用Broadcast广播变量优化;第二,可以使用Kryo序列化类库,提升序列化性能和效率;第三,如果外部变量是某种比较大的集合,那么可以考虑使用fastutil改写外部变量,首先从源头上就减少内存的占用,通过广播变量进一步减少内存占用,再通过Kryo序列化类库进一步减少内存占用。 2、在你的算子函数里,也就是task要执行的计算逻辑里面,如果有逻辑中,出现,要创建比较大的Map、List等集合,可能会占用较大的内存空间,而且可能涉及到消耗性能的遍历、存取等集合操作;那么此时,可以考虑将这些集合类型使用fastutil类库重写,使用了fastutil集合类以后,就可以在一定程度上,减少task创建出来的集合类型的内存占用。避免executor内存频繁占满,频繁唤起GC,导致性能下降。 关于fastutil调优的说明: fastutil其实没有你想象中的那么强大,也不会跟官网上说的效果那么一鸣惊人。广播变量、Kryo序列化类库、fastutil,都是之前所说的,对于性能来说,类似于一种调味品,烤鸡,本来就很好吃了,然后加了一点特质的孜然麻辣粉调料,就更加好吃了一点。分配资源、并行度、RDD架构与持久化,这三个就是烤鸡;broadcast、kryo、fastutil,类似于调料。 比如说,你的spark作业,经过之前一些调优以后,大概30分钟运行完,现在加上broadcast、kryo、fastutil,也许就是优化到29分钟运行完、或者更好一点,也许就是28分钟、25分钟。 shuffle调优,15分钟;groupByKey用reduceByKey改写,执行本地聚合,也许10分钟;跟公司申请更多的资源,比如资源更大的YARN队列,1分钟。 fastutil的使用: 第一步:在pom.xml中引用fastutil的包 <dependency> <groupId>fastutil</groupId> <artifactId>fastutil</artifactId> <version>5.0.9</version> </dependency> 速度比较慢,可能是从国外的网去拉取jar包,可能要等待5分钟,甚至几十分钟,不等 List<Integer> => IntList 基本都是类似于IntList的格式,前缀就是集合的元素类型;特殊的就是Map,Int2IntMap,代表了key-value映射的元素类型。除此之外,刚才也看到了,还支持object、reference。
调节数据本地化等待时长
PROCESS_LOCAL:进程本地化,代码和数据在同一个进程中,也就是在同一个executor中;计算数据的task由executor执行,数据在executor的BlockManager中;性能最好
NODE_LOCAL:节点本地化,代码和数据在同一个节点中;比如说,数据作为一个HDFS block块,就在节点上,而task在节点上某个executor中运行;或者是,数据和task在一个节点上的不同executor中;数据需要在进程间进行传输
NO_PREF:对于task来说,数据从哪里获取都一样,没有好坏之分
RACK_LOCAL:机架本地化,数据和task在一个机架的两个节点上;数据需要通过网络在节点之间进行传输
ANY:数据和task可能在集群中的任何地方,而且不在一个机架中,性能最差 spark.locality.wait,默认是3s
Spark在Driver上,对Application的每一个stage的task,进行分配之前,都会计算出每个task要计算的是哪个分片数据,RDD的某个partition;Spark的task分配算法,优先,会希望每个task正好分配到它要计算的数据所在的节点,这样的话,就不用在网络间传输数据; 但是呢,通常来说,有时,事与愿违,可能task没有机会分配到它的数据所在的节点,为什么呢,可能那个节点的计算资源和计算能力都满了;所以呢,这种时候,通常来说,Spark会等待一段时间,默认情况下是3s钟(不是绝对的,还有很多种情况,对不同的本地化级别,都会去等待),到最后,实在是等待不了了,就会选择一个比较差的本地化级别,比如说,将task分配到靠它要计算的数据所在节点,比较近的一个节点,然后进行计算。 但是对于第二种情况,通常来说,肯定是要发生数据传输,task会通过其所在节点的BlockManager来获取数据,BlockManager发现自己本地没有数据,会通过一个getRemote()方法,通过TransferService(网络数据传输组件)从数据所在节点的BlockManager中,获取数据,通过网络传输回task所在节点。 对于我们来说,当然不希望是类似于第二种情况的了。最好的,当然是task和数据在一个节点上,直接从本地executor的BlockManager中获取数据,纯内存,或者带一点磁盘IO;如果要通过网络传输数据的话,那么实在是,性能肯定会下降的,大量网络传输,以及磁盘IO,都是性能的杀手。
我们什么时候要调节这个参数? 观察日志,spark作业的运行日志,推荐大家在测试的时候,先用client模式,在本地就直接可以看到比较全的日志。 日志里面会显示,starting task。。。,PROCESS LOCAL、NODE LOCAL 观察大部分task的数据本地化级别 如果大多都是PROCESS_LOCAL,那就不用调节了 如果是发现,好多的级别都是NODE_LOCAL、ANY,那么最好就去调节一下数据本地化的等待时长 调节完,应该是要反复调节,每次调节完以后,再来运行,观察日志 看看大部分的task的本地化级别有没有提升;看看,整个spark作业的运行时间有没有缩短 你别本末倒置,本地化级别倒是提升了,但是因为大量的等待时长,spark作业的运行时间反而增加了,那就还是不要调节了 怎么调节? spark.locality.wait,默认是3s;6s,10s 默认情况下,下面3个的等待时长,都是跟上面那个是一样的,都是3s spark.locality.wait.process spark.locality.wait.node spark.locality.wait.rack new SparkConf() .set("spark.locality.wait", "10")
降低cache操作内存占比
JVM调优的第一个点:降低cache操作的内存占比 spark中,堆内存又被划分成了两块儿,一块儿是专门用来给RDD的cache、persist操作进行RDD数据缓存用的;另外一块儿,就是我们刚才所说的,用来给spark算子函数的运行使用的,存放函数中自己创建的对象。 默认情况下,给RDD cache操作的内存占比,是0.6,60%的内存都给了cache操作了。但是问题是,如果某些情况下,cache不是那么的紧张,问题在于task算子函数中创建的对象过多,然后内存又不太大,导致了频繁的minor gc,甚至频繁full gc,导致spark频繁的停止工作。性能影响会很大。 针对上述这种情况,大家可以在之前我们讲过的那个spark ui。yarn去运行的话,那么就通过yarn的界面,去查看你的spark作业的运行统计,很简单,大家一层一层点击进去就好。可以看到每个stage的运行情况,包括每个task的运行时间、gc时间等等。如果发现gc太频繁,时间太长。此时就可以适当调价这个比例。 降低cache操作的内存占比,大不了用persist操作,选择将一部分缓存的RDD数据写入磁盘,或者序列化方式,配合Kryo序列化类,减少RDD缓存的内存占用;降低cache操作内存占比;对应的,算子函数的内存占比就提升了。这个时候,可能,就可以减少minor gc的频率,同时减少full gc的频率。对性能的提升是有一定的帮助的。 一句话,让task执行算子函数时,有更多的内存可以使用。 spark.storage.memoryFraction,0.6 -> 0.5 -> 0.4 -> 0.2
调节executor堆外内存与连接等待时长
executor堆外内存 有时候,如果你的spark作业处理的数据量特别特别大,几亿数据量;然后spark作业一运行,时不时的报错,shuffle file cannot find,executor、task lost,out of memory(内存溢出); 可能是说executor的堆外内存不太够用,导致executor在运行的过程中,可能会内存溢出;然后可能导致后续的stage的task在运行的时候,可能要从一些executor中去拉取shuffle map output文件,但是executor可能已经挂掉了,关联的block manager也没有了;所以可能会报shuffle output file not found;resubmitting task;executor lost;spark作业彻底崩溃。 上述情况下,就可以去考虑调节一下executor的堆外内存。也许就可以避免报错;此外,有时,堆外内存调节的比较大的时候,对于性能来说,也会带来一定的提升。
--conf spark.yarn.executor.memoryOverhead=2048 spark-submit脚本里面,去用--conf的方式,去添加配置;一定要注意!!!切记,不是在你的spark作业代码中,用new SparkConf().set()这种方式去设置,不要这样去设置,是没有用的!一定要在spark-submit脚本中去设置。 spark.yarn.executor.memoryOverhead(看名字,顾名思义,针对的是基于yarn的提交模式) 默认情况下,这个堆外内存上限大概是300多M;后来我们通常项目中,真正处理大数据的时候,这里都会出现问题,导致spark作业反复崩溃,无法运行;此时就会去调节这个参数,到至少1G(1024M),甚至说2G、4G 通常这个参数调节上去以后,就会避免掉某些JVM OOM的异常问题,同时呢,会让整体spark作业的性能,得到较大的提升。
JVM调优:垃圾回收 处于垃圾回收过程中,所有的工作线程全部停止;相当于只要一旦进行垃圾回收,spark / executor停止工作,无法提供响应
executor,优先从自己本地关联的BlockManager中获取某份数据 如果本地block manager没有的话,那么会通过TransferService,去远程连接其他节点上executor的block manager去获取
正好碰到那个exeuctor的JVM在垃圾回收
此时呢,就会没有响应,无法建立网络连接;会卡住;ok,spark默认的网络连接的超时时长,是60s;如果卡住60s都无法建立连接的话,那么就宣告失败了。 碰到一种情况,偶尔,偶尔,偶尔!!!没有规律!!!某某file。一串file id。uuid(dsfsfd-2342vs--sdf--sdfsd)。not found。file lost。 这种情况下,很有可能是有那份数据的executor在jvm gc。所以拉取数据的时候,建立不了连接。然后超过默认60s以后,直接宣告失败。 报错几次,几次都拉取不到数据的话,可能会导致spark作业的崩溃。也可能会导致DAGScheduler,反复提交几次stage。TaskScheduler,反复提交几次task。大大延长我们的spark作业的运行时间。 可以考虑调节连接的超时时长。 --conf spark.core.connection.ack.wait.timeout=300 spark-submit脚本,切记,不是在new SparkConf().set()这种方式来设置的。 spark.core.connection.ack.wait.timeout(spark core,connection,连接,ack,wait timeout,建立不上连接的时候,超时等待时长) 调节这个值比较大以后,通常来说,可以避免部分的偶尔出现的某某文件拉取失败,某某文件lost掉了。。。
为什么在这里讲这两个参数呢? 因为比较实用,在真正处理大数据(不是几千万数据量、几百万数据量),几亿,几十亿,几百亿的时候。很容易碰到executor堆外内存,以及gc引起的连接超时的问题。file not found,executor lost,task lost。 调节上面两个参数,还是很有帮助的。
shuffle
什么样的情况下,会发生shuffle? 在spark中,主要是以下几个算子:groupByKey、reduceByKey、countByKey、join,等等。 什么是shuffle? groupByKey,要把分布在集群各个节点上的数据中的同一个key,对应的values,都给集中到一块儿,集中到集群中同一个节点上,更严密一点说,就是集中到一个节点的一个executor的一个task中。 然后呢,集中一个key对应的values之后,才能交给我们来进行处理,<key, Iterable<value>>;reduceByKey,算子函数去对values集合进行reduce操作,最后变成一个value;countByKey,需要在一个task中,获取到一个key对应的所有的value,然后进行计数,统计总共有多少个value;join,RDD<key, value>,RDD<key, value>,只要是两个RDD中,key相同对应的2个value,都能到一个节点的executor的task中,给我们进行处理。
map端输出文件
shuffle中的写磁盘的操作,基本上就是shuffle中性能消耗最为严重的部分。 通过上面的分析,一个普通的生产环境的spark job的一个shuffle环节,会写入磁盘100万个文件。 磁盘IO对性能和spark作业执行速度的影响,是极其惊人和吓人的。 基本上,spark作业的性能,都消耗在shuffle中了,虽然不只是shuffle的map端输出文件这一个部分,但是这里也是非常大的一个性能消耗点。
new SparkConf().set("spark.shuffle.consolidateFiles", "true") 开启shuffle map端输出文件合并的机制;默认情况下,是不开启的,就是会发生如上所述的大量map端输出文件的操作,严重影响性能。
开启了map端输出文件的合并机制之后: 第一个stage,同时就运行cpu core个task,比如cpu core是2个,并行运行2个task;每个task都创建下一个stage的task数量个文件; 第一个stage,并行运行的2个task执行完以后;就会执行另外两个task;另外2个task不会再重新创建输出文件;而是复用之前的task创建的map端输出文件,将数据写入上一批task的输出文件中。 第二个stage,task在拉取数据的时候,就不会去拉取上一个stage每一个task为自己创建的那份输出文件了;而是拉取少量的输出文件,每个输出文件中,可能包含了多个task给自己的map端输出。
提醒一下(map端输出文件合并): 只有并行执行的task会去创建新的输出文件;下一批并行执行的task,就会去复用之前已有的输出文件;但是有一个例外,比如2个task并行在执行,但是此时又启动要执行2个task;那么这个时候的话,就无法去复用刚才的2个task创建的输出文件了;而是还是只能去创建新的输出文件。 要实现输出文件的合并的效果,必须是一批task先执行,然后下一批task再执行,才能复用之前的输出文件;负责多批task同时起来执行,还是做不到复用的。
map端内存缓冲与reduce端内存占比
spark.shuffle.file.buffer,默认32k spark.shuffle.memoryFraction,0.2
以实际的生产经验来说,这两个参数没有那么重要
默认情况下,shuffle的map task,输出到磁盘文件的时候,统一都会先写入每个task自己关联的一个内存缓冲区。 这个缓冲区大小,默认是32kb。 每一次,当内存缓冲区满溢之后,才会进行spill操作,溢写操作,溢写到磁盘文件中去。
reduce端task,在拉取到数据之后,会用hashmap的数据格式,来对各个key对应的values进行汇聚。 针对每个key对应的values,执行我们自定义的聚合函数的代码,比如_ + _(把所有values累加起来) reduce task,在进行汇聚、聚合等操作的时候,实际上,使用的就是自己对应的executor的内存,executor(jvm进程,堆),默认executor内存中划分给reduce task进行聚合的比例,是0.2。 问题来了,因为比例是0.2,所以,理论上,很有可能会出现,拉取过来的数据很多,那么在内存中,放不下;这个时候,默认的行为,就是说,将在内存放不下的数据,都spill(溢写)到磁盘文件中去。
原理说完之后,来看一下,默认情况下,不调优,可能会出现什么样的问题? 默认,map端内存缓冲是每个task,32kb。 默认,reduce端聚合内存比例,是0.2,也就是20%。 如果map端的task,处理的数据量比较大,但是呢,你的内存缓冲大小是固定的。可能会出现什么样的情况? 每个task就处理320kb,32kb,总共会向磁盘溢写320 / 32 = 10次。 每个task处理32000kb,32kb,总共会向磁盘溢写32000 / 32 = 1000次。 在map task处理的数据量比较大的情况下,而你的task的内存缓冲默认是比较小的,32kb。可能会造成多次的map端往磁盘文件的spill溢写操作,发生大量的磁盘IO,从而降低性能。 reduce端聚合内存,占比。默认是0.2。如果数据量比较大,reduce task拉取过来的数据很多,那么就会频繁发生reduce端聚合内存不够用,频繁发生spill操作,溢写到磁盘上去。而且最要命的是,磁盘上溢写的数据量越大,后面在进行聚合操作的时候,很可能会多次读取磁盘中的数据,进行聚合。 默认不调优,在数据量比较大的情况下,可能频繁地发生reduce端的磁盘文件的读写。 这两个点之所以放在一起讲,是因为他们俩是有关联的。数据量变大,map端肯定会出点问题;reduce端肯定也会出点问题;出的问题是一样的,都是磁盘IO频繁,变多,影响性能。
调优: 调节map task内存缓冲:spark.shuffle.file.buffer,默认32k(spark 1.3.x不是这个参数,后面还有一个后缀,kb;spark 1.5.x以后,变了,就是现在这个参数) 调节reduce端聚合内存占比:spark.shuffle.memoryFraction,0.2 在实际生产环境中,我们在什么时候来调节两个参数? 看Spark UI,如果你的公司是决定采用standalone模式,那么狠简单,你的spark跑起来,会显示一个Spark UI的地址,4040的端口,进去看,依次点击进去,可以看到,你的每个stage的详情,有哪些executor,有哪些task,每个task的shuffle write和shuffle read的量,shuffle的磁盘和内存,读写的数据量;如果是用的yarn模式来提交,课程最前面,从yarn的界面进去,点击对应的application,进入Spark UI,查看详情。 如果发现shuffle 磁盘的write和read,很大。这个时候,就意味着最好调节一些shuffle的参数。进行调优。首先当然是考虑开启map端输出文件合并机制。 调节上面说的那两个参数。调节的时候的原则。spark.shuffle.file.buffer,每次扩大一倍,然后看看效果,64,128;spark.shuffle.memoryFraction,每次提高0.1,看看效果。 不能调节的太大,太大了以后过犹不及,因为内存资源是有限的,你这里调节的太大了,其他环节的内存使用就会有问题了。 调节了以后,效果?map task内存缓冲变大了,减少spill到磁盘文件的次数;reduce端聚合内存变大了,减少spill到磁盘的次数,而且减少了后面聚合读取磁盘文件的数量。
HashShuffleManager和SortShuffleManager
spark.shuffle.manager:hash、sort、tungsten-sort(自己实现内存管理) spark.shuffle.sort.bypassMergeThreshold:200
HashShuffleManager;这种manager,实际上,从spark 1.2.x版本以后,就不再是默认的选择了。
spark 1.2.x版本以后,默认的shuffle manager,是什么呢?SortShuffleManager。
SortShuffleManager与HashShuffleManager两点不同: 1、SortShuffleManager会对每个reduce task要处理的数据,进行排序(默认的)。 2、SortShuffleManager会避免像HashShuffleManager那样,默认就去创建多份磁盘文件。一个task,只会写入一个磁盘文件,不同reduce task的数据,用offset来划分界定。
自己可以设定一个阈值,默认是200,当reduce task数量少于等于200;map task创建的输出文件小于等于200的;最后会将所有的输出文件合并为一份文件。 这样做的好处,就是避免了sort排序,节省了性能开销。而且还能将多个reduce task的文件合并成一份文件。节省了reduce task拉取数据的时候的磁盘IO的开销。
在spark 1.5.x以后,对于shuffle manager又出来了一种新的manager,tungsten-sort(钨丝),钨丝sort shuffle manager。官网上一般说,钨丝sort shuffle manager,效果跟sort shuffle manager是差不多的。 但是,唯一的不同之处在于,钨丝manager,是使用了自己实现的一套内存管理机制,性能上有很大的提升, 而且可以避免shuffle过程中产生的大量的OOM,GC,等等内存相关的异常。
来一个总结,现在相当于把spark的shuffle的东西又多讲了一些。大家理解的更加深入了。hash、sort、tungsten-sort。如何来选择? 1、需不需要数据默认就让spark给你进行排序?就好像mapreduce,默认就是有按照key的排序。如果不需要的话,其实还是建议搭建就使用最基本的HashShuffleManager,因为最开始就是考虑的是不排序,换取高性能; 2、什么时候需要用sort shuffle manager?如果你需要你的那些数据按key排序了,那么就选择这种吧,而且要注意,reduce task的数量应该是超过200的,这样sort、merge(多个文件合并成一个)的机制,才能生效把。但是这里要注意,你一定要自己考量一下,有没有必要在shuffle的过程中,就做这个事情,毕竟对性能是有影响的。 3、如果你不需要排序,而且你希望你的每个task输出的文件最终是会合并成一份的,你自己认为可以减少性能开销;可以去调节bypassMergeThreshold这个阈值,比如你的reduce task数量是500,默认阈值是200,所以默认还是会进行sort和直接merge的;可以将阈值调节成550,不会进行sort,按照hash的做法,每个reduce task创建一份输出文件,最后合并成一份文件。(一定要提醒大家,这个参数,其实我们通常不会在生产环境里去使用,也没有经过验证说,这样的方式,到底有多少性能的提升) 4、如果你想选用sort based shuffle manager,而且你们公司的spark版本比较高,是1.5.x版本的,那么可以考虑去尝试使用tungsten-sort shuffle manager。看看性能的提升与稳定性怎么样。 总结: 1、在生产环境中,不建议大家贸然使用第三点和第四点: 2、如果你不想要你的数据在shuffle时排序,那么就自己设置一下,用hash shuffle manager。 3、如果你的确是需要你的数据在shuffle时进行排序的,那么就默认不用动,默认就是sort shuffle manager;或者是什么?如果你压根儿不care是否排序这个事儿,那么就默认让他就是sort的。调节一些其他的参数(consolidation机制)。(80%,都是用这种) spark.shuffle.manager:hash、sort、tungsten-sort new SparkConf().set("spark.shuffle.manager", "hash") new SparkConf().set("spark.shuffle.manager", "tungsten-sort") // 默认就是,new SparkConf().set("spark.shuffle.manager", "sort") new SparkConf().set("spark.shuffle.sort.bypassMergeThreshold", "550")
MapPartitions提升Map类操作性能
spark中,最基本的原则,就是每个task处理一个RDD的partition。
MapPartitions操作的优点: 如果是普通的map,比如一个partition中有1万条数据;ok,那么你的function要执行和计算1万次。 但是,使用MapPartitions操作之后,一个task仅仅会执行一次function,function一次接收所有的partition数据。只要执行一次就可以了,性能比较高。
MapPartitions的缺点:一定是有的。 如果是普通的map操作,一次function的执行就处理一条数据;那么如果内存不够用的情况下,比如处理了1千条数据了,那么这个时候内存不够了,那么就可以将已经处理完的1千条数据从内存里面垃圾回收掉,或者用其他方法,腾出空间来吧。 所以说普通的map操作通常不会导致内存的OOM异常。 但是MapPartitions操作,对于大量数据来说,比如甚至一个partition,100万数据,一次传入一个function以后,那么可能一下子内存不够,但是又没有办法去腾出内存空间来,可能就OOM,内存溢出。
什么时候比较适合用MapPartitions系列操作,就是说,数据量不是特别大的时候,都可以用这种MapPartitions系列操作,性能还是非常不错的,是有提升的。比如原来是15分钟,(曾经有一次性能调优),12分钟。10分钟->9分钟。 但是也有过出问题的经验,MapPartitions只要一用,直接OOM,内存溢出,崩溃。 在项目中,自己先去估算一下RDD的数据量,以及每个partition的量,还有自己分配给每个executor的内存资源。看看一下子内存容纳所有的partition数据,行不行。如果行,可以试一下,能跑通就好。性能肯定是有提升的。 但是试了一下以后,发现,不行,OOM了,那就放弃吧。
filter过后使用coalesce减少分区数量
默认情况下,经过了这种filter之后,RDD中的每个partition的数据量,可能都不太一样了。(原本每个partition的数据量可能是差不多的) 问题: 1、每个partition数据量变少了,但是在后面进行处理的时候,还是要跟partition数量一样数量的task,来进行处理;有点浪费task计算资源。 2、每个partition的数据量不一样,会导致后面的每个task处理每个partition的时候,每个task要处理的数据量就不同,这个时候很容易发生什么问题?数据倾斜。。。。 比如说,第二个partition的数据量才100;但是第三个partition的数据量是900;那么在后面的task处理逻辑一样的情况下,不同的task要处理的数据量可能差别达到了9倍,甚至10倍以上;同样也就导致了速度的差别在9倍,甚至10倍以上。 这样的话呢,就会导致有些task运行的速度很快;有些task运行的速度很慢。这,就是数据倾斜。
针对上述的两个问题,我们希望应该能够怎么样? 1、针对第一个问题,我们希望可以进行partition的压缩吧,因为数据量变少了,那么partition其实也完全可以对应的变少。比如原来是4个partition,现在完全可以变成2个partition。那么就只要用后面的2个task来处理即可。就不会造成task计算资源的浪费。(不必要,针对只有一点点数据的partition,还去启动一个task来计算) 2、针对第二个问题,其实解决方案跟第一个问题是一样的;也是去压缩partition,尽量让每个partition的数据量差不多。那么这样的话,后面的task分配到的partition的数据量也就差不多。不会造成有的task运行速度特别慢,有的task运行速度特别快。避免了数据倾斜的问题。 有了解决问题的思路之后,接下来,我们该怎么来做呢?实现? coalesce算子 主要就是用于在filter操作之后,针对每个partition的数据量各不相同的情况,来压缩partition的数量。减少partition的数量,而且让每个partition的数据量都尽量均匀紧凑。 从而便于后面的task进行计算操作,在某种程度上,能够一定程度的提升性能。
使用foreach优化写数据库性能
默认的foreach的性能缺陷在哪里? 首先,对于 每条数据,都要单独去调用一次function,task为每个数据,都要去执行一次function函数。 如果100万条数据,(一个partition),调用100万次。性能比较差。 另外一个非常非常重要的一点 如果每个数据,你都去创建一个数据库连接的话,那么你就得创建100万次数据库连接。 但是要注意的是,数据库连接的创建和销毁,都是非常非常消耗性能的。虽然我们之前已经用了数据库连接池,只是创建了固定数量的数据库连接。 你还是得多次通过数据库连接,往数据库(MySQL)发送一条SQL语句,然后MySQL需要去执行这条SQL语句。如果有100万条数据,那么就是100万次发送SQL语句。 以上两点(数据库连接,多次发送SQL语句),都是非常消耗性能的。
foreachPartition,在生产环境中,通常来说,都使用foreachPartition来写数据库的
用了foreachPartition算子之后,好处在哪里? 1、对于我们写的function函数,就调用一次,一次传入一个partition所有的数据 2、主要创建或者获取一个数据库连接就可以 3、只要向数据库发送一次SQL语句和多组参数即可
在实际生产环境中,清一色,都是使用foreachPartition操作;但是有个问题,跟mapPartitions操作一样,如果一个partition的数量真的特别特别大,比如真的是100万,那基本上就不太靠谱了。 一下子进来,很有可能会发生OOM,内存溢出的问题。 一组数据的对比:生产环境 一个partition大概是1千条左右 用foreach,跟用foreachPartition,性能的提升达到了2~3分钟。
使用repartition解决Spark SQL低并行度的性能问题
并行度:之前说过,并行度是自己可以调节,或者说是设置的。 1、spark.default.parallelism 2、textFile(),传入第二个参数,指定partition数量(比较少用) 咱们的项目代码中,没有设置并行度,实际上,在生产环境中,是最好自己设置一下的。官网有推荐的设置方式,你的spark-submit脚本中,会指定你的application总共要启动多少个executor,100个;每个executor多少个cpu core,2~3个;总共application,有cpu core,200个。 官方推荐,根据你的application的总cpu core数量(在spark-submit中可以指定,200个),自己手动设置spark.default.parallelism参数,指定为cpu core总数的2~3倍。400~600个并行度。600。 承上启下 你设置的这个并行度,在哪些情况下会生效?哪些情况下,不会生效? 如果你压根儿没有使用Spark SQL(DataFrame),那么你整个spark application默认所有stage的并行度都是你设置的那个参数。(除非你使用coalesce算子缩减过partition数量) 问题来了,Spark SQL,用了。用Spark SQL的那个stage的并行度,你没法自己指定。Spark SQL自己会默认根据hive表对应的hdfs文件的block,自动设置Spark SQL查询所在的那个stage的并行度。你自己通过spark.default.parallelism参数指定的并行度,只会在没有Spark SQL的stage中生效。 比如你第一个stage,用了Spark SQL从hive表中查询出了一些数据,然后做了一些transformation操作,接着做了一个shuffle操作(groupByKey);下一个stage,在shuffle操作之后,做了一些transformation操作。hive表,对应了一个hdfs文件,有20个block;你自己设置了spark.default.parallelism参数为100。 你的第一个stage的并行度,是不受你的控制的,就只有20个task;第二个stage,才会变成你自己设置的那个并行度,100。 问题在哪里? Spark SQL默认情况下,它的那个并行度,咱们没法设置。可能导致的问题,也许没什么问题,也许很有问题。Spark SQL所在的那个stage中,后面的那些transformation操作,可能会有非常复杂的业务逻辑,甚至说复杂的算法。如果你的Spark SQL默认把task数量设置的很少,20个,然后每个task要处理为数不少的数据量,然后还要执行特别复杂的算法。 这个时候,就会导致第一个stage的速度,特别慢。第二个stage,1000个task,刷刷刷,非常快。
解决上述Spark SQL无法设置并行度和task数量的办法,是什么呢? repartition算子,你用Spark SQL这一步的并行度和task数量,肯定是没有办法去改变了。但是呢,可以将你用Spark SQL查询出来的RDD,使用repartition算子,去重新进行分区,此时可以分区成多个partition,比如从20个partition,分区成100个。 然后呢,从repartition以后的RDD,再往后,并行度和task数量,就会按照你预期的来了。就可以避免跟Spark SQL绑定在一个stage中的算子,只能使用少量的task去处理大量数据以及复杂的算法逻辑。
reduceByKey本地聚合介绍
reduceByKey,相较于普通的shuffle操作(比如groupByKey),它的一个特点,就是说,会进行map端的本地聚合。 对map端给下个stage每个task创建的输出文件中,写数据之前,就会进行本地的combiner操作,也就是说对每一个key,对应的values,都会执行你的算子函数() + _)
用reduceByKey对性能的提升: 1、在本地进行聚合以后,在map端的数据量就变少了,减少磁盘IO。而且可以减少磁盘空间的占用。 2、下一个stage,拉取数据的量,也就变少了。减少网络的数据传输的性能消耗。 3、在reduce端进行数据缓存的内存占用变少了。 4、reduce端,要进行聚合的数据量也变少了。
总结: reduceByKey在什么情况下使用呢? 1、非常普通的,比如说,就是要实现类似于wordcount程序一样的,对每个key对应的值,进行某种数据公式或者算法的计算(累加、类乘) 2、对于一些类似于要对每个key进行一些字符串拼接的这种较为复杂的操作,可以自己衡量一下,其实有时,也是可以使用reduceByKey来实现的。但是不太好实现。如果真能够实现出来,对性能绝对是有帮助的。(shuffle基本上就占了整个spark作业的90%以上的性能消耗,主要能对shuffle进行一定的调优,都是有价值的)
控制shuffle reduce端缓冲大小以避免oom
map端的task是不断的输出数据的,数据量可能是很大的。 但是,其实reduce端的task,并不是等到map端task将属于自己的那份数据全部写入磁盘文件之后,再去拉取的。map端写一点数据,reduce端task就会拉取一小部分数据,立即进行后面的聚合、算子函数的应用。 每次reduece能够拉取多少数据,就由buffer来决定。因为拉取过来的数据,都是先放在buffer中的。然后才用后面的executor分配的堆内存占比(0.2),hashmap,去进行后续的聚合、函数的执行。
再来说说,reduce端缓冲大小的另外一面,关于性能调优的一面: 咱们假如说,你的Map端输出的数据量也不是特别大,然后你的整个application的资源也特别充足。200个executor、5个cpu core、10G内存。 其实可以尝试去增加这个reduce端缓冲大小的,比如从48M,变成96M。那么这样的话,每次reduce task能够拉取的数据量就很大。需要拉取的次数也就变少了。比如原先需要拉取100次,现在只要拉取50次就可以执行完了。 对网络传输性能开销的减少,以及reduce端聚合操作执行的次数的减少,都是有帮助的。 最终达到的效果,就应该是性能上的一定程度上的提升。 一定要注意,资源足够的时候,再去做这个事儿。
reduce端缓冲(buffer),可能会出什么问题? 可能是会出现,默认是48MB,也许大多数时候,reduce端task一边拉取一边计算,不一定一直都会拉满48M的数据。可能大多数时候,拉取个10M数据,就计算掉了。 大多数时候,也许不会出现什么问题。但是有的时候,map端的数据量特别大,然后写出的速度特别快。reduce端所有task,拉取的时候,全部达到自己的缓冲的最大极限值,缓冲,48M,全部填满。 这个时候,再加上你的reduce端执行的聚合函数的代码,可能会创建大量的对象。也许,一下子,内存就撑不住了,就会OOM。reduce端的内存中,就会发生内存溢出的问题。 针对上述的可能出现的问题,我们该怎么来解决呢? 这个时候,就应该减少reduce端task缓冲的大小。我宁愿多拉取几次,但是每次同时能够拉取到reduce端每个task的数量,比较少,就不容易发生OOM内存溢出的问题。(比如,可以调节成12M) 在实际生产环境中,我们都是碰到过这种问题的。这是典型的以性能换执行的原理。reduce端缓冲小了,不容易OOM了,但是,性能一定是有所下降的,你要拉取的次数就多了。就走更多的网络传输开销。 这种时候,只能采取牺牲性能的方式了,spark作业,首先,第一要义,就是一定要让它可以跑起来。分享一个经验,曾经写过一个特别复杂的spark作业,写完代码以后,半个月之内,就是跑不起来,里面各种各样的问题,需要进行troubleshooting。调节了十几个参数,其中就包括这个reduce端缓冲的大小。总算作业可以跑起来了。 然后才去考虑性能的调优。
gc导致shuffle文件拉取失败
比如,executor的JVM进程,可能内存不是很够用了。那么此时可能就会执行GC。minor GC or full GC。总之一旦发生了JVM之后,就会导致executor内,所有的工作线程全部停止,比如BlockManager,基于netty的网络通信。
下一个stage的executor,可能是还没有停止掉的,task想要去上一个stage的task所在的exeuctor,去拉取属于自己的数据,结果由于对方正在gc,就导致拉取了半天没有拉取到。 就很可能会报出,shuffle file not found。但是,可能下一个stage又重新提交了stage或task以后,再执行就没有问题了,因为可能第二次就没有碰到JVM在gc了。
有时会出现的一种情况,非常普遍,在spark的作业中;shuffle file not found。(spark作业中,非常非常常见的)而且,有的时候,它是偶尔才会出现的一种情况。有的时候,出现这种情况以后,会重新去提交stage、task。重新执行一遍,发现就好了。没有这种错误了。 log怎么看?用client模式去提交你的spark作业。比如standalone client;yarn client。一提交作业,直接可以在本地看到刷刷刷更新的log。
spark.shuffle.io.maxRetries 3 第一个参数,意思就是说,shuffle文件拉取的时候,如果没有拉取到(拉取失败),最多或重试几次(会重新拉取几次文件),默认是3次。 spark.shuffle.io.retryWait 5s 第二个参数,意思就是说,每一次重试拉取文件的时间间隔,默认是5s钟。 默认情况下,假如说第一个stage的executor正在进行漫长的full gc。第二个stage的executor尝试去拉取文件,结果没有拉取到,默认情况下,会反复重试拉取3次,每次间隔是五秒钟。最多只会等待3 * 5s = 15s。如果15s内,没有拉取到shuffle file。就会报出shuffle file not found。 针对这种情况,我们完全可以进行预备性的参数调节。增大上述两个参数的值,达到比较大的一个值,尽量保证第二个stage的task,一定能够拉取到上一个stage的输出文件。避免报shuffle file not found。然后可能会重新提交stage和task去执行。那样反而对性能也不好。 spark.shuffle.io.maxRetries 60 spark.shuffle.io.retryWait 60s 最多可以忍受1个小时没有拉取到shuffle file。只是去设置一个最大的可能的值。full gc不可能1个小时都没结束吧。 这样呢,就可以尽量避免因为gc导致的shuffle file not found,无法拉取到的问题。
troubleshooting之解决yarn队列资源不足导致application直接失败
用户session模块:
聚合模块负责内容:
用户访问session的聚合统计。(与MYSQL当中已经存在的user用户信息进行聚合,通过session信息当中的user_id与mysql当中的userid进行join,再拼接需要的信息返回最后聚合的RDD)
Session数据来源:
MOCKdata类产生
获取指定日期范围内的用户访问行为数据
getActionRDDByDateRange:通过paramutils.getParam来截取startDate和endDate → sql语句"select * "+ "from user_visit_action "+ "where date>='" + startDate + "' "+ "and date<='" + endDate + "'" 来生成所需要的DataFrame,将DF转为javaRDD( ),得到第一个actionRDD。
获取sessionid2到访问行为数据的映射的RDD
getSessionid2ActionRDD:利用mapPartition对actionRDD进行操作→封装成List<Tuple2<String, Row>> list ,每个tuple是<sessionID,row>→返回list ;
(调优点:在数据量不是特别大的时候,都可以用这种MapPartitions系列操作,原理是正常每个task执行一个partition的数据,有多少条数据,function就要计算多少次,使用mapPartition之后会将所有partition的数据聚合到一起给一个task执行,function只执行一次。 如果数据量太大,则会出现OOM)
对行为数据按session粒度进行聚合
* @param actionRDD 行为数据RDD
* @return session粒度聚合数据
aggregateBySession: 利用groupbykey对session粒度进行分组,JavaPairRDD<String, Iterable<Row>> sessionid2ActionsRDD = sessinoid2actionRDD.groupByKey();→ 对每一个session分组聚合,将session中所有的搜索词和点击品类都聚合起来(对groupby之后的sessinoid2actionRDD进行maptopair,通过iterator.hasnext对访问行为进行遍历,计算开始和结束的时间,访问步长,同时获取searchkeyword和clickCategoryId两个字段,而且两个字段不一定同时出现,要判空,在写进两个stringbuffer里面。拼接成聚合的字段partAggrinfo,包含sessionID,searchkeyword,clickCategoryIds,visitLength,stepLength,starttime,返回Tuple2<Long, String>(userid, partAggrInfo);)
查询用户数据并且映射成<userid,Row>的格式
SQL查询出来转化为 DF再转化为userInfoRDD,最后maptopair返回Tuple2<Long, Row>(row.getLong(0), row);
将session粒度聚合数据,与用户信息进行join
JavaPairRDD<Long, Tuple2<String, Row>> userid2FullInfoRDD =
userid2PartAggrInfoRDD.join(userid2InfoRDD);
对join起来的数据进行拼接,并且返回<sessionid,fullAggrInfo>格式的数据
sessionid2FullAggrInfoRDD : 从本来的partAggrInfo里抽出sessionID。 对之前聚合userInfo当中的age,professional,sex,city等信息与partAggrInfo进行拼接,组成fullAgggrInfo。 返回Tuple2<String, String>(sessionid, fullAggrInfo);
过滤session数据,并进行聚合统计
JavaPairRDD<String, String> filterSessionAndAggrStat:截取 sessionid2AggrInfoRDD
当中的日期,年纪,职业,性别,搜索关键词,商品品类等字段,拼接成paramter
根据筛选参数进行过滤
JavaPairRDD<String, String> filteredSessionid2AggrInfoRDD = sessionid2AggrInfoRDD.filter:先从tuple中截取聚合数据,接着依据年龄,职业,城市,搜索词等进行筛选划分,在经过多个过滤条件之后,说明这个session就是需要技术的session,用自定义的Accumulator进行计数,同时利用它来计算步长和时长。 最后返回 filteredSessionid2AggrInfoRDD
获取通过筛选条件的session的访问明细数据RDD
JavaPairRDD<String, Row> sessionid2detailRDD = sessionid2aggrInfoRDD
.join(sessionid2actionRDD)
.mapToPair
返回sessionid2detailRDD
随机抽取session模块
第一步,计算出每天每小时的session数量
获取<yyyy-MM-dd_HH,aggrInfo>格式的RDD
JavaPairRDD<String, String> time2sessionidRDD = sessionid2AggrInfoRDD.mapToPair
利用DateUtils.getDateHour将aggrInfo当中的start转换成yyyy-MM-dd HH的形式作为dateHour,返回Tuple2<String, String>(dateHour, aggrInfo)
得到每天每小时的session数量
我们就用这个countByKey操作,给大家演示第三种和第四种方案
新建map对之前需要抽取的RDD进行基于日期时间的count分组,Map<String, Object> countMap = time2sessionidRDD.countByKey();
第二步,使用按时间比例随机抽取算法,计算出每天每小时要抽取session的索引
将<yyyy-MM-dd_HH,count>格式的map,转换成<yyyy-MM-dd,<HH,count>>的格式
Map<String, Map<String, Long>> dateHourCountMap =
new HashMap<String, Map<String, Long>>();
实现随机抽取算法;
同样新建一个hashmap,对之前countmap进行迭代,切片和获取date和hour,再分别放入hourcountMap和dateHourcountMap
开始实现我们的按时间比例随机抽取算法
总共要抽取100个session,先按照天数,进行平分
int extractNumberPerDay = 100 / dateHourCountMap.size();
session随机抽取功能
*
* 用到了一个比较大的变量,随机抽取索引map
* 之前是直接在算子里面使用了这个map,那么根据我们刚才讲的这个原理,每个task都会拷贝一份map副本
* 还是比较消耗内存和网络传输性能的
*
* 将map做成广播变量
创建Map<String, Map<String, List<Integer>>> dateHourExtractMap =
new HashMap<String, Map<String, List<Integer>>>(); →通过dateHourCountMap
循环求得一天的session总数→遍历每个小时获取时间和session数→计算每个小时session数量占据当天session数量的比例(如果要抽取的数量大于每小时session数量,则让抽取数量等于每小时数量)→获取当前小时的存放随机数的list→生成根据所需要抽取数量范围内的随机数并进行判重,再将随机数写入list当中
第三步:遍历每天每小时的session,然后根据随机索引进行抽取
// 执行groupByKey算子,得到<dateHour,(session aggrInfo)>
JavaPairRDD<String, Iterable<String>> time2sessionsRDD = time2sessionidRDD.groupByKey(); → 用flatMap算子,遍历所有的<dateHour,(session aggrInfo)>格式的数据→会遍历每天每小时的session→判断当前session的index是否在随机抽取的list当中→将需要抽取的sessionID从聚合信息里拿出来→返回extractSessionids这个RDD包含需要抽取sessionID
第四步:获取抽取出来的session的明细数据
用抽取出来的sessionID去join sessionID对应的明细操作
JavaPairRDD<String, Tuple2<String, Row>> extractSessionDetailRDD =
extractSessionidsRDD.join(sessionid2actionRDD);
再用foreachPartition对聚合后的数据进行封装写入到mysql当中
再计算不同时长步长的session的范围占比同样封装写入mysql当中
top10热门品类模块
获取符合条件的session的访问明细
Sessionid2detailRDD = filterSeesionid2AggrInfoRDD。Join(sessionid2actionRDD)
第一步:获取符合条件的session访问过的所有品类
*/
// 获取session访问过的所有品类id
// 访问过:指的是,点击过、下单过、支付过的品类
JavaPairRDD<Long, Long> categoryidRDD = sessionid2detailRDD.flatMapToPair(
切割聚合信息里的clickCategoryId,orderCategoryIds,payCategoryIds以pairRDD的形式写进list当中。
去重,对categoryID进行去重,如果不去重,排序会对重复的categoryid已经countInfo进行排序,最后可能拿到重复的数据。
第二步:计算各品类的点击、下单和支付的次数
访问明细中,其中三种访问行为是:点击、下单和支付
分别来计算各品类点击、下单和支付的次数,可以先对访问明细数据进行过滤
分别过滤出点击、下单和支付行为,然后通过map、reduceByKey等算子来进行计算
// 计算各个品类的点击次数
JavaPairRDD<Long, Long> clickCategoryId2CountRDD =
getClickCategoryId2CountRDD(sessionid2detailRDD);
先对是否有进行点击行为进行过滤,再封装成Tuple2<Long, Long>(clickCategoryId, 1L);
形式的RDD进行reducebykey的计算
// 计算各个品类的下单次数
JavaPairRDD<Long, Long> orderCategoryId2CountRDD =
getOrderCategoryId2CountRDD(sessionid2detailRDD);
先对是否有进行点击行为进行过滤,再封装成Tuple2<Long, Long>(Long.valueOf(orderCategoryId), 1L)
形式的RDD进行reducebykey的计算
// 计算各个品类的支付次数
JavaPairRDD<Long, Long> payCategoryId2CountRDD =
getPayCategoryId2CountRDD(sessionid2detailRDD);
先对是否有进行点击行为进行过滤,再封装成 Tuple2<Long, Long>(Long.valueOf(payCategoryId), 1L)形式的RDD进行reducebykey的计算
第三步:join各品类与它的点击、下单和支付的次数
*
* categoryidRDD中,是包含了所有的符合条件的session,访问过的品类id
*
* 上面分别计算出来的三份,各品类的点击、下单和支付的次数,可能不是包含所有品类的
* 比如,有的品类,就只是被点击过,但是没有人下单和支付
*
* 所以,这里,就不能使用join操作,要使用leftOuterJoin操作,就是说,如果categoryidRDD不能
* join到自己的某个数据,比如点击、或下单、或支付次数,那么该categoryidRDD还是要保留下来的
* 只不过,没有join到的那个数据,就是0了
JavaPairRDD<Long, String> categoryid2countRDD = joinCategoryAndData(
categoryidRDD, clickCategoryId2CountRDD, orderCategoryId2CountRDD,
payCategoryId2CountRDD);
通过leftouterjoin来实现每个categoryID对应的点击数、订单数和付款数拼接到一起。
第四步:封装你要进行排序算法需要的几个字段:点击次数、下单次数和支付次数
* 实现Ordered接口要求的几个方法
*
* 跟其他key相比,如何来判定大于、大于等于、小于、小于等于
*
* 依次使用三个次数进行比较,如果某一个相等,那么就比较下一个
*
* (自定义的二次排序key,必须要实现Serializable接口,表明是可以序列化的,负责会报错)
重写caompare和compareTO方法 利用相减来确定排序
compare(Object o1,Object o2)方法是java.util.Comparator<T>接口的方法,它实际上用的是待比较对象的compareTo(Object o)方法。
compareTo(Object o)方法是java.lang.Comparable<T>接口中的方法,当需要对某个类的对象进行排序时,该类需要实现Comparable<T>接口的,必须重写public int compareTo(T o)方法。
第五步:将数据映射成<CategorySortKey,info>格式的RDD,然后进行二次排序(降序)
sortKey2countRDD = categoryid2countRDD.mapToPair(
将需要排序的点击数和订单数CategorySortKey sortKey = new CategorySortKey(clickCount,orderCount, payCount); 封装排序key,再倒序排序取出top10
取出前10个写入mysql(take10)
Top10活跃session
第一步:将top10热门品类的id,生成一份RDD
-
第二步:计算top10品类被各session点击的次数
*/
JavaPairRDD<String, Iterable<Row>> sessionid2detailsRDD =
sessionid2detailRDD.groupByKey();
// 计算出该session,对每个品类的点击次数
// 返回结果,<categoryid,sessionid,count>格式
list.add(new Tuple2<Long, String>(categoryid, value))
获取到to10热门品类,被各个session点击的次数
JavaPairRDD<Long, String> top10CategorySessionCountRDD = top10CategoryIdRDD
.join(categoryid2sessionCountRDD)
第三步:分组取TopN算法实现,获取每个品类的top10活跃用户
*/
JavaPairRDD<Long, Iterable<String>> top10CategorySessionCountsRDD =
top10CategorySessionCountRDD.groupByKey();
定义top10排序数组,通过冒泡排序比较写入到mysql当中
第四步:获取top10活跃session的明细数据,并写入MySQL
*/
JavaPairRDD<String, Tuple2<String, Row>> sessionDetailRDD =
top10SessionRDD.join(sessionid2detailRDD);
页面转化率计算模块:
第一步:查询指定日期范围内的用户访问行为数据
JavaRDD<Row> actionRDD = SparkUtils.getActionRDDByDateRange(
sqlContext, taskParam);
通过SQL查询本地mysql日期范围内用户的行为数据,转化为DF在转为RDD。
第二步:将查询出来的用户访问数据映射成一个RDD
// 对用户访问行为数据做一个映射,将其映射为<sessionid,访问行为>的格式
// 咱们的用户访问页面切片的生成,是要基于每个session的访问数据,来进行生成的
JavaPairRDD<String, Row> sessionid2actionRDD = getSessionid2actionRDD(actionRDD);
sessionid2actionRDD = sessionid2actionRDD.cache(); // persist(StorageLevel.MEMORY_ONL
private static JavaPairRDD<String, Row> getSessionid2actionRDD(
JavaRDD<Row> actionRDD) {
return actionRDD.mapToPair(new PairFunction<Row, String, Row>() {
private static final long serialVersionUID = 1L;
@Override
public Tuple2<String, Row> call(Row row) throws Exception {
String sessionid = row.getString(2);
return new Tuple2<String, Row>(sessionid, row);
}
});
}
第三步:对<sessionid,访问行为> RDD,做一次groupByKey操作
// 因为我们要拿到每个session对应的访问行为数据,才能够去生成切片
JavaPairRDD<String, Iterable<Row>> sessionid2actionsRDD = sessionid2actionRDD.groupByKey();
第四步:最核心的一步,每个session的单跳页面切片的生成,以及页面流的匹配,算法
JavaPairRDD<String, Integer> pageSplitRDD = generateAndMatchPageSplit(
sc, sessionid2actionsRDD, taskParam);
Map<String, Object> pageSplitPvMap = pageSplitRDD.countByKey();
通过对session访问行为的迭代和对传入页面流参数的切割,同时对Collections.sort
的comparator进行override,实现对页面切片的时间升序排序(利用工具类对row当中的时间进行转化再相减)
通过对row的切割对pageid和lastpageID进行匹配,字段拼接形成切片,通过for循环对用户的页面流进行匹配结果封装成 list.add(new Tuple2<String, Integer>(pageSplit, 1));
第五步:获取起始页面的PV
long startPagePv = getStartPagePv(taskParam, sessionid2actionsRDD);
通过对传入参数的截取,让当前pageID和各个切片的起始startPageID进行匹配,再进行countRDD获得次数。
第六步:计算目标页面流的各个页面切片的转化率
Map<String, Double> convertRateMap = computePageSplitConvertRate(
taskParam, pageSplitPvMap, startPagePv);
通过for循环,获取目标页面流中的各个页面切片(pv)
如果是i=1,说明是起始页面和目标页面之间的转化率,否则是目标页面与上一页面切片之间的转化率。转化率结果写进convertRateMap.put(targetPageSplit, convertRate)中,double类型保留两位小数; lastPageSplitPv = targetPageSplitPv;
第七步:持久化页面切片转化率
persistConvertRate(taskid, convertRateMap);
} 做一些字段拼接处理,写入mysql
区域产品TOP3
// 查询用户指定日期范围内的点击行为数据(city_id,在哪个城市发生的点击行为)
// 技术点1:Hive数据源的使用
JavaPairRDD<Long, Row> cityid2clickActionRDD = getcityid2ClickActionRDDByDate(
sqlContext, startDate, endDate);
String sql =
"SELECT "
+ "city_id,"
+ "click_product_id product_id "
+ "FROM user_visit_action "
+ "WHERE click_product_id IS NOT NULL "
+ "AND date>='" + startDate + "' "
+ "AND date<='" + endDate + "'";
return new Tuple2<Long, Row>(cityid, row);
区域热门商品TOP3模块:
第一步:查询指定日期范围内的点击行为数据
(Hive数据源的使用,如果在本地运行,SQLcontext从mysql拿数据,在生产环境运行,就要从Hive拿数据)
private static JavaPairRDD<Long, Row> getcityid2ClickActionRDDByDate(
SQLContext sqlContext, String startDate, String endDate) {
// 从user_visit_action中,查询用户访问行为数据
// 第一个限定:click_product_id,限定为不为空的访问行为,那么就代表着点击行为
// 第二个限定:在用户指定的日期范围内的数据
String sql =
"SELECT "
+ "city_id,"
+ "click_product_id product_id "
+ "FROM user_visit_action "
+ "WHERE click_product_id IS NOT NULL "
+ "AND date>='" + startDate + "' "
+ "AND date<='" + endDate + "'";
第二步:从MySQL中查询城市信息
// 技术点2:异构数据源之MySQL的使用
JavaPairRDD<Long, Row> cityid2cityInfoRDD = getcityid2CityInfoRDD(sqlContext);
// 构建MySQL连接配置信息(直接从配置文件中获取)
String url = null;
String user = null;
String password = null;
boolean local = ConfigurationManager.getBoolean(Constants.SPARK_LOCAL);
if(local) {
url = ConfigurationManager.getProperty(Constants.JDBC_URL);
user = ConfigurationManager.getProperty(Constants.JDBC_USER);
password = ConfigurationManager.getProperty(Constants.JDBC_PASSWORD);
} else {
url = ConfigurationManager.getProperty(Constants.JDBC_URL_PROD);
user = ConfigurationManager.getProperty(Constants.JDBC_USER_PROD);
password = ConfigurationManager.getProperty(Constants.JDBC_PASSWORD_PROD);
}
用HashMap的方式去存储mysql参数
Map<String, String> options = new HashMap<String, String>();
options.put("url", url);
options.put("dbtable", "city_info");
options.put("user", user);
options.put("password", password);
// 通过SQLContext去从MySQL中查询数据
DataFrame cityInfoDF = sqlContext.read().format("jdbc")
.options(options).load();
再转化成RDD Tuple2<Long, Row>(cityid, row)
第三步:生成点击商品基础信息临时表
// 技术点3:将RDD转换为DataFrame,并注册临时表
generateTempClickProductBasicTable(sqlContext,
cityid2clickActionRDD, cityid2cityInfoRDD);
第四步:生成各区域各商品点击次数的临时表
generateTempAreaPrdocutClickCountTable(sqlContext);
JavaPairRDD<Long, Tuple2<Row, Row>> joinedRDD =
cityid2clickActionRDD.join(cityid2cityInfoRDD);
// 将上面的JavaPairRDD,转换成一个JavaRDD<Row>(才能将RDD转换为DataFrame)
利用StructType schema来映射
JavaRDD<Row> mappedRDD = joinedRDD.map(
List<StructField> structFields = new ArrayList<StructField>();
structFields.add(DataTypes.createStructField("city_id", DataTypes.LongType, true));
structFields.add(DataTypes.createStructField("city_name", DataTypes.StringType, true));
structFields.add(DataTypes.createStructField("area", DataTypes.StringType, true));
structFields.add(DataTypes.createStructField("product_id", DataTypes.LongType, true));
StructType schema = DataTypes.createStructType(structFields);
DataFrame df = sqlContext.createDataFrame(mappedRDD, schema);
第五步:生成包含完整商品信息的各区域各商品点击次数的临时表
generateTempAreaFullProductClickCountTable(sqlContext);
两个自定义函数
UDF:concat2(),将两个字段拼接起来,用指定的分隔符
@Override
public String call(Long v1, String v2, String split) throws Exception {
return String.valueOf(v1) + split + v2;
}
UDAF:group_concat_distinct(),将一个分组中的多个字段值,用逗号拼接起来,同时进行去重
对传进来的城市字段进行去重判定和拼接以及合并
第六步:使用开窗函数获取各个区域内点击次数排名前3的热门商品
(1.6的spark只能在生产环境使用开窗函数,本地模式下跑不起来,2.0就都可以用了)
JavaRDD<Row> areaTop3ProductRDD = getAreaTop3ProductRDD(sqlContext);
System.out.println("areaTop3ProductRDD: " + areaTop3ProductRDD.count());
生成各区域各商品点击次数临时表
String sql =
"SELECT "
+ "area,"
+ "product_id,"
+ "count(*) click_count, "
+ "group_concat_distinct(concat_long_string(city_id,city_name,':')) city_infos "
+ "FROM tmp_click_product_basic "
+ "GROUP BY area,product_id ";
生成区域商品点击次数临时表(包含了商品的完整信息)
private static void generateTempAreaFullProductClickCountTable(SQLContext sqlContext) {
// 将之前得到的各区域各商品点击次数表,product_id
// 去关联商品信息表,product_id,product_name和product_status
// product_status要特殊处理,0,1,分别代表了自营和第三方的商品,放在了一个json串里面
// get_json_object()函数,可以从json串中获取指定的字段的值
// if()函数,判断,如果product_status是0,那么就是自营商品;如果是1,那么就是第三方商品
// area, product_id, click_count, city_infos, product_name, product_status
计算出来商品经营类型
// 你拿到到了某个区域top3热门的商品,那么其实这个商品是自营的,还是第三方的
// 其实是很重要的一件事
// 技术点:内置if函数的使用
String sql =
"SELECT "
+ "tapcc.area,"
+ "tapcc.product_id,"
+ "tapcc.click_count,"
+ "tapcc.city_infos,"
+ "pi.product_name,"
+ "if(get_json_object(pi.extend_info,'product_status')='0','Self','Third Party') product_status "
+ "FROM tmp_area_product_click_count tapcc "
+ "JOIN product_info pi ON tapcc.product_id=pi.product_id ";
CASE WHEN 对城市区域打等级
"CASE "
+ "WHEN area='China North' OR area='China East' THEN 'A Level' "
+ "WHEN area='China South' OR area='China Middle' THEN 'B Level' "
+ "WHEN area='West North' OR area='West South' THEN 'C Level' "
开窗函数rank row_number desen_rank的区别
1.ROW_NUMBER()
定义:ROW_NUMBER()函数作用就是将select查询到的数据进行排序,每一条数据加一个序号,他不能用做于学生成绩的排名,一般多用于分页查询。
2.RANK()
定义:RANK()函数,顾名思义排名函数,可以对某一个字段进行排名,这里为什么和ROW_NUMBER()不一样那,ROW_NUMBER()是排序,当存在相同成绩的学生时,ROW_NUMBER()会依次进行排序,他们序号不相同,而Rank()则出现相同的排名。
3、
DENSE_RANK()密集的排名他和RANK()区别在于,排名的连续性,DENSE_RANK()排名是连续的,RANK()是跳跃的排名,所以一般情况下用的排名函数就是RANK()。
写入mysql
List<Row> rows = areaTop3ProductRDD.collect();
System.out.println("rows: " + rows.size());
persistAreaTop3Product(taskid, rows);
实时广告黑名单流量计算:
动态黑名单生成:
获取原始数据:
// 一条一条的实时日志
// timestamp province city userid adid
// 某个时间点 某个省份 某个城市 某个用户 某个广告
// 计算出每5个秒内的数据中,每天每个用户每个广告的点击量
// 通过对原始实时日志的处理
// 将日志的格式处理成<yyyyMMdd_userid_adid, 1L>格式
从原始日志中,截取日期(timestamp)和userid、aid拼接成key
String key = datekey + "_" + userid + "_" + adid;
return new Tuple2<String, Long>(key, 1L);
// 针对处理后的日志格式,执行reduceByKey算子即可
// (每个batch中)每天每个用户对每个广告的点击量
<yyyyMMdd_userid_adid, clickCount>
JavaPairDStream<String, Long> dailyUserAdClickCountDStream = dailyUserAdClickDStream.reduceByKey(
new Function2<Long, Long, Long>() {
private static final long serialVersionUID = 1L;
@Override
public Long call(Long v1, Long v2) throws Exception {
return v1 + v2;
}
});
// }, 1000);
分区写入MYSQL当中
每个5s的batch中,当天每个用户对每支广告的点击次数
// <yyyyMMdd_userid_adid, clickCount>
dailyUserAdClickCountDStream.foreachRDD(new Function<JavaPairRDD<String,Long>, Void>() {
private static final long serialVersionUID = 1L;
@Override
public Void call(JavaPairRDD<String, Long> rdd) throws Exception {
对大于100次点击的用户进行拉黑
现在我们在mysql里面,已经有了累计的每天各用户对各广告的点击量
// 遍历每个batch中的所有记录,对每条记录都要去查询一下,这一天这个用户对这个广告的累计点击量是多少
// 从mysql中查询
// 查询出来的结果,如果是100,如果你发现某个用户某天对某个广告的点击量已经大于等于100了
// 那么就判定这个用户就是黑名单用户,就写入mysql的表中,持久化
JavaPairDStream<String, Long> blacklistDStream = dailyUserAdClickCountDStream.filter(
new Function<Tuple2<String,Long>, Boolean>() {
private static final long serialVersionUID = 1L;
@Override
public Boolean call(Tuple2<String, Long> tuple)
throws Exception {
String key = tuple._1;
String[] keySplited = key.split("_");
// yyyyMMdd -> yyyy-MM-dd
String date = DateUtils.formatDate(DateUtils.parseDateKey(keySplited[0]));
long userid = Long.valueOf(keySplited[1]);
long adid = Long.valueOf(keySplited[2]);
// 从mysql中查询指定日期指定用户对指定广告的点击量
IAdUserClickCountDAO adUserClickCountDAO = DAOFactory.getAdUserClickCountDAO();
int clickCount = adUserClickCountDAO.findClickCountByMultiKey(
date, userid, adid);
// 判断,如果点击量大于等于100,ok,那么不好意思,你就是黑名单用户
// 那么就拉入黑名单,返回true
if(clickCount >= 100) {
return true;
}
// 反之,如果点击量小于100的,那么就暂时不要管它了
return false;
}
});
对过滤了黑名单用户的blacklistDStream,需要进行一次全局去重(DS→RDD.Distinct)
// 里面的每个batch,其实就是都是过滤出来的已经在某天对某个广告点击量超过100的用户
// 遍历这个dstream中的每个rdd,然后将黑名单用户增加到mysql中
JavaDStream<Long>
blacklistUseridDStream = blacklistDStream.map(
new Function<Tuple2<String,Long>, Long>() {
private static final long serialVersionUID = 1L;
@Override
public Long call(Tuple2<String, Long> tuple) throws Exception {
String key = tuple._1;
String[] keySplited = key.split("_");
Long userid = Long.valueOf(keySplited[1]);
return userid;
}
});
JavaDStream<Long> distinctBlacklistUseridDStream = blacklistUseridDStream.transform(
new Function<JavaRDD<Long>, JavaRDD<Long>>() {
private static final long serialVersionUID = 1L;
@Override
public JavaRDD<Long> call(JavaRDD<Long> rdd) throws Exception {
return rdd.distinct();
}
});
将上述黑名单写入MYSQL当中,完成动态黑名单
根据动态黑名单对数据进行过滤
// 刚刚接受到原始的用户点击行为日志之后
// 根据mysql中的动态黑名单,进行实时的黑名单过滤(黑名单用户的点击行为,直接过滤掉,不要了)
// 使用transform算子(将dstream中的每个batch RDD进行处理,转换为任意的其他RDD,功能很强大)
// 首先,从mysql中查询所有黑名单用户,将其转换为一个rdd
IAdBlacklistDAO adBlacklistDAO = DAOFactory.getAdBlacklistDAO();
List<AdBlacklist> adBlacklists = adBlacklistDAO.findAll();
List<Tuple2<Long, Boolean>> tuples = new ArrayList<Tuple2<Long, Boolean>>();
for(AdBlacklist adBlacklist : adBlacklists) {
tuples.add(new Tuple2<Long, Boolean>(adBlacklist.getUserid(), true));
}
//生成pairRDD
JavaPairRDD<Long, Boolean> blacklistRDD = sc.parallelizePairs(tuples);
return new Tuple2<Long, Tuple2<String, String>>(userid, tuple);
// 将原始日志数据rdd,与黑名单rdd,进行左外连接
// 如果说原始日志的userid,没有在对应的黑名单中,join不到,左外连接
// 用inner join,内连接,会导致数据丢失
JavaPairRDD<Long, Tuple2<Tuple2<String, String>, Optional<Boolean>>> joinedRDD =
mappedRDD.leftOuterJoin(blacklistRDD);
JavaPairRDD<Long, Tuple2<Tuple2<String, String>, Optional<Boolean>>> filteredRDD = joinedRDD.filter(
new Function<Tuple2<Long,Tuple2<Tuple2<String,String>,Optional<Boolean>>>, Boolean>() {
Optional<Boolean> optional = tuple._2._2;
// 如果这个值存在,那么说明原始日志中的userid,join到了某个黑名单用户
if(optional.isPresent() && optional.get()) {
return false; // false表示过滤
}
return true; //表示非黑名单用户忽略
再返回结果RDD
计算广告点击流量实时统计
获取黑名单过滤后的数据
private static JavaPairDStream<String, Long> calculateRealTimeStat(
JavaPairDStream<String, String> filteredAdRealTimeLogDStream) {
封装key和tuple
String key = datekey + "_" + province + "_" + city + "_" + adid;
return new Tuple2<String, Long>(key, 1L);
用updatebykey算子更新状态
JavaPairDStream<String, Long> aggregatedDStream = mappedDStream.updateStateByKey(
同步状态判断
首先根据optional判断,之前这个key,是否有对应的状态
long clickCount = 0L;
如果说,之前是存在这个状态的,那么就以之前的状态作为起点,进行值的累加
if(optional.isPresent()) {
clickCount = optional.get();
}
values,代表了,batch rdd中,每个key对应的所有的值
for(Long value : values) {
clickCount += value;
}
return Optional.of(clickCount);
计算的结果分区写到MYSQL当中去
aggregatedDStream.foreachRDD(new Function<JavaPairRDD<String,Long>, Void>() {
private static final long serialVersionUID = 1L;
@Override
public Void call(JavaPairRDD<String, Long> rdd) throws Exception {
rdd.foreachPartition(new VoidFunction<Iterator<Tuple2<String,Long>>>() {
private static final long serialVersionUID = 1L;
@Override
public void call(Iterator<Tuple2<String, Long>> iterator)
throws Exception {
计算每天各省份的top3热门广告
获取过滤数据
JavaDStream<Row> rowsDStream = adRealTimeStatDStream.transform(
String key = date + "_" + province + "_" + adid;
return new Tuple2<String, Long>(key, clickCount);
}
});
计算点击量
JavaPairRDD<String, Long> dailyAdClickCountByProvinceRDD = mappedRDD.reduceByKey(
new Function2<Long, Long, Long>() {
private static final long serialVersionUID = 1L;
@Override
public Long call(Long v1, Long v2)
throws Exception {
return v1 + v2;
}
});
使用Spark SQL,通过开窗函数,获取到各省份的top3热门广告
将dailyAdClickCountByProvinceRDD转换为DataFrame
注册为一张临时表
映射成DF
StructType schema = DataTypes.createStructType(Arrays.asList(
DataTypes.createStructField("date", DataTypes.StringType, true),
DataTypes.createStructField("province", DataTypes.StringType, true),
DataTypes.createStructField("ad_id", DataTypes.LongType, true),
DataTypes.createStructField("click_count", DataTypes.LongType, true)));
HiveContext sqlContext = new HiveContext(rdd.context());
DataFrame dailyAdClickCountByProvinceDF = sqlContext.createDataFrame(rowsRDD, schema);
开窗函数查询前三:
"SELECT "
+ "date,"
+ "province,"
+ "ad_id,"
+ "click_count "
+ "FROM ( "
+ "SELECT "
+ "date,"
+ "province,"
+ "ad_id,"
+ "click_count,"
+ "ROW_NUMBER() OVER(PARTITION BY province ORDER BY click_count DESC) rank "
+ "FROM tmp_daily_ad_click_count_by_prov "
+ ") t "
+ "WHERE rank>=3"
// rowsDStream
// 每次都是刷新出来各个省份最热门的top3广告
// 将其中的数据批量更新到MySQL中
计算最近1小时滑动窗口内的广告点击趋势
获取数据
private static void calculateAdClickCountByWindow(
JavaPairInputDStream<String, String> adRealTimeLogDStream) {
// 映射成<yyyyMMddHHMM_adid,1L>格式
return new Tuple2<String, Long>(timeMinute + "_" + adid, 1L);
// 过来的每个batch rdd,都会被映射成<yyyyMMddHHMM_adid,1L>的格式
// 每次出来一个新的batch,都要获取最近1小时内的所有的batch
// 然后根据key进行reduceByKey操作,统计出来最近一小时内的各分钟各广告的点击次数
JavaPairDStream<String, Long> aggrRDD = pairDStream.reduceByKeyAndWindow(
new Function2<Long, Long, Long>() {
private static final long serialVersionUID = 1L;
@Override
public Long call(Long v1, Long v2) throws Exception {
return v1 + v2;
}
}, Durations.minutes(60), Durations.seconds(10));
分区写入到MYSQL
aggrRDD.foreachRDD(new Function<JavaPairRDD<String,Long>, Void>() {
private static final long serialVersionUID = 1L;
@Override
public Void call(JavaPairRDD<String, Long> rdd) throws Exception {
rdd.foreachPartition(new VoidFunction<Iterator<Tuple2<String,Long>>>() {
while(iterator.hasNext()) {
Tuple2<String, Long> tuple = iterator.next();
String[] keySplited = tuple._1.split("_");
// yyyyMMddHHmm
String dateMinute = keySplited[0];
long adid = Long.valueOf(keySplited[1]);
long clickCount = tuple._2;
String date = DateUtils.formatDate(DateUtils.parseDateKey(
dateMinute.substring(0, 8)));
String hour = dateMinute.substring(8, 10);
String minute = dateMinute.substring(10);
实时计算实现高可靠:
1、updateStateByKey、window等有状态的操作,自动进行checkpoint,必须设置checkpoint目录
checkpoint目录:容错的文件系统的目录,比如说,常用的是HDFS
SparkStreaming.checkpoint("hdfs://192.168.1.105:9090/checkpoint")
设置完这个基本的checkpoint目录之后,有些会自动进行checkpoint操作的DStream,就实现了HA高可用性;checkpoint,相当于是会把数据保留一份在容错的文件系统中,一旦内存中的数据丢失掉;那么就可以直接从文件系统中读取数据;不需要重新进行计算
2、Driver高可用性
第一次在创建和启动StreamingContext的时候,那么将持续不断地将实时计算程序的元数据(比如说,有些dstream或者job执行到了哪个步骤),如果后面,不幸,因为某些原因导致driver节点挂掉了;那么可以让spark集群帮助我们自动重启driver,然后继续运行时候计算程序,并且是接着之前的作业继续执行;没有中断,没有数据丢失
第一次在创建和启动StreamingContext的时候,将元数据写入容错的文件系统(比如hdfs);spark-submit脚本中加一些参数;保证在driver挂掉之后,spark集群可以自己将driver重新启动起来;而且driver在启动的时候,不会重新创建一个streaming context,而是从容错文件系统(比如hdfs)中读取之前的元数据信息,包括job的执行进度,继续接着之前的进度,继续执行。
使用这种机制,就必须使用cluster模式提交,确保driver运行在某个worker上面;但是这种模式不方便我们调试程序,一会儿还要最终测试整个程序的运行,打印不出log;我们这里仅仅是用我们的代码给大家示范一下:
JavaStreamingContextFactory contextFactory = new JavaStreamingContextFactory() {
@Override
public JavaStreamingContext create() {
JavaStreamingContext jssc = new JavaStreamingContext(...);
JavaDStream<String> lines = jssc.socketTextStream(...);
jssc.checkpoint(checkpointDirectory);
return jssc;
}
};
JavaStreamingContext context = JavaStreamingContext.getOrCreate(checkpointDirectory, contextFactory);
context.start();
context.awaitTermination();
spark-submit
--deploy-mode cluster
--supervise
3、实现RDD高可用性:启动WAL预写日志机制
spark streaming,从原理上来说,是通过receiver来进行数据接收的;接收到的数据,会被划分成一个一个的block;block会被组合成一个batch;针对一个batch,会创建一个rdd;启动一个job来执行我们定义的算子操作。
checkpoint目录中的,一份磁盘文件中去;作为数据的冗余副本。
无论你的程序怎么挂掉,或者是数据丢失,那么数据都不肯能会永久性的丢失;因为肯定有副本。
4、WAL(Write-Ahead Log)预写日志机制
spark.streaming.receiver.writeAheadLog.enable true
实时计算模块的调优:
1、并行化数据接收:处理多个topic的数据时比较有效
int numStreams = 5;
List<JavaPairDStream<String, String>> kafkaStreams = new ArrayList<JavaPairDStream<String, String>>(numStreams);
for (int i = 0; i < numStreams; i++) {
kafkaStreams.add(KafkaUtils.createStream(...)); //针对每一个topic创建一个sparkstreaming
}
JavaPairDStream<String, String> unifiedStream = streamingContext.union(kafkaStreams.get(0), kafkaStreams.subList(1, kafkaStreams.size()));
//使用union合并把多个sparkstreaming合并起来处理
unifiedStream.print();
2、spark.streaming.blockInterval:增加block数量,增加每个batch rdd的partition数量,增加处理并行度
receiver从数据源源源不断地获取到数据;首先是会按照block interval,将指定时间间隔的数据,收集为一个block;默认时间是200ms,官方推荐不要小于50ms;接着呢,会将指定batch interval时间间隔内的block,合并为一个batch;创建为一个rdd,然后启动一个job,去处理这个batch rdd中的数据
batch rdd,它的partition数量是多少呢?一个batch有多少个block,就有多少个partition;就意味着并行度是多少;就意味着每个batch rdd有多少个task会并行计算和处理。
当然是希望可以比默认的task数量和并行度再多一些了;可以手动调节block interval;减少block interval;每个batch可以包含更多的block;有更多的partition;也就有更多的task并行处理每个batch rdd。
.set("spark.streaming.blockInterval", "50");
定死了,初始的rdd过来,直接就是固定的partition数量了
3、inputStream.repartition(<number of partitions>):重分区,增加每个batch rdd的partition数量
有些时候,希望对某些dstream中的rdd进行定制化的分区
对dstream中的rdd进行重分区,去重分区成指定数量的分区,这样也可以提高指定dstream的rdd的计算并行度
4、调节并行度
spark.default.parallelism
reduceByKey(numPartitions)
5、使用Kryo序列化机制:
spark streaming,也是有不少序列化的场景的
提高序列化task发送到executor上执行的性能,如果task很多的时候,task序列化和反序列化的性能开销也比较可观
默认输入数据的存储级别是StorageLevel.MEMORY_AND_DISK_SER_2,receiver接收到数据,默认就会进行持久化操作;首先序列化数据,存储到内存中;如果内存资源不够大,那么就写入磁盘;而且,还会写一份冗余副本到其他executor的block manager中,进行数据冗余。
.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer");
6、batch interval:每个的处理时间必须小于batch interval
实际上你的spark streaming跑起来以后,其实都是可以在spark ui上观察它的运行情况的;可以看到batch的处理时间;
如果发现batch的处理时间大于batch interval,就必须调节batch interval
尽量不要让batch处理时间大于batch interval
比如你的batch每隔5秒生成一次;你的batch处理时间要达到6秒;就会出现,batch在你的内存中日积月累,一直囤积着,没法及时计算掉,释放内存空间;而且对内存空间的占用越来越大,那么此时会导致内存空间快速消耗
如果发现batch处理时间比batch interval要大,就尽量将batch interval调节大一些
技术难点:
自定义Accumulator
Session信息聚合模块:
这里的实现思路是,我们自己自定义一个Accumulator,实现较为复杂的计算逻辑,一个Accumulator维护了所有范围区间的数量的统计逻辑
低耦合,如果说,session数量计算逻辑要改变,那么不用变更session遍历的相关的代码;只要维护一个Accumulator里面的代码即可;不用借助一些其他的分布式或者锁维护中间状态。
对步长时长进行初始化:
ublic String zero(String v) {
return Constants.SESSION_COUNT + "=0|"
+ Constants.TIME_PERIOD_1s_3s + "=0|"
+ Constants.TIME_PERIOD_4s_6s + "=0|"
+ Constants.TIME_PERIOD_7s_9s + "=0|"
+ Constants.TIME_PERIOD_10s_30s + "=0|"
+ Constants.TIME_PERIOD_30s_60s + "=0|"
+ Constants.TIME_PERIOD_1m_3m + "=0|"
+ Constants.TIME_PERIOD_3m_10m + "=0|"
+ Constants.TIME_PERIOD_10m_30m + "=0|"
+ Constants.TIME_PERIOD_30m + "=0|"
+ Constants.STEP_PERIOD_1_3 + "=0|"
+ Constants.STEP_PERIOD_4_6 + "=0|"
+ Constants.STEP_PERIOD_7_9 + "=0|"
+ Constants.STEP_PERIOD_10_30 + "=0|"
+ Constants.STEP_PERIOD_30_60 + "=0|"
+ Constants.STEP_PERIOD_60 + "=0";
/**
* addInPlace和addAccumulator
* 可以理解为是一样的
*
* 这两个方法,其实主要就是实现,v1可能就是我们初始化的那个连接串
* v2,就是我们在遍历session的时候,判断出某个session对应的区间,然后会用Constants.TIME_PERIOD_1s_3s
* 所以,我们,要做的事情就是
* 在v1中,找到v2对应的value,累加1,然后再更新回连接串里面去
*
*/
@Override
public String addInPlace(String v1, String v2) {
return add(v1, v2);
}
@Override
public String addAccumulator(String v1, String v2) {
return add(v1, v2);
}
/**
* session统计计算逻辑
* @param v1 连接串
* @param v2 范围区间
* @return 更新以后的连接串
*/
private String add(String v1, String v2) {
// 校验:v1为空的话,直接返回v2
if(StringUtils.isEmpty(v1)) {
return v2;
}
// 使用StringUtils工具类,从v1中,提取v2对应的值,并累加1
String oldValue = StringUtils.getFieldFromConcatString(v1, "\\|", v2);
if(oldValue != null) {
// 将范围区间原有的值,累加1
int newValue = Integer.valueOf(oldValue) + 1;
// 使用StringUtils工具类,将v1中,v2对应的值,设置成新的累加后的值
return StringUtils.setFieldInConcatString(v1, "\\|", v2, String.valueOf(newValue));
}
return v1;
}
}
广播变量的使用
这种默认的,task执行的算子中,使用了外部的变量,每个task都会获取一份变量的副本,MAP会通过网络去传输到各个task当中去,是要占内存的。不必要的内存消耗和占用。
map,本身是不小,存放数据的一个单位是Entry,还有可能会用链表的格式的来存放Entry链条。所以map是比较消耗内存的数据格式。
比如,map是1M。总共,你前面调优都调的特好,资源给的到位,配合着资源,并行度调节的绝对到位,1000个task。大量task的确都在并行运行。
你的task在创建对象的时候,也许会发现堆内存放不下所有对象,也许就会导致频繁的垃圾回收器的回收,GC。GC的时候,一定是会导致工作线程停止,也就是导致Spark暂停工作那么一点时间。频繁GC的话,对Spark作业的运行的速度会有相当可观的影响。
广播变量的好处,不是每个task一份变量副本,而是变成每个节点的executor才一份副本。这样的话,就可以让变量产生的副本大大减少。
广播变量,初始的时候,就在Drvier上有一份副本。
task在运行的时候,想要使用广播变量中的数据,此时首先会在自己本地的Executor对应的BlockManager中,尝试获取变量副本;如果本地没有,那么就从Driver远程拉取变量副本,并保存在本地的BlockManager中;此后这个executor上的task,都会直接使用本地的BlockManager中的副本。
executor的BlockManager除了从driver上拉取,也可能从其他节点的BlockManager上拉取变量副本,举例越近越好。
优化点:
final Broadcast<Map<String, Map<String, IntList>>> dateHourExtractMapBroadcast =
sc.broadcast(fastutilDateHourExtractMap);
项目总体性能优化 :
分配更多的资源
问题:
1、分配哪些资源?
2、在哪里分配这些资源?
3、为什么多分配了这些资源以后,性能会得到提升?
答案:
1、分配哪些资源?executor、cpu per executor、memory per executor、driver memory
2、在哪里分配这些资源?在我们在生产环境中,提交spark作业时,用的spark-submit shell脚本,里面调整对应的参数
/usr/local/spark/bin/spark-submit \
--class cn.spark.sparktest.core.WordCountCluster \
--num-executors 3 \ 配置executor的数量
--driver-memory 100m \ 配置driver的内存(影响不大)
--executor-memory 100m \ 配置每个executor的内存大小
--executor-cores 3 \ 配置每个executor的cpu core数量
/usr/local/SparkTest-0.0.1-SNAPSHOT-jar-with-dependencies.jar \
3、调节到多大,算是最大呢?
第一种,Spark Standalone,公司集群上,搭建了一套Spark集群,你心里应该清楚每台机器还能够给你使用的,大概有多少内存,多少cpu core;那么,设置的时候,就根据这个实际的情况,去调节每个spark作业的资源分配。比如说你的每台机器能够给你使用4G内存,2个cpu core;20台机器;executor,20;4G内存,2个cpu core,平均每个executor。
第二种,Yarn。资源队列。资源调度。应该去查看,你的spark作业,要提交到的资源队列,大概有多少资源?500G内存,100个cpu core;executor,50;10G内存,2个cpu core,平均每个executor。
一个原则,你能使用的资源有多大,就尽量去调节到最大的大小(executor的数量,几十个到上百个不等;executor内存;executor cpu core)
增加每个executor的内存量。增加了内存量以后,对性能的提升,有两点:
1、如果需要对RDD进行cache,那么更多的内存,就可以缓存更多的数据,将更少的数据写入磁盘,甚至不写入磁盘。减少了磁盘IO。
2、对于shuffle操作,reduce端,会需要内存来存放拉取的数据并进行聚合。如果内存不够,也会写入磁盘。如果给executor分配更多内存以后,就有更少的数据,需要写入磁盘,甚至不需要写入磁盘。减少了磁盘IO,提升了性能。
对于task的执行,可能会创建很多对象。如果内存比较小,可能会频繁导致JVM堆内存满了,然后频繁GC,垃圾回收,minor GC和full GC。(速度很慢)。内存加大以后,带来更少的GC,垃圾回收,避免了速度变慢,速度变快了
增加executor:
如果executor数量比较少,那么,能够并行执行的task数量就比较少,就意味着,我们的Application的并行执行的能力就很弱。
比如有3个executor,每个executor有2个cpu core,那么同时能够并行执行的task,就是6个。6个执行完以后,再换下一批6个task。
增加每个executor的cpu core,也是增加了执行的并行能力。每个executor上面的每一个CPUcore执行一个task。原本20个executor,每个才2个cpu core。能够并行执行的task数量,就是40个task。现在每个executor的cpu core,增加到了5个。能够并行执行的task数量,就是100个task。
调节并行度:
并行度:其实就是指的是,Spark作业中,各个stage的task数量,也就代表了Spark作业的在各个阶段(stage)的并行度。
如果不调节并行度,导致并行度过低,会怎么样?
假设,现在已经在spark-submit脚本里面,给我们的spark作业分配了足够多的资源,比如50个executor,每个executor有10G内存,每个executor有3个cpu core。基本已经达到了集群或者yarn队列的资源上限。
task没有设置,或者设置的很少,比如就设置了,100个task。50个executor,每个executor有3个cpu core,也就是说,你的Application任何一个stage运行的时候,都有总数在150个cpu core,可以并行运行。但是你现在,只有100个task,平均分配一下,每个executor分配到2个task,ok,那么同时在运行的task,只有100个,每个executor只会并行运行2个task。每个executor剩下的一个cpu core,就浪费掉了。
你的资源虽然分配足够了,但是问题是,并行度没有与资源相匹配,导致你分配下去的资源都浪费掉了。
合理的并行度的设置,应该是要设置的足够大,大到可以完全合理的利用你的集群资源;比如上面的例子,总共集群有150个cpu core,可以并行运行150个task。那么就应该将你的Application的并行度,至少设置成150,才能完全有效的利用你的集群资源,让150个task,并行执行;而且task增加到150个以后,即可以同时并行运行,还可以让每个task要处理的数据量变少;比如总共150G的数据要处理,如果是100个task,每个task计算1.5G的数据;现在增加到150个task,可以并行运行,而且每个task主要处理1G的数据就可以。
合理设置并行度,就可以完全充分利用你的集群计算资源,并且减少每个task要处理的数据量,最终,就是提升你的整个Spark作业的性能和运行速度。
1、task数量,至少设置成与Spark application的总cpu core数量相同(最理想情况,比如总共150个cpu core,分配了150个task,一起运行,差不多同一时间运行完毕)
2、官方是推荐,task数量,设置成spark application总cpu core数量的2~3倍,比如150个cpu core,基本要设置task数量为300~500;
实际情况,与理想情况不同的,有些task会运行的快一点,比如50s就完了,有些task,可能会慢一点,要1分半才运行完,所以如果你的task数量,刚好设置的跟cpu core数量相同,可能还是会导致资源的浪费,因为,比如150个task,10个先运行完了,剩余140个还在运行,但是这个时候,有10个cpu core就空闲出来了,就导致了浪费。那如果task数量设置成cpu core总数的2~3倍,那么一个task运行完了以后,另一个task马上可以补上来,就尽量让cpu core不要空闲,同时也是尽量提升spark作业运行的效率和速度,提升性能。
3、如何设置一个Spark Application的并行度?(要在一开始上下文阶段设置)
spark.default.parallelism
SparkConf conf = new SparkConf()
.set("spark.default.parallelism", "500")
RDD重构 :
默认情况下,多次对一个RDD执行算子,去获取不同的RDD;都会对这个RDD以及之前的父RDD,全部重新计算一次;读取HDFS->RDD1->RDD2-RDD4
这种情况,是绝对绝对,一定要避免的,一旦出现一个RDD重复计算的情况,就会导致性能急剧降低。
另外一种情况,从一个RDD到几个不同的RDD,算子和计算逻辑其实是完全一样的,结果因为人为的疏忽,计算了多次,获取到了多个RDD。
第一,RDD架构重构与优化
尽量去复用RDD,差不多的RDD,可以抽取称为一个共同的RDD,供后面的RDD计算时,反复使用。
第二,公共RDD一定要实现持久化
对于要多次计算和使用的公共RDD,一定要进行持久化。
持久化,也就是说,将RDD的数据缓存到内存中/磁盘中,(BlockManager),以后无论对这个RDD做多少次计算,那么都是直接取这个RDD的持久化的数据,比如从内存中或者磁盘中,直接提取一份数据。
第三,持久化,是可以进行序列化的
如果正常将数据持久化在内存中,那么可能会导致内存的占用过大,这样的话,也许,会导致OOM内存溢出。
当纯内存无法支撑公共RDD数据完全存放的时候,就优先考虑,使用序列化的方式在纯内存中存储。将RDD的每个partition的数据,序列化成一个大的字节数组,就一个对象;序列化后,大大减少内存的空间占用。
序列化的方式,唯一的缺点就是,在获取数据的时候,需要反序列化。
如果序列化纯内存方式,还是导致OOM,内存溢出;就只能考虑磁盘的方式,内存+磁盘的普通方式(无序列化)。
内存+磁盘,序列化
第四,为了数据的高可靠性,而且内存充足,可以使用双副本机制,进行持久化
持久化的双副本机制,持久化后的一个副本,因为机器宕机了,副本丢了,就还是得重新计算一次;持久化的每个数据单元,存储一份副本,放在其他节点上面;从而进行容错;一个副本丢了,不用重新计算,还可以使用另外一份副本。
这种方式,仅仅针对你的内存资源极度充足
设置序列化:
set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
使用场景:
算子函数中用到了外部变量,会序列化,使用Kryo
当使用了序列化的持久化级别时,在将每个RDD partition序列化成一个大的字节数组时,就会使用Kryo进一步优化序列化的效率和性能
在进行stage间的task的shuffle操作时,节点与节点之间的task会互相大量通过网络拉取和传输文件,此时,这些数据既然通过网络传输,也是可能要序列化的,就会使用Kryo
使用后的效果:
Kryo序列化机制,一旦启用以后,会生效的几个地方:
1、算子函数中使用到的外部变量
2、持久化RDD时进行序列化,StorageLevel.MEMORY_ONLY_SER
3、shuffle
1、算子函数中使用到的外部变量,使用Kryo以后:优化网络传输的性能,可以优化集群中内存的占用和消耗。
2、持久化RDD,优化内存的占用和消耗;持久化RDD占用的内存越少,task执行的时候,创建的对象,就不至于频繁的占满内存,频繁发生GC。当使用了序列化的持久化级别时,在将每个RDD partition序列化成一个大的字节数组时,就会使用Kryo进一步优化序列化的效率和性能。
3、shuffle:可以优化网络传输的性能。在进行stage间的task的shuffle操作时,节点与节点之间的task会互相大量通过网络拉取和传输文件,此时,这些数据既然通过网络传输,也是可能要序列化的,就会使用Kryo
默认情况下,Spark内部是使用Java的序列化机制,ObjectOutputStream / ObjectInputStream,对象输入输出流机制,来进行序列化
这种默认序列化机制的好处在于,处理起来比较方便;也不需要我们手动去做什么事情,只是,你在算子里面使用的变量,必须是实现Serializable接口的,可序列化即可。
但是缺点在于,默认的序列化机制的效率不高,序列化的速度比较慢;序列化以后的数据,占用的内存空间相对还是比较大。
可以手动进行序列化格式的优化
Spark支持使用Kryo序列化机制。Kryo序列化机制,比默认的Java序列化机制,速度要快,序列化后的数据要更小,大概是Java序列化机制的1/10。
所以Kryo序列化优化以后,可以让网络传输的数据变少;在集群中耗费的内存资源大大减少。
使用方法:
SparkConf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
首先第一步,在SparkConf中设置一个属性,spark.serializer,org.apache.spark.serializer.KryoSerializer类;
Kryo之所以没有被作为默认的序列化类库的原因,就要出现了:主要是因为Kryo要求,如果要达到它的最佳性能的话,那么就一定要注册你自定义的类(比如,你的算子函数中使用到了外部自定义类型的对象变量,这时,就要求必须注册你的类,否则Kryo达不到最佳性能)。
第二步,注册你使用到的,需要通过Kryo序列化的,一些自定义类,SparkConf.registerKryoClasses()
项目中的使用:
.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
.registerKryoClasses(new Class[]{CategorySortKey.class}) →二次排序的自定义key
数据本地化/等待时长:
Spark在Driver上,对Application的每一个stage的task,进行分配之前,都会计算出每个task要计算的是哪个分片数据,RDD的某个partition;Spark的task分配算法,优先,会希望每个task正好分配到它要计算的数据所在的节点,这样的话,就不用在网络间传输数据;
但是呢,,可能task没有机会分配到它的数据所在的节点,为什么呢,可能那个节点的计算资源和计算能力都满了;所以呢,这种时候,通常来说,Spark会等待一段时间,默认情况下是3s钟(不是绝对的,还有很多种情况,对不同的本地化级别,都会去等待),到最后,实在是等待不了了,就会选择一个比较差的本地化级别,比如说,将task分配到靠它要计算的数据所在节点,比较近的一个节点,然后进行计算。
但是对于第二种情况,通常来说,肯定是要发生数据传输,task会通过其所在节点的BlockManager来获取数据,BlockManager发现自己本地没有数据,会通过一个getRemote()方法,通过TransferService(网络数据传输组件)从数据所在节点的BlockManager中,获取数据,通过网络传输回task所在节点。
对于我们来说,当然不希望是类似于第二种情况的了。最好的,当然是task和数据在一个节点上,直接从本地executor的BlockManager中获取数据,纯内存,或者带一点磁盘IO。
PROCESS_LOCAL:进程本地化,代码和数据在同一个进程中,也就是在同一个executor中;计算数据的task由executor执行,数据在executor的BlockManager中;性能最好
NODE_LOCAL:节点本地化,代码和数据在同一个节点中;比如说,数据作为一个HDFS block块,就在节点上,而task在节点上某个executor中运行;或者是,数据和task在一个节点上的不同executor中;数据需要在进程间进行传输
NO_PREF:对于task来说,数据从哪里获取都一样,没有好坏之分
RACK_LOCAL:机架本地化,数据和task在一个机架的两个节点上;数据需要通过网络在节点之间进行传输
ANY:数据和task可能在集群中的任何地方,而且不在一个机架中,性能最差
spark.locality.wait,默认是3s
Shuffle的优化:
shuffle中的写磁盘的操作,基本上就是shuffle中性能消耗最为严重的部分。(最消耗资源的部分)
new SparkConf().set("spark.shuffle.consolidateFiles", "true")
开启shuffle map端输出文件合并的机制;默认情况下,是不开启的,就是会发生如上所述的大量map端输出文件的操作,严重影响性能。
new SparkConf().set("spark.shuffle.consolidateFiles", "true")
开启shuffle map端输出文件合并的机制;默认情况下,是不开启的,就是会发生如上所述的大量map端输出文件的操作,严重影响性能。
开启之后运行表现:
开启了map端输出文件的合并机制之后:
第一个stage,同时就运行cpu core个task,比如cpu core是2个,并行运行2个task;每个task都创建下一个stage的task数量个文件;
第一个stage,并行运行的2个task执行完以后;就会执行另外两个task;另外2个task不会再重新创建输出文件;而是复用之前的task创建的map端输出文件,将数据写入上一批task的输出文件中。
第二个stage,task在拉取数据的时候,就不会去拉取上一个stage每一个task为自己创建的那份输出文件了;而是拉取少量的输出文件,每个输出文件中,可能包含了多个task给自己的map端输出。
只有并行执行的task会去创建新的输出文件;下一批并行执行的task,就会去复用之前已有的输出文件;但是有一个例外,比如2个task并行在执行,但是此时又启动要执行2个task;那么这个时候的话,就无法去复用刚才的2个task创建的输出文件了;而是还是只能去创建新的输出文件。
要实现输出文件的合并的效果,必须是一批task先执行,然后下一批task再执行,才能复用之前的输出文件;负责多批task同时起来执行,还是做不到复用的。
优化点:
1、第一个stage阶段 ,map task写入磁盘文件的IO,减少:100万文件 -> 20万文件
2、第二个stage,每个task原本要拉取第一个stage的对应的task数量份文件减少,减少网络传输。
Map端内存缓冲与reduce端的内存占比:
默认情况下,shuffle的map task,输出到磁盘文件的时候,统一都会先写入每个task自己关联的一个内存缓冲区。
这个缓冲区大小,默认是32kb。
每一次,当内存缓冲区满溢之后,才会进行spill操作,溢写操作,溢写到磁盘文件中去。
设置参数
spark.shuffle.file.buffer,默认32k
spark.shuffle.memoryFraction,0.2 (对应spark1.6 JVM模型当中的shuffle空间)
Reduce端:
reduce端task,在拉取到数据之后,会用hashmap的数据格式,来对各个key对应的values进行汇聚。
针对每个key对应的values,执行我们自定义的聚合函数的代码,比如_ + _(把所有values累加起来)
reduce task,在进行汇聚、聚合等操作的时候,实际上,使用的就是自己对应的executor的内存,executor(jvm进程,堆),默认executor内存中划分给reduce task进行聚合的比例,是0.2。
问题来了,因为比例是0.2,所以,理论上,很有可能会出现,拉取过来的数据很多,那么在内存中,放不下;这个时候,默认的行为,就是说,将在内存放不下的数据,都spill(溢写)到磁盘文件中去。
不调优会发生的情况:
在map task处理的数据量比较大的情况下,而你的task的内存缓冲默认是比较小的,32kb。可能会造成多次的map端往磁盘文件的spill溢写操作,发生大量的磁盘IO,从而降低性能。
reduce端聚合内存,占比。默认是0.2。如果数据量比较大,reduce task拉取过来的数据很多,那么就会频繁发生reduce端聚合内存不够用,频繁发生spill操作,溢写到磁盘上去。而且最要命的是,磁盘上溢写的数据量越大,后面在进行聚合操作的时候,很可能会多次读取磁盘中的数据,进行聚合。
默认不调优,在数据量比较大的情况下,可能频繁地发生reduce端的磁盘文件的读写。
调优思路:
看Spark UI,如果你的公司是决定采用standalone模式,那么狠简单,你的spark跑起来,会显示一个Spark UI的地址,4040的端口,进去看,依次点击进去,可以看到,你的每个stage的详情,有哪些executor,有哪些task,每个task的shuffle write和shuffle read的量,shuffle的磁盘和内存,读写的数据量;如果是用的yarn模式来提交,课程最前面,从yarn的界面进去,点击对应的application,进入Spark UI,查看详情。
如果发现shuffle 磁盘的write和read,很大。这个时候,就意味着最好调节一些shuffle的参数。进行调优。首先当然是考虑开启map端输出文件合并机制。
ShuffleManger调优 :
1、需不需要数据默认就让spark给你进行排序?就好像mapreduce,默认就是有按照key的排序。如果不需要的话,其实还是建议搭建就使用最基本的HashShuffleManager,因为最开始就是考虑的是不排序,换取高性能;
2、什么时候需要用sort shuffle manager?如果你需要你的那些数据按key排序了,那么就选择这种吧,而且要注意,reduce task的数量应该是超过200的,这样sort、merge(多个文件合并成一个)的机制,才能生效把。但是这里要注意,你一定要自己考量一下,有没有必要在shuffle的过程中,就做这个事情,毕竟对性能是有影响的。
3、如果你不需要排序,而且你希望你的每个task输出的文件最终是会合并成一份的,你自己认为可以减少性能开销;可以去调节bypassMergeThreshold这个阈值,比如你的reduce task数量是500,默认阈值是200,所以默认还是会进行sort和直接merge的;可以将阈值调节成550,不会进行sort,按照hash的做法,每个reduce task创建一份输出文件,最后合并成一份文件。 spark.shuffle.sort.bypassMergeThreshold:200
算子调优 :
spark中,最基本的原则,就是每个task处理一个RDD的partition。
MapPartitions操作的优点:
如果是普通的map,比如一个partition中有1万条数据;ok,那么你的function要执行和计算1万次。(RDD中的每个partition都要被task处理一次)
但是,使用MapPartitions操作之后,一个task仅仅会执行一次function,function一次接收所有的partition数据。只要执行一次就可以了,性能比较高。(RDD当中的partition合并成一大块交给task处理。)
MapPartitions的缺点:
如果是普通的map操作,一次function的执行就处理一条数据;那么如果内存不够用的情况下,比如处理了1千条数据了,那么这个时候内存不够了,那么就可以将已经处理完的1千条数据从内存里面垃圾回收掉,或者用其他方法,腾出空间来吧。
所以说普通的map操作通常不会导致内存的OOM异常。
但是MapPartitions操作,对于大量数据来说,比如甚至一个partition,100万数据,一次传入一个function以后,那么可能一下子内存不够,但是又没有办法去腾出内存空间来,可能就OOM,内存溢出。
适用场景:
什么时候比较适合用MapPartitions系列操作,就是说,数据量不是特别大的时候,都可以用这种MapPartitions系列操作,性能还是非常不错的,是有提升的。
但是也有过出问题的经验,MapPartitions只要一用,直接OOM,内存溢出,崩溃。
在项目中,自己先去估算一下RDD的数据量,以及每个partition的量,还有自己分配给每个executor的内存资源。看看一下子内存容纳所有的partition数据,行不行。如果行,可以试一下,能跑通就好。性能肯定是有提升的。
但是试了一下以后,发现,不行,OOM了,那就放弃吧。
Filter算子过后使用coalesce调优
默认情况下,经过了这种filter之后,RDD中的每个partition的数据量,可能都不太一样了。(原本每个partition的数据量可能是差不多的)
问题:
1、每个partition数据量变少了,但是在后面进行处理的时候,还是要跟partition数量一样数量的task,来进行处理;有点浪费task计算资源。
2、每个partition的数据量不一样,会导致后面的每个task处理每个partition的时候,每个task要处理的数据量就不同,这个时候很容易发生什么问题?数据倾斜。。。。
coalesce算子
主要就是用于在filter操作之后,针对每个partition的数据量各不相同的情况,来压缩partition的数量。减少partition的数量,而且让每个partition的数据量都尽量均匀紧凑。
从而便于后面的task进行计算操作,在某种程度上,能够一定程度的提升性能。
使用foreachPartition读写数据库
默认的foreach的性能缺陷在哪里?
首先,对于每条数据,都要单独去调用一次function,task为每个数据,都要去执行一次function函数。
如果100万条数据,(一个partition),调用100万次。性能比较差。
另外一个非常非常重要的一点
如果每个数据,你都去创建一个数据库连接的话,那么你就得创建100万次数据库连接。
但是要注意的是,数据库连接的创建和销毁,都是非常非常消耗性能的。虽然我们之前已经用了数据库连接池,只是创建了固定数量的数据库连接。
用了foreachPartition算子之后,好处在哪里?
1、对于我们写的function函数,就调用一次,一次传入一个partition所有的数据
2、主要创建或者获取一个数据库连接就可以
只要向数据库发送一次SQL语句和多组参数即可
缺点:数据太多容易OOM、内存溢出。
SQL的重分区:
Spark SQL自己会默认根据hive表对应的hdfs文件的block,自动设置Spark SQL查询所在的那个stage的并行度。你自己通过spark.default.parallelism参数指定的并行度,只会在没有Spark SQL的stage中生效。
解决上述Spark SQL无法设置并行度和task数量的办法,是什么呢?
repartition算子,你用Spark SQL这一步的并行度和task数量,肯定是没有办法去改变了。但是呢,可以将你用Spark SQL查询出来的RDD,使用repartition算子,去重新进行分区,此时可以分区成多个partition,比如从20个partition,分区成100个。
然后呢,从repartition以后的RDD,再往后,并行度和task数量,就会按照你预期的来了。就可以避免跟Spark SQL绑定在一个stage中的算子,只能使用少量的task去处理大量数据以及复杂的算法逻辑。
ReduceBykey的使用:
reduceByKey,相较于普通的shuffle操作(比如groupByKey),它的一个特点,就是说,会进行map端的本地聚合。
对map端给下个stage每个task创建的输出文件中,写数据之前,就会进行本地的combiner操作,也就是说对每一个key,对应的values,都会执行你的算子函数() + _)
用reduceByKey对性能的提升:
1、在本地进行聚合以后,在map端的数据量就变少了,减少磁盘IO。而且可以减少磁盘空间的占用。
2、下一个stage,拉取数据的量,也就变少了。减少网络的数据传输的性能消耗。
3、在reduce端进行数据缓存的内存占用变少了。
4、reduce端,要进行聚合的数据量也变少了。
JVM调优:
JVM调优的第一个点:降低cache操作的内存占比
spark中,堆内存又被划分成了两块儿,一块儿是专门用来给RDD的cache、persist操作进行RDD数据缓存用的(storage);另外一块儿,就是我们刚才所说的,用来给spark算子函数的运行使用的,存放函数中自己创建的对象(unroll)。
默认情况下,给RDD cache操作的内存占比,是0.6,60%的内存都给了cache操作了。但是问题是,如果某些情况下,cache不是那么的紧张,问题在于task算子函数中创建的对象过多,然后内存又不太大,导致了频繁的minor gc,甚至频繁full gc,导致spark频繁的停止工作。性能影响会很大。
针对上述这种情况,大家可以在之前我们讲过的那个spark ui。yarn去运行的话,那么就通过yarn的界面,去查看你的spark作业的运行统计,很简单,大家一层一层点击进去就好。可以看到每个stage的运行情况,包括每个task的运行时间、gc时间等等。如果发现gc太频繁,时间太长。此时就可以适当调价这个比例。
降低cache操作的内存占比,大不了用persist操作,选择将一部分缓存的RDD数据写入磁盘,或者序列化方式,配合Kryo序列化类,减少RDD缓存的内存占用;降低cache操作内存占比;对应的,算子函数的内存占比就提升了。这个时候,可能,就可以减少minor gc的频率,同时减少full gc的频率。对性能的提升是有一定的帮助的。
Executor堆外内存的调节:
--conf spark.yarn.executor.memoryOverhead=2048
spark-submit脚本里面,去用--conf的方式,去添加配置;不是在你的spark作业代码中,用new SparkConf().set()这种方式去设置。
spark.yarn.executor.memoryOverhead(看名字,顾名思义,针对的是基于yarn的提交模式)
默认情况下,这个堆外内存上限大概是300多M;后来我们通常项目中,真正处理大数据的时候,这里都会出现问题,导致spark作业反复崩溃,无法运行;此时就会去调节这个参数,到至少1G(1024M),甚至说2G、4G
通常这个参数调节上去以后,就会避免掉某些JVM OOM的异常问题,同时呢,会让整体spark作业的性能,得到较大的提升。
简历复盘:
电商用户行为分析项目复盘
一 2019.2 --2019.7 公司名称:久远银海 项目名称:电商用户行为分析大数据平台
项目背景:
该项目主要是为公司内部人员提供数据依据,对公司电商网站用户的浏览情况有一个实时的了解。通过获取公司内部人员在J2EE平台提交的任务参数,利用大数据以任务参数对用户访问网站的浏览数据进行筛选、计算,最后由前台将计算好的数据进行大屏展示,从而便于公司高层及设计人员对网站页面、产品以及用户标签做出定位,并做出决策。
项目架构:hive + spark + mysql(离线)
nginx + flume + kafka + spark + mysql(实时)
负责的模块:用户访问session分析、页面单跳转化率、区域热门商品统计、广告点击流量实时统计
工作任务:使用flume监控日志服务器的日志文件夹,将数据采集到kafka中,用spark实时处理用户访问的session数据,保存到mysql中去。(实时)
使用spark获取Hive中的数据,并对数据进行计算处理,最终保存到Hive表中。(离线)
技术难点:
一 自定义累加器:
Spark提供的Accumulator,主要用于多个节点对一个变量进行共享性的操作。Accumulator只提供了累加的功能,只能累加,不能减少,累加器只能在Driver端构建,并只能从Driver端读取结果,在Task端只能进行累加。因为value是被@transient修饰的,序列化后无法获得其值。
1、Driver端初始化构建Accumulator并初始化,同时完成了Accumulator注册,Accumulators.register(this)时Accumulator会在序列化后发送到Executor端
2、Executor端接收到Task之后会进行反序列化操作,反序列化得到RDD和function。同时在反序列化的同时也去反序列化Accumulator(在readObject方法中完成),同时也会向TaskContext完成注册
3、完成任务计算之后,随着Task结果一起返回给Driver
4、Driver接收到ResultTask完成的状态更新后,会去更新Value的值 然后在Action操作执行后就可以获取到Accumulator的值了
二 spark数据倾斜现象原因以及调优;
前提是定位数据倾斜,是OOM了,还是任务执行缓慢,看日志,看WebUI
解决方法:
避免不必要的shuffle,如使用广播小表的方式,将reduce-side-join提升为map-side-join
分拆发生数据倾斜的记录,分成几个部分进行,然后合并join后的结果
改变并行度,可能并行度太少了,导致个别task数据压力大
两阶段聚合,先局部聚合,再全局聚合 自定义paritioner,分散key的分布,使其更加均匀
三 消费 kafka 数据一致性语义保证;
at lest once:消息不丢,但可能重复
at most once:消息会丢,但不会重复
Exactly Once:消息不丢,也不重复。
数据一致性保证:保证消息不丢、消息不重复
消息不丢:副本机制+ack,可以保证消息不丢。
数据重复:brocker保存了消息之后,在发送ack之前宕机了,producer认为消息没有发送成功进行重试,导致数据重复。
数据乱序:前一条消息发送失败,后一条消息发送成功,前一条又重试,成功了,导致数据乱序。
消息一致性保证:主要就是保证Exactly Once,即:数据不丢、数据不重复
0.11之前的kafka版本:保证消息不丢,要在消息发送端和消费端都要进行保证。保证消息不重复,就是要对消息幂等,即去重
消息发送端 request.required.acks 设置数据可靠性级别:
request.required.acks=1:当且仅当leader收到消息后返回commit确认信号后,消息发送成功。但有弊端,leader宕机,也就是还没有 将消息同步到follower,这是会发生消息丢失。 request.required.acks=0:消息发送了,即认为成功,可靠性最低。
request.required.acks=-1:发送端等待isr列表所有的成员确认消息,才算成功,可靠性最高延迟最大。 消息消费端
消费者关闭自动提交,enable.auto.commit:false,消费者收到消息处理完业务逻辑后,再手动提交commitSync offsets。这样 可以保证消费者即使在消息处理过程中挂掉,下次重启,也可以从之前的offsets进行消费,消息不丢。
(3)消息去重:主要借助于业务系统本身的业务处理或大数据组件幂等。如:hbase 、elasticsearch幂等。
Hbse幂等:将消息从kafka消费出来,保存到hbase中,使用id主键+时间戳,只有插入成功后才往 kafka 中持久化 offset。这样的好处 是,如果在中间任意一个阶段发生报错,程序恢复后都会从上一次持久化 offset 的位置开始消费数据,而不会造成数据丢失。如果中途有 重复消费的数据,则插入 hbase 的 rowkey 是相同的,数据只会覆盖不会重复,最终达到数据一致。
四 spark读取kafka数据丢失
producer角度:
1.设置request.required.asks参数
(1)asks = 0时,只要消息发送成功就会发送下一条数据,吞吐量最高,但这种情况即使数据丢失我们也无法知道
(2)asks = 1时,消息发送成功,并且leader接收成功后才会发送下一条数据,这种情况如果leader刚接收到数据,还没有同步到follower时,假如leader节点挂掉也会导致数据的丢失
(3)asks = -1时,消息发送成功,要等待leader把消息同步到follower之后才会发送下一条数据,吞吐量最低,但最可靠
2.设置retry > 0 ,retry.backoff.ms retry的时间间隔
在kafka中错误分为2种,一种是可恢复的,另一种是不可恢复的。
可恢复性的错误:
如遇到在leader的选举、网络的抖动等这些异常时,如果我们在这个时候配置的retries大于0的,也就是可以进行重试操作,那么等到leader选举完成后、网络稳定后,这些异常就会消息,错误也就可以恢复,数据再次重发时就会正常发送到broker端。需要注意retries(重试)之间的时间间隔,以确保在重试时可恢复性错误都已恢复。
不可恢复性的错误:
如:超过了发送消息的最大值(max.request.size)时,这种错误是不可恢复的,如果不做处理,那么数据就会丢失,因此我们需要注意在发生异常时把这些消息写入到DB、缓存本地文件中等等,把这些不成功的数据记录下来,等错误修复后,再把这些数据发送到broker端。
consumer角度:
1.给consumer设置groupid,consumer在group中才能对kafka中的数据进行消费
2.设置enable.auto.commit = false,由自己手动提交offset;因为自动提交是由时间间隔控制的,不管数据的处理是否发生异常。
3.auto.offset.reset = earliest(最早) /latest(最晚),这个根据业务需求来设置,如果需要分析以前的数据,就设置为earliest,不需要分析以前的数据就设置为latest。
broker角度:
1.replication-factor >=2,设置topic的副本个数
2.min.insync.replicas = 2,分区ISR队列集合中最少有多少个副本
3.unclean.leander.election.enable = false,是否允许从ISR队列中选举leader副本,默认值是false,如果设置成true,则可能会造成数据丢失。