第一章 需求分析
需求分析与设计
项目需求背景
"某APP上线后 经营得当 使用户 日活量增多 出现以下问题"
"营销分析断层:"
市场营销成本居高不下,投放拉新的效果追踪出现断层,无法追踪各渠道实际转化率,难以准确分析 ROI。
"产品迭代无法量化:"
缺少实时的用户行为分析能力,使产品功能不知道怎么改 改好了也不知道效果怎么样
"用户运营不精准:"
“千人一面”的全量用户营销,投入产出难以把控,不精准的粗犷方式难以真正提升存量用户的长期活跃度。
"全局运营指标监控不实时:"
有运营的 BI 系统,但运营指标监控不及时,未形成核心的指标预警机制,决策滞后
项目组成
"数据采集"
数据从哪来,怎么来,到哪去
"数据仓库"
各种数据的中央存储系统,提供数据的存储、管理和分析功能 为用户提供 数据报表 和决策支持
"可视化展示"
将数据转为更方便查看的状态 如数据表、折线图、饼状图
"服务治理"
对于数仓数据的元数据管理和用于维护集群环境的集群监控服务
开发目标
"开发一个综合性的数据采集平台、数据分析平台、可视化展示平台以及数据治理平台"
"技术架构"
以HDFS作为最底层存储
以Hive作为数仓基础设施
以Spark作为核心运算引擎
以Flume、Datax、Azkaban(任务调度)、Atlas(元数据管理)、Griffin(数据质量监测系统)等
作为外围粘合辅助系统
以Kylin/Clickhouse作为OLAP(联机数据分析)分析引擎
"前端展示"
报表与数据可视化平台
模型分析平台
需求分析
行为域基础分析
"行为域基础(流量)分析"
整体概况: 从产品整体的使用情况出发,对产品整体的使用情况有基础了解。
用户获取: 从获客渠道和版本的方向出发 观察用户更容易从那个渠道访问APP
活跃与留存:从用户的访问和粘性出发,可以观察出产品在用户访问、回访等方面的趋势变化,
事件转化: 根据选择的事件和属性,生成该事件的发生次数、人数、分布等数据指标,可以了解整体的用户转化以及收益相关的数据情况。
用户特征:根据地址位置、性别、操作系统等一些基础属性,将用户进行分组,方便了解用户的分布占比情况。
"整体流量概况"
每日新增多少用户 累计多少用户 每日的全部访问的人均次数/时长/深度
"访问渠道分析"
每个渠道的用户的使用情况 各渠道新用户人均访问时长
"用户分布分析"
用户的地址 性别 在那个渠道注册的 什么时候注册的
"APP版本分析"
看每个版本的使用情况 版本访问流量 人均访问时长
"活跃度分析"
帮助了解新老用户在使用产品时的一些行为特征 新老用户人均使用时长 访问次数
"用户留存/流失分析"
提供用户7日,次日,次周,次月留存/流失的数据,帮助了解新老用户的使用粘性
"事件转化分析"
各类关键事件(如收藏,分享,转发,加购等),发生次数、人数以及分布情况
自定义收益类事件,神策自动生成该事件的发生次数、人数以及分布情况
用户留存:用户从指定时间开始,经历一段时间以后仍然有活跃行为,则记为1次留存。最常见的是新用户留存
用户流失:人为定义一个时间点为流失节点,比如用户12个月未登录之类。达到节点的,即为流失用户
行为域进阶分析
"转化漏斗分析"
漏斗模型主要用于分析一个多步骤过程中每一步的转化与流失情况。
如:浏览商品--添加购物车--结算购物车--点击付款--完成付款
"事件留存分析"
留存分析是用来分析用户 参与情况/活跃程度 的分析模型 新生教程
新手引导页面的优化 某个功能修改后的期望
"行为分布分析"
告诉你 哪些功能比较常用 策略调整前后 提高用户复购率 提升核心用户量
"行为归因分析" ***
分析某个广告位、推广位对目标事件的转化贡献率
"行为路径分析"
主要用于分析用户在使用产品时的路径分布情况
"行为间隔分析"
产品,运营,市场等人员的日常工作都需要观察某某业务的转化情况
复杂注册流程的整个过程花费的时长分布 新用户登录到第一次下单的间隔分布
"其他高阶分析"
对于暂时无法满足的高级数据需求,提供了更加自由的自定义查询功能
"行为归因分析" ***
目标转化事件: 选择产品的目标事件 比如这个产品的目标是 客户付款这个事件
前向关键事件: 选择目标转化事件的前向事件 提升归因模型的计算精度 一般选择与目标事件有强关联的事件
关联属性 将目标转化事件和前向关键事件进行关联的属性
待归因事件 一般为与广告曝光、推荐曝光等运营相关事件,如:点击广告位、点击推荐位..
业务域分析
"业务域的数据大概包括交易域、营销域、运营活动域等等"
购物车分析 交易金额分析 复购率分析 优惠券分析 团购分析
秒杀限时购分析 其他营销活动 广告运营位分析 拉新注册分析 会员分析(用户画像)
用户画像分析
"用户画像是根据用户社会属性、生活习惯和消费行为等信息而抽象出的一个标签化的用户模型。"
就是给用户贴标签 标签是通过对用户信息分析而来的高度精炼的特征标识
作用: 精准营销 根据历史用户特征,分析产品的潜在用户和用户的潜在需求
用户统计 根据用户的属性、行为特征对用户进行分类后,统计不同特征下的用户数量、分布
数据挖掘 以用户画像为基础构建推荐系统、搜索引擎、广告投放系统,提升服务精准度
服务产品 对产品进行用户画像,对产品进行受众分析
行业报告&用户研究 通过用户画像分析了解行业动态,如人群消费习惯、消费偏好 不同地域品类消费差异
基本属性画像:性别 地域 注册时间 手机号 手机类型
行为习惯画像:月登陆次数 月下单次数 月收藏次数 月点赞次数
消费习惯画像:月消费金额 周消费金额 月优惠券使用金额
项目框架
"技术选型主要考虑因素:数据量大小、业务需求、行业内经验、技术成熟度、开发维护成本、总成本预算"
数据采集传输 Flume、 Kafka、 Sqoop、 DataX、 Logstash
数据存储: Mysql、 HDFS、 HBase、 Redis、 MongoDB
数据计算: Hive、 Tez、 Spark、 Flink、 Storm
数据查询: Presto、 Kylin、 Impala、 Druid
数据可视化 Echarts、 Superset、 QuickBI、 DataV
任务调度: Azkaban、 Oozie、 DolphinScheduler
集群监控: Zabbix、 Ganglia、 Prometheus
元数据管理 Atlas
系统数据流程设计
组件版本选型 框架选型尽量不要选择最新的框架,选择最新框架半年前左右的稳定版
服务器选型 物理机: 云主机
集群资源规划设计 确认集群规模
流程设计
"数据生成"
业务数据已经在业务系统的数据库中 历史数据 其他第三方数据
"数据采集汇聚"
行为域数据 业务域数据
"数据仓库&用户画像"
数据仓库 用户画像
"数据服务& OLAP分析平台"
用户明细数据 固定报表查询 规范模型自助多维分析
"其他辅助系统"
Azkaban/Oozie任务调度系统 Atlas元数据和血缘追溯管理(数据治理)
第二章 数据仓库
数仓分层
常见分层模型
- ODS层:原始数据层,存放原始数据,直接加载原始日志、数据,数据保持原貌不被处理。
- DWD层:对ODS层数据进行清洗(去除空值,脏数据,超过极限范围的数据)、脱敏。
- DWS层:以DWD层为基础,按天进行轻度汇总。一行信息代表一个主题对象一天的汇总行为,例如一个用户一天下单次数。
- ADS层:为各种统计报表提供数据。
数据仓库为什么分层
- 拆解复杂任务:每一层只处理简单的任务,方便定位问题。
- 减少重复开发:规范数据分层,通过的中间层数据,能够减少计大的重复计算,增加一次计算结
果的复用性。 - 隔离原始数据:不论始数据的异常还是数据的敏感性,使真实数据与统计数据解耦开。
- 清晰数据结构:每一个数据分层都有它的作用域,这样我们在使用表的时候能更方便地定位和理
解。 - 方便数据血缘追踪:我们最终给业务呈现的是一个能直接使用业务表,但是它的来源有很多,如
果有一张来源表出问题了,我们希望能够快速准确地定位到问题,并清楚它的危害范围。
数据集市与数据仓库
数仓命名规范
表命名
- ODS层命名为ods_表名
- _DWD层命名为dwd_dim/fact_表名
- DWS层命名为dws_表名
- _DWT层命名为dwt_表名
- ADS层命名为ads_表名
- 临时表命名为xxx_tmp
- 用户行为表,以log为后缀
脚本命名
- 数据源 to 目标_db/log.sh
- 用户行为脚本以log为后缀
- 业务数据脚本以db为后缀
表字段类型
数仓理论
第三章 数据采集
埋点
埋点:植入业务系统 收集事件数据的SDK(工具代码)
后端埋点:java PHP 前端埋点:原生APP 页面JS
用于收据数据
行为数据采集开发
概述
Flume采集开发
采集数据
行为数据 通过埋点 记录用户行为存到文件中
业务数据 通过业务系统 如淘宝 京东 记录的是一条条事件
客户端 Client: 生产数据
事件 Event : 一个数据单元 由消息头和消息体组成
流 Flow: Event 事件从源头到目的地的抽象过程
选择器 selector: 作用于源文件Source端 决定数据发往那个地方
拦截器 interceptor: Flume允许使用拦截器拦截数据 作用于 源文件 和 目标地址
代理 Agent : 一个独立的Flume进程,包含组件Source、 Channel、 Sink
源文件Source :数据收集组件 (文件 路径 命令)
管道 Channel:中转事件Event的一个临时存储,保存由Source组件传递过来的事件Event(内存 文件 数据表)
目标地址Sink ;从Channel中读取并移除Event, 将Event传递到下一个代理Agent(文件 HDFS Flume 数据库)
sources
# 命名此代理上的组件
a1.sources = r1
a1.sinks = k1
a1.channels = c1
a1.sources.r1.channels = c1
#类型
a1.sources.r1.type = TAILDIR
#文件组名称:可跟多个
a1.sources.r1.filegroups = f1
#读取该文件夹下以app开头的文件
a1.sources.r1.filegroups.f1 = /opt/module/applog/log/app.*
#存储读取文件偏移量,断点续传的关键
a1.sources.r1.positionFile = /var/log/flume/taildir_position.json
#日志为1K一条时,batchSize设置为500最合适(在一个事物中可以处理多少个event,超过就会提交;batchSize一定不能大于transactionCapacity)
a1.sources.r1.batchSize = 500
#控制从同一个文件连续读取的最大次数量
a1.sources.ri.maxBatchCount = 1000
#是否添加存储绝对路径文件名的标头。
a1.sources.r1.fileHeader = true
#将绝对路径文件名附加到事件标头时使用的标头键。
a1.sources.r1.fileHeaderKey=value--
channel
## channel1
a1.sources.r1.channels = c1
#channel类型
a1.channels.c1.type = file
#存放检查点的目录(断点续传的关键)
a1.channels.c1.checkpointDir = /opt/module/flume/checkpoint/
#数据存放目录,Filechannel会先把数据存在文件内
a1.channels.c1.dataDirs = /opt/module/flume/data/
#最大的一个文件的大小
a1.channels.c1.maxFileSize = 2146435071
#先把1000000字节的数据存在内存
a1.channels.c1.capacity = 1000000
#从内存写入到磁盘时,如果写入失败,会自动重试,6S如果还没有写入成功,断开
a1.channels.c1.keep-alive = 6
sink
a1.sources.r1.channels = c1
#类型
a1.sinks.k1.type = avro
#连接avro source
a1.sinks.k1.hostname = node03
#端口号
a1.sinks.k1.port = 4646
#描述接收器
a2.sinks.k1.channel = c1
a2.sinks.k1.type = HDFS
a2.sinks.k1.hdfs.path = hdfs://hdfs-yjx/logdata/applog/%Y-%m-%d/
a2.sinks.k1.hdfs.filePrefix = yjx-
a2.sinks.k1.hdfs.fileSuffix = .log.snappy
#多久生成一个新的文件
a2.sinks.k1.hdfs.rollInterval = 300
#设置每个文件的滚动大小大概是128M
a2.sinks.k1.hdfs.rollSize = 104857600
#文件的滚动的Event数量100
a2.sinks.k1.hdfs.rollCount = 100
a2.sinks.k1.hdfs.fileType = CompressedStream
a2.sinks.k1.hdfs.codeC = snappy
a2.sinks.k1.hdfs.useLocalTimeStamp = true
拦截器开发
- 从日志数据提取事件时间戳
- flume获取自定义的拦截器对象,就是通过调用builder的 build()方法来获取
- 为什么要这么设计
- 就是为了让你自己控制拦截器的构造过程,可以往里面传递配置文件参数
关键问题
数据延迟达到的问题
- 表现:后续流程处理中,有同时发现,2021-06-06文件夹中的日志,存在2021-06-05号的数据
- 原因:数据上报延迟,而sink在分桶的时候用的是本地时间戳
- 解决:配置了拦截器,获取日志时间戳,得到事件时间
数据积压的问题
- 表现:后续处理流程中,有同事发现,到了17:30了,hdfs中的日志数据还在16:00
- 原因:通道阻塞,数据积压。如何发现:同事反馈,自己确认(通过查看监控系统,发现下游的agent中,channel的数据填充率长期保持接近100%)
- 解决:减少上游数据的发送节点,增加下游数据的接受节点,负载均衡
业务数据说明
业务域的数据来自业务系统的数据库,表模型非常多且关系复杂
实体表
实体表,是对一个事物(实体)进行属性描述的表(商品信息表,会员信息表)
事实表
对一件发生过的事情(事实)进行描述的表(订单表,优惠券领取、使用记录表)
字典表
一个“码”,对应一个名称
业务数据采集工具
Sqoop数据抽取工具
-
sqoop 是 apache 旗下一款“Hadoop中的各种存储系统(HDFS、HIVE、HBASE) 和关系数据库(mysql、
oracle、sqlserver等)服务器之间传送数据”的工具。 -
核心的功能有两个:
-
导入数据:MySQL,Oracle 导入数据到 Hadoop 的 HDFS、HIVE、HBASE 等数据存储系统
-
导出数据:从 Hadoop 的文件系统中导出数据到关系数据库 mysql 等 Sqoop 的本质还是一个命令行工
具,和 HDFS,Hive 相比,并没有什么高深的理论。 -
底层工作机制
DataX数据抽取工具
-
DataX 是阿里巴巴集团内被广泛使用的离线数据同步工具/平台,实现包括 MySQL、Oracle、SqlServer、
Postgre、HDFS、Hive、HBase、TableStore(OTS)、MaxCompute(ODPS)、DRDS 等各种异构数据源之间高
效的数据同步功能。 -
官网地址:https://github.com/alibaba/DataX
-
DataX本身作为离线数据同步框架,采用Framework + plugin架构构建。将数据源读取和写入抽象成为
Reader/Writer插件,纳入到整个同步框架中。
业务数据采集开发
数据抽取策略
本层直接对接DATAX/SQOOP从业务库抽取过来的各类数据表
实体/字典表/维度表
- 实体表小表(品类信息表,活动信息表,优惠券信息表等),每天抽取过来一份全量(或者一周、一月)
- 实体表大表(商品信息表),每天抽取过来一份增量数据
事实表
- 订单相关表
- 优惠券领取使用记录表
- 秒杀订阅记录表
- 每天都会抽取一份增量数据
总原则
- 小表——全量抽取
- 大表(而且会变化)——增量抽取
原始数据层ODS
从各个地方抽取来的源数据 进行简单分类
数据仓库层DW
从ODS层获取的数据按照主题建立各种数据模型
数据服务层ADS
生成具体的报表 供使用者进行分析
原始数据层ODS 数据仓库准备区,为DWD层提供基础原始数据
明细数据层DWD 为DW层提供来源明细数据,提供业务系统细节数据的长期沉淀
服务数据层DWS 为DW、ST层提供细粒度数据,细化成DWB和DWS;
主题数据层DWT 根据DW层数据按照各种维度或多种维度组合把需要查询数据进行汇总统计并作为单独的列进行存储
应用数据层ADS 面向用户应用和分析需求,包括前端报表、分析图表 面向最终结果用户
第四章 数据功能分析
数据入仓处理
行为域ODS开发
ODS层功能
ODS(Operation Data Store):操作数据层
- 主要作用:直接映射操作数据(原始数据),数据备份;
- 建模方法:与原始数据结构保持完全一致
- 存储周期:相对来说,存储周期较短;视数据规模,增长速度,以及业务的需求而定;对于埋点日
志数据ODS层存储,通常可以选择3个月或者半年;存1年的是土豪公司(或者确有需要,当然,也
有可能是数据量很小)
入仓要求
- 原始日志格式
普通文本文件,JSON数据格式,导入hive表后,要求可以很方便地select各个字段 - 分区表(减少扫描量)
- 外部表(删除表时不会直接删除数据,相对效率较高)
入仓方案
- Spark解析:通过Spark程序解析Json文件再写入Hive表中
- getJsonObject():将整个Json看作一个字段先存入Hive表中,再通过Hive自带的函数
(getJsonObject)解析再写入另一张表 - JsonSerDe:通过Hive兼容的Json解析器直接将Json数据解析到一张表中。(Hive3.X之后提供原生
的JsonSerDe)
创建表
ROW FORMAT:文件中的数据如何解析(json、csv、特殊分隔符文本文件,自定义格式数据)
STORED AS :是什么文件(textFile、ORC、Parquet ……)
入库脚本开发
入库命令
- 入库命令
load data inpath 'hdfs://hdfs-yjx/yjx/app/ods/ods_app_event_log/dt=${start_date}'
into table ods.ods_app_event_log
partition(dt='${start_date}');
- 存储符合分区格
修复分区
msck repair table ods.ods_app_event_log;
或
添加指定分区
alter table ods.ods_app_event_log add if not exists partition(dt='${start_date}');
脚本开发
#!/bin/bash
行为域DWD开发
DWD详细设计
存储规划
数据类型 | 源表 | 目标表 |
---|---|---|
app端埋点日志 | ods.ods_app_event_log | dwd.dwd_app_event_detail |
web端埋点日志 | oods.ods_web_event_log | dwd.dwd_web_event_detail |
wx小程序埋点日志 | ods.ods_wx_event_log | dwd.dwd_wx_event_detail |
技术选型
由于本层数据ETL的需求较为复杂,用hive sql实现非常困难
因而此环节将开发spark程序来实现
ETL:Extract+Transfrom+Load (Kettle(免费)、DataStage(收费))
需求分析
清洗过滤
数据解析
- 将Json打平,解析成扁平格式
- properties字段不用扁平化,转成Map类型存储即可
SESSION分割
数据规范处理
-
数据口径统一
数据集成
- 将日志中的GPS经纬度坐标解析成省、市、县(区)信息;(为了方便后续的地域维度分析)
- 将日志中的IP地址解析成省、市、县(区)信息;(为了方便后续的地域维度分析)
ID_MAPPING(全局用户标识生成)
-
为每一个用户每一条访问记录,标识一个全局唯一ID(给匿名访问记录,绑定到一个id上)
-
选取合适的用户标识对于提高用户行为分析的准确性有非常大的影响,尤其是漏斗、留存、Session 等用户相关的分析功能。
-
因此,我们在进行任何数据接入之前,都应当先确定如何来标识用户。
-
本需求的意义:
-
只使用deviceid来做用户标识的方案:
-
只用account来做用户标识的方案:
-
本质上,就形成了“设备”和“账号”的绑定(动态)
-
只要识别出来一个用户,则为这个用户专门生成一个整数类型的自增的全局唯一ID(guid)
-
新老访客标记
- 新访客,标记为1
- 老访客,标记为0
保存结果
- 最后,将数据输出为parquet格式,压缩编码用snappy
- 注:parquet和orc都是列式存储的文件格式,两者对于分析运算性的读取需求,都有相似优点在实际性能测试中(读、写、压缩性能),ORC略优于PARQUE
ORC与Parquet总结对比(补)
嵌套结构的模型(补)
关键设计
-
GEOHASH编码介绍
-
GEOHASH编码原理
GeoHash算法的步骤:
组码
-
地理位置参考点映射
create external table dim.dim_area_dict
(
geohash string,
province string,
city string,
region string
)
stored as parquet
location 'hdfs://hdfs-yjx/yjx/app/dim/dim_area_dict';
- GEOHASH编码工具包
gps坐标 转码成 geohash编码,这个算法不需要自己手写,有现成的工具包
<dependency>
<groupId>ch.hsr</groupId>
<artifactId>geohash</artifactId>
<version>1.3.0</version>
</dependency>
api调用示例:
String geohashcode = GeoHash.withCharacterPrecision(45.667, 160.876547, 6).toBase32();
IP地址地理位置解析
-
IP查找算法
10.10.5.1 - 10.10.20.2 10 - 20 江苏省,南京市,雨花区
120.8.50.6 - 120.8.60.254 50 - 60 浙江省,杭州市,武林区
log -> ip:120.8.55.98 -> 55
10.10.5.1
00001010.00001010.00000101.00000001
10*256^3+10*256^2+5*256^1+1*256^0
——————————————————————————————————
IP地址:a.b.c.d
公式:a*256^3+b*256^2+c*256^1+d*256^0
IP地理位置处理工具包
- 开源工具包ip2region(含ip数据库)
- 项目地址: https://gitee.com/lionsoul/ip2region
使用方法:
-
引入 jar 包依赖
<dependency> <groupId>org.lionsoul</groupId> <artifactId>ip2region</artifactId> <version>1.7.2</version> </dependency>
-
Api调用代码
// 初始化配置参数 val config = new DbConfig // 构造搜索器,dbFile是ip地址库字典文件所在路径 val searcher = new DbSearcher(config, "initdata/ip2region.db") // 使用搜索器,调用查找算法获取地理位置信息 val block = searcher.memorySearch("39.99.177.94") println(block)
难点设计ID_MAPPING
在登录状态下,日志中会采集到用户的登录id(account),可以做到用户身份的精确标识;而在匿名状
态下,日志中没有采集到用户的登录id
如何准确标识匿名状态下的用户,是一件棘手而又重要的事情;
难点解析
备选方案
只使用设备 ID
-
适用场景
-
局限性
关联设备 ID 和登录 ID(一对一)
-
适用场景
-
局限性
关联设备 ID 和登录 ID(多对一)
-
适用场景
-
局限性
关联设备 ID 和登录 ID(动态修正)
- 基本原则,与方案3相同
- 修正之处,一个设备ID被绑定到某个登陆ID(A)之后,如果该设备在后续一段时间(比如一个月内)被
一个新的登陆ID(B)更频繁使用,则该设备ID会被调整至绑定登陆ID(B)