【Spark】基于Spark的大型电商网站交互式行为分析系统项目实战

1、项目背景

(1)Spark在美团的实践
美团是数据驱动的互联网服务,用户每天在美团上的点击、浏览、下单支付行为都会产生海量的日志,这些日志数据将被汇总处理、分析、挖掘与学习,为美团的各种推荐、搜索系统甚至公司战略目标制定提供数据支持。大数据处理渗透到了美团各业务线的各种应用场景,选择合适、高效的数据处理引擎能够大大提高数据生产的效率,进而间接或直接提升相关团队的工作效率。
美团最初的数据处理以Hive SQL为主,底层计算引擎为MapReduce,部分相对复杂的业务会由工程师编写MapReduce程序实现。随着业务的发展,单纯的Hive SQL查询或者MapReduce程序已经越来越难以满足数据处理和分析的需求。
一方面,MapReduce计算模型对多轮迭代的DAG作业支持不给力,每轮迭代都需要将数据落盘,极大地影响了作业执行效率,另外只提供Map和Reduce这两种计算因子,使得用户在实现迭代式计算(比如:机器学习算法)时成本高且效率低。
另一方面,在数据仓库的按天生产中,由于某些原始日志是半结构化或非结构化数据,因此,对其进行清洗和转换操作时,需要结合SQL查询以及复杂的过程式逻辑处理,这部分工作之前是由Hive SQL结合Python脚本来完成的。这种方式存在效率问题,当数据量比较大的时候,流程的运行时间较长,这些ETL流程通常处于比较上游的位置,会直接影响到一系列下游的完成时间以及各种重要数据报表的生成。
基于以上原因,美团在2014年的时候引入了Spark。为了充分利用现有Hadoop集群的资源,我们采用了Spark on Yarn模式,所有的Spark app以及Map Reduce作业会通过Yarn统一调度执行。
(2)Spark在美团数据平台架构中的位置

(3)Spark在交互式用户行为分析系统中的实践
经过近两年的推广和发展,从最开始只有少数团队尝试用Spark解决数据处理、机器学习等问题,到现在已经覆盖了美团各大业务线的各种应用场景。从上游的ETL生产,到下游的SQL查询分析以及机器学习等,Spark正在逐步替代MapReduce作业,成为美团大数据处理的主流计算引擎。
目前美团Hadoop集群用户每天提交的Spark作业数和MapReduce作业数比例为4:1,对于一些上游的Hive ETL流程,迁移到Spark之后,在相同的资源使用情况下,作业执行速度提升了十倍,极大地提升了业务方的生产效率。
Spark在美团的实践,包括我们基于Spark所做的平台化工作以及Spark在生产环境下的应用案例。其中包含Zeppelin结合的交互式开发平台,也有使用Spark任务完成的ETL数据转换工具,数据挖掘组基于Spark开发了特征平台和数据挖掘平台,另外还有基于Spark的交互式用户行为分析系统以及在SEM投放服务中的应用。
美团的交互式用户行为分析系统,用于提供对海量的流量数据进行交互式分析的功能,系统的主要用户为公司内部的PM和运营人员。普通的BI类报表系统,只能够提供对聚合后的指标进行查询,比如PV、UV等相关指标。
但是PM以及运营人员除了查看一些聚合指标以外,还需要根据自己的需求去分析某一类用户的流量数据,进而了解各种用户群体在App上的行为轨迹。根据这些数据,PM可以优化产品设计,运营人员可以为自己的运营工作提供数据支持,用户核心的几个诉求包括:
1)自助查询:不同的PM或运营人员可能随时需要执行各种各样的分析功能,因此系统需要支持用户自助使用。
2)响应速度:大部分分析功能都必须在几分钟内完成。
3)可视化:可以通过可视化的方式查看分析结果。
要解决上面的几个问题,技术人员需要解决以下两个核心问题:
1)海量数据的处理:用户的流量数据全部存储在Hive中,数据量非常庞大,每天的数据量都在数十亿的规模。
2)快速计算结果:系统需要能够随时接收用户提交的分析任务,并在几分钟之内计算出他们想要的结果。
要解决上面两个问题,目前可供选择的技术主要有两种:MapReduce和Spark。在初期架构中选择了使用MapReduce这种较为成熟的技术,但是通过测试发现,基于MapReduce开发的复杂分析任务需要数小时才能完成,这会造成极差的用户体验,用户无法接受。
尝试使用Spark这种内存式的快速大数据计算引擎作为系统架构中的核心部分,主要使用了Spark Core以及Spark SQL两个组件,来实现各种复杂的业务逻辑。实践中发现,虽然Spark的性能非常优秀,但是在目前的发展阶段中,还是或多或少会有一些性能以及OOM方面的问题。
因此在项目的开发过程中,对大量Spark作业进行了各种各样的性能调优,包括算子调优、参数调优、shuffle调优以及数据倾斜调优等,最终实现了所有Spark作业的执行时间都在数分钟左右。并且在实践中解决了一些shuffle以及数据倾斜导致的OOM问题,保证了系统的稳定性。
(4)系统架构与工作流程

该系统上线后效果良好:90%的Spark作业运行时间都在5分钟以内,剩下10%的Spark作业运行时间在30分钟左右,该速度足以快速响应用户的分析需求。通过反馈来看,用户体验非常良好。目前每个月该系统都要执行数百个用户行为分析任务,有效并且快速地支持了PM和运营人员的各种分析需求。
(5)概括
概括起来就是,
数据来源有:用户行为数据(用户在系统上的点击、浏览、下单支付行为都会产生海量的日志)和业务数据库(商品的信息、会员的信息、商家的信息等等)
对数据所做的操作有:汇总处理、分析、挖掘与机器学习。
实现的功能有:为美团的各种推荐、搜索系统甚至公司战略目标制定提供数据支持。
Spark在美团的发展历程:
阶段一:HiveQL + MapReduce 。
存在的问题:底层是MapReduce,执行速度慢,对DAG类型的任务支持不好。
阶段二:Spark。
实现的效果:Spark作业:MR作业 = 4:1,作业执行效率提升了10倍。
(6)基于Flume的美团日志收集系统——Flume-NG
美团的日志收集系统负责美团的所有业务日志的收集,并分别给Hadoop/Spark平台提供离线数据和Storm平台提供实时数据流,基于Flume设计搭建而成,目前每天收集和处理约T级别的日志数据。

整个日志收集系统分为三层:Agent层,Collector层和Store层。
Agent层每个机器部署一个进程,负责对单机的日志收集工作;
Collector层部署在中心服务器上,负责接收Agent层发送的日志,并且将日志根据路由规则写到相应的Store层中;
Store层负责提供永久或者临时的日志存储服务,或者将日志流导向其它服务器。
Agent到Collector使用LoadBalance策略,将所有的日志均衡地发到所有的Collector上,达到负载均衡的目标,同时并处理单个Collector失效的问题。
Collector层的目标主要有三个:SinkHdfs, SinkKafka和SinkBypass。分别提供离线的数据到Hdfs,和提供实时的日志流到Kafka和Bypass。其中SinkHdfs又根据日志量的大小分为SinkHdfs_b,SinkHdfs_m和SinkHdfs_s三个Sink,以提高写入到Hdfs的性能。
对于Store来说,Hdfs负责永久地存储所有日志;Kafka存储最新的7天日志,并给实时系统提供实时日志流;Bypass负责给其它服务器和应用提供实时日志流。

2、项目概述

(1)项目的执行流程
1)用户在系统界面中选择某个分析功能对应的菜单,并进入对应的任务创建界面,然后选择筛选条件和任务参数,并提交任务。
2)由于系统需要满足不同类别的用户行为分析功能(目前系统中已经提供了十个以上分析功能),因此需要为每一种分析功能都开发一个Spark作业。一个分析功能就是一个Spark作业的模板,也就是一个Spark的代码。
3)采用J2EE技术开发了Web服务作为后台系统,在接收到用户提交的任务之后,根据任务类型选择其对应的Spark作业,启动一条子线程来执行Spark-submit命令以提交Spark作业。任务保存数据库后,根据任务id和任务类型启动对应的spark应用。
4)Spark作业运行在Yarn集群上,并针对Hive中的海量数据进行计算,最终将计算结果写入数据库中。执行结果数据需要通过任务id和任务进行关联。
5)用户通过系统界面查看任务分析结果,J2EE系统负责将数据库中的计算结果返回给界面进行展现。用户根据任务id查看执行结果。
6)在项目设计过程中需要考虑项目结构的扩展性、容错性和可靠性。
(2)说明
Task核心抽象:每个用户的一个需求分析功能就是就是一个Task对象,在数据库中存储为一条数据,用唯一标识符表示。任务参数的类型是一个Text的json格式数据,内部包含的是具体的任务过滤参数。
一条Task数据在RDBMS中的字段有:TASK_ID(表的主键)、TASK_NAME(任务的名称)、CREATE_TIME(创建时间)、START_TIME(对应的SPARK APPLICATION的启动时间)、FINISH_TIME(SPARK APPLICATION运行的结束时间)、TASK_TYPE(任务的类型)、TASK_STATUS(对应SPARK APPLICATION运行的最终状态)、TASK_PARAM(JSON格式数据,封装了用户所有的对应任务的参数)。
交互式指的是前端创建一个任务,后台就会将该任务对应的Spark应用进行启动操作,并通过传递的任务id将执行完成的结果返回给前端。
(3)模块划分
该系统分为4个模块。
模块一:用户访问SESSION分析。
所用技术:Spark Core。
实现功能:统计以会话为维度的情况下各个指标的值,比如会话长度、会话时间、会话访问的pv量、跳出会话数量。
其中模块一分为:
1)数据清洗、过滤、筛选:依据用户筛选条件筛选SESSION。
2)需求一:用户访问SESSION的聚合统计。
3)需求二:按照时间比例随机抽取SESSION。
4)需求三:获取点击、下单和支付次数排名前10的品类。
5)需求四:获取Top10品类的点击次数最多的10个SESSION。
模块二:页面单跳转化率统计(跳出率分析)。
所用技术:Spark Core。
实现功能:计算两个页面之间的转换率、页面的跳出率、整个系统的跳出率等等。
计算规则:跳出率 = 跳出会话的数量 / 总会话数量。
其中跳出会话数量为只访问一个页面的会话数量。
或者,跳出率 = 从当前页面进入系统其它页面的用户数量 / 进入该页面的总用户数量。
模块三:各区域热门商品统计。
所用技术:Spark SQL。
实现功能:统计各个地域省份中各个商品的销售情况,并且获取销售最好的商品列表。
模块四:广告点击流量实时统计。
所用技术:Spark Streaming。
实现功能:统计系统中各个广告的点击情况。

3、模块一:用户访问SESSION分析

(1)会话(session)的概念
当用户访问系统的时候就会产生一个会话,会话使用唯一标识符id来表示。在一个会话中可以产生多条数据(产生>=1条数据)。
(2)数据处理过程
将数据按照会话id进行聚合操作:
1)根据taskID获取任务的过滤参数。
2)读取数据。
3)对数据进行过滤(按照过滤参数的要求进行数据过滤)。
4)按照会话进行数据聚合。
(3)需求一:用户访问SESSION的聚合统计
总会话长度、总会话数量、总无效会话数量(会话长度如果小于1秒,就认为是无效会话)。
分访问时长区间计算各个区间的会话数量:
0-4s/5-10s/11-30s/31-60s/1min-3min/3-6min/6min+
分时间区间来计算各个区间的会话数量/会话长度/无效会话数量:
0-1h/1-2h/2-3h/…/22-23h/23-24h
分pv的区间计算各个区间的会话数量和访问时长:
1-5pv/6-10pv/11-20pv/21-40pv/41-60pv/60pv+
实现功能:让PM从整体角度来看一下各个条件下的用户访问情况。
(4)需求二:按照时间比例随机抽取SESSION
我们需要看数据的特征和分布趋势,然而拿所有数据,这样数据量太大,不太现实,所以我们从所有数据中进行一个数据抽样,随机抽样的样本之间是概率公平的。
步骤:假设一天24小时,总会话数量10万,现在抽取1千个会话。
1)计算总的会话数量(10万)以及各个小时的会话数量,如下。
0-1h:300
1-2h:50


10-11h:10000

2) 计算出需要抽取的比例(1000/100000=0.01)。
3)计算在各个小时段需要抽取的数量大小。
0-1h:3
1-2h:1(最少1个)

10-11h:100

4)进行具体时间段的数据抽取工作。
实现功能:让PM获取用户的访问轨迹,可以针对性的进行业务规则的调整。
(5)需求三:获取点击、下单、支付次数排名前10的品类
每个会话中,都可能产生多次点击、下单、支付的操作行为(产生>=0次行为)。
步骤:
1)获取计算出所有session中各个品类的点击、下单、支付的次数。
会话1 品类1 点击次数 下单次数 支付次数
会话1 品类2 点击次数 下单次数 支付次数
会话1 品类3 点击次数 下单次数 支付次数
会话1 品类4 点击次数 下单次数 支付次数
会话2 品类1 点击次数 下单次数 支付次数
会话3 品类1 点击次数 下单次数 支付次数
会话4 品类1 点击次数 下单次数 支付次数
会话5 品类1 点击次数 下单次数 支付次数
2)累加计算各个品类的总次数(点击、下单、支付)。
3)获取top10的数据。
(6)需求四:获取Top10品类的点击次数最多的10个SESSION
步骤:
1)利用需求三的结果。
2)直接过滤获取session。
3)计算session的数量,然后获取点击次数最多的前10个session。
(7)模块一涉及到的数据
1)user_visit_action:用户行为数据表
存储在Hive分区表中的数据,数据按天分区。
涉及到的字段:
data:String
userId:String
sessionID:String
pageId:Long,
actionTime:String,
searchKeyword:String,
clickCategoryID:String,
clickProductID:String,
orderCategoryIDs:String,
orderProductIDs:String,
payCategoryIDs:String,
payProductIDs:String,
cityID:Int
2)user_info: 用户/会员信息表
基于业务系统数据库的会员信息表做扩充得到的一个表结构数据,该表存储在Hive中。在Hive中存储时进行分区设置,每个分区中的数据是对应日期的有访问系统操作的用户。
涉及到的字段:
userID:Long,
userName:String,
name:String,
sex:String,
age:Int,
professional:String

4、模块三:各个区域热门商品统计Top10信息

(1)涉及的数据表
1)用户行为数据表:user_visit_action
涉及到的字段: click_product_id(点击商品ID)、city_id(城市ID)
存储位置:Hive
2)城市信息表:city_info
涉及到的字段:city_id(城市ID)、city_name(城市名称)、province_name(省份名称)、area(城市所属区域)
存储位置:RDBMS
数据量不大,而且数据结构比较稳定,一般不需要更新,可以直接将数据存储在RDBMS中,通过SparkSQL的read.jdbc来读取数据,而对业务系统的RDBMS不会产生太大的压力。
3)商品信息表:product_info
涉及到的字段:product_id(商品ID)、product_name(商品名称)、extend_info(扩展信息,存储的是一个JSON格式的数据),例如:

{
	product_type:0/1 		// 商品类型,0表示自营商品,1表示第三方商品
}

原始的存储位置:RDBMS
处理过程中数据的存储位置:Hive
因为数据量比较大,而且随时存在着更新操作,所以直接连接RDBMS不现实,一般采用将数据同步到Hive中的方式来解决连接问题。其中同步的策略如下:

  • 第一次做一个全量同步。
  • 每天凌晨进行数据增量同步(按照插入的时间和更新的时间这两个字段进行同步操作)(具体同步频率考虑对业务系统的压力)。
  • 每个月进行一次全量的同步(可以保证数据不会出现大量的丢失情况)。

数据同步所用的技术:Sqoop、SparkSQL。
数据同步中需要注意的问题:容错问题(如果运行到一半的时候,宕机了怎么进行恢复)、同步的数据全不全等。
(2)数据同步系统
数据收集系统:收集用户行为数据,即用户访问系统的时候实时产生的数据。
数据同步系统:业务系统中的数据和RDBMS中的数据。
ETL:数据清洗过滤的操作。
数据同步系统会提供一个web界面,供操作者创建数据同步任务,包含数据库的连接信息、数据的存储信息、增量更新的字段等等,然后将任务参数保存到元数据库中。
实现数据同步过程中需要注意的几个点:
1)需要考虑数据的更新操作。
2)是否需要考虑分区表。
增量部分的实现步骤:(SparkSQL + Hive0.13.1)

// 从元数据库中获取同步任务的相关参数
val insertDateFieldName, updateDateFieldName, url, tablename, properties,dbtype_database_tablename,primite keys

// 同步的数据时间所在区间:[startDate, endDate)
val startDate = "昨天,eg:2017-06-10"
val endDate = "今天,eg:2017-06-11"
val predicates = Array(s"${insertDateFieldName} >= '${startDate}' AND ${insertDateFieldName} < '${endDate}'", s"${updateDateFieldName} >= '${startDate}' AND ${updateDateFieldName} < '${endDate}'")

// 读取关系型数据库中的数据
val df = sqlContext.read.jdbc(url, tablename, predicates, properties).distinct

// 进行更新操作(基于主键进行更新操作)
val hiveDF = sqlContext.read.table(dbtype_database_tablename)
val hiveDF2 = hiveDF.select(primite keys).toDF(hive temp primite keys)
val df2 = df.join(hiveDF2, join primite keys seq, 'outer')

// 得到只存在于df中的数据
val df3 = df2.where(hive temp primite keys is null).select(原始数据的列)

// 得到只存在于hiveDF2中的数据
val df4 = df2.where(primite keys is null).select(hive temp primite keys)
val df5 = df4.join(hiveDF, join primite keys seq).select("原始数据中的列")

// 得到在df和hiveDF2中都存在的数据(可以直接插入)
val df6 = df2.where(primite keys is not null AND hive temp primite keys is not null).select("原始数据的列")
val resultDF = df3.unionALL(df5).unionALL(df6)
resultDF.write.mode("overwrite").insertInto(dbtype_database_tablename)

(3)注意点
1)使用row_number这类的窗口函数的时候,必须使用HiveContext对象。
2)当使用HiveContext的时候,有可能出现OOM异常,需要给定JVM参数:

-XX:PermSize=128M -XX:MaxPermSize=256M  

5、模块四:广告点击流量实时统计分析

(1)需求分析
需求:对收集得到的用户点击广告数据进行实时数据分析,然后将结果保存到MySQL中。
用户点击广告数据存储位置:Kafka。
数据处理技术:Spark Streaming
数据处理结果保存位置:MySQL。
(2)用户点击广告的数据也是一种用户行为数据,怎样从用户行为数据中抽取出用户点击广告的数据,并如何将其保存到Kafka中?
解决方案:
1)使用Flume的Interceptors和Flume的扇出机制。
2)使用不同类型的URL来区分用户点击广告数据和其他用户行为数据。
用户点击广告数据:http://track.ibeifeng.com/track1.gif?xxx。
其它用户行为数据: http://track.ibeifeng.com/track2.gif?xxx。
3)实时进行ETL操作,将ETL操作的结果保存到HIVE中,同时对于用户广告点击数据,继续保存到Kafka中,供下一个Spark Streaming程序处理。
(3)Nginx的接收到的数据如何发送给Flume的Agent?
1)将Nginx接收到的数据作为日志写到日志文件中,使用exec scoure监控日志文件即可完成数据的传递过程。
2)Nginx将接收的数据转发给tomcat中的web应用, web应用直接将数据发送给kafka/flume。
(4)实现功能
1)黑名单功能
针对恶意用户点击的数据,在统计过程中需要过滤。
黑名单用户定义规则:最近5分钟内点击广告次数超过100次,黑名单每隔3分钟执行一次。黑名单不支持在程序中将用户从黑名单中删除的操作,如果需要支持该功能,需要工作人员手动进行黑名单的删除操作。
2)实时累加统计每天各个省份Top5的热门广告点击情况,使用函数updateStateByKey。
3)实时统计最近一段时间的广告点击情况,使用window窗口分析函数。
(5)数据格式
存储在Kafka中的数据以空格为分隔符。涉及到的字段信息:timestamp(时间戳,点击广告的时间)、province(省份名称)、city(城市名称)、user_id(用户ID)、ad_id(广告ID)。

6、Spark应用提交

根据taskID启动对应spark的应用的方式有以下三种(Spark on Yarn):
(1)Spark应用通过调用spark-submit命令启动
Java程序中调用本地的shell脚本来启动Spark的应用,shell脚本中是spark-submit的命令。
优点:比较简单。
缺点:需要将shell脚本放到所有可能执行的服务器上,并且Spark应用的jar文件和Spark的环境(spark-submit脚本和相关的lib)要放到所有需要执行的服务器上。
(2)Spark应用通过调用spark-submit命令启动
Java程序通过SSH调用远程机器上的shell脚本执行spark-submit命令。
优点:简单、jar和shell以及环境不需要放到太多的机器上。
缺点:如果放置Spark应用jar文件的机器宕机的话,需要考虑容错的机制。
(3)直接在Spark代码中进行任务提交到yarn(在windows上运行可能出错,linux上不不会,需要配置HADOOP_HOME环境变量)。
1)需要将yarn-site.xml放置到应用的classpath环境中,yarn-site.xml中给定yarn的资源管理的IP地址和端口号。
2)在构建Spark应用的时候,需要在SparkConf对象中给定一些相关属性。

val conf = new SparkConf()
		.setMaster("yarn-cluster")
		.setAppName("xxxxx")
		.setJars("spark应用jar文件在当前机器上所在路径")

7、测试运行

步骤:
1)将usertrack.sql的sql代码在MySQL中执行,将表插入到数据库usertrack。
2)修改tb_task中的数据值。
3)修改配置文件usertrack.properties中的内容,将JDBC的配置给定为现有环境中的具体配置项。
4)编写具体的测试启动类。

发布了238 篇原创文章 · 获赞 617 · 访问量 132万+

猜你喜欢

转载自blog.csdn.net/gongxifacai_believe/article/details/104182420