北风网用户行为分析

离线日志采集流程

互联网:网站,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
返回sessionid2detailRDD

2、统计出符合条件的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,则可能会造成数据丢失。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值