实时日志分析统计业务
实时日志分析统计业务
1.背景介绍
我们知道网站用户访问流量是不间断的,基于网站的访问日志,即Web log分析是典型的流式实时计算应用场景,比如百度统计,它可以做流量分析,来源分析,网站分析,转化分析,另外还有特定场景分析,比如安全分析,用来识别CC攻击,SQL注入分析,脱库等,这里我们实现一个类似于百度分析的系统
1.1 实施环境
- Python 2.7.5
- Spark 2.4.0-cdh6.3.1
- Spark-Streaming API
2.原理
百度统计 (tongji.baidu.com) 是百度推出的一款免费的专业网站流量分析工具,能够告诉用户访客是如何找到并浏览用户的网站的,以及在网站上浏览了哪些页面,这些信息可以帮助用户在其网站上的使用体验,不断提升网站的投资回报率
百度统计提供了几十种图形化报告,包括:趋势分析,来源分析,页面分析,访客分析,定制分析等多种统计分析服务
这里我们参考百度统计的功能,基于Spark Streaming 简单实现一个分析系统,使之包括以下分析功能
- 流量分析
- 一段时间内用户网站的流量变化趋势,针对不同的IP对用户网站的流量进行细分,常见指标是总PV和各IP的PV
- 来源分析
- 各种搜索引擎来源给用户网站带来的流量情况,需要精确到具体的搜索引擎,具体关键词。通过来源分析,用户可以及时了解哪种类型的来源为其带来了更多访客。常见指标杲搜索引擎,关键词和终端类型的PV
- 网站分析
- 各个页面的访问情况,包括及时了解哪些页面最吸引访客以及哪些页面最容易导致访客流失, 从而帮助用户更有针对性地改善网站质量。常见指标是各页面的PV。
2.1日志实时采集
Web log一般在HTTP服务器收集,比如Nginx access日志文件。一个典型的方案是 Nginx日志文件+Flume +Kafka + Spark Streaming,如下所述:
- 1.接收服务器用Nginx,根据负载可以部署多台,数据落地至本地日志文件;
- 2.每个 Nginx节点上部署Flume,使用tail-f实时读取 Nginx日志,发送至
KafKa 集群; - 3.专用的 Kafka集群用户连接实时日志与Spark集群
- 4.Spark Streaming程序实时消费 Kafka 集群上的数据,实时分析,输出;
- 5.结果写入MySQL 数据库。
当然,还可以进一步优化,比如CGI程序直接发日志消息到Kafka,节省了写访问日志的磁盘开销。这里主要专注Spark Streaming的应用,所以我们不做详细论述。
2.2流失分析系统实现
我们简单模拟—下数据收集和发送的环节
用一个 Python脚本随机生成Nginx访问日志,并通过脚本的方式自动上传至HDFS,然后移动至指定目录。
SparkStreaming程序监控HDFS目录,自动处理新的文件。
生成Nginx日志的Python代码如下,保存为文件sample_web_log.py 。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import random
import time
class WebLogGeneration(object):
# 类属性,由所有类的对象共享
site_url_base = "http://www.xxx.com/"
# 基本构造函数
def __init__(self):
# 前面7条是IE,所以大概浏览器类型70%为IE ,接入类型上,20%为移动设备,分别是7和8条,5% 为空
# https://github.com/mssola/user_agent/blob/master/all_test.go
self.user_agent_dist = {0.0:"Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Trident/6.0)",
0.1:"Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Trident/6.0)",
0.2:"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; .NET CLR 2.0.50727)",
0.3:"Mozilla/4.0 (compatible; MSIE6.0; Windows NT 5.0; .NET CLR 1.1.4322)",
0.4:"Mozilla/5.0 (Windows NT 6.1; Trident/7.0; rv:11.0) like Gecko",
0.5:"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:41.0) Gecko/20100101 Firefox/41.0",
0.6:"Mozilla/4.0 (compatible; MSIE6.0; Windows NT 5.0; .NET CLR 1.1.4322)",
0.7:"Mozilla/5.0 (iPhone; CPU iPhone OS 7_0_3 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11B511 Safari/9537.53",
0.8:"Mozilla/5.0 (Linux; Android 4.2.1; Galaxy Nexus Build/JOP40D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19",
0.9:"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36",
1:" ",}
self.ip_slice_list = [10, 29, 30, 46, 55, 63, 72, 87, 98,132,156,124,167,143,187,168,190,201,202,214,215,222]
self.url_path_list = ["login.php","view.php","list.php","upload.php","admin/login.php","edit.php","index.html"]
self.http_refer = [ "http://www.baidu.com/s?wd={query}","http://www.google.cn/search?q={query}","http://www.sogou.com/web?query={query}","http://one.cn.yahoo.com/s?p={query}","http://cn.bing.com/search?q={query}"]
self.search_keyword = ["spark","hadoop","hive","spark mlib","spark sql"]
def sample_ip(self):
slice = random.sample(self.ip_slice_list, 4) #从ip_slice_list中随机获取4个元素,作为一个片断返回
return ".".join([str(item) for item in slice]) # todo
def sample_url(self):
return random.sample(self.url_path_list,1)[0]
def sample_user_agent(self):
dist_uppon = random.uniform(0, 1)
return self.user_agent_dist[float('%0.1f' % dist_uppon)]
# 主要搜索引擎referrer参数
def sample_refer(self):
if random.uniform(0, 1) > 0.2: # 只有20% 流量有refer
return "-"
refer_str=random.sample(self.http_refer,1)
query_str=random.sample(self.search_keyword,1)
return refer_str[0].format(query=query_str[0])
def sample_one_log(self,count = 3):
time_str = time.strftime("%Y-%m-%d %H:%M:%S",time.localtime())
while count >1:
query_log = "{ip} - - [{local_time}] \"GET /{url} HTTP/1.1\" 200 0 \"{refer}\" \"{user_agent}\" \"-\"".format(ip=self.sample_ip(),local_time=time_str,url=self.sample_url(),refer=self.sample_refer(),user_agent=self.sample_user_agent())
print query_log
count = count -1
if __name__ == "__main__":
web_log_gene = WebLogGeneration()
#while True:
# time.sleep(random.uniform(0, 3))
web_log_gene.sample_one_log(random.uniform(10, 100))
这是一条日志的示例,为一行形式,各字段间用空格分隔,字符串类型的值用双
引号包围:
46.202.124.63 - - [2015-11-26 09:54:27] "GET /view.php HTTP/1.1" 200 0 "http://www.google.cn/search?q=hadoop" "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Trident/6.0)" "-"
然后需要一个简单的脚本来调用上面的脚本以随机生成日志,上传至HDFS,
然后移动到目标目录:
#!/bin/bash
# HDFS命令
HDFS="/opt/cloudera/parcels/CDH/lib/hadoop/bin/hadoop fs"
# Streaming程序监听的目录,注意跟后面Streaming程序的配置要保持一致
streaming_dir="/spark/streaming"
# 清空旧数据
$HDFS -rm "${streaming_dir}"'/tmp/*' > /dev/null 2>&1
$HDFS -rm "${streaming_dir}"'/*' > /dev/null 2>&1
# 一直运行
while [ 1 ]; do
./sample_web_log.py > test.log
# 给日志文件加上时间戳,避免重名
tmplog="access.`date +'%s'`.log"
# 先放在临时目录,再move至Streaming程序监控的目录下,确保原子性
# 临时目录用的是监控目录的子目录,因为子目录不会被监控
$HDFS -put test.log ${streaming_dir}/tmp/$tmplog
$HDFS -mv ${streaming_dir}/tmp/$tmplog ${streaming_dir}/
echo "`date +"%F %T"` put $tmplog to HDFS succeed"
sleep 1
done
Spark Streaming程序代码如下所示,可以在 binl/spark-shell交互式环境下运行
如果要以Spark程序的方式运行,按注释中的说明调整一下StreamingContext的生成方式即可。
启动bin/spark-shell时,为了避免因DEBUG日志信息太多而影响观察输出,可以将DEBUG日志重定向至文件,屏幕上只显示主要输出,方法是
./bin/spark-shell 2>spark-shell-debug.log
// 导入类
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
// 设计计算的周期,单位秒
val batch = 10
/*
* 这是bin/spark-shell交互式模式下创建StreamingContext的方法
* 非交互式请使用下面的方法来创建
*/
val ssc = new StreamingContext(sc, Seconds(batch))
/*
// 非交互式下创建StreamingContext的方法
val conf = new SparkConf().setAppName("NginxAnay")
val ssc = new StreamingContext(conf, Seconds(batch))
*/
/*
* 创建输入DStream,是文本文件目录类型
* 本地模式下也可以使用本地文件系统的目录,比如 file:///home/spark/streaming
*/
val lines = ssc.textFileStream("hdfs:///spark/streaming")
/*
* 下面是统计各项指标,调试时可以只进行部分统计,方便观察结果
*/
// 1. 总PV
lines.count().print()
// 2. 各IP的PV,按PV倒序
// 空格分隔的第一个字段就是IP
lines.map(line => {(line.split(" ")(0), 1)}).reduceByKey(_ + _).transform(rdd => {
rdd.map(ip_pv => (ip_pv._2, ip_pv._1)).
sortByKey(false).
map(ip_pv => (ip_pv._2, ip_pv._1))
}).print()
// 3. 搜索引擎PV
val refer = lines.map(_.split("\"")(3))
// 先输出搜索引擎和查询关键词,避免统计搜索关键词时重复计算
// 输出(host, query_keys)
val searchEnginInfo = refer.map(r => {
val f = r.split('/')
val searchEngines = Map(
"www.google.cn" -> "q",
"www.yahoo.com" -> "p",
"cn.bing.com" -> "q",
"www.baidu.com" -> "wd",
"www.sogou.com" -> "query"
)
if (f.length > 2) {
val host = f(2)
if (searchEngines.contains(host)) {
val query = r.split('?')(1)
if (query.length > 0) {
val arr_search_q = query.split('&').filter(_.indexOf(searchEngines(host)+"=") == 0)
if (arr_search_q.length > 0)
(host, arr_search_q(0).split('=')(1))
else
(host, "")
} else {
(host, "")
}
} else
("", "")
} else
("", "")
})
// 输出搜索引擎PV
searchEnginInfo.filter(_._1.length > 0).map(p => {(p._1, 1)}).reduceByKey(_ + _).print()
// 4. 关键词PV
searchEnginInfo.filter(_._2.length > 0).map(p => {(p._2, 1)}).reduceByKey(_ + _).print()
// 5. 终端类型PV
lines.map(_.split("\"")(5)).map(agent => {
val types = Seq("iPhone", "Android")
var r = "Default"
for (t <- types) {
if (agent.indexOf(t) != -1)
r = t
}
(r, 1)
}).reduceByKey(_ + _).print()
// 6. 各页面PV
lines.map(line => {(line.split("\"")(1).split(" ")(1), 1)}).reduceByKey(_ + _).print()
// 启动计算,等待执行结束(出错或Ctrl-C退出)
ssc.start()
ssc.awaitTermination()
打开两个终端,一个调用上面的bash脚本模拟提交日志,一个在交互式环境下运行上面的
Streaming程序。你可以看到各项指标的输出,比如某个批次下的输出为(依次对应上面的6个计
算项)∶
1.总PV
-------------------------------------------
Time: 1448533850000 ms
-------------------------------------------
44374
2.各IP的PV,按PV倒序
-------------------------------------------
Time: 1448533850000 ms
-------------------------------------------
(72.63.87.30,30)
(63.72.46.55,30)
(98.30.63.10,29)
(72.55.63.46,29)
(63.29.10.30,29)
(29.30.63.46,29)
(55.10.98.87,27)
(46.29.98.30,27)
(72.46.63.30,27)
(87.29.55.10,26)
3.搜索引擎PV
-------------------------------------------
Time: 1448533850000 ms
-------------------------------------------
(cn.bing.com,1745)
(www.baidu.com,1773)
(www.google.cn,1793)
(www.sogou.com,1845)
4.关键字PV
-------------------------------------------
Time: 1448533850000 ms
-------------------------------------------
(spark,1426)
(hadoop,1455)
(spark sql,1429)
(spark mlib,1426)
(hive,1420)
5.终端类型PV
-------------------------------------------
Time: 1448533850000 ms
-------------------------------------------
(Android,4281)
(Default,35745)
(iPhone,4348)
6.各页面PV
-------------------------------------------
Time: 1448533850000 ms
-------------------------------------------
(/edit.php,6435)
(/admin/login.php,6271)
(/login.php,6320)
(/upload.php,6278)
(/list.php,6411)
(/index.html,6309)
(/view.php,6350)
查看数据更直观的做法是用图形来展示,常见做法是将结果写入外部DB,然后通过一些图形化报
表展示系统展示出来。
除了常规的每个固定周期进行一次统计,我们还可以对连续多个周期的数据进行统计。
以统计总PV为例,上面的示例是每10秒统计一次,可能还需要每分钟统计一次,相当于6个10秒的周期。我们可以利用窗口方法实现,不同的代码如下:
// 窗口方法必须配置checkpint,可以这样配置:
ssc.checkpoint("hdfs:///spark/checkpoint")
// 这是常规每10秒一个周期的PV统计
lines.count().print()
// 这是每分钟(连续多个周期)一次的PV统计
lines.countByWindow(Seconds(batch*6), Seconds(batch*6)).print()
使用相同的办法运行程序之后,我们首先会看到连续 6 次 10 秒周期的 PV 统计输出:
-------------------------------------------
Time: 1448535090000 ms
-------------------------------------------
1101
-------------------------------------------
Time: 1448535100000 ms
-------------------------------------------
816
-------------------------------------------
Time: 1448535110000 ms
-------------------------------------------
892
-------------------------------------------
Time: 1448535120000 ms
-------------------------------------------
708
-------------------------------------------
Time: 1448535130000 ms
-------------------------------------------
881
-------------------------------------------
Time: 1448535140000 ms
-------------------------------------------
872
在这之后,有一个 1 分钟周期的 PV 统计输出,它的值刚好是上面 6 次计算结果的总和:
-------------------------------------------
Time: 1448535140000 ms
-------------------------------------------
5270
3.开发准备
下面我们将会进行一些开发准备工作,修改好相应的执行权限。
3.1准备生成日志的 Python 代码
3.1.1 编辑代码
- 新建文件
touch sample_web_log.py
vi sample_web_log.py
编辑完所有 Python 代码后保存并退出。
默认情况下,文件 sample_web_log.py 存放于 /home/yuorUser 目录中。
3.1.2 修改代码的执行权限
当前编辑完成的代码还不能直接通过 ./sample_web_log.py 的方式执行,我们需要为其增加可执行权限。
chmod +x sample_web_log.py
完成以上步骤后,就可以利用该代码来模拟生成日志了。
3.2 启动 Spark Shell
接下来需要启动 Spark Shell 来定制 Streaming 任务。为了避免因 DEBUG 日志信息太多而影响观察输出,可以将 DEBUG 日志重定向至文件,屏幕上只显示主要输出。
请通过以下代码来启动 Spark Shell 。启动需要耗费一定的时间,请耐心等待。
spark-shell 2>spark-shell-debug.log
等到出现 scala> 提示符时,就表明已经成功启动 Spark Shell 了。
请不要关闭运行 Spark Shell 的终端,其他任何的终端命令请在新打开的终端中执行。
[hejiahao@hadoop02 ~]$ spark-shell 2>spark-shell-debug.log
Spark context Web UI available at http://hadoop02:4040
Spark context available as 'sc' (master = yarn, app id = application_1593578511528_0006).
Spark session available as 'spark'.
Welcome to
____ __
/ __/__ ___ _____/ /__
_\ \/ _ \/ _ `/ __/ '_/
/___/ .__/\_,_/_/ /_/\_\ version 2.4.0-cdh6.3.1
/_/
Using Scala version 2.11.12 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_181)
Type in expressions to have them evaluated.
Type :help for more information.
scala>
4.实现步骤
下面我们开始对日志进行相应的分析。
4.1 创建日志目录
在稍后的步骤中,我们会将 Python 代码生成的日志保存到文件里。因此首先需要为保存日志文件而创建一个空目录。
在 /home/yourUser 目录下新建 streaming 目录,并增设 tmp 临时文件夹。
所需要用到的命令如下:
mkdir /home/yourUser/streaming
mkdir /home/yourUser/streaming/tmp
4.2 通过 bash 脚本生成日志
需要一个简单的脚本来调用上面的脚本以随机生成日志。
新建终端中输入以下命令,新建脚本文件并对其进行编辑:
touch genLog.sh
vi genLog.sh
genLog.sh 文件中需要填入以下内容:
#!/bin/bash
while [ 1 ]; do
./sample_web_log.py > test.log
tmplog="access.`date +'%s'`.log"
cp test.log streaming/tmp/$tmplog
mv streaming/tmp/$tmplog streaming/
echo "`date +"%F %T"` generating $tmplog succeed"
sleep 1
done
同时需要修改该脚本文件的执行权限。
chmod +x genLog.sh
4.3 在 Spark Streaming 中进行日志分析
首先是引用相关的包。由于我们使用的是 Spark Shell(即以交互式模式进行编程),在它启动的过程中就已经创建了 SparkContext 对象 sc,因此我们可以直接使用 sc 对象。
导入 Streaming 的相关类:
import org.apache.spark.streaming.{Seconds, StreamingContext}
scala> import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.streaming.{Seconds, StreamingContext}
scala>
设计计算的周期为 10 秒。
val batch = 10
在交互式模式下直接创建 StreamingContext 。
val ssc = new StreamingContext(sc, Seconds(batch))
创建输入 DStream,是文本文件目录类型。我们在这里使用本地文件系统的目录,即之前创建的 /home/yourUser/streaming 目录:
val lines = ssc.textFileStream("file:///home/hejiahao/streaming")
接下来就可以统计各项指标。
首先是总的 PV 。
lines.count().print()
其次是各 IP 的 PV ,按 PV 倒序。空格分隔的第一个字段就是 IP 。
lines.map(line => {(line.split(" ")(0), 1)}).reduceByKey(_ + _).transform(rdd => {
rdd.map(ip_pv => (ip_pv._2, ip_pv._1)).
sortByKey(false).
map(ip_pv => (ip_pv._2, ip_pv._1))
}).print()
第三个是搜索引擎的 PV 。
val refer = lines.map(_.split("\"")(3))
// 先输出搜索引擎和查询关键词,避免统计搜索关键词时重复计算
// 输出(host, query_keys)
val searchEnginInfo = refer.map(r => {
val f = r.split('/')
val searchEngines = Map(
"www.google.cn" -> "q",
"www.yahoo.com" -> "p",
"cn.bing.com" -> "q",
"www.baidu.com" -> "wd",
"www.sogou.com" -> "query"
)
if (f.length > 2) {
val host = f(2)
if (searchEngines.contains(host)) {
val query = r.split('?')(1)
if (query.length > 0) {
val arr_search_q = query.split('&').filter(_.indexOf(searchEngines(host)+"=") == 0)
if (arr_search_q.length > 0)
(host, arr_search_q(0).split('=')(1))
else
(host, "")
} else {
(host, "")
}
} else
("", "")
} else
("", "")
})
// 输出搜索引擎PV
searchEnginInfo.filter(_._1.length > 0).map(p => {(p._1, 1)}).reduceByKey(_ + _).print()
第四是关键词的 PV 。
searchEnginInfo.filter(_._2.length > 0).map(p => {(p._2, 1)}).reduceByKey(_ + _).print()
第 5 是终端类型的 PV 。
lines.map(_.split("\"")(5)).map(agent => {
val types = Seq("iPhone", "Android")
var r = "Default"
for (t <- types) {
if (agent.indexOf(t) != -1)
r = t
}
(r, 1)
}).reduceByKey(_ + _).print()
最后是各个页面的 PV
lines.map(line => {(line.split("\"")(1).split(" ")(1), 1)}).reduceByKey(_ + _).print()
各项统计指标定制好之后,就可以启动计算,等待执行结束。
ssc.start()
ssc.awaitTermination()
4.4 开始生成日志并查看结果
先不要关闭运行着 Spark Streaming 的终端,回到之前创建 genLog.sh 文件的终端里(或者新打开一个),运行 genLog.sh 脚本。
./genLog.sh
在 Spark Streaming 的终端内,就可以看到输出的分析结果了。
总的 PV,各 IP 的 PV,各搜索引擎的 PV,各关键词的 PV,终端类型的 PV,各页面的 PV:
-------------------------------------------
Time: 1600763240000 ms
-------------------------------------------
(29.63.143.87,2)
(46.10.190.187,2)
(222.168.187.72,1)
(187.10.202.168,1)
(63.132.167.124,1)
(156.46.215.55,1)
(202.98.168.187,1)
(187.55.214.98,1)
(168.156.87.132,1)
(10.72.143.156,1)
...
-------------------------------------------
Time: 1600763240000 ms
-------------------------------------------
(www.google.cn,28)
(www.sogou.com,19)
(cn.bing.com,28)
(www.baidu.com,21)
-------------------------------------------
Time: 1600763240000 ms
-------------------------------------------
(hive,21)
(spark mlib,13)
(spark sql,17)
(spark,17)
(hadoop,28)
-------------------------------------------
Time: 1600763240000 ms
-------------------------------------------
(Default,484)
(iPhone,63)
(Android,60)
-------------------------------------------
Time: 1600763240000 ms
-------------------------------------------
(/upload.php,80)
(/edit.php,105)
(/admin/login.php,73)
(/login.php,87)
(/index.html,86)
(/view.php,99)
(/list.php,77)
观察完毕,请通过 Ctrl + C 关闭日志生成的进程和 Spark Streaming 的进程。
5.实施总结
利用 Spark Streaming ,以流量分析、来源分析和页面访问情况分析等角度完成了一个类似于百度统计的日志分析系统。在这个过程中,以 Spark Streaming 的编程方式,以及如何对日志中各个字段进行处理。实验本身是每 10 秒进行一次统计的,除此之外,还可以尝试对连续多个周期的数据进行统计。

浙公网安备 33010602011771号