项目一实时数仓数据采集

目录

1. 项目介绍

1.1项目背景

1.2项目需求

 1.3目标

 1.4 二次开发

2. 项目部署

2.1业务数据采集

2.2 导入脚本编写和测试

2.3内容数据采集

2.3.1说明:

 2.3.3 配置管理中心

2.3.4 注册域名

2.4 日志数据采集

2.4.1 nginx服务器的搭建

2.4.2 启动nginx,并测试

2.4.3 配置管理中心

2.4.4 日志切分

2.4.5 编写flume的采集方案

2.4.6 自定义拦截器

2.5 数仓ODS层建模

2.5.1 行为日志数据的ODS层维护

2.5.2 内容数据的ODS层建模

2.5.3 业务数据的ODS层建模

2.6 azkaban管理脚本调度

2.7 钉钉警报系统开发

2.7.1 DingDing SDK的安装

三. Supervisor的应用

3.2设置前台管理进程

3.2.1行为数据的网络穿透的前台进程

3.2.2内容数据的网络穿透的前台进程

3.2.3管理内容数据的采集方案放到前台进程

3.2.4 行为日志数据采集方案

3.2.5 使用supervisord管理prometheus

 3.2.5 使用supervisord管理grafana

4、使用Nginx OPS进行监控

4.1 采集接口监控的指标

4.2 下载安装 nginx-lua-prometheus

 4.3修改core.conf添加nginx lua 库

  4.4编写nginx metric采集配置

4.5 加载metric接口配置

 4.6 metric接口配置到Prometheus

  4.7在Grafana上展示我们配置的数据指标


条件:做这个项目一定要把需要搭建的环境搭建好才能做。这里会涉及到模拟公司内容的业务数据、内容数据、行为数据进行采集。

想有个做项目的环境可以看这篇哦

项目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



 上面的脚本的功能:

  1. 加载sqoop 和mysql
  2. 定义一个日期函数,为了导入昨天的数据
  3. 对查询中的ad_info表并行导入,以及执行sql语句
  4. 对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

https://oapi.dingtalk.com/robot/send?access_token=bff198a6a41bfc4bd664c04fc4adffc05228d26826a0379c0a06d2a39ab91344

下滑

然后我们根据webhook发送http协议的消息吧

下面这个是在群设置里面下拉哦!

复制出机器人的webhook

我的机器人的webhook是

https://oapi.dingtalk.com/robot/send?access_token=968a72fd883cdccd901b748b092b0a394b55f8ce02a0af113c0d624231212de8

自定义关键词方式的测试(关键词: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 action

stderr_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 news

stderr_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.sh

stderr_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-http

supervisorctl 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.sh

stderr_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-spool

supervisorctl 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.yml

stderr_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 prometheus

supervisorctl 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 grafana 

supervisorctl 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

猜你喜欢

转载自blog.csdn.net/weixin_45682261/article/details/123765021