埋点日志采集
埋点在本项目中有三大类
App端行为日志
PC web端行为日志
微信小程序端行为日志
需求
日志生成在N台服务器中,现在需要使用flume采集到HDFS
- 3类日志采集后要分别存储到不同的hdfs路径
- 日志中的手机号,账号需要脱敏处理(加密)
- 不同日期的数据要写到不同的文件夹,且分配应以事件时间为依据
- 因为日志服务器所在子网跟HDFS集群不在同一个网段,需要中转传输
上游
a1.sources = r1
a1.channels = c1
a1.sinks = k1 k2
a1.sources.r1.channels = c1
a1.sources.r1.type = TAILDIR
a1.sources.r1.filegroups = g1 g2
a1.sources.r1.filegroups.g1 = /opt/data/logdata/app/event.*
a1.sources.r1.filegroups.g2 = /opt/data/logdata/wx/event.*
a1.sources.r1.headers.g1.datatype = app
a1.sources.r1.headers.g2.datatype = wx
a1.sources.r1.batchSize = 100
a1.sources.r1.interceptors = i1
a1.sources.r1.interceptors.i1.type = cn._51doit.flnm.demo02.FieldEncryptInterceptor$FieldEncryptInterceptorBuilder
a1.sources.r1.interceptors.i1.headerName = timestamp
a1.sources.r1.interceptors.i1.timestamp_field = timeStamp
a1.sources.r1.interceptors.i1.to_encrypt_field = account
a1.channels.c1.type = file
a1.channels.c1.checkpointDir = /opt/data/flumedata/file-channel/checkpoint
a1.channels.c1.dataDirs = /opt/data/flumedata/file-channel/data
a1.sinks.k1.channel = c1
a1.sinks.k1.type = avro
a1.sinks.k1.hostname = linux02
a1.sinks.k1.port = 41414
a1.sinks.k1.batch-size = 100
a1.sinks.k2.channel = c1
a1.sinks.k2.type = avro
a1.sinks.k2.hostname = linux03
a1.sinks.k2.port = 41414
a1.sinks.k2.batch-size = 100
# 定义sink组及其配套的sink处理器
a1.sinkgroups = g1
a1.sinkgroups.g1.sinks = k1 k2
a1.sinkgroups.g1.processor.type = failover
a1.sinkgroups.g1.processor.priority.k1 = 5
a1.sinkgroups.g1.processor.priority.k2 = 1
a1.sinkgroups.g1.processor.maxpenalty = 10000
启动命令
bin/flume-ng agent -c conf/ -f agentsconf/shangyou -n a1 -Dflume.root.logger=INFO,console
下游
a1.sources = r1
a1.channels = c1
a1.sinks = k1
a1.sources.r1.channels = c1
a1.sources.r1.type = avro
a1.sources.r1.bind = 0.0.0.0
a1.sources.r1.port = 41414
a1.sources.r1.batchSize = 100
a1.channels.c1.type = file
a1.channels.c1.checkpointDir = /opt/data/flumedata/file-channel/checkpoint
a1.channels.c1.dataDirs = /opt/data/flumedata/file-channel/data
a1.sinks.k1.channel = c1
a1.sinks.k1.type = hdfs
a1.sinks.k1.hdfs.path = hdfs://linux01:8020/logdata/%{datatype}/%Y-%m-%d/
a1.sinks.k1.hdfs.filePrefix = DoitEduData
a1.sinks.k1.hdfs.fileSuffix = .log.gz
a1.sinks.k1.hdfs.rollInterval = 600
a1.sinks.k1.hdfs.rollSize = 268435456
a1.sinks.k1.hdfs.rollCount = 0
a1.sinks.k1.hdfs.batchSize = 100
a1.sinks.k1.hdfs.codeC = gzip
a1.sinks.k1.hdfs.fileType = CompressedStream
a1.sinks.k1.hdfs.useLocalTimeStamp = false
拦截器jia包代码
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.flume.Context;
import org.apache.flume.Event;
import org.apache.flume.interceptor.Interceptor;
import java.io.UnsupportedEncodingException;
import java.util.List;
/**
* @author 涛哥
* @nick_name "deep as the sea"
* @contact qq:657270652 wx:doit_edu
* @site www.doitedu.cn
* @date 2021-01-11
* @desc 项目字段加密及时间戳提取拦截器
*/
public class FieldEncryptInterceptor implements Interceptor {
String timestamp_field;
String to_encrypt_field;
String headerName;
public FieldEncryptInterceptor(String timestamp_field, String to_encrypt_field, String headerName) {
this.timestamp_field = timestamp_field;
this.to_encrypt_field = to_encrypt_field;
this.headerName = headerName;
}
public void initialize() {
}
public Event intercept(Event event) {
// 根据要加密的字段,从event中提取原值(用json解析)
try {
String line = new String(event.getBody());
JSONObject jsonObject = JSON.parseObject(line);
String toEncryptField = jsonObject.getString(to_encrypt_field);
String timeStampField = jsonObject.getString(timestamp_field);
// 加密
if (StringUtils.isNotBlank(toEncryptField)) {
String encrypted = DigestUtils.md5Hex(toEncryptField);
// 将加密后的值替换掉原值
jsonObject.put(to_encrypt_field, encrypted);
// 转回json,并放回event
String res = jsonObject.toJSONString();
event.setBody(res.getBytes("UTF-8"));
}
// 放入时间戳到header中
event.getHeaders().put(headerName, timeStampField);
} catch (Exception e) {
event.getHeaders().put("datatype", "malformed");
e.printStackTrace();
}
return event;
}
public List<Event> intercept(List<Event> list) {
for (Event event : list) {
intercept(event);
}
return list;
}
public void close() {
}
public static class FieldEncryptInterceptorBuilder implements Interceptor.Builder {
String timestamp_field;
String to_encrypt_field;
String headerName;
public Interceptor build() {
return new FieldEncryptInterceptor(timestamp_field, to_encrypt_field, headerName);
}
public void configure(Context context) {
timestamp_field = context.getString("timestamp_field");
headerName = context.getString("headerName");
to_encrypt_field = context.getString("to_encrypt_field");
}
}
}
项目背景(为什么要有大数据)
某些APP上线后,由于业务模式新尹,市场需求大,经过一段时间的精心运营后,逐渐积累起了上千万,用户,以及三四百万的日活量,app的业务能和产品.数量也急速膨胀,随着规模的增长,逐渐凸显出大量问题.
- 营销分析断层:市场销售成本居高不下,投放拉新的效果追踪出现断层,无法追各渠道实际化率,难以准确分析ROI
- 用户运营不精准:"千人一面"的全量用户营销,投入产量难以把控,不精准的粗犷方式难以真正提升存量用户的长期活跃度
- 全局运营指标监控不实时:有运营的BI系统,但运营指标监控不及时,未形成核心的指标预警机制,决策滞后
公司急需告别这种粗放的,严重依赖人力的运营情况,继续建设一套强大的数据运营平台,用于驱动营销渠道效果评估,用于精细化运营改进,产品功能及用户体验优化,老板看板辅助管理决策,产品个性化推荐改造,用户标签体系构建等应用市场
从各方面为公司的进一步发展提供有力的数据支撑
需求总览:行为事件域分析
基础统计分析
- 整体概况:从产品的整体的使用情况出发,对产品的整体使用情况有基础了解
- 用户获取:从获客渠道和版本的方向出发,根据不同渠道,不同的版本生成一些可以了解渠道优略的指标,可以清晰的观察每个渠道的流量,转化等情况
- 活跃与留存:从用户的访问和粘性出发,可以观察出产品在用户访问,回访等方面的趋势变化,清楚的了解用户对产品的粘性和沉浸程度
- 事件转化;根据选择的事件和属性,生成该事件的发生次数,人数,分布等数据指标,可以了解整体的用户转化以及收益相关的数据情况
- 用户特征:根据地理位置.性别操作系统当一些基础属性,将进行分组,方便了解用户的分布占比情况
基础统计分析指标概览
整体概况
产品整体的使用情况,包括用户量、访问情况、留存等帮助对产品整体指标有一个大致的了解
累计用户量:产品上线至今的累计用户量
每日新增用户量
每日的全部访问人数、次数
每日的全部访问的人均次数/时长/深度
新老用户访问占比
每日新老用户的分布情况
新用户/全部用户的7日留存:起始和后续事件都为用户进行页面访问
各页面的访问次数分布:基于pageview事件中的页面标题属性进行分组
访问终端(app /pc web /微信小程序 /H5)分布:按照访问的操作系统分组
用户获取
渠道访问
每个渠道的用户的使用情况,包括渠道中新用户的占比、留存等,了解产品在获客层面上的优势与不足;其中App的渠道数据,会根据iOS,Android进行细分
新增用户量:全部新用户数量,包括自然流量和渠道流量
渠道新增用户量:仅计算渠道流量新增用户数
各渠道新用户人均访问时长
异常流量:App 异常流量,定义为打开5秒内即进行关闭操作的访问行为
版本数据
App 每个版本的使用情况,帮助了解在产品升级的过程中,是否在活跃和留存方面有所改善
版本访问流量
人均访问时长
各版本留存:各版本的用户7日留存
活跃与留存
访问流量
产品的每日访问数据,指标集中在新老用户的访问行为上,提供访问次数、时长、次数分布、访问时段高峰等指标,帮助了解新老用户在使用产品时的一些行为特征
访问用户数
新老用户访问占比
新老用户人均使用时长
新老用户启动/访问次数
每日/每周启动时段
用户每日访问产品的时段分布
用户每周访问产品的星期分布
用户留存
提供用户7日,次日,次周,次月留存的数据,帮助了解新老用户的使用粘性
7日留存/流失
次日留存/流失
次周留存/流失
次月留存/流失
事件转化
事件转化
各类关键事件(如收藏,分享,转发,加购等),发生次数、人数以及分布情况
新老用户事件发生次数/人数/人均次数
事件次数的分布
收益类事件转化
用户自定义收益类事件,神策会自动生成该事件的发生次数、人数以及分布情况,会根据您选择的数值类型属性,计算该数值的总值、人均值以及次均值
新老用户收益事件发生次数/人数/人均次数
新老用户收益事件
用户特征
访问省份分布
访问城市分布
访问性别分布
访问操作系统分布
新老用户占比
技术选型
数据采集:flume
存储平台:hdfs
基础设施:hive
运算引擎:mapreduce/spark
资源调度:yarn
任务调度:azkaban/oozie
元数据管理:atlas(或自研系统)
OLAP引擎:kylin/presto (或clickhouse)
前端界面:superset(或自研javaweb系统)
分层设计
分层原因
数据仓库中的数据表,往往是分层管理、分层计算的;
所谓分层,具体来说,就是将大量的数据表按照一定规则和定义来进行逻辑划分;
ADS层: 应用服务层
DWS层:数仓服务(service/summary)层(轻度聚合)
DWD层:数仓明细层
ODS层:操作数据(最原始的数据)层 – 贴源层
DIM层:存储维表
ODS层:对应着外部数据源ETL到数仓体系之后的表!
DWD层:数仓明细层;一般是对ODS层的表按主题进行加工和划分;本层中表记录的还是明细数据;
DWS层:数仓服务层;
ADS层: 应用层,主要是一些结果报表!
分层的意义:数据管理更明晰!运算复用度更高!需求开发更快捷!便于解耦底层业务(数据)变化!
分层详解
ODS层
数据内容:存放flume采集过来的原始日志
存储格式:以json格式文本文件存储
存储周期:3个月
DWD层
数据内容:对ODS层数据做ETL处理后的扁平化明细数据
存储格式:以orc / parquet文件格式存储
存储周期:6个月
DWS层
数据内容:根据主题分析需求,从DWD中轻度聚合后的数据
存储格式:以ORC/PARQUET文件格式存储
存储周期:1年
ADS层
数据内容:根据业务人员需求,从DWS计算出来的报表
存储格式:以ORC/PARQUET文件格式存储
存储周期:3年
DIM层
存储各种维表
模型设计
ODS层
与原始日志数据保持完全一致
我们有APP端日志,PC端日志,微信小程序端日志,分别对应ODS的三个表
ODS.ACTION_APP_LOG
ODS.ACTION_WEB_LOG
ODS.ACTION_WXAPP_LOG
建表时,一般采用外部表;
表的数据文件格式:跟原始日志文件一致
分区:按天分区(视数据量和计算频度而定,如数据量大且需每小时计算一次,则可按小时粒度分区)
DWD层
建模思想
通常是对ODS层数据进行精细化加工处理
不完全星型模型
事实表中,不是所有维度都按维度主键信息存储(维度退化)
地域维度信息:年月日周等时间维度信息,这些维度信息,基本不会发生任何改变,并且在大部分主题分析场景中,都需要使用,直接在事实表中存储维度值
页面信息:页面类别信息,频道信息,业务活动信息,会员等级信息等,可能发生缓慢变化的维度信息,事实表中遵循经典理论存储维度主键,具体维度值则在主题分析计算时临时关联
事实表
app_event_detail: APP-Event事件明细表
web_event_detail: WEB-Event事件明细表
wxapp_event_detail: 小程序-Event事件明细表
维度表
coupon_info
ad_info
campain_info
lanmu_info
page_info
page_type
pindao_info
promotion_location
huodong_info
miaosha_info
product
product_detail
product_type
shop_info
tuangou_info
user_info
DWS层
建模思想
主题建模
维度建模
最主要思路:按照分析主题,"汇总"各类数据成大宽表
也有一些做法是,将DWS层的表设计成“轻度聚合表”
主要表模型
流量会话聚合天/月表
日新日活维度聚合表
事件会话聚合天/月表
访客连续活跃区间表
新用户留存维度聚合表
运营位维度聚合表
渠道拉新维度聚合表
访客分布维度聚合表
用户事件链聚合表(支撑转化分析,高级留存分析等)
……更多
ODS层详细设计ODS层功能
ODS:操作数据层
主要作用:直接映射操作数据(原始数据),数据备份;
建模方法:与原始数据结构保持完全一致
存储周期:相对来说,存储周期较短;视数据规模,增长速度,以及业务的需求而定;对于埋点日志数据ODS层存储,通常可以选择3个月或者半年;存1年的是土豪公司(或者确有需要,当然,也有可能是数据量很小)
ODS层开发手册
3类日志数据
数据类型 输入路径(HDFS目录) 目标位置(HIVE表)
app端埋点日志 /logdata/app/2020-07-26/ ods.event_app_log 分区:2020/07/26
web端埋点日志 /logdata/web/2020-07-26/ ods.event_web_log 分区:2020/07/26
wx小程序埋点日志 /logdata/wxapp/2020-07-26/ ods.event_wxapp_log 分区:2020/07/26
H5页面埋点日志 /
表明命名规范:层级.主题_数据域_功能_粒度
比如:app事件明细表: dwd.event_app_detail_d
要求:
原始日志格式
普通文本文件,JSON数据格式,导入hive表后,要求可以很方便地select各个字段
分区表
外部表
创建外部表
由于原始数据是普通文本文件,而文件内容是json格式的一条一条记录
在创建hive表结构进行映射时,有两种选择:
1.将数据视为无结构的string
2.将数据按json格式进行映射(这需要外部工具包JsonSerde 的支持)
本项目采用方案2来进行建表映射
hive完整建表语句和解析
--指定表名
create table t3(
id int,
name string
)
--指定分区
PARTITIONED BY (dt string)
--指定分隔符
ROW FORMAT
SERDE 'org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe'
WITH SERDEPROPERTIES (
'field.delim'=',',
'line.delim'='\n'
)
--指定输入输出格式
STORED AS
INPUTFORMAT
'org.apache.hadoop.mapred.TextInputFormat'
OUTPUTFORMAT
'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
--表属性
TBLPROPERTIES(
'EXTERNAL'='TRUE',
'comment'='this is a ods table',
'orc.compress'='snappy'
)
;
-- ODS层,app埋点日志对应表模型创建
--先删除表防止有这个表明
DROP TABLE IF EXISTS `ods.event_app_log`;
--指定表明和外部表的属性
CREATE EXTERNAL TABLE `ods.event_app_log`(
`account` string ,
`appid` string ,
`appversion` string ,
`carrier` string ,
`deviceid` string ,
`devicetype` string ,
`eventid` string ,
`ip` string ,
`latitude` double ,
`longitude` double ,
`nettype` string ,
`osname` string ,
`osversion` string ,
`properties` map<string,string> ,
`releasechannel` string ,
`resolution` string ,
`sessionid` string ,
`timestamp` bigint
)
--指定分区
PARTITIONED BY (`dt` string)
--指定分隔符这边是以json格式切割
ROW FORMAT SERDE
'org.apache.hive.hcatalog.data.JsonSerDe'
--指定输入输出格式
STORED AS INPUTFORMAT
'org.apache.hadoop.mapred.TextInputFormat'
OUTPUTFORMAT
'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
--指定输出路径
LOCATION
'hdfs://linux01:8020/user/hive/warehouse/ods.db/event_app_log'
--表属性版本号之类的没啥用
TBLPROPERTIES (
'bucketing_version'='2',
'transient_lastDdlTime'='1610337798'
);
接下来就是具体操作
流程是我们会从别的数据库通过flime导到自己的hdfs中,然后经过自己的hive处理成结构化数据再放回到自己的hdfs上,原来的数据就没了
flime就是上面的上游下游,hive建表就是上面的埋点数据建模然后load一下
load data inpath '/logdata/app/2021-01-10' into table ods.event_app_log partition(dt='2021-01-10');
-- 数据入库
load data inpath '/logdata/app/2021-01-10' into table ods.event_app_log partition(dt='2021-01-10');
-- 如何删除一个表中已存在分区
alter table ods.event_app_log drop partition(dt='2020-01-10');
-- 不适用load,如何添加一个分区到已存在的表中
alter table ods.event_app_log add partition(dt='2020-01-11') location '/abc/ddd/'
-- 入库脚本开发
#!/bin/bash
######################################
#
# @author :
# @date : 2021-01-11
# @desc : app端埋点日志入库
# @other
######################################
export JAVA_HOME=/opt/apps/jdk1.8.0_141/
export HIVE_HOME=/opt/apps/hive-3.1.2/
DT=$(date -d'-1 day' +%Y-%m-%d)
${HIVE_HOME}/bin/hive -e "
load data inpath '/logdata/app/${DT}' into table ods.event_app_log partition(dt='${DT}')
"
if [ $? -eq 0 ]
then
echo "${DT}app success"
else
echo "wx failed"
fi