目录
3.2.5 使用supervisord管理prometheus
条件:做这个项目一定要把需要搭建的环境搭建好才能做。这里会涉及到模拟公司内容的业务数据、内容数据、行为数据进行采集。
想有个做项目的环境可以看这篇哦
项目0单节点的虚拟机做大数据开发_林柚晞的博客-CSDN博客
真的没有放过任何一个细节,项目的软件我全部分享出来了。
下面我们开始实时数仓开发吧!
1. 项目介绍
1.1项目背景
该项目的数据的上下文限定(数据范围): 公司C端产品的日志(app、网站、小程序)数据、内容数据、业务数据、元数据(用于描述数据的数据)
1.2项目需求
1)用户行为数据(行为日志)
1. 包括用户启动APP,各页面的浏览,点击,点赞,分享,收藏 广告的点击等行为日志
2. 需要进行客户端埋点: (这个项目埋点已经做好,只需要设计接口)
2)资讯数据(内容数据)
1. 数据的搜集有两种模式,拉模式和推模式。
2. 数据埋点已经做好,只需要设计接口即可
3)业务数据
该项目只是收集广告相关数据
1.3目标
1. 针对不同类型的三种数据源设计采集方案,落地到hdfs上
2. 在hdfs上以天为目录存储数据(注意:时间应该以日志里的时间为基准,所以要拦截日志中的时间,设置成目录)
3. 再维护数据仓库的ODS层的hive外部表。 还要对表设计分区(获取数据的脚本,新增分区脚本的设计, 他们之间有依赖关系)
4. 使用azkaban对脚本进行调度,
5. 使用prometheus和grafana监控这些接口指标等
1.4 二次开发
1. 日志时间的拦截--->自定义拦截器
2. 开发azkaban的警报系统(钉钉报警)
2. 项目部署
2.1业务数据采集
打开navicat
连接->mysql->如下图填写,确定
真实内容:(连接名自定义)
# 只读权限
HOST: mysql.qfbigdata.com
PORT: 3306
USER: qf001
PWD: QF-common1001-###
我们也可以使用sqoop测试一下
sqoop list-tables \
--connect "jdbc:mysql://mysql.qfbigdata.com:3306/biz" \
--username "qf001" \
--password "QF-common1001-###"
2.2 导入脚本编写和测试
mkdir -p /opt/apps/realtime/scripts/
vim /opt/apps/realtime/scripts/news-business-sqoop-start.sh
#!/bin/sh
# filename: news-business-sqoop-start.sh
# desc: 导入mysql业务表 [meta,ad_info]数据到hdfs
# 请写你安装的SQOOP的路径
SQOOP_HOME=/usr/local/sqoop/
MYSQL_CONNECT=jdbc:mysql://mysql.qfbigdata.com:3306/biz
MYSQL_USERNAME=qf001
MYSQL_PWD=QF-common1001-###
# 定义一个日期参数,如果指定日期,就导入该日期前一天的数据,否则默认为昨日数据
exec_date=$1
if [ "${exec_date}" ] ;then
exec_date=`date -d "${exec_date} 1 days ago" +%Y%m%d`
else
exec_date=`date -d "1 days ago" +%Y%m%d`
fi
echo " news-business-sqoop-start.sh exce date: ${exec_date}"
SQL_DATE=`date -d "${exec_date} 1 days" +%Y-%m-%d`
# ad_info表数据
# --split-by 列作为并行导入时的分隔列
# -m 3个并行度
# $CONDITIONS SQOOP执行SQL查询时,会加上 1=0 先去验证SQL语法是否正确如果正确,真执行的时候会变成 1=1
${SQOOP_HOME}/bin/sqoop import \
--connect ${MYSQL_CONNECT} \
--username ${MYSQL_USERNAME} \
--password ${MYSQL_PWD} \
-e "select id ,cast(ad_id as char(50)) as ad_id,cast(advertiser_id as char(50)) as advertiser_id,advertiser_name,DATE_FORMAT(create_time,'%Y-%m-%d %H:%i:%s')as create_time from ad_info where 1=1 and create_time<='${SQL_DATE} 00:00:00' AND \$CONDITIONS" \
--split-by id \
--target-dir /collection/news_business/ad_info/${exec_date} \
--delete-target-dir \
-m 3
# meta 表数据
${SQOOP_HOME}/bin/sqoop import \
--connect ${MYSQL_CONNECT} \
--username ${MYSQL_USERNAME} \
--password ${MYSQL_PWD} \
-e "select id,field,field_type,field_desc,app_version,cast(status as char(50)) as status,DATE_FORMAT(create_time,'%Y-%m-%d %H:%i:%s')as create_time from meta where 1=1 and create_time<='${SQL_DATE} 00:00:00' AND \$CONDITIONS" \
--split-by id \
--target-dir /collection/news_business/meta/${exec_date} \
--delete-target-dir \
-m 2
上面的脚本的功能:
- 加载sqoop 和mysql
- 定义一个日期函数,为了导入昨天的数据
- 对查询中的ad_info表并行导入,以及执行sql语句
- 对meta表进行查询
修改权限:
[root@qianfeng01 scripts]# chmod u+x /opt/apps/realtime/scripts/news-business-sqoop-start.sh
运行
# 导入昨天的数据
[root@qianfeng01 scripts]# ./news-business-sqoop-start.sh
# 导入指定时间的前一天的数据
[root@qianfeng01 scripts]# ./news-business-sqoop-start.sh 2022-03-26
2.3内容数据采集
2.3.1说明:
1. 我们可以使用flume的http Source作为接口,采集新闻咨询这类的内容数据
2. 北京服务器是向远程地址的9666端口发送的数据,因此采集方案中要使用9666
2.3.2采集方案的编写和测试
vim /opt/apps/realtime/scripts/news-article-flume-http.properties
# filename: news-article-flume-http.properties
# 定义一个名字为 b1001 的agent 以及三大核心组件的名字,并关联
b1001.channels = ch-1
b1001.sources = src-1
b1001.sinks = k1
b1001.sinks.k1.channel = ch-1
b1001.sources.src-1.channels = ch-1
# 设置source的相关属性
b1001.sources.src-1.type = http
b1001.sources.src-1.bind=0.0.0.0
b1001.sources.src-1.port=9666
# 设置channel的相关属性
b1001.channels.ch-1.type = memory
b1001.channels.ch-1.capacity = 10000
b1001.channels.ch-1.transactionCapacity = 100
#设置sink的相关属性
b1001.sinks.k1.type = hdfs
b1001.sinks.k1.hdfs.path = hdfs://qianfeng01:8020/collection/news_article/%Y%m%d
b1001.sinks.k1.hdfs.filePrefix = news-%Y%m%d_%H
b1001.sinks.k1.hdfs.fileSuffix = .gz
b1001.sinks.k1.hdfs.codeC = gzip
b1001.sinks.k1.hdfs.useLocalTimeStamp = true
b1001.sinks.k1.hdfs.writeFormat = Text
b1001.sinks.k1.hdfs.fileType = CompressedStream
# 禁用按照event条数来滚动生成文件
b1001.sinks.k1.hdfs.rollCount = 0
# 如果一个文件达到10M滚动
b1001.sinks.k1.hdfs.rollSize = 10485760
# 1分钟滚动生成新文件,和文件大小的滚动一起,那个先达到,执行那个
b1001.sinks.k1.hdfs.rollInterval = 60
# 参加上边连接官网说明,理论上batchSize 越大,吞吐越高。 但是HDFS Sink 调用 Hadoop RPC(包括 open、flush、close ..)超时会抛出异常,如果发生在 flush 数据阶段,部分 event 可能已写入 HDFS,事务回滚后当前 BatchSize 的 event 还会再次写入造成数据重复。 batchSize越大可能重复的数据就越多. 同时batchSize值,不能大于channel的transactionCapacity值
b1001.sinks.k1.hdfs.batchSize = 100
# 每个HDFS SINK 开启多少线程来写文件
b1001.sinks.k1.hdfs.threadsPoolSize = 10
# 如果一个文件超过多长时间没有写入,就自动关闭文件,时间单位是秒
b1001.sinks.k1.hdfs.idleTimeout = 60
这就是一个定义了的flume的三大核心组件的脚本
开启采集方案,并且设置指标监听端口:
flume-ng agent -c /usr/local/flume/conf -f /opt/apps/realtime/scripts/news-article-flume-http.properties -n b1001 -Dflume.root.logger=INFO,console -Dflume.monitoring.type=http -Dflume.monitoring.port=31002
webui访问监听端口
http://qianfeng01:31002/metrics
模拟发射数据作为source发射到hdfs上
curl -X POST qianfeng01:9666 -d '[{"header":{"name":"article"},"body":"123"}]'
编写采集方案脚本
vim /opt/apps/realtime/scripts/news-article-flume-http-start.sh
/usr/local/flume/bin/flume-ng agent -c /usr/local/flume/conf -f /opt/apps/realtime/scripts/news-article-flume-http.properties -n b1001 -Dflume.root.logger=INFO,console -Dflume.monitoring.type=http -Dflume.monitoring.port=31002
#修改权限
chmod u+x /opt/apps/realtime/scripts/news-article-flume-http-start.sh
#执行代码
/opt/apps/realtime/scripts/news-article-flume-http-start.sh
2.3.3 配置管理中心
mkdir /usr/local/frpc
cd /usr/local/frpc
wget http://doc.qfbigdata.com/qf/project/soft/frp/frpc_0.33.linux_adm64.tgz
tar -xvzf frpc_0.33.linux_adm64.tgz
2.3.4 注册域名
News是我的username,这个可以自己随便填,就是把news换掉。
/usr/local/frpc/frpc http --sd news -l 9666 -s frp.qfbigdata.com:7001 -u news
执行完之后这个界面不要动,就让它卡着,自己新开创一个窗口玩。
测试一下这个域名
curl -X POST http://news.frp.qfbigdata.com:8002 -d '[{"header":{"name":"article"},"body":"123"}]'
一发送就有消息,有个页面蓝了
配置到管理中心
curl -X POST \
http://metadata.frp.qfbigdata.com:8112/api/v1/meta/register \
-F data_url=http://news.frp.qfbigdata.com:8002 \
-F type=2 \
-F name=news
我把这个语句复制出来
{"code":200,"data":{"id":6579,"data_url":"http://news.frp.qfbigdata.com:8002","type":"2","name":"news","created_at":1648305709,"updated_at":1648305709},"error_code":0,"msg":"ok","status":200}
上面已经出现了ok
2.4 日志数据采集
2.4.1 nginx服务器的搭建
1)搭建Openresty
其实在上篇文章已经讲了如何安装。
yum -y install yum-utils
yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
yum -y install openresty
我们来看看/usr/local/下面有没有Openresty
记住这个niginx服务器的文件夹,如果niginx执行出错了,openresty也不能删,不要想着重新安装,我至今没有找到如果openresty文件夹删了如何复原的办法。
2)部署目录结构
下面这个不是代码,是一个脚本的目录框架
-- 所有脚本
/opt/apps/realtime/scripts/
-- nginx的主配置文件目录
/opt/apps/realtime/conf/
-- nginx的副配置文件目录
/opt/apps/realtime/conf/vhost/
--nginx的日志存储文件位置(即我们要搜集的日志数据)
/opt/apps/realtime/logs/
--nginx的日志存储文件的切分文件的存储位置
/opt/apps/realtime/data/
下面是代码
mkdir /opt/apps/realtime/conf /opt/apps/realtime/conf/vhost/ /opt/apps/realtime/logs/ /opt/apps/realtime/data/
下面是lua的配置文件,要复制到以下文件夹中
cp /usr/local/openresty/nginx/conf/mime.types /opt/apps/realtime/conf/
3)配置nginx的主配置文件 (使用lua编写的哇)
vim /opt/apps/realtime/conf/core.conf
# core.conf
# nginx 用户和组
user root root;
# work进程数,
worker_processes 4;
# 错误日志路径,和日志级别
error_log logs/nginx_error.log error;
# nginx pid文件
pid logs/nginx.pid;
# 单个worker最大打开的文件描述符个数
worker_rlimit_nofile 65535;
events
{
#使用epoll模型
use epoll;
# 单个worker进程允许的最多连接数
worker_connections 65535;
}
http
{
include mime.types;
default_type application/octet-stream;
gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_http_version 1.0;
gzip_comp_level 2;
gzip_types text/plain application/x-javascript text/css application/xml;
gzip_vary on;
underscores_in_headers on;
log_format main
'$remote_addr - $remote_user [$time_local] '
'$request_length '
'"$request" $status $bytes_sent $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'"$gzip_ratio" "$request_time" '
'"$upstream_addr" "$upstream_status" "$upstream_response_time"';
# 定义我们数据采集的 access 日志格式
log_format collect-app '$cad';
open_log_file_cache max=1000 inactive=60s;
keepalive_timeout 0;
client_max_body_size 20m;
include /opt/apps/realtime/conf/vhost/*.conf;
}
4)测试主配置文件是否格式正确
openresty -p /opt/apps/realtime/ -c conf/core.conf -t
报错啦:
[root@qianfeng01 ~]# openresty -p /opt/apps/realtime/ -c conf/core.conf -t
nginx: [emerg] unknown "cad" variable
nginx: configuration file /opt/apps/realtime/conf/core.conf test failed#没有定义主配置文件cad这个变量
5)配置副配置文件
vim /opt/apps/realtime/conf/vhost/minor1.conf
#minor1.conf
server {
listen 8802 default_server;# lua_need_request_body on;
client_max_body_size 5M;
client_body_buffer_size 5M;
location /data/v1 {
set $cad '';
content_by_lua_block {
-- cjson模块
local cjson = require "cjson"
-- 读取请求体信息
ngx.req.read_body()
-- 请求体信息存放到 body_data变量中
local body_data = ngx.req.get_body_data()
-- 如果请求体为空,返回错误
if body_data == nil then
ngx.say([[{"code":500,"msg":"req body nil"}]])
return
end
-- 定义当前时间
local current_time = ngx.now()*1000
-- 请求的URL project参数中获取其值
local project = ngx.var.arg_project
-- 定义一个字典,存放有当前服务为日志增加的信息,如ctime表示接受到请求的时间,ip地址等
local data={}
data["project"] = project
data["ctime"] = current_time
if ngx.var.http_x_forwarded_for == nil then
data["ip"] = ngx.var.remote_addr;
else
data["ip"] = ngx.var.http_x_forwarded_for
end
-- 将增加的信息编码为json
local meta = cjson.encode(data)
-- 将编码的json信息做base64 和 body_data拼接
local res = ngx.encode_base64(meta) .. "-" .. ngx.unescape_uri(body_data)
-- 将数据赋值给我们定义的nginx变量cad中,我们定义的log_format就使用这个变量的值
ngx.var.cad = res
ngx.say([[{"code":200,"msg":"ok"}]])
}
access_log logs/realtime-access.log collect-app;
}
}
我们再次验证一下主配置文件
openresty -p /opt/apps/realtime/ -c conf/core.conf -t
这次成功的截图
2.4.2 启动nginx,并测试
1)启动
openresty -p /opt/apps/realtime -c conf/core.conf
说到openesty,我用了太多这个代码了,这里我把之前常用的代码给大家看看
#启动主配置文件测试
openresty -p /opt/apps/realtime -c conf/core.conf -t
#暂停openrety的服务
openresty -p /opt/apps/realtime -c conf/core.conf -s stop
#开启openrety的服务
openresty -p /opt/apps/realtime -c conf/core.conf
#查看openrety端口是否活跃
ps -ef | grep nginx
2)查看端口号
ps -aux | grep nginx
netstat -ntlp | grep nginx
3)手动测试 nginx是否能采集到数据
curl qianfeng01:8802/data/v1?project=news -d test_data
现在我们来找一下日志吧
[root@qianfeng01 ~]# cd /opt/apps/realtime/
[root@qianfeng01 realtime]# ll
总用量 0
drwx------. 2 root root 6 3月 27 11:08 client_body_temp
drwxr-xr-x. 3 root root 54 3月 27 11:03 conf
drwxr-xr-x. 2 root root 6 3月 27 11:00 data
drwx------. 2 root root 6 3月 27 11:08 fastcgi_temp
drwxr-xr-x. 2 root root 108 3月 27 11:08 logs
drwx------. 2 root root 6 3月 27 11:08 proxy_temp
drwx------. 2 root root 6 3月 27 11:08 scgi_temp
drwxr-xr-x. 2 root root 124 3月 26 22:34 scripts
drwx------. 2 root root 6 3月 27 11:08 uwsgi_temp[root@qianfeng01 realtime]# cd logs
[root@qianfeng01 logs]# ll
总用量 12
-rw-r--r--. 1 root root 0 3月 27 11:08 access.log
-rw-r--r--. 1 root root 62 3月 27 11:05 error.log
-rw-r--r--. 1 root root 0 3月 27 11:08 nginx_error.log
-rw-r--r--. 1 root root 6 3月 27 11:11 nginx.pid
-rw-r--r--. 1 root root 95 3月 27 11:13 realtime-access.log 《-这个就是我们发的message[root@qianfeng01 logs]# cat realtime-access.log
eyJjdGltZSI6MTY0ODM1MDc4MDQ0NywicHJvamVjdCI6Im5ld3MiLCJpcCI6IjE5Mi4xNjguMTAuMTAxIn0=-test_data
解密看一看(注意是上面等于号前的内容是消息头,使用base64解密消息头)
[root@qianfeng01 logs]# echo eyJjdGltZSI6MTY0ODM1MDc4MDQ0NywicHJvamVjdCI6Im5ld3MiLCJpcCI6IjE5Mi4xNjguMTAuMTAxIn0 | base64 -d
{"ctime":1648350780447,"project":"news","ip":"192.168.10.101"}base64: 输入无效
[root@qianfeng01 logs]#
2.4.3 配置管理中心
1)注册域名 (这个注册是针对用户行为日志的注册,上面有一个注册是内容数据的注册,不一样哦。)
下面我拿action作为我注册名,所以你想注册可以自定义,但是别用我已经注册过的。
/usr/local/frpc/frpc http --sd action -l 8802 -s frp.qfbigdata.com:7001 -u action
让这个页面卡着别动,新开一个窗口继续做项目
红线画的是我的注册域名
2)测试 (把我下面的action切换成自己的域名名称)
curl http://action.frp.qfbigdata.com:8002/data/v1?project=news -d test_data
3)配置到管理中心
还是老样子把下面的action换成自己的名字
curl -X POST \
http://meta.frp.qfbigdata.com:8112/api/v1/meta/register \
-F data_url=http://action.frp.qfbigdata.com:8002/data/v1?project=news \
-F type=1 \
-F name=action#上面是我的配置管理中心,下面是一个模板
curl -X POST \
http://meta.frp.qfbigdata.com:8112/api/v1/meta/register \
-F data_url=http://yourname.frp.qfbigdata.com:8002/data/v1?project=news \
-F type=1 \
-F name=yourname
查看一下我们的日志
真的服务器已经发了好多数据
数据格式是两部分,一部分是meta,一部分是body,用-拼接的。 可以使用base64解密
现在我们来解密一下吧
我们先解密一下meta
echo eyJjdGltZSI6MTY0ODM1MjA4NTM0MywicHJvamVjdCI6Im5ld3MiLCJpcCI6IjM5LjEwNy45Ny4xNTQifQ== | base64 -d
解密一下body
echo eyJjb250ZW50Ijp7InV1aWQiOiI3NDI2MzQ5OS1kNjUyLTRlNGUtYTE1NS1mOTg3ZDI4NTc4ZWIiLCJkaXN0aW5jdF9pZCI6IjEwMjUiLCJwcm9wZXJ0aWVzIjp7Im1vYmlsZSI6IigwMDg2KTE1ODY4NTU5MTk1IiwiZW1haWwiOiJDcmF3Zm9yZEVtYXJkMDI5N0BnaXRodWIuY29tIiwibmlja19uYW1lIjoiRmxvcmlhbiBKZXdlc3NLcmVpZ2Vy5rC1MTM1MCIsIm5hbWUiOiLmhZXlrrnoi6XkupEiLCJnZW5kZXIiOiLlpbMiLCJhZ2UiOiIzOCIsInNpZ251cF90aW1lIjoxNjQ4MzUyMDUwMDAwfSwidHlwZSI6InByb2ZpbGVfc2V0In19 | base64 -d
因为介于之前发过不符合格式的测试数据,会影响到采集数据的格式
所以要把realtime-access.log删除,并且重新启动openresty,再搞注册域名那个代码。
openresty -p /opt/apps/realtime -c conf/core.conf -s stop
rm -rf realtime-access.log
openresty -p /opt/apps/realtime -c conf/core.conf
#下面这个代码我必须说明一下,先在之前的执行这个代码窗口按下ctrl+c暂停进程
/usr/local/frpc/frpc http --sd action -l 8802 -s frp.qfbigdata.com:7001 -u action
查看另一个窗口
2.4.4 日志切分
1)问题
当移动文件时,nginx会产生新文件名realtime-access.log 和移动后的文件名realtime-access-13734523.log共用一个inode, 因此当flume不断的采集移动的新文件时,其实采集的是同一个数据块,发生了数据重复问题。
如何解决这个问题?
让nginx在产生新文件名realtime-access.log时,使用新的inode。
2)编写行为日志切分脚本
vim /opt/apps/realtime/scripts/split-access-log.sh
#!/bin/sh
# filename: split-access-log.sh
# desc: 此脚本用于切割Nginx Access日志到指定的目录/opt/apps/realtime/data/下,供Flume采集使用# 帮助
usage() {
echo "Usage:"
echo " split-access-log.sh [-f log_file] [-d data_dir] [-p pid_file]"
echo "Description:"
echo " log_file: nginx access file absolute path"
echo " data_dir: split data dir"
echo " pid_file: nginx pid file absolute path"
echo "Warning: if no parmas, use default value"
exit -1
}
default(){
echo "use default value:"
echo " log_file=/opt/apps/realtime/logs/realtime-access.log"
echo " data_dir=/opt/apps/realtime/data/"
echo " pid_file=/opt/apps/realtime/logs/nginx.pid"
# 我们的access日志文件
log_file="/opt/apps/realtime/logs/realtime-access.log"
# 切分后文件所放置的目录
data_dir="/opt/apps/realtime/data/"
# Nginx pid 文件
pid_file="/opt/apps/realtime/logs/nginx.pid"
}while getopts 'f:d:p:h' OPT; do
case $OPT in
f) log_file="$OPTARG";;
d) data_dir="$OPTARG";;
p) pid_file="$OPTARG";;
h) usage;;
?) usage;;
*) usage;;
esac
done# 当没有参数传入时
if [ $# -eq 0 ];then
default
fi# 重命名access, 注意mv 的过程日志是不会丢失的,因为nginx是以inode来表示数据文件的,而不是文件名,这里mv的操作不会改变inode
if [ ! "${log_file}" ] || [ ! "${data_dir}" ] || [ ! ${pid_file} ]; then
echo "some parmas is empty,please set "
exit -1
fi
# 切分之前,先判断日志文件是否有数据,如果有数据再切分,防止切分出来很多空文件
line=`tail -n 1 ${log_file}`
if [ ! "$line" ];then
echo "Warning: access log file no data, do not split!"
exit 0
fi
mv ${log_file} ${data_dir}realtime-access-$(date +"%s").log
# 向nginx 发送 USR1信号,让其重新打开一个新的日志文件
kill -USR1 `cat ${pid_file}`
echo "finish!"
修改一下权限和执行这个脚本
chmod u+x /opt/apps/realtime/scripts/split-access-log.sh
/opt/apps/realtime/scripts/split-access-log.sh
4)测试
检查data和logs目录下是否有新文件(看data目录下还是发现了新文件)
cd /opt/apps/realtime/data/
5)编写定时器,2分钟一次
cd /opt/apps/realtime/scripts/
crontab -e
0 * * * * /usr/sbin/ntpdate -u ntp1.aliyun.com
*/2 * * * * /opt/apps/realtime/scripts/split-access-log.sh >/dev/null
2.4.5 编写flume的采集方案
1)编写采集方案
vim /opt/apps/realtime/scripts/news-access-flume-spool.properties
# filename: news-access-flume-spool.properties
# 定义一个名字为 a1001 的agent
a1001.channels = ch-1
a1001.sources = src-1
a1001.sinks = k1
a1001.sinks.k1.channel = ch-1
a1001.sources.src-1.channels = ch-1
a1001.sources.src-1.type = spooldir
a1001.sources.src-1.spoolDir = /opt/apps/realtime/data/
# 正则匹配我们需要的数据文件
a1001.sources.src-1.includePattern = ^realtime.*.log
# 如果想在header信息中加入你传输的文件的文件名,设置下面参数为true,同时设置文件header的key,我们这里设置成fileName,之后你就可以在sink端通过 %{fileName}, 取出header中的fileName变量中的值,这个值就是文件名
# a1001.sources.src-1.basenameHeader = true
# a1001.sources.src-1.basenameHeaderKey = fileName
# 积累多少个event后,一起发到channel, 这个值在生成环境中我们需要根据数据量配置batchSize大的下,通常来讲们的batchSize越大,吞吐就高,但是也要受到 channel 的capacity,transactionCapacity的限制,不能大于channel的transactionCapacity值。 关于这三个参数的区别及说明参看 [官方wiki](https://cwiki.apache.org/confluence/display/FLUME/BatchSize%2C+ChannelCapacity+and+ChannelTransactionCapacity+Properties)
a1001.sources.src-1.batchSize = 100
a1001.channels.ch-1.type = memory
a1001.channels.ch-1.capacity = 10000
a1001.channels.ch-1.transactionCapacity = 100
a1001.sinks.k1.type = hdfs
a1001.sinks.k1.hdfs.path = hdfs://qianfeng01:8020/collection/news_log/%Y%m%d
a1001.sinks.k1.hdfs.filePrefix = realtime-access-%Y%m%d_%H
a1001.sinks.k1.hdfs.fileSuffix = .gz
a1001.sinks.k1.hdfs.codeC = gzip
a1001.sinks.k1.hdfs.useLocalTimeStamp = true
a1001.sinks.k1.hdfs.writeFormat = Text
a1001.sinks.k1.hdfs.fileType = CompressedStream
# 禁用安装event条数来滚动生成文件
a1001.sinks.k1.hdfs.rollCount = 0
# 如果一个文件达到10M滚动
a1001.sinks.k1.hdfs.rollSize = 10485760
# 1分钟滚动生成新文件,和文件大小的滚动一起,那个先达到,执行那个
a1001.sinks.k1.hdfs.rollInterval = 60
# 参加上边连接官网说明,理论上batchSize 越大,吞吐越高。 但是HDFS Sink 调用 Hadoop RPC(包括 open、flush、close ..)超时会抛出异常,如果发生在 flush 数据阶段,部分 event 可能已写入 HDFS,事务回滚后当前 BatchSize 的 event 还会再次写入造成数据重复。 batchSize越大可能重复的数据就越多. 同时batchSize值,不能大于channel的transactionCapacity值
a1001.sinks.k1.hdfs.batchSize = 100
# 每个HDFS SINK 开启多少线程来写文件
a1001.sinks.k1.hdfs.threadsPoolSize = 10
# 如果一个文件超过多长时间没有写入,就自动关闭文件,时间单位是秒
a1001.sinks.k1.hdfs.idleTimeout = 60
2)编写运行采集方案的脚本
因为flume只是配置,执行肯定还是要敲命令行的,还得用个shell去执行flume。
vim /opt/apps/realtime/scripts/news-access-flume-spool-start.sh
#!/bin/bash
/usr/local/flume/bin/flume-ng agent -c /usr/local/flume/conf -f /opt/apps/realtime/scripts/news-access-flume-spool.properties -n a1001 -Dflume.root.logger=INFO,console -Dflume.monitoring.type=http -Dflume.monitoring.port=31001
3)修改权限,并运行脚本
chmod u+x /opt/apps/realtime/scripts/news-access-flume-spool-start.sh
#特别说明在执行这个代码之前要把hadoop打开啊,不然报错!执行代码 start-all.sh
/opt/apps/realtime/scripts/news-access-flume-spool-start.sh
报错信息:其实我用的flume是把数据下沉到hdfs上,不执行hadoop是sink就完了
执行成功的界面
我们来看看内容数据
奈何我是边运行边做日志,导致我浏览器不能打开
只能在虚拟机上给大家看看我的hadoop文件
hdfs dfs -ls /
hdfs dfs -ls /collection
我们发现还是多了一个new_log,那就是成功了
2.4.6 自定义拦截器
1) 为什么要自定义拦截器
因为hdfs上的时间目录,应该用event数据的真正时间,不应该是a1001.sinks.k1.hdfs.useLocalTimeStamp指定的时间点。
2)编写拦截器代码
我们先打开一下idea进行开发拦截器
file->new->project
pom.xml插件
<?xml version="1.0" encoding="UTF-8"?>
<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.0</modelVersion><groupId>org.example</groupId>
<artifactId>sz2103_flume</artifactId>
<version>1.0-SNAPSHOT</version><properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.flume</groupId>
<artifactId>flume-ng-core</artifactId>
<version>1.8.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.4.3</version>
<configuration>
<!-- put your configurations here -->
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
代码
package com.qf.flume.interceptor;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.codec.binary.Base64;
import org.apache.flume.Context;
import org.apache.flume.Event;
import org.apache.flume.interceptor.Interceptor;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
public class CollectionInterceptor implements Interceptor {
@Override
public void initialize() { }
@Override
public Event intercept(Event event) {
//获取event的正文,也就是原始的base64字符串
byte[] body = event.getBody(); // base64字符串变成字节数组
String data = new String(body); //才是真正的base64字符串
//拆分成两部分
String[] arr = data.split("-");
//解析第一部分 eyJwcm9qZWN0IjoibmV3cyIsImN0aW1lIjoxNjMyMzc5NjA3NTQ0LCJpcCI6IjM5Ljk5LjE3MC41MyJ9
byte[] bytes = Base64.decodeBase64(arr[0]); //变成字节数组
String first = new String(bytes); // {"project":"news","ctime":1632379607544,"ip":"39.99.170.53"}
JSONObject jsonObject = JSONObject.parseObject(first); //转成json对象
Long ctime = jsonObject.getLong("ctime");
//将ctime字符串转成yyyyMMdd格式
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
String ctimeFormat = sdf.format(ctime); // 20210923
//将ctimeFormat放入header的键值对
event.getHeaders().put("ctime",ctimeFormat);
//解析第二部分
byte[] bytes1 = Base64.decodeBase64(arr[1]); //变成字节数组
String second = new String(bytes1); //变成json格式的字符串 "content": {"distinct_id": "51818968","event": "AppClick", "properties": {"element_page": "新闻列表页","screen_width": "640", "app_version": "1.0","os": "GNU/Linux","battery_level": "11","device_id": "886113235750", "client_time": "2020-05-18 13:53:56", "ip": "61.233.88.41","is_charging": "1", "manufacturer": "Apple","carrier": "中国电信","screen_height": "320","imei": "886113235750","model": "", "network_type": "WIFI","element_name": "tag" } }
JSONObject jsonObject1 = JSONObject.parseObject(second); //转成json对象
//将第一部分解析出来的内容,追加到第二部分里
jsonObject1.put("project",jsonObject.get("project"));
jsonObject1.put("ctime",jsonObject.get("ctime"));
jsonObject1.put("ip",jsonObject.get("ip"));
//将新的jsonObject1,存储到event的正文里
event.setBody(jsonObject1.toJSONString().getBytes());
return event;
}
@Override
public List<Event> intercept(List<Event> events) {
List<Event> list = new ArrayList<>();
for (Event event : events) {
list.add(intercept(event));
}
return list;
}
@Override
public void close() { }
public static class MyBuilder implements Builder{
@Override
public Interceptor build() {
return new CollectionInterceptor();
}
@Override
public void configure(Context context) {
}
}
}
打包放入flume的lib目录下(/usr/local/flume/lib/)
右边双击package
我扔了一个最大的文件到flume的lib下
下面是要更改一下采集方案
cd /opt/apps/realtime/scripts/
vim news-access-flume-spool.properties
# filename: news-access-flume-spool.properties
# 定义一个名字为 a1001 的agent
a1001.channels = ch-1
a1001.sources = src-1
a1001.sinks = k1
a1001.sinks.k1.channel = ch-1
a1001.sources.src-1.channels = ch-1
a1001.sources.src-1.type = spooldir
a1001.sources.src-1.spoolDir = /opt/apps/realtime/data/
# 正则匹配我们需要的数据文件
a1001.sources.src-1.includePattern = ^realtime.*.log
# 如果想在header信息中加入你传输的文件的文件名,设置下面参数为true,同时设置文件header的key,我们这里设置成fileName,之后你就可以在sink端通过 %{fileName}, 取出header中的fileName变量中的值,这个值就是文件名
# a1001.sources.src-1.basenameHeader = true
# a1001.sources.src-1.basenameHeaderKey = fileName
# 积累多少个event后,一起发到channel, 这个值在生成环境中我们需要根据数据量配置batchSize大的下,通常来讲们的batchSize越大,吞吐就高,但是也要受到 channel 的capacity,transactionCapacity的限制,不能大于channel的transactionCapacity值。 关于这三个参数的区别及说明参看 [官方wiki](https://cwiki.apache.org/confluence/display/FLUME/BatchSize%2C+ChannelCapacity+and+ChannelTransactionCapacity+Properties)
a1001.sources.src-1.batchSize = 100
a1001.sources.src-1.interceptors=i1
a1001.sources.src-1.interceptors.i1.type=com.qf.flume.interceptor.CollectionInterceptor$MyBuilder
a1001.channels.ch-1.type = memory
a1001.channels.ch-1.capacity = 10000
a1001.channels.ch-1.transactionCapacity = 100
a1001.sinks.k1.type = hdfs
a1001.sinks.k1.hdfs.path = hdfs://qianfeng01:8020/collection/news_log/%{ctime}
a1001.sinks.k1.hdfs.filePrefix = realtime-access-%{ctime}
a1001.sinks.k1.hdfs.fileSuffix = .gz
a1001.sinks.k1.hdfs.codeC = gzip
a1001.sinks.k1.hdfs.useLocalTimeStamp = false
a1001.sinks.k1.hdfs.writeFormat = Text
a1001.sinks.k1.hdfs.fileType = CompressedStream
# 禁用安装event条数来滚动生成文件
a1001.sinks.k1.hdfs.rollCount = 0
# 如果一个文件达到10M滚动
a1001.sinks.k1.hdfs.rollSize = 10485760
# 1分钟滚动生成新文件,和文件大小的滚动一起,那个先达到,执行那个
a1001.sinks.k1.hdfs.rollInterval = 60
# 参加上边连接官网说明,理论上batchSize 越大,吞吐越高。 但是HDFS Sink 调用 Hadoop RPC(包括 open、flush、close ..)超时会抛出异常,如果发生在 flush 数据阶段,部分 event 可能已写入 HDFS,事务回滚后当前 BatchSize 的 event 还会再次写入造成数据重复。 batchSize越大可能重复的数据就越多. 同时batchSize值,不能大于channel的transactionCapacity值
a1001.sinks.k1.hdfs.batchSize = 100
# 每个HDFS SINK 开启多少线程来写文件
a1001.sinks.k1.hdfs.threadsPoolSize = 10
# 如果一个文件超过多长时间没有写入,就自动关闭文件,时间单位是秒
a1001.sinks.k1.hdfs.idleTimeout = 60
然后我们重新启动一下采集方案,查看hdfs的目录和文件 ,最好查看一下新采集的文件 是否是解密后的样子
2.5 数仓ODS层建模
在ODS层维护一个数据库名:
介于之前打开了hdfs,现在要启动hive。
没打开hadoop要自己打开(start-all.sh)
hive --service metastore &
hive --service hiveserver2 &
hive
是在hive中执行!
create database ods_news comment "realtime acquisition and monitoring ods";
2.5.1 行为日志数据的ODS层维护
1)构建一个外部分区表,直接映射hdfs的数据目录
这个是要hive解析json文件,hive的lib目录下一定要有json的jar包,如果是按照我之前的安装环境,不存在报错'org.openx.data.jsonserde.JsonSerDe问题
use ods_news;
drop table if exists `news_log`;
create external table if not exists `news_log`(
project string,
ctime string,
ip string,
content struct<distinct_id:string,event:string,properties:struct<model:string,network_type:string,is_charging:string,app_version:string,element_name:string,element_page:string,carrier:string,os:string,imei:string,battery_level:string,screen_width:string,screen_height:string,device_id:string,client_time:string,ip:string,manufacturer:string>>
)
PARTITIONED BY(logday string)
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
LOCATION '/collection/news_log/';
2)为外部分区表维护一个脚本,要另开一个窗口,别在hive界面搞
vim /opt/apps/realtime/scripts/news_log_add_partition.sh
#!/bin/sh
# filename: news_log_add_partition.sh# 定义一个日期参数,如果指定日期,就导入该日期前一天的数据,否则默认为昨日数据
exec_date=$1
if [ "${exec_date}" ] ;then
exec_date=`date -d "${exec_date} 1 days ago" +%Y%m%d`
else
exec_date=`date -d "1 days ago" +%Y%m%d`
fi
echo "news_log_add_partition.sh exce date: ${exec_date}"#如果已有分区,先删除分区,再添加
ADD_PARTITION_SQL="
alter table ods_news.news_log drop if exists PARTITION(logday='${exec_date}');
alter table ods_news.news_log add partition (logday='${exec_date}') location 'hdfs://qianfeng01:8020/collection/news_log/${exec_date}/';
"
echo "${ADD_PARTITION_SQL}"
hive -e "${ADD_PARTITION_SQL}"
3)修改权限,并测试
chmod u+x /opt/apps/realtime/scripts/news_log_add_partition.sh
/opt/apps/realtime/scripts/news_log_add_partition.sh 20220327
查看news_log的分区以及具体数据
4)为普通的news_log 维护一个parquet类型的分区表(在hive中执行)
CREATE EXTERNAL TABLE if not exists `ods_news.news_log_parquet` (
event string COMMENT '事件名称',
ctime string COMMENT '服务端接收到日志时间',
distinct_id string COMMENT '用户ID',
model string COMMENT '手机型号',
network_type string COMMENT '网络类型',
is_charging string COMMENT '是否充电中',
app_version string COMMENT 'app版本',
element_name string COMMENT '元素名称',
element_page string COMMENT '元素所在页面',
carrier string COMMENT '运营商',
os string COMMENT '操作系统',
imei string COMMENT '手机标识IMEI',
battery_level string COMMENT '手机电量',
screen_height string COMMENT '屏幕高度',
screen_width string COMMENT '屏幕宽度',
device_id string COMMENT '手机设备ID,目前和IMEI一致',
client_time string COMMENT '日志上报的客户端时间',
ip string COMMENT 'IP地址',
manufacturer string COMMENT '手机制造商'
)
PARTITIONED BY(logday string)
STORED AS PARQUET
LOCATION '/collection/news_log_parquet/';
5)为news_log_parquet表编写脚本,动态添加每天的数据
vim /opt/apps/realtime/scripts/news_log_parquet_add_partition.sh
#!/bin/sh
# filename: news_log_parquet_add_partition.sh
# desc: news原始json表数据转换为parquet# 定义一个日期参数,如果指定日期,就导入该日期前一天的数据,否则默认为昨日数据
exec_date=$1
if [ "${exec_date}" ] ;then
exec_date=`date -d "${exec_date} 1 days ago" +%Y%m%d`
else
exec_date=`date -d "1 days ago" +%Y%m%d`
fi
echo "news_parquet.sh exce date: ${exec_date}"# 从news表查询指定日期数据,通过动态分区方式转换到parquet表
NEWS_PARQUET_SQL="
set hive.exec.dynamic.partition=true;
set hive.exec.dynamic.partition.mode=nonstrict;
insert overwrite table ods_news.news_log_parquet partition(logday)
select
ctime,
content.event,
content.distinct_id,
content.properties.model,
content.properties.network_type,
content.properties.is_charging,
content.properties.app_version,
content.properties.element_name,
content.properties.element_page,
content.properties.carrier,
content.properties.os,
content.properties.imei,
content.properties.battery_level,
content.properties.screen_height,
content.properties.screen_width,
content.properties.device_id,
content.properties.client_time,
content.properties.ip string,
content.properties.manufacturer,
logday
from ods_news.news_log
where logday='${exec_date}';
"
echo "${ADD_PARTITION_SQL}"
/usr/local/hive/bin/hive -e "${NEWS_PARQUET_SQL}"
6)修改权限,并测试
chmod u+x /opt/apps/realtime/scripts/news_log_parquet_add_partition.sh
/opt/apps/realtime/scripts/news_log_parquet_add_partition.sh 20220327
2.5.2 内容数据的ODS层建模
1)把数据放到hdfs中
特别说明一下:我在做项目的时候,内容数据采集出了问题。就只有20211223这天的数据,可以假装导入到hdfs上中的collection里面的内容数据
链接:https://pan.baidu.com/s/16rbc060hpZiH1zUTCOeXqA
提取码:sun6
--来自百度网盘超级会员V2的分享
先把上面那个文件放到虚拟机本地
在hdfs上创建文件夹
[root@qianfeng01 ~]# hdfs dfs -mkdir -p /collection/new_article/20211223
[root@qianfeng01 ~]# hdfs dfs -ls /collection/new_article/
Found 1 items
drwxr-xr-x - root supergroup 0 2022-03-27 14:36 /collection/new_article/20211223
然后把文件上传到hdfs中
[root@qianfeng01 ~]# hdfs dfs -put article.txt /collection/new_article/20211223/
[root@qianfeng01 ~]# hdfs dfs -ls /collection/new_article/20211223/
Found 1 items
-rw-r--r-- 1 root supergroup 103823891 2022-03-27 14:40 /collection/new_article/20211223/article.txt
2)构建表 (hive)
CREATE EXTERNAL TABLE if not exists `ods_news.news_article` (
article_id string COMMENT '新闻ID',
type_name string COMMENT '新闻类型',
pub_time string COMMENT '新闻发布时间',
content string COMMENT '新闻内容',
source_name string COMMENT '新闻来源',
tags string COMMENT '新闻标签'
)
PARTITIONED BY(logday string)
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
WITH SERDEPROPERTIES ( 'ignore.malformed.json' = 'true')
LOCATION '/collection/news_article/';
导数据(hive中)
alter table ods_news.news_article add partition(logday="20211223") location "/collection/news_article/20211223";
#下面是我做项目日期发现自己有数据哈哈,然后插了数据。
alter table ods_news.news_article add partition(logday="20220326") location "/collection/news_article/20220326";
确定数据在hive查询
select * from ods_news.news_article limit 1;
3)构建脚本,动态添加分区
vim /opt/apps/realtime/scripts/news_article_add_partition.sh
#!/bin/sh
# filename: news_article_add_partition.sh
# desc: 添加分区[news_article]# 定义一个日期参数,如果指定日期,就导入该日期前一天的数据,否则默认为昨日数据
exec_date=$1
if [ "${exec_date}" ] ;then
exec_date=`date -d "${exec_date} 1 days ago" +%Y%m%d`
else
exec_date=`date -d "1 days ago" +%Y%m%d`
fi
echo "news_article_add_partition.sh exce date: ${exec_date}"#如果已有分区,先删除分区,再添加
ADD_PARTITION_SQL="
alter table ods_news.news_article drop if exists PARTITION(logday='${exec_date}');
alter table ods_news.news_article add partition (logday='${exec_date}') location 'hdfs://qianfeng01:8020/collection/news_article/${exec_date}/';
"
echo "${ADD_PARTITION_SQL}"
/usr/local/hive/bin/hive -e "${ADD_PARTITION_SQL}"
4)修改权限以及执行shell脚本
chmod u+x /opt/apps/realtime/scripts/news_article_add_partition.sh
/opt/apps/realtime/scripts/news_article_add_partition.sh
2.5.3 业务数据的ODS层建模
1)构建表
创建meta外部表
drop table ods_news.meta;
CREATE EXTERNAL TABLE `ods_news.meta` (
`id` string comment '序号',
`field` string COMMENT '字段名称',
`field_type` string COMMENT '字段类型',
`field_desc` string COMMENT '字段说明',
`app_version` string COMMENT '上线版本号',
`status` string COMMENT '字段状态,0 下线 1 上线',
`create_time` string COMMENT '创建时间'
)
PARTITIONED BY(logday string)
row format delimited
fields terminated by ","
STORED AS textfile
LOCATION '/collection/news_business/meta/';
创建ad_info外部表
drop table ods_news.ad_info;
CREATE EXTERNAL TABLE `ods_news.ad_info` (
`id` string comment '序号',
`ad_id` string COMMENT '广告ID',
`advertiser_id` string COMMENT '广告主ID',
`advertiser_name` string COMMENT '广告主名字',
`create_time` string COMMENT '创建时间'
)
PARTITIONED BY(logday string)
row format delimited
fields terminated by ","
STORED AS textfile
LOCATION '/collection/news_business/ad_info/';
2)编写添加分区脚本
vim /opt/apps/realtime/scripts/news_business_add_partition.sh
#!/bin/sh
# filename: news_business_add_partition.sh
# desc: 添加分区[meta,ad_info]# 定义一个日期参数,如果指定日期,就导入该日期前一天的数据,否则默认为昨日数据
exec_date=$1
if [ "${exec_date}" ] ;then
exec_date=`date -d "${exec_date} 1 days ago" +%Y%m%d`
else
exec_date=`date -d "1 days ago" +%Y%m%d`
fi
echo "biz_add_partition.sh exce date: ${exec_date}"#如果已有分区,先删除分区,再添加
ADD_PARTITION_SQL="
alter table ods_news.ad_info drop if exists PARTITION(logday='${exec_date}');
alter table ods_news.ad_info add partition (logday='${exec_date}') location 'hdfs://qianfeng01:8020/collection/news_business/ad_info/${exec_date}/';
alter table ods_news.meta drop if exists PARTITION(logday='${exec_date}');
alter table ods_news.meta add partition (logday='${exec_date}') location 'hdfs://qianfeng01:8020/collection/news_business/meta/${exec_date}/';
"
echo "${ADD_PARTITION_SQL}"
/usr/local/hive/bin/hive -e "${ADD_PARTITION_SQL}"
3)执行验证
chmod u+x /opt/apps/realtime/scripts/news_business_add_partition.sh
/opt/apps/realtime/scripts/news_business_add_partition.sh
2.6 azkaban管理脚本调度
做这部是为了把之前的所有脚本放在azkaban按照时间先后进行执行。
azkaban只是一个调度工具哈。
1)配置azkaban的flow2.0版本的流文件:collection.flow
就是在自己电脑新建一个txt文档,然后改名成collection.flow
里面敲的代码是
nodes:
- name: start
type: noop
- name: news_log
type: command
config:
command: /opt/apps/realtime/scripts/news_log_add_partition.sh
dependsOn:
- start
- name: news_article
type: command
config:
command: /opt/apps/realtime/scripts/news_article_add_partition.sh
dependsOn:
- start
- name: news-business
type: command
config:
command: /opt/apps/realtime/scripts/news-business-sqoop-start.sh
dependsOn:
- start
- name: news_log2
type: command
config:
command: /opt/apps/realtime/scripts/news_log_parquet_add_partition.sh
dependsOn:
- news_log
- name: news-business2
type: command
config:
command: /opt/apps/realtime/scripts/news_business_add_partition.sh
dependsOn:
- news-business
- name: end
type: noop
dependsOn:
- news-business2
- news_log2
- news_article
再新建一个txt文件,把名字改成version.project
azkaban-flow-version: 2.0
然后把 collection.flow和version.project 打包成XX.zip文件,然后上传到azkaban。
上传azkaban是开azkaban的solo模式
start-solo.sh
浏览器访问: qianfeng01:8081
我的浏览器实在访问不了,找完bug再测试一下(先鸽一下下)
2.7 钉钉警报系统开发
由于Azkaban 本身只支持邮件报警,但是邮件报警的方式并不友好。我们一般将报警信息通过IM(即时通讯)工具发出来,感知更为及时。可以根据企业使用IM工具自定义报警,我们这里选择DingDing
我们开发计划如下:
- 下载DingDing SDK,安装到windows的本地Maven仓库
- 实现Azkaban的Alerter接口
- 编译打包部署
2.7.1 DingDing SDK的安装
1)下载SDK到windows上,并安装到本地Maven仓库
方式1:在linux使用wget指令下载
wget -P /tmp/ http://doc.qfbigdata.com/qf/project/soft/dingding/dingtalk-sdk-java.zip方式2:可以在windows上打开浏览器,输入地址,进行下载
http://doc.qfbigdata.com/qf/project/soft/dingding/dingtalk-sdk-java.zip
我自然是选择方式2
步骤2)解压到某一个目录(D:/)下
方式1: 在linux上解压, unzip dingtalk-sdk-java.zip
方式2: 在windows上解压解压出来两个jar包,一个是普通class的jar包,一个是source的jar包
--:taobao-sdk-java-auto_1479188381469-20200519.jar
--:taobao-sdk-java-auto_1479188381469-20200519-source.jar
大家看看我的路径 d:/data/dingtalkjar/
步骤3)在命令提示符下,安装
打开cmd
在cmd中输入 注意 Dfile后面是你自己的dingding的jar路径!
mvn install:install-file -Dfile=D:/data/dingtalkjartaobao-sdk-java-auto_1479188381469-20200519.jar -DgroupId=com.dingtalk -DartifactId=dingtalk-api -Dversion=1.0.0 -Dpackaging=jar
然后我们打开idea进行开发啦!
在idea中新建一个project。然后在pom.xml中添加依赖
<?xml version="1.0" encoding="UTF-8"?> <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.0</modelVersion> <groupId>org.example</groupId> <artifactId>DingDingproject</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>com.dingtalk</groupId> <artifactId>dingtalk-api</artifactId> <version>1.0.0</version> <scope>compile</scope> </dependency> </dependencies> </project>
钉钉开放平台:
上面这个链接是官方的链接,需要的可以自己琢磨开发一下钉钉机器人
我现在给大家示范的是这个自定义机器人。
打开电脑版的钉钉,要自己创建一个群,然后添加一个自定义机器人
复制一下Webhook
下滑
然后我们根据webhook发送http协议的消息吧
下面这个是在群设置里面下拉哦!
复制出机器人的webhook
我的机器人的webhook是
自定义关键词方式的测试(关键词:hxd)
要在linux系统里面敲这个指令,别在cmd里面,无法执行的。
下面这个webhook要换成自己机器人的!
curl https://oapi.dingtalk.com/robot/send?access_token=968a72fd883cdccd901b748b092b0a394b55f8ce02a0af113c0d624231212de8 -H 'Content-Type:application/json' -d '{"msgtype":"text","text":{"content":"helloworld 喵酱 helloketty"}}'
然后看看我的手机
回到开发,在idea项目的pom.xml中添加以下内容
<dependencies>
<dependency>
<groupId>com.dingtalk</groupId>
<artifactId>dingtalk-api</artifactId>
<version>1.0.0</version>
<scope>compile</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-codec/commons-codec -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>
</dependencies>
编写的第一个demo
package com.qf.dingding.test;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URLEncoder;
public class TestDemo01 {
public static void main(String[] args) throws Exception {
Long timestamp = System.currentTimeMillis();
System.out.println("timestamp:"+timestamp);
String secret = "please input the secret of your robot";
String stringToSign = timestamp + "\n" + secret;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256"));
byte[] signData = mac.doFinal(stringToSign.getBytes("UTF-8"));
String sign = URLEncoder.encode(new String(Base64.encodeBase64(signData)),"UTF-8");
System.out.println(sign);
}
}
机器人的加签
打开电脑版的钉钉,进入群,然后点击一下机器人,点击机器人设置
这是我发现这个机器人不能加签,所以我就换了
在群助手里面添加机器人
选择上图最后一个灰色的机器人
设置了关键词和加签
三. Supervisor的应用
1)开启Supervisor
systemctl start supervisord
netstat -antp |grep 9001
tcp 0 0 0.0.0.0:9001 0.0.0.0:* LISTEN 939/python
也可以在浏览器中输入 ip:9001 查看。
3.2设置前台管理进程
3.2.1行为数据的网络穿透的前台进程
vim /etc/supervisord.d/access-frpc.conf
下面那个网络穿透是行为日志数据的采集,不能盲目看我的代码
; 行为数据的网络穿透启动配置文件
[program:access-frpc] ; 我们的应用配置格式为 program:我们应用名称(自己定义)
command=/usr/local/frpc/frpc http --sd action -l 8802 -s frp.qfbigdata.com:7001 -u actionstderr_logfile=/var/log/supervisor/access-frpc.err ;错误日志文件
stdout_logfile=/var/log/supervisor/access-frpc.log ;标准输出日志文件,我们通过该文件查看access-frpc这个应用的运行日志stdout_logfile_maxbytes=10MB ; 标准输出日志文件多大滚动一次
stdout_logfile_backups=10 ; 标准输出日志文件最多备份多少个
user=root ; 以什么用户启动
autostart=true ; 是否在supervisord启动时,直接就启动应用
autorestart=true ; crash 后是否自动重启
startsecs=10 ;应用进程启动多少秒之后,此时状态如果是running状态,就认为是成功
startretries=3 ; 当进程启动失败后,最大尝试启动的次数, 如果超过指定次数,应用会被标记为Fail状态
stopasgroup=true ; 是否停止由应用本身创建的子进程,此选项接受的停止信号是stop信号
killasgroup=true ; 是否停止由应用本身创建的子进程,此选项接受的停止信号是SIGKILL信号
redirect_stderr=false ; 如果是true,stderr的日志会被写入stdout日志文件中
启动进程
# 读取配置文件
supervisorctl reread
# 更新启动access-frpc
supervisorctl update access-frpc
# 查看启动状态,如果一切ok,将看到如下图信息
supervisorctl status access-frpc
# 如果想停止access-frpc
supervisorctl stop access-frpc
# 如果想再次启动
supervisorctl start access-frpc
# 注意一旦你修改了配置文件内容,一定要先reread,然后 update 就可以了
web端访问:ip:9001
3.2.2内容数据的网络穿透的前台进程
vim /etc/supervisord.d/article-frpc.conf
; 内容数据的网络穿透启动配置文件
[program:article-frpc] ; 我们的应用配置格式为 program:我们应用名称(自己定义)
command=/usr/local/frpc/frpc http --sd news -l 9666 -s frp.qfbigdata.com:7001 -u newsstderr_logfile=/var/log/supervisor/article-frpc.err ;错误日志文件
stdout_logfile=/var/log/supervisor/article-frpc.log ;标准输出日志文件,我们通过该文件查看access-frpc这个应用的运行日志stdout_logfile_maxbytes=10MB ; 标准输出日志文件多大滚动一次
stdout_logfile_backups=10 ; 标准输出日志文件最多备份多少个
user=root ; 以什么用户启动
autostart=true ; 是否在supervisord启动时,直接就启动应用
autorestart=true ; crash 后是否自动重启
startsecs=10 ;应用进程启动多少秒之后,此时状态如果是running状态,就认为是成功
startretries=3 ; 当进程启动失败后,最大尝试启动的次数, 如果超过指定次数,应用会被标记为Fail状态
stopasgroup=true ; 是否停止由应用本身创建的子进程,此选项接受的停止信号是stop信号
killasgroup=true ; 是否停止由应用本身创建的子进程,此选项接受的停止信号是SIGKILL信号
redirect_stderr=false ; 如果是true,stderr的日志会被写入stdout日志文件中
执行supervisord
supervisorctl reread
supervisorctl update article-frpc
supervisorctl status article-frpc
3.2.3管理内容数据的采集方案放到前台进程
vim /etc/supervisord.d/flume-http.conf
; 内容数据的采集方案配置文件
[program:flume-http] ; 我们的应用配置格式为 program:我们应用名称(自己定义)
command=/opt/apps/realtime/scripts/news-article-flume-http-start.shstderr_logfile=/var/log/supervisor/flume-http.err ;错误日志文件
stdout_logfile=/var/log/supervisor/flume-http.log ;标准输出日志文件,我们通过该文件查看access-frpc这个应用的运行日志stdout_logfile_maxbytes=10MB ; 标准输出日志文件多大滚动一次
stdout_logfile_backups=10 ; 标准输出日志文件最多备份多少个
user=root ; 以什么用户启动
autostart=true ; 是否在supervisord启动时,直接就启动应用
autorestart=true ; crash 后是否自动重启
startsecs=10 ;应用进程启动多少秒之后,此时状态如果是running状态,就认为是成功
startretries=3 ; 当进程启动失败后,最大尝试启动的次数, 如果超过指定次数,应用会被标记为Fail状态
stopasgroup=true ; 是否停止由应用本身创建的子进程,此选项接受的停止信号是stop信号
killasgroup=true ; 是否停止由应用本身创建的子进程,此选项接受的停止信号是SIGKILL信号
redirect_stderr=false ; 如果是true,stderr的日志会被写入stdout日志文件中
supervisorctl reread
supervisorctl update flume-httpsupervisorctl status flume-http
3.2.4 行为日志数据采集方案
vim /etc/supervisord.d/flume-spool.conf
; 行为数据的采集方案配置文件
[program:flume-spool] ; 我们的应用配置格式为 program:我们应用名称(自己定义)
command=/opt/apps/realtime/scripts/news-access-flume-spool-start.shstderr_logfile=/var/log/supervisor/flume-spool.err ;错误日志文件
stdout_logfile=/var/log/supervisor/flume-spool.log ;标准输出日志文件,我们通过该文件查看access-frpc这个应用的运行日志stdout_logfile_maxbytes=10MB ; 标准输出日志文件多大滚动一次
stdout_logfile_backups=10 ; 标准输出日志文件最多备份多少个
user=root ; 以什么用户启动
autostart=true ; 是否在supervisord启动时,直接就启动应用
autorestart=true ; crash 后是否自动重启
startsecs=10 ;应用进程启动多少秒之后,此时状态如果是running状态,就认为是成功
startretries=3 ; 当进程启动失败后,最大尝试启动的次数, 如果超过指定次数,应用会被标记为Fail状态
stopasgroup=true ; 是否停止由应用本身创建的子进程,此选项接受的停止信号是stop信号
killasgroup=true ; 是否停止由应用本身创建的子进程,此选项接受的停止信号是SIGKILL信号
redirect_stderr=false ; 如果是true,stderr的日志会被写入stdout日志文件中
supervisorctl reread
supervisorctl update flume-spoolsupervisorctl status flume-spool
3.2.5 使用supervisord管理prometheus
[root@qianfeng01 ~]# vim /etc/supervisord.d/prometheus.conf
嘿嘿,下面那个command后面就是prometheus的启动代码!
; 行为数据的网络穿透启动配置文件
[program:prometheus] ; 我们的应用配置格式为 program:我们应用名称(自己定义)
command=/usr/local/prometheus/prometheus --storage.tsdb.path="/usr/local/prometheus/data/" --log.level=debug --web.enable-lifecycle --web.enable-admin-api --config.file=/usr/local/prometheus/prometheus.ymlstderr_logfile=/var/log/supervisor/prometheus.err ;错误日志文件
stdout_logfile=/var/log/supervisor/prometheus.log ;标准输出日志文件,我们通过该文件查看access-frpc这个应用的运行日志stdout_logfile_maxbytes=10MB ; 标准输出日志文件多大滚动一次
stdout_logfile_backups=10 ; 标准输出日志文件最多备份多少个
user=root ; 以什么用户启动
autostart=true ; 是否在supervisord启动时,直接就启动应用
autorestart=true ; crash 后是否自动重启
startsecs=10 ;应用进程启动多少秒之后,此时状态如果是running状态,就认为是成功
startretries=3 ; 当进程启动失败后,最大尝试启动的次数, 如果超过指定次数,应用会被标记为Fail状态
stopasgroup=true ; 是否停止由应用本身创建的子进程,此选项接受的停止信号是stop信号
killasgroup=true ; 是否停止由应用本身创建的子进程,此选项接受的停止信号是SIGKILL信号
redirect_stderr=false ; 如果是true,stderr的日志会被写入stdout日志文件中
supervisorctl reread
supervisorctl update prometheussupervisorctl status prometheus
3.2.5 使用supervisord管理grafana
vim /etc/supervisord.d/grafana.conf
; 行为数据的网络穿透启动配置文件
[program:grafana] ; 我们的应用配置格式为 program:我们应用名称(自己定义)
directory=/usr/local/grafana/ ; 运行程序前会切换到配置的目录中
command=bash -c "bin/grafana-server -config conf/grafana.ini" ; 我们要执行的命令stderr_logfile=/var/log/supervisor/grafana.err ;错误日志文件
stdout_logfile=/var/log/supervisor/grafana.log ;标准输出日志文件,我们通过该文件查看access-frpc这个应用的运行日志stdout_logfile_maxbytes=10MB ; 标准输出日志文件多大滚动一次
stdout_logfile_backups=10 ; 标准输出日志文件最多备份多少个
user=root ; 以什么用户启动
autostart=true ; 是否在supervisord启动时,直接就启动应用
autorestart=true ; crash 后是否自动重启
startsecs=10 ;应用进程启动多少秒之后,此时状态如果是running状态,就认为是成功
startretries=3 ; 当进程启动失败后,最大尝试启动的次数, 如果超过指定次数,应用会被标记为Fail状态
stopasgroup=true ; 是否停止由应用本身创建的子进程,此选项接受的停止信号是stop信号
killasgroup=true ; 是否停止由应用本身创建的子进程,此选项接受的停止信号是SIGKILL信号
redirect_stderr=false ; 如果是true,stderr的日志会被写入stdout日志文件中
supervisorctl reread
supervisorctl update grafanasupervisorctl status grafana
4、使用Nginx OPS进行监控
4.1 采集接口监控的指标
1. 数据采集接口的实时 QPS
2. 数据采集接口的各个状态码下的 QPS
3. 数据采集接口的 P99 延时
4.2 下载安装 nginx-lua-prometheus
cd /usr/local/openresty/
wget http://doc.qfbigdata.com/qf/project/soft/prometheus/nginx-lua-prometheus.tgz
tar -xvzf nginx-lua-prometheus.tgz && rm -rf nginx-lua-prometheus.tgz
4.3修改core.conf添加nginx lua 库
vim /opt/apps/realtime/conf/core.conf
# 修改 /opt/apps/realtime/conf/core.conf, 在http模块中增加如下,添加lua prometheus库
lua_package_path "/usr/local/openresty/nginx-lua-prometheus/?.lua;;";
4.4编写nginx metric采集配置
vim /opt/apps/realtime/conf/vhost/nginx-metric.conf
# nginx-metric.conf
lua_shared_dict prometheus_metrics 10M;
init_by_lua_block {
-- 初始化Prometheus
prometheus = require("prometheus").init("prometheus_metrics")
-- 定义一个counter类型metric,记录http请求个数,metric的标签为 主机 端口 请求路径 请求转态码
http_requests_endpoint = prometheus:counter("nginx_http_requests_endpoint", "Number of HTTP requests_endpoint",{"host","port","endpoint", "status"})
-- 定义一个histogram类型的metric,记录http 请求时间,metric 标签依然为 主机 端口 请求路径 请求转态码
-- 这里我们使用默认的 5ms-10s 之间的20个bucket来记录请求时间分布
http_request_time = prometheus:histogram("nginx_http_request_time","HTTP request time"
,{"host","port","endpoint", "status"})
-- 定义一个gauge类型metric,记录nginx的连接数,标签为nginx的连接状态
http_connections = prometheus:gauge("nginx_http_connections","Number of HTTP connections", {"state"})}
init_worker_by_lua 'prometheus:init_worker()';
log_by_lua_block {
-- 请求的主机名
local host = ngx.var.host
-- 请求的url路径
local endpoint = ngx.var.uri
-- 状态码
local status = ngx.var.status
-- 端口号
local port = ngx.var.server_port
-- 如果请求是一些静态文件,则统一归并为一类
if string.find(endpoint, "static") ~= nil or string.find(endpoint, ".js") ~= nil or string.find(endpoint, ".css") ~= nil or string.find(endpoint, ".jpg") ~= nil or string.find(endpoint, ".html") ~= nil or string.find(endpoint, ".ico") ~= nil then
endpoint = "static"
status = "static"
else
endpoint = ngx.var.uri
end
-- 请求数的 metric
if endpoint ~= nil then
http_requests_endpoint:inc(1, {host,port,endpoint,status})
end
local request_time = ngx.var.request_time
-- 请求时间的 metric
if endpoint ~= nil and request_time~= nil then
http_request_time:observe(tonumber(request_time), {host,port,endpoint,status})
end
}server {
listen 9527;
# 暴露metrics 接口给Prometheus 拉取数据
location /metrics {
content_by_lua_block {
-- nginx 连接状态
if ngx.var.connections_active ~= nil then
http_connections:set(ngx.var.connections_active, {"active"})
http_connections:set(ngx.var.connections_reading, {"reading"})
http_connections:set(ngx.var.connections_waiting, {"waiting"})
http_connections:set(ngx.var.connections_writing, {"writing"})
end
prometheus:collect()
}
}}
4.5 加载metric接口配置
# 执行测试配置文件
openresty -p /opt/apps/realtime -c conf/core.conf -t
# 如果一切OK,执行加载配置命令. 之前采集的服务我们已经启动nginx,如果未启动请先启动nginx
openresty -p /opt/apps/realtime -c conf/core.conf
# 验证一下,执行如下命令请求我们配置好metrics接口,会看到类似如下metric信息,表示OK
curl localhost:9527/metrics
4.6 metric接口配置到Prometheus
vim /usr/local/prometheus/prometheus.yml
# filename : prometheus.yml
# 全局配置
global:
scrape_interval: 15s # 设置每15秒pull一次数据,默认是1min
# 每一个job的配置,
scrape_configs:
# 默认的metric路径是 '/metrics'
# scheme defaults to 'http'.
# Prometheus 自己的metric
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'nginx-metric'
scrape_interval: 5s
static_configs:
- targets: ['localhost:9527']
# 执行如下命令,热加载Prometheus 配置
curl -X POST http://localhost:9090/-/reload
4.7在Grafana上展示我们配置的数据指标
# 这里将我们需要的数据指标已经做成了Grafna支持的JSON配置文件,你只需要下载后,导入到Grafna就可以了
# 下载Json配置
wget http://doc.qfbigdata.com/qf/project/collect-monitor/config/collect-app-nginx.json