用户行为日志的内容,主要包括的是用户的各项行为信息以及在进行该行为时所处的环境信息。
为什么我们需要收集这些信息呢?
因为这些信息可以帮助我们优化产品(东软睿购跨境电商项目)和为统计各项指标提供数据支撑。
获取这些信息的手段有代码埋点(前端/后端)、可视化埋点以及全埋点等。
通过和进行前后端项目开发的同学进行沟通,本项目收集和分析的用户行为信息主要有**页面的浏览记录、动作记录、曝光记录、启动记录以及错误记录。**接下来,就上述这五种记录,我们进行了分析,沟通好了有哪些数据需要采集。
页面浏览记录,记录的是访客对页面的浏览行为,该行为的环境信息主要有用户信息、时间信息、地理位置信息、设备信息、应用信息、渠道信息及页面信息等。
上图当中就是我们最为常见的一张用户在进行页面浏览时的情况,那么我们这对该种情况下,可以收集到的信息就有以下几种:
信息类型 | 具体描述 |
---|---|
用户信息 | 用户ID、设备ID |
时间信息 | 用户跳入页面的时间 |
地理位置信息 | 用户浏览页面时所处的地理位置 |
设备信息 | 设备品牌、设备型号、设备系统 |
应用信息 | 指用户访问的应用信息,例如应用的版本信息 |
渠道信息 | 指该应用下载的渠道 |
页面信息 | 页面ID、页面对象等 |
其中上述的信息都会使用json的形式来接收,示例如下所示
{
"common": { -- 环境信息
"ar": "230000", -- 地区编码
"ba": "iPhone", -- 手机品牌
"ch": "Appstore", -- 渠道
"is_new": "1",--是否首日使用,首次使用的当日,该字段值为1,过了24:00,该字段置为0。
"md": "iPhone 8", -- 手机型号
"mid": "YXfhjAYH6As2z9Iq", -- 设备id
"os": "iOS 13.2.9", -- 操作系统
"uid": "485", -- 用户id
"vc": "v2.1.134" -- app版本号
},
"page": { --页面信息
"during_time": 7648, -- 持续时间毫秒
"item": "3", -- 目标id
"item_type": "sku_id", -- 目标类型
"last_page_id": "login", -- 上页类型
"page_id": "good_detail", -- 页面ID
"sourceType": "promotion" -- 来源类型
},
"ts": 1585744374423 --跳入时间戳
}
动作记录,记录的是用户的业务操作行为,该行为的环境信息主要有用户信息、时间信息、地理位置信息、设备信息、应用信息、渠道信息及动作目标对象信息等。
上图中,当我们点击领卷购买时,就会产生动作记录,那么我们这对该种情况下,可以收集到的信息就有以下几种:
信息类型 | 具体描述 |
---|---|
用户信息 | 用户ID、设备ID |
时间信息 | 动作时间 |
地理位置信息 | 动作发生时所处的地理位置 |
设备信息 | 设备品牌、设备型号、设备系统 |
应用信息 | 指用户访问的应用信息,如应用版本 |
渠道信息 | 指应用下载的渠道 |
动作目标信息 | 动作目标对象相关信息,包括对象类型,对象ID |
其中上述的信息都会使用json的形式来接收,示例如下所示
{
"common": { -- 环境信息
"ar": "230000", -- 地区编码
"ba": "iPhone", -- 手机品牌
"ch": "Appstore", -- 渠道
"is_new": "1",--是否首日使用,首次使用的当日,该字段值为1,过了24:00,该字段置为0。
"md": "iPhone 8", -- 手机型号
"mid": "YXfhjAYH6As2z9Iq", -- 设备id
"os": "iOS 13.2.9", -- 操作系统
"uid": "485", -- 用户id
"vc": "v2.1.134" -- app版本号
},
"actions": [ --动作(事件)
{
"action_id": "favor_add", --动作id
"item": "3", --目标id
"item_type": "sku_id", --目标类型
"ts": 1585744376605 --动作时间戳
}
],
"page": { --页面信息
"during_time": 7648, -- 持续时间毫秒
"item": "3", -- 目标id
"item_type": "sku_id", -- 目标类型
"last_page_id": "login", -- 上页类型
"page_id": "good_detail", -- 页面ID
"sourceType": "promotion" -- 来源类型
},
"ts": 1585744374423 --跳入时间戳
}
曝光记录,记录的是曝光行为,该行为的环境信息主要有用户信息、时间信息、地理位置信息、设备信息、应用信息、渠道信息及曝光对象信息等。
上图的绿框中就是页面当中的曝光信息。那么我们这对该种情况下,可以收集到的信息就有以下几种:
信息类型 | 具体描述 |
---|---|
用户信息 | 用户ID、设备ID |
时间信息 | 动作时间 |
地理位置信息 | 动作发生时所处的地理位置 |
设备信息 | 设备品牌、设备型号、设备系统 |
应用信息 | 指用户访问的应用信息,如应用版本 |
渠道信息 | 指应用下载的渠道 |
曝光对象信息 | 曝光对象相关信息,包括对象类型,对象ID |
其中上述的信息都会使用json的形式来接收,示例如下所示
{
"common": { -- 环境信息
"ar": "230000", -- 地区编码
"ba": "iPhone", -- 手机品牌
"ch": "Appstore", -- 渠道
"is_new": "1",--是否首日使用,首次使用的当日,该字段值为1,过了24:00,该字段置为0。
"md": "iPhone 8", -- 手机型号
"mid": "YXfhjAYH6As2z9Iq", -- 设备id
"os": "iOS 13.2.9", -- 操作系统
"uid": "485", -- 用户id
"vc": "v2.1.134" -- app版本号
},
"displays": [
{
"displayType": "query", -- 曝光类型
"item": "3", -- 曝光对象id
"item_type": "sku_id", -- 曝光对象类型
"order": 1, -- 出现顺序
"pos_id": 2 -- 曝光位置
},
{
"displayType": "promotion",
"item": "6",
"item_type": "sku_id",
"order": 2,
"pos_id": 1
}
],
"page": { --页面信息
"during_time": 7648, -- 持续时间毫秒
"item": "3", -- 目标id
"item_type": "sku_id", -- 目标类型
"last_page_id": "login", -- 上页类型
"page_id": "good_detail", -- 页面ID
"sourceType": "promotion" -- 来源类型
},
"ts": 1585744374423 --跳入时间戳
}
启动日志以启动为单位,及一次启动行为,生成一条启动日志。一条完整的启动日志包括一个启动记录,一个本次启动时的报错记录,以及启动时所处的环境信息,包括用户信息、时间信息、地理位置信息、设备信息、应用信息、渠道信息等。
上图就是页面当中的启动信息。那么我们这对该种情况下,可以收集到的信息就有以下几种:
信息类型 | 具体描述 |
---|---|
用户信息 | 用户ID、设备ID |
时间信息 | 动作时间 |
地理位置信息 | 动作发生时所处的地理位置 |
设备信息 | 设备品牌、设备型号、设备系统 |
应用信息 | 指用户访问的应用信息,如应用版本 |
渠道信息 | 指应用下载的渠道 |
开屏广告信息 | 包括广告ID等信息 |
其中上述的信息都会使用json的形式来接收,示例如下所示
{
"common": {
"ar": "370000",
"ba": "Honor",
"ch": "wandoujia",
"is_new": "1",
"md": "Honor 20s",
"mid": "eQF5boERMJFOujcp",
"os": "Android 11.0",
"uid": "76",
"vc": "v2.1.134"
},
"start": {
"entry": "icon", --icon手机图标 notice 通知 install 安装后启动
"loading_time": 18803, --启动加载时间
"open_ad_id": 7, --广告页ID
"open_ad_ms": 3449, -- 广告总共播放时间
"open_ad_skip_ms": 1989 -- 用户跳过广告时点
},
"err":{ --错误
"error_code": "1234", --错误码
"msg": "***********" --错误信息
},
"ts": 1585744304000
}
错误记录,记录的是用户在使用应用过程中的报错行为,该行为的环境信息主要有用户信息、时间信息、地理位置信息、设备信息、应用信息、渠道信息、以及可能与报错相关的页面信息、动作信息、曝光信息和动作信息。
示例如下所示:
"err":{ --错误
"error_code": "1234", --错误码
"msg": "***********" --错误信息
},
通过上述分析,我们可以大致将这五种记录归结为在用户使用的过程当中可以产生两种日志,分别是页面日志和启动日志。
页面日志,以页面浏览为单位,即一个页面浏览记录,生成一条页面埋点日志。一条完整的页面日志包含,一个页面浏览记录,若干个用户在该页面所做的动作记录,若干个该页面的曝光记录,以及一个在该页面发生的报错记录。除上述行为信息,页面日志还包含了这些行为所处的各种环境信息,包括用户信息、时间信息、地理位置信息、设备信息、应用信息、渠道信息等。
一条完整的页面日志示例如下所示:
{
"common": { -- 环境信息
"ar": "230000", -- 地区编码
"ba": "iPhone", -- 手机品牌
"ch": "Appstore", -- 渠道
"is_new": "1",--是否首日使用,首次使用的当日,该字段值为1,过了24:00,该字段置为0。
"md": "iPhone 8", -- 手机型号
"mid": "YXfhjAYH6As2z9Iq", -- 设备id
"os": "iOS 13.2.9", -- 操作系统
"uid": "485", -- 用户id
"vc": "v2.1.134" -- app版本号
},
"actions": [ --动作(事件)
{
"action_id": "favor_add", --动作id
"item": "3", --目标id
"item_type": "sku_id", --目标类型
"ts": 1585744376605 --动作时间戳
}
],
"displays": [
{
"displayType": "query", -- 曝光类型
"item": "3", -- 曝光对象id
"item_type": "sku_id", -- 曝光对象类型
"order": 1, -- 出现顺序
"pos_id": 2 -- 曝光位置
},
{
"displayType": "promotion",
"item": "6",
"item_type": "sku_id",
"order": 2,
"pos_id": 1
},
{
"displayType": "promotion",
"item": "9",
"item_type": "sku_id",
"order": 3,
"pos_id": 3
}
],
"page": { --页面信息
"during_time": 7648, -- 持续时间毫秒
"item": "3", -- 目标id
"item_type": "sku_id", -- 目标类型
"last_page_id": "login", -- 上页类型
"page_id": "good_detail", -- 页面ID
"sourceType": "promotion" -- 来源类型
},
"err":{ --错误
"error_code": "1234", --错误码
"msg": "***********" --错误信息
},
"ts": 1585744374423 --跳入时间戳
}
启动日志以启动为单位,及一次启动行为,生成一条启动日志。一条完整的启动日志包括一个启动记录,一个本次启动时的报错记录,以及启动时所处的环境信息,包括用户信息、时间信息、地理位置信息、设备信息、应用信息、渠道信息等。
一条完整的启动日志示例如下所示:
{
"common": {
"ar": "370000",
"ba": "Honor",
"ch": "wandoujia",
"is_new": "1",
"md": "Honor 20s",
"mid": "eQF5boERMJFOujcp",
"os": "Android 11.0",
"uid": "76",
"vc": "v2.1.134"
},
"start": {
"entry": "icon", --icon手机图标 notice 通知 install 安装后启动
"loading_time": 18803, --启动加载时间
"open_ad_id": 7, --广告页ID
"open_ad_ms": 3449, -- 广告总共播放时间
"open_ad_skip_ms": 1989 -- 用户跳过广告时点
},
"err":{ --错误
"error_code": "1234", --错误码
"msg": "***********" --错误信息
},
"ts": 1585744304000
}
对于用户行为日志的生成,使用的是已经提供好的模拟用户行为日志生成的工具,其中包括一个用sparingboot写的模拟用户行为日志生成的程序,一个yml配置文件,一个logback配置文件以及一个path.json文件,我们可以通过配置文件来更改相关的用户行为日志生成的信息,比如生成的用户行为日志中每一条记录发生的时间等信息。
下面介绍一下日志如何通过该脚本生成:
1)首先,我们将这些文件上传到服务器上的指定文件夹下(我存到的是/opt/module/applog下):
2)我们在该路径下执行 java - jar mock_log_data.jar 便可以运行该程序生成对应的日志信息,日志信息存储在log文件当中:
因为本次我们共有三台服务器用于开发该项目,我们选择两台服务器模拟日志服务器来模拟生成日志(hadoop102和hadoop103),因此我们需要在这两台服务器上的’/opt/module/applog’位置处都添加日志生成工具,日志产生的位置在‘/opt/module/applog/log’下。
为了方便集群当中两台服务器上的日志的生成,进行集群日志生成脚本的编写,本次项目中,我们的脚本统一放在了hadoop102这台服务器的/home/hadoop/bin的目录下,我们把该路径设置为系统路径:
接下来,我们开始在该目录下进行脚本的编写,该脚本lg.sh 的作用是同时运行hadoop102和hadoop103上模拟生成日志的工具,脚本的内容如下所示:
#!/bin/bash
for i in hadoop102 hadoop103; do #遍历两台服务器,分别执行对应的命令
echo "========== $i =========="
ssh $i "cd /opt/module/applog/; java -jar mock_log_data.jar >/dev/null 2>&1 &"
done
为了不将日志信息打印到控制台上,使用了 >/dev/null 2>&1将标准输出和错误输出都丢失掉。
之后我们再使用 chmod u+x lg.sh 来修改脚本的权限,使脚本可执行。
至此为止,我们模拟业务生成部分已经完成,可以正式开始搭建用户行为日志数据采集通道。
在本次项目中,我们在hadoop102和hadoop103两台服务器上分别使用Flume来监听日志文件的变化,将产生的用户行为日志数据发送到Kafka的topic_log主题当中。之后,我们在hadoop104上启动flume来监听Kafka中主题topic_log,将其中的数据存放到指定的目录下。为了合理的存放数据,我们将数据存放到了该行为所发送的那一天的目录下。
为了方便我们查询集群中hadoop,zookeeper等集群进程的启动情况,我们编写相关脚本(/home/hadoop/bin目录下)xcall.sh。具体内容如下:
#! /bin/bash
for i in hadoop102 hadoop103 hadoop104
do
echo "========== $i =========="
ssh $i "$*"
done
之后我们在对应的目录下修改该脚本的目录权限
[root@hadoop102 bin]$ chmod 777 xcall.sh
如果我们想使用该脚本查看所有的java进行信息,只需要输入:
[root@hadoop102 bin]$ xcall.sh jps
本次hadoop的版本使用的是3.1.3版本,集群的部署规划如下图所示:
hadoop102 | hadoop103 | hadoop104 | |
---|---|---|---|
HDFS | NameNode DataNode | DataNode | SecondaryNameNode DataNode |
YARN | NodeManager | ResourceManager NodeManager | NodeManager |
在将hadoop安装包解压到指定文件目录(/opt/module/hadoop-3.1.3)下后,我们需要对集群进行相关配置。
1)首先我们对核心文件进行配置
vim /opt/module/hadoop-3.1.3/etc/hadoop/core-site.xml
文件的内容如下图所示:
<configuration>
<property>
<name>fs.defaultFSname>
<value>hdfs://hadoop102:8020value>
property>
<property>
<name>hadoop.tmp.dirname>
<value>/opt/module/hadoop-3.1.3/datavalue>
property>
<property>
<name>hadoop.http.staticuser.username>
<value>rootvalue>
property>
<property>
<name>hadoop.proxyuser.root.hostsname>
<value>*value>
property>
<property>
<name>hadoop.proxyuser.root.groupsname>
<value>*value>
property>
<property>
<name>hadoop.proxyuser.root.usersname>
<value>*value>
property>
configuration>
2)我们对HDFS的配置文件进行配置
vim /opt/module/hadoop-3.1.3/etc/hadoop/core-site.xml
具体内容如下所示:
<configuration>
<property>
<name>dfs.namenode.http-addressname>
<value>hadoop102:9870value>
property>
<property>
<name>dfs.namenode.secondary.http-addressname>
<value>hadoop104:9868value>
property>
<property>
<name>dfs.replicationname>
<value>3value>
property>
configuration>
3)我们对yarn的配置文件进行配置
vim /opt/module/hadoop-3.1.3/etc/hadoop/yarn-site.xml
具体内容如下所示:
<configuration>
<property>
<name>yarn.nodemanager.aux-servicesname>
<value>mapreduce_shufflevalue>
property>
<property>
<name>yarn.resourcemanager.hostnamename>
<value>hadoop103value>
property>
<property>
<name>yarn.nodemanager.env-whitelistname> <value>JAVA_HOME,HADOOP_COMMON_HOME,HADOOP_HDFS_HOME,HADOOP_CONF_DIR,CLASSPATH_PREPEND_DISTCACHE,HADOOP_YARN_HOME,HADOOP_MAPRED_HOMEvalue>
property>
<property>
<name>yarn.scheduler.minimum-allocation-mbname>
<value>512value>
property>
<property>
<name>yarn.scheduler.maximum-allocation-mbname>
<value>8192value>
property>
<property>
<name>yarn.nodemanager.resource.memory-mbname>
<value>8192value>
property>
<property>
<name>yarn.nodemanager.pmem-check-enabledname>
<value>falsevalue>
property>
<property>
<name>yarn.nodemanager.vmem-check-enabledname>
<value>falsevalue>
property>
configuration>
4)MapReduce的配置文件
vim /opt/module/hadoop-3.1.3/etc/hadoop/yarn-site.xml
具体内容如下:
<configuration>
<property>
<name>mapreduce.framework.namename>
<value>yarnvalue>
property>
configuration>
5)配置workers
vim /opt/module/hadoop-3.1.3/etc/hadoop/workers
文件内容:
hadoop102
hadoop103
hadoop104
之后,为了查看程序的历史运行情况,需要配置一下历史服务器。具体配置步骤如下:
1)配置mapred-site.xml
在文件当中添加如下内容:
<property>
<name>mapreduce.jobhistory.addressname>
<value>hadoop102:10020value>
property>
<property>
<name>mapreduce.jobhistory.webapp.addressname>
<value>hadoop102:19888value>
property>
2)配置日志的聚集(yarn-site.xml)
yarn.log-aggregation-enable
true
yarn.log.server.url
http://hadoop102:19888/jobhistory/logs
yarn.log-aggregation.retain-seconds
604800
最后,我们需要将hadoop分发到三台服务器当中:
xsync /opt/module/hadoop-3.1.3/
为了我们可以在后续启动hadoop集群方便,接下来我们编写Hadoop的群起脚本(/home/hadoop/bin/)myhadoop.sh:
#! /bin/bash
if [$# -lt 1 ]
then
echo "No Args Input..."
exit;
fi
case $1 in
"start")
echo " =================== 启动 hadoop集群 ==================="
echo " --------------- 启动 hdfs ---------------"
ssh hadoop102 "/opt/module/hadoop-3.1.3/sbin/start-dfs.sh"
echo " --------------- 启动 yarn ---------------"
ssh hadoop103 "/opt/module/hadoop-3.1.3/sbin/start-yarn.sh"
echo " --------------- 启动 historyserver ---------------"
ssh hadoop102 "/opt/module/hadoop-3.1.3/bin/mapred --daemon start historyserver"
;;
"stop")
echo " =================== 关闭 hadoop集群 ==================="
echo " --------------- 关闭 historyserver ---------------"
ssh hadoop102 "/opt/module/hadoop-3.1.3/bin/mapred --daemon stop historyserver"
echo " --------------- 关闭 yarn ---------------"
ssh hadoop103 "/opt/module/hadoop-3.1.3/sbin/stop-yarn.sh"
echo " --------------- 关闭 hdfs ---------------"
ssh hadoop102 "/opt/module/hadoop-3.1.3/sbin/stop-dfs.sh"
*)
echo "Input Args Error..."
;;
esac
之后我们授予该脚本执行的权限
chmod 777 myhadoop.sh
我们本次项目中zookeeper使用的版本是3.5.7,具体的集群规划如下所示:
服务器hadoop102 | 服务器hadoop103 | 服务器hadoop104 | |
---|---|---|---|
Zookeeper | Zookeeper | Zookeeper | Zookeeper |
对于zookeeper的安装,我们首先将压缩包解压到指定的目录,之后配置服务器的编号,hadoop102对应的服务器的编号为2,hadoop103对应的编号为3,hadoop104对应的编号为4。
注:编号配置在/opt/module/zookeeper-3.5.7/zkData/myid文件中
之后,我们对zoo.cfg文件进行配置:
1)首先,我们重命名opt/module/zookeeper-3.5.7/conf目录下的zoo_sample.cfg为zoo.cfg
mv zoo_sample.cfg zoo.cfg
2)之后,我们打开zoo.cfg文件,修改数据存储路径配置以及添加相关配置。
修改数据存储路径配置:
dataDir=/opt/module/zookeeper-3.5.7/zkData
之后添加相关配置:
#######################cluster##########################
server.2=hadoop102:2888:3888
server.3=hadoop103:2888:3888
server.4=hadoop104:2888:3888
为了后续方便我们启停ZK集群,我们接下来编写Zookeeper的启停脚本:
脚本地址:/home/hadoop/bin/zk.sh
#! /bin/bash
case $1 bin
"start"){
for i in hadoop102 hadoop103 hadoop104
do
echo "---------- zookeeper $i 启动 ------------"
ssh $i "/opt/module/zookeeper-3.5.7/bin/zkServer.sh start"
done
};;
"stop"){
for i in hadoop102 hadoop103 hadoop104
do
echo ---------- zookeeper $i 停止 ------------
ssh $i "/opt/module/zookeeper-3.5.7/bin/zkServer.sh stop"
done
};;
"status"){
for i in hadoop102 hadoop103 hadoop104
do
echo ---------- zookeeper $i 状态 ------------
ssh $i "/opt/module/zookeeper-3.5.7/bin/zkServer.sh status"
done
};;
esac
之后,我们为脚本增加执行权限:
chmod u+x zk.sh
上述环境安装成功之后,我们接下来对环境进行测试,查看是否能使用脚本启动停止Hadoop、Zookeeper、Kafka
接下来按以下顺序执行脚本:
myhadoop.sh start
zk.sh start
kafka.sh start
具体效果如下所示:
集群正常运行,说明我们的环境已经搭建成功。
在环境都搭建好之后,我们首先需要进行的任务是将服务器本地磁盘上的日志文件中的数据通过flume监控采集到Kafka当中。
按照我们的规划,需要采集的用户行为日志文件分布在hadoop102,hadoop103两台日志服务器上,因此我们需要在hadoop102,hadoop103两台节点配置日志采集Flume。日志采集Flume需要采集日志文件内容,并对日志格式(JSON)进行校验,然后将校验通过的日志发送到Kafka。
在此处source,channel和sink的选择中,我们选择使用TaildirSource和KafkaChannel,并且配置了日志校验拦截器。
为什么要选择TaildirSource呢?因为TailDirSource相比于ExecSource、SpoolingDirectorySource有以下这些优点:
TailDirSourceSpoolingDirectorySource监控目录,支持断点续传。:断点续传、多目录。Flume1.6以前需要自己自定义Source记录每次读取文件位置,实现断点续传。
而虽然ExecSource可以实时搜集数据,但是在Flume不运行或者Shell命令出错的情况下,数据将会丢失。
SpoolingDirectorySource监控目录,支持断点续传。Spooling Directory Source可监听一个目录,同步目录中的新文件到sink,被同步完的文件可被立即删除或被打上标记。适合用于同步新文件,但不适合对实时追加日志的文件进行监听并同步。
在对接flume到kafka时,一种常见的策略是使用TailDirSource->MemoryChannel->KafkaSink这种方式,将采集到的数据先经过内存管道,然后输送到Kafka当中,但是这种方式会有以下这些弊端:
1.MemoryChannel数据会有堆积,内存可能溢出;
2.这种方式经历了多个组件,传输的效率变低,出现问题的概率也会对应着变大。
而如果我们使用了KafkaChannel来替代MemoryChannel+KafkaSink,将KafkaChannel作为缓存,省去Sink,效率就会变高。在这种方式下,TailDirSource来监控日志文件,之后将数据输送到KafkaChannel当中,然后KafkaChannel直接将数据输送到Kafka集群当中,因为没有Sink,KafkaChannel在此处就相当于Kafka的生产者,即使数据量很大,Kafka也完全可以承受的住。
1)首先,我们现在Flume的job目录下创建file_to_kafka.conf配置文件
vim job/file_to_kafka.conf
2)编写配置文件内容:
#为各组件命名
a1.sources = r1
a1.channels = c1
#source信息
a1.sources.r1.type = TAILDIR
a1.sources.r1.filegroups = f1
a1.sources.r1.filegroups.f1 = /opt/module/applog/log/app.* #采集该目录下以app.开头的文件中的数据
a1.sources.r1.positionFile = /opt/module/flume/taildir_position.json
a1.sources.r1.interceptors = i1
a1.sources.r1.interceptors.i1.type = com.neu.flume.interceptor.ETLInterceptor$Builder #自定义flume拦截器
#channel信息
a1.channels.c1.type = org.apache.flume.channel.kafka.KafkaChannel
a1.channels.c1.kafka.bootstrap.servers = hadoop102:9092,hadoop103:9092
a1.channels.c1.kafka.topic = topic_log
a1.channels.c1.parseAsFlumeEvent = false
#绑定source和channel的关系
a1.sources.r1.channels = c1
之后,我们需要编写一个Flume的拦截器,来将异常的数据(不是json格式的数据)在上游过滤出去。
1)首先,我们需要创建一个maven项目并导入依赖:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>com.neugroupId>
<artifactId>flume-interceptorartifactId>
<version>1.0-SNAPSHOTversion>
<dependencies>
<dependency>
<groupId>org.apache.flumegroupId>
<artifactId>flume-ng-coreartifactId>
<version>1.9.0version>
<scope>providedscope>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.62version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-pluginartifactId>
<version>2.3.2version>
<configuration>
<source>1.8source>
<target>1.8target>
configuration>
plugin>
<plugin>
<artifactId>maven-assembly-pluginartifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependenciesdescriptorRef>
descriptorRefs>
configuration>
<executions>
<execution>
<id>make-assemblyid>
<phase>packagephase>
<goals>
<goal>singlegoal>
goals>
execution>
executions>
plugin>
plugins>
build>
project>
2)之后,我们在com.neu.flume.interceptor包下创建一个ETLInterceptor类。我们第一步要实现interceptor接口;第二步我们需要实现四个抽象方法:initialize()、intercept(Event event)、intercept(List events)、close(),我们主要的代码编写工作在intercept两个接口当中进行;第三步我们需要构建一个静态内部类来实现接口Builder,重写它的bulid()方法即可。
3)对于具体的编写逻辑,我们需要在intercept(Event event)中通过event.body获取当前的数据,之后将数据转换成字符串类型log,然后我们通过fastjson中的JSONObject.parseObject(log)方法来判断当前的log数据是否是json对象,如果是json,则返回当前event,如果不是json,则返回空。之后,我们再在intercept(List events)方法中将null值过滤掉即可。
具体的代码实现如下:
ETLInterceptor类:
package com.neu.flume.interceptor;
import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import org.apache.flume.Context;
import org.apache.flume.Event;
import org.apache.flume.interceptor.Interceptor;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.List;
/**
* @author majie
* 1.实现接口interceptor
* 2.实现四个抽象方法
* 3.构建一个静态内部类来实现接口Builder,重写它的bulid()方法
*/
public class ETLInterceptor implements Interceptor {
@Override
public void initialize() {
}
@Override
public Event intercept(Event event) {
//需求:过滤event中的数据是否时json格式
byte[] body = event.getBody();
String log = new String(body, StandardCharsets.UTF_8);
boolean flag = JSONUtils.isJson(log);
return flag ? event : null;
}
@Override
public List<Event> intercept(List<Event> events) {
//将处理过之后为null的event删除掉
Iterator<Event> iterator = events.iterator();
while (iterator.hasNext()){
Event event = iterator.next();
if(intercept(event) == null){
iterator.remove();
}
}
return events;
}
@Override
public void close() {
}
public static class Builder implements Interceptor.Builder{
@Override
public Interceptor build() {
return new ETLInterceptor();
}
@Override
public void configure(Context context) {
}
}
}
JSONUtils类:
package com.neu.flume.interceptor;
import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
public class JSONUtils {
public static boolean isJson(String log){
boolean flag = false;
//判断log是否时json
try {
JSONObject.parseObject(log);
flag = true;
}catch (JSONException e){
}
return flag;
}
}
在代码编写完成之后,我们需要将代码使用maven中的package来将我们的代码进行打包,为了可以将打包的代码中携带有依赖文件,我们添加了插件到pom.xml文件中:
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-pluginartifactId>
<version>2.3.2version>
<configuration>
<source>1.8source>
<target>1.8target>
configuration>
plugin>
<plugin>
<artifactId>maven-assembly-pluginartifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependenciesdescriptorRef>
descriptorRefs>
configuration>
<executions>
<execution>
<id>make-assemblyid>
<phase>packagephase>
<goals>
<goal>singlegoal>
goals>
execution>
executions>
plugin>
plugins>
build>
之后,我们将打好的jar包放到hadoop102的/opt/module/flume/lib文件夹下:
在编写完成相关代码以及flume配置文件之后,接下来我们进行功能的测试,检测是否可以使用我们编写的代码来实现将日志采集到flume当中的功能。
1)首先启动Zookeeper、Kafka集群
zk.sh start
kafka.sh start
[root@hadoop102 flume]$ bin/flume-ng agent -n a1 -c conf/ -f job/file_to_kafka.conf -Dflume.root.logger=info,console
3)启动一个Kafka的Console-Consumer
[root@hadoop102 kafka]$ bin/kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic topic_log
4)生成模拟数据
[root@hadoop102 ~]$ lg.sh
我们可以查看到,已经成功消费到了数据,说明我们该部分的功能已经测试成功。
我们需要将hadoop102节点的Flume的配置文件和拦截器jar包,向另一台日志服务器发送一份。
[root@hadoop102 flume]$ scp -r job hadoop103:/opt/module/flume/
[root@hadoop102 flume]$ scp lib/flume-interceptor-1.0-SNAPSHOT-jar-with-dependencies.jar hadoop103:/opt/module/flume/lib/
为了方便开启日志采集Flume进程,我们在hadoop102节点的/home/root/bin目录下创建脚本f1.sh
vim f1.sh
脚本的具体内容如下所示:
#!/bin/bash
case $1 in
"start"){
for i in hadoop102 hadoop103
do
echo " --------启动 $i 采集flume-------"
ssh $i "nohup /opt/module/flume/bin/flume-ng agent -n a1 -c /opt/module/flume/conf/ -f /opt/module/flume/job/file_to_kafka.conf >/dev/null 2>&1 &"
done
};;
"stop"){
for i in hadoop102 hadoop103
do
echo " --------停止 $i 采集flume-------"
ssh $i "ps -ef | grep file_to_kafka.conf | grep -v grep |awk '{print \$2}' | xargs -n1 kill -9 "
done
};;
esac
之后,我们为脚本增加执行权限:
chmod x f1.sh
测试f1启动:
[root@hadoop102 module]$ f1.sh start
[root@hadoop102 module]$ f1.sh stop
接下来,我们需要将Kafka当中汇聚的日志的数据通过Flume采集到HDFS的指定目录当中。按照规划,该Flume需将Kafka中topic_log的数据发往HDFS。并且对每天产生的用户行为日志进行区分,将不同天的数据发往HDFS不同天的路径。
此处选择KafkaSource、FileChannel、HDFSSink。这里我们也可以将FileChannel换成MemoryChannel,如果内存较为充足的话。
对于KafkaSource+FileChannel的组合,我们也可以使用KafkaChannel来进行替换,而且性能会更好,那么为什么我们不使用KafkaChannel呢?
因为我们需要在source端编写一个拦截器,用来提取每个事件中对应的事件戳,从而确定该条json数据的事件时间。
1)首先,我们现在Flume的job目录下创建file_to_kafka.conf配置文件
vim job/kafka_to_hdfs.conf
2)编写配置文件内容:
## 组件命名
a1.sources=r1
a1.channels=c1
a1.sinks=k1
## source1信息
a1.sources.r1.type = org.apache.flume.source.kafka.KafkaSource
a1.sources.r1.batchSize = 5000 #每批次的大小
a1.sources.r1.batchDurationMillis = 2000 #每批次间隔时间
a1.sources.r1.kafka.bootstrap.servers = hadoop102:9092,hadoop103:9092,hadoop104:9092
a1.sources.r1.kafka.topics=topic_log #主题
a1.sources.r1.interceptors = i1 #提取时间戳的拦截器
a1.sources.r1.interceptors.i1.type = com.root.flume.interceptor.TimestampInterceptor$Builder
## channel1信息
a1.channels.c1.type = file
a1.channels.c1.checkpointDir = /opt/module/flume/checkpoint/behavior1
a1.channels.c1.dataDirs = /opt/module/flume/data/behavior1/
a1.channels.c1.maxFileSize = 2146435071
a1.channels.c1.capacity = 1000000
a1.channels.c1.keep-alive = 6
## sink1信息
a1.sinks.k1.type = hdfs
a1.sinks.k1.hdfs.path = /origin_data/gmall/log/topic_log/%Y-%m-%d
a1.sinks.k1.hdfs.filePrefix = log-
a1.sinks.k1.hdfs.rollInterval = 10
a1.sinks.k1.hdfs.rollSize = 134217728
a1.sinks.k1.hdfs.rollCount = 0
a1.sinks.k1.hdfs.fileType = CompressedStream
a1.sinks.k1.hdfs.codeC = gzip #使用gzip压缩
## 拼装
a1.sources.r1.channels = c1
a1.sinks.k1.channel= c1
在配置文件当中的“%Y-%m-%d”是占位符,只要在event的header中有key为“timestamp”,value值为时间戳,我们就可以使用占位符,来将该时间戳转换过来的时间作为变量在配置文件当中使用。
1)FileChannel优化
通过配置dataDirs指向多个路径,每个路径对应不同的硬盘,增大Flume吞吐量。
checkpointDir和backupCheckpointDir也尽量配置在不同硬盘对应的目录中,保证checkpoint坏掉后,可以快速使用backupCheckpointDir恢复数据
2)HDFS Sink优化
HDFS小文件处理:官方默认的这三个参数配置写入HDFS后会产生小文件,hdfs.rollInterval、hdfs.rollSize、hdfs.rollCount。基于以上hdfs.rollInterval=3600,hdfs.rollSize=134217728,hdfs.rollCount =0几个参数综合作用,效果如下:
(1)文件在达到128M时会滚动生成新文件
(2)文件创建超3600秒时会滚动生成新文件
我们后续可以更改这几个参数来进行调整小文件的数量。
由于最开始,在使用flume采集Kafka主题topic_log中的数据到HDFS上去时,在指定存放路径的地方,我们使用的是系统当前的时间,也就是在Flume采集该条数据时系统所处的时间,那么就会出现一个问题。可能我们在2022年6月1日23点59分采集到了一部分数据从日志中,但是由于网络的拥堵,可能到了2022年6月2日0点1分才进入到了flume日志消费的采集通道当中,这时,我们前一天的数据便被错误的归到了下一天当中。
解决方案:
因此,我们不能使用系统当前的数据(处理时间),而应该加一个拦截器,提取处event当中的时间戳加到header当中去,从而使用事件时间来作为存放路径的依据。
由于我们已经在数据进入到kafka之前使用拦截器剔除掉了不是json格式的数据,因此在此处,我们只需要先将event.body的数据转换为json格式,然后再提取出时间戳放到header中即可。我们在com.neu.flume.interceptor包下创建TimeStampInterceptor类进行编写。
具体代码如下所示:
TimeStampInterceptor类:
package com.neu.flume.interceptor;
import com.alibaba.fastjson.JSONObject;
import org.apache.flume.Context;
import org.apache.flume.Event;
import org.apache.flume.interceptor.Interceptor;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
public class TimeStampInterceptor implements Interceptor {
@Override
public void initialize() {
}
@Override
public Event intercept(Event event) {
Map<String, String> headers = event.getHeaders();
String log = new String(event.getBody(), StandardCharsets.UTF_8);
JSONObject jsonObject = JSONObject.parseObject(log);
String ts = jsonObject.getString("ts");
headers.put("timestamp", ts);
return event;
}
@Override
public List<Event> intercept(List<Event> events) {
for (Event event : events) {
intercept(event);
}
return events;
}
@Override
public void close() {
}
public static class Builder implements Interceptor.Builder {
@Override
public Interceptor build() {
return new TimeStampInterceptor();
}
@Override
public void configure(Context context) {
}
}
}
之后我们只需要重新打包,并将打好的包放入到hadoop104的/opt/module/flume/lib文件夹下面即可。
在编写完成相关代码以及flume配置文件之后,接下来我们进行功能的测试,检测是否可以使用我们编写的代码来实现将数据从kafka当中采集到hdfs上。
1)首先启动Zookeeper、Kafka集群
zk.sh start
kafka.sh start
2)之后,启动hadoop102的日志采集flume
[root@hadoop102 flume]$ f1.sh start
3)启动hadoop104上的日志消费flume
[root@hadoop104 flume]$ bin/flume-ng agent -n a1 -c conf/ -f job/kafka_to_hdfs.conf -Dflume.root.logger=info,console
4)生成2022年5月1日的模拟数据
[root@hadoop102 ~]$ lg.sh
5)观察HDFS上是否有出现数据
我们可以查看到,已经成功将日志数据采集到HDFS上,说明我们该部分的功能已经测试成功。
由于采集通道中需要执行的脚本过多,因此,我们最终再次编写一个脚本,去按序执行之前的那些脚本。在/home/root/bin目录下创建脚本cluster.sh:
[root@hadoop102 bin]$ vim cluster.sh
脚本中的内容如下所示:
#!/bin/bash
case $1 in
"start"){
echo ================== 启动 集群 ==================
#启动 Zookeeper集群
zk.sh start
#启动 Hadoop集群
myhadoop.sh start
#启动 Kafka采集集群
kafka.sh start
#启动 Flume采集集群
f1.sh start
#启动 Flume消费集群
f2.sh start
};;
"stop"){
echo ================== 停止 集群 ==================
#停止 Flume消费集群
f2.sh stop
#停止 Flume采集集群
f1.sh stop
#停止 Kafka采集集群
kafka.sh stop
#停止 Hadoop集群
myhadoop.sh stop
#停止 Zookeeper集群
zk.sh stop
};;
esac
之后,我们增加脚本的执行权限:
[root@hadoop102 bin]$ chmod +x cluster.sh
通过上述脚本,我们便可以轻松快捷的打开日志采集的通道,用来采集用户行为日志数据到HDFS上。
1)测试cluster集群启动脚本
[root@hadoop102 module]$ cluster.sh start
[root@hadoop102 module]$ cluster.sh stop
至此,用户行为数据采集通道全部搭建完成。
我们在进行日志采集时,只需要启动Flume1,Kafka以及Flume2即可,由于我们编写了采集通道的启停脚本,因此我们在进行用户行为数据采集时,只需要输入以下命令就可以将这些集群启动:
[root@hadoop102 module]$ cluster.sh start
之后,我们使用模拟生成日志的脚本生成对应天的日志数据,该采集通道就会自动的将数据采集并传输到HDFS上指定的路径上去。