当今,利用分析的最流行和有效的企业用例之一是日志分析。 如今,几乎每个组织都日复一日地运行着多个系统和基础架构。 为了有效地保持业务运行,这些组织需要知道其基础架构是否正在发挥最大的潜力。 查找涉及分析系统和应用程序日志,甚至可能对日志数据进行预测分析。 通常,所涉及的日志数据量很大,这取决于所涉及的组织基础结构的类型以及在其上运行的应用程序。
日志数据处理管道。
由于计算限制,我们只能在一台机器上分析数据样本的日子已经一去不复返了。 在大数据,更好的分布式计算以及像Apache Spark这样的框架进行大数据处理和开源分析的支持下,我们每天可以对数十亿条日志消息执行可伸缩的日志分析。 本案例研究指南的目的是采用动手方法,展示我们如何利用Spark来对半结构化日志数据进行大规模的日志分析。 如果您对使用Spark的可伸缩SQL感兴趣,请随时使用Spark大规模查看SQL 。
尽管有许多出色的开源框架和工具可用于日志分析(例如Elasticsearch) ,但该分为两部分的教程的目的是展示如何利用Spark来大规模分析日志。 在现实世界中,您当然可以在分析日志数据时自由选择自己的工具箱。
让我们开始吧!
主要目标:NASA日志分析
Spark使您可以廉价地将日志转储并存储到磁盘上的文件中,同时仍提供丰富的API来进行大规模数据分析。 这个动手案例研究将向您展示如何在NASA的实际生产日志上使用Apache Spark,同时学习数据争用以及探索性数据分析的基本而强大的技术。 在这项研究中,我们将分析佛罗里达州NASA肯尼迪航天中心网络服务器上的日志数据集。
完整的数据集(包含对NASA肯尼迪航天中心的所有HTTP请求的价值,为两个月)可在此处免费下载。 或者,如果您更喜欢FTP:
7月1日至7月31日,ASCII格式,已压缩20.7 MB gzip,未压缩205.2 MB: ftp : //ita.ee.lbl.gov/traces/NASA_access_log_Jul95.gz
8月4日至8月31日,ASCII 格式,压缩后的21.8 MB gzip,未压缩的167.8 MB: ftp : //ita.ee.lbl.gov/traces/NASA_access_log_Aug95.gz
接下来,如果要继续学习,请从我的GitHub下载该教程,并将这两个文件都放置在与该教程的Jupyter Notebook相同的目录中。
设置依赖
第一步是确保您有权访问Spark会话和集群。 对于此步骤,您可以使用自己的本地Spark设置或基于云的设置。 通常, 如今大多数云平台都提供Spark集群,并且您还有免费的选择,包括Databricks社区版 。 本教程假定您已经设置了Spark,因此我们不会花费额外的时间从头开始配置或设置Spark。
通常,在启动Jupyter Notebook服务器时,预配置的Spark设置通常已经预先加载了必要的环境变量或依赖项。 就我而言,我可以在笔记本中使用以下命令检查它们:
spark
这些结果表明,我的集群目前正在运行Spark 2.4.0。 我们还可以使用以下代码检查sqlContext是否存在:
sqlContext
<pyspark.sql.context.SQLContext at 0x7fb1577b6400>
现在,如果您没有预先配置这些变量并出现错误,则可以使用以下代码加载和配置它们:
# configure spark variables
from pyspark.
context
import SparkContext
from pyspark.
sql .
context
import SQLContext
from pyspark.
sql .
session
import SparkSession
sc
= SparkContext
(
)
sqlContext
= SQLContext
( sc
)
spark
= SparkSession
( sc
)
# load up other dependencies
import
re
import pandas
as pd
我们还需要加载其他库以使用DataFrame和正则表达式 。 使用正则表达式是解析日志文件的主要方面之一。 该工具提供了强大的模式匹配技术,可用于提取和查找半结构化和非结构化数据中的模式。
来自xkcd的Perl问题带。
正则表达式可能非常有效且功能强大,但是它们也可能使人感到困惑和困惑。 不过不用担心,通过实践,您可以真正利用它们的最大潜力。 以下示例展示了在Python中使用正则表达式的方法。 在这里,我们尝试查找给定输入句子中单词“ spark”的所有出现。
m
=
re .
finditer
( r
'.*?(spark).*?'
,
"I'm searching for a spark in PySpark"
,
re .
I
)
for match
in m:
print
( match
, match.
start
(
)
, match.
end
(
)
)
<_sre.SRE_Match object; span=(0, 25), match=“I'm searching for a spark”> 0 25
<_sre.SRE_Match object; span=(25, 36), match=' in PySpark'> 25 36
让我们继续分析的下一部分。
加载和查看NASA日志数据集
鉴于我们的数据存储在以下路径中(以平面文件的形式),让我们将其加载到DataFrame中。 我们将分步进行。 以下代码加载磁盘的日志数据文件名:
import
glob
raw_data_files
=
glob .
glob
(
'*.gz'
)
raw_data_files
['NASA_access_log_Jul95.gz', 'NASA_access_log_Aug95.gz']
现在,我们将使用sqlContext.read.text()或spark.read.text()读取文本文件。 这段代码产生一个带有单个字符串列的DataFrame,称为value :
base_df = spark.read.text(raw_data_files)
base_df.printSchema()
root
|-- value: string (nullable = true)
此输出使我们能够查看将很快检查的日志数据模式的文本。 您可以使用以下代码查看保存日志数据的数据结构类型:
type ( base_df )
pyspark.sql.dataframe.DataFrame
在整个教程中,我们使用Spark DataFrames。 但是,如果需要,还可以通过添加以下代码将DataFrame转换为Spark的原始数据结构()的弹性分布式数据集(RDD) :
base_df_rdd
= base_df.
rdd
type
( base_df_rdd
)
pyspark.rdd.RDD
现在让我们看一下DataFrame中的实际日志数据:
base_df. show ( 10 , truncate = False )
![数据框中的日志数据。 The log data within the dataframe.](https://opensource.com/sites/default/files/uploads/processing_and_wrangling_nasa_logs_figure_4_600_0.png)
数据框中的日志数据。
此结果肯定看起来像标准的半结构服务器日志数据。 在此文件有用之前,我们肯定需要进行一些数据处理和整理。 请记住,从RDD访问数据略有不同,如下所示:
base_df_rdd. take ( 10 )
![从弹性分布式数据集中记录数据。 Log data from the resilient distributed datasets.](https://opensource.com/sites/default/files/uploads/processing_and_wrangling_nasa_logs_figure_5_600_0.png)
从弹性分布式数据集中记录数据。
现在我们已经加载并查看了日志数据,让我们对其进行处理和纠缠。
数据整理
在本节中,我们将清理并解析日志数据集,以从每条日志消息中提取具有有意义信息的结构化属性。
日志数据理解
如果您熟悉Web服务器日志,您将认识到上面显示的数据是“ 通用日志格式” 。 这些字段是:
remotehost rfc931 authuser [date] "request" status bytes
领域 | 描述 |
---|---|
remotehost | 远程主机名(如果DNS主机名不可用或DNSLookup关闭, 则为 IP号)。 |
rfc931 | 用户的远程登录名(如果存在)。 |
authuser | 通过HTTP服务器验证后,远程用户的用户名。 |
[date] | 请求的日期和时间。 |
“request” | 完全来自浏览器或客户端的请求。 |
status | 服务器发送回客户端的HTTP状态代码 。 |
bytes | 传输到客户端的字节数( Content-Length )。 |
现在,我们需要从日志数据中解析,匹配和提取这些属性的技术。
使用正则表达式进行数据解析和提取
接下来,我们必须将半结构化日志数据解析为单独的列。 我们将使用特殊的内置regexp_extract() 函数进行解析。 此函数将列与具有一个或多个捕获 组的正则表达式匹配,并允许您提取匹配的组之一。 对于要提取的每个字段,我们将使用一个正则表达式。
到目前为止,您一定已经听说或使用了很多正则表达式。 如果您发现正则表达式令人困惑(当然可以 ),并且想了解更多有关它们的信息,建议您访问RegexOne网站 。 您可能还会发现Goyvaerts和Levithan撰写的Regular Expressions Cookbook是有用的参考。
让我们看一下数据集中正在处理的日志总数:
print ( ( base_df. count ( ) , len ( base_df. columns ) ) )
(3461613, 1)
看起来我们总共有大约346万条日志消息。 数目不小! 让我们提取并查看一些示例日志消息:
sample_logs
=
[ item
[
'value'
]
for item
in base_df.
take
(
15
)
]
sample_logs
样本日志消息。
![样本日志消息。 Sample log messages.](https://opensource.com/sites/default/files/uploads/processing_and_wrangling_nasa_logs_figure_6_600.png)
样本日志消息。
提取主机名
让我们写一些正则表达式从日志中提取主机名:
host_pattern
= r
'(^ \S + \. [ \S + \. ]+ \S +) \s '
hosts
=
[
re .
search
( host_pattern
, item
) .
group
(
1
)
if
re .
search
( host_pattern
, item
)
else
'no match'
for item
in sample_logs
]
hosts
['199.72.81.55',
'unicomp6.unicomp.net',
'199.120.110.21',
'burger.letters.com',
…,
…,
'unicomp6.unicomp.net',
'd104.aa.net',
'd104.aa.net']
提取时间戳
让我们使用正则表达式从日志中提取时间戳字段:
ts_pattern
= r
' \[ ( \d {2}/ \w {3}/ \d {4}: \d {2}: \d {2}: \d {2} - \d {4})]'
timestamps
=
[
re .
search
( ts_pattern
, item
) .
group
(
1
)
for item
in sample_logs
]
timestamps
['01/Jul/1995:00:00:01 -0400',
'01/Jul/1995:00:00:06 -0400',
'01/Jul/1995:00:00:09 -0400',
…,
…,
'01/Jul/1995:00:00:14 -0400',
'01/Jul/1995:00:00:15 -0400',
'01/Jul/1995:00:00:15 -0400']
提取HTTP请求方法,URI和协议
现在,让我们使用正则表达式从日志中提取HTTP请求方法, URI和协议模式字段:
method_uri_protocol_pattern
= r
' \" ( \S +) \s ( \S +) \s *( \S *) \" '
method_uri_protocol
=
[
re .
search
( method_uri_protocol_pattern
, item
) .
groups
(
)
if
re .
search
( method_uri_protocol_pattern
, item
)
else
'no match'
for item
in sample_logs
]
method_uri_protocol
[('GET', '/history/apollo/', 'HTTP/1.0'),
('GET', '/shuttle/countdown/', 'HTTP/1.0'),
…,
…,
('GET', '/shuttle/countdown/count.gif', 'HTTP/1.0'),
('GET', '/images/NASA-logosmall.gif', 'HTTP/1.0')]
提取HTTP状态代码
现在,让我们使用正则表达式从日志中提取HTTP状态代码:
status_pattern
= r
' \s ( \d {3}) \s '
status
=
[
re .
search
( status_pattern
, item
) .
group
(
1
)
for item
in sample_logs
]
print
( status
)
['200', '200', '200', '304', …, '200', '200']
提取HTTP响应内容大小
现在,让我们使用正则表达式从日志中提取HTTP响应内容的大小:
content_size_pattern
= r
' \s ( \d +)$'
content_size
=
[
re .
search
( content_size_pattern
, item
) .
group
(
1
)
for item
in sample_logs
]
print
( content_size
)
['6245', '3985', '4085', '0', …, '1204', '40310', '786']
放在一起
现在,让我们利用以前构建的所有正则表达式模式,并使用regexp_extract(...)
方法构建DataFrame,并将所有日志属性整齐地提取到其单独的列中。
from pyspark.
sql .
functions
import regexp_extract
logs_df
= base_df.
select
( regexp_extract
(
'value'
, host_pattern
,
1
) .
alias
(
'host'
)
,
regexp_extract
(
'value'
, ts_pattern
,
1
) .
alias
(
'timestamp'
)
,
regexp_extract
(
'value'
, method_uri_protocol_pattern
,
1
) .
alias
(
'method'
)
,
regexp_extract
(
'value'
, method_uri_protocol_pattern
,
2
) .
alias
(
'endpoint'
)
,
regexp_extract
(
'value'
, method_uri_protocol_pattern
,
3
) .
alias
(
'protocol'
)
,
regexp_extract
(
'value'
, status_pattern
,
1
) .
cast
(
'integer'
) .
alias
(
'status'
)
,
regexp_extract
(
'value'
, content_size_pattern
,
1
) .
cast
(
'integer'
) .
alias
(
'content_size'
)
)
logs_df.
show
(
10
, truncate
=
True
)
print
(
( logs_df.
count
(
)
,
len
( logs_df.
columns
)
)
)
![使用regexp_extract(...)提取的日志数据帧 Log dataframe extracted using regexp_extract(...)](https://opensource.com/sites/default/files/uploads/processing_and_wrangling_nasa_logs_figure_7_600.png)
使用regexp_extract(...)提取的日志数据帧
寻找缺失值
缺失值和空值是数据分析和机器学习的祸根。 让我们看看我们的数据解析和提取逻辑如何工作。 首先,让我们验证原始DataFrame中没有空行:
( base_df
.
filter
( base_df
[
'value'
]
.
isNull
(
)
)
.
count
(
)
)
0
都好! 现在,如果我们的数据解析和提取工作正常,则不应有任何行具有潜在的空值。 让我们尝试对其进行测试:
bad_rows_df
= logs_df.
filter
( logs_df
[
'host'
] .
isNull
(
) |
logs_df
[
'timestamp'
] .
isNull
(
) |
logs_df
[
'method'
] .
isNull
(
) |
logs_df
[
'endpoint'
] .
isNull
(
) |
logs_df
[
'status'
] .
isNull
(
) |
logs_df
[
'content_size'
] .
isNull
(
) |
logs_df
[
'protocol'
] .
isNull
(
)
)
bad_rows_df.
count
(
)
33905
哎哟! 看起来我们的数据中缺少超过33K的值! 我们可以处理吗?
请记住,这不是常规的熊猫(链接)DataFrame,您可以直接查询它并获取哪些列为空。 我们所谓的大数据集驻留在磁盘上,该磁盘可能存在于spark集群的多个节点中。 那么,我们如何找出哪些列具有潜在的空值?
查找空计数
我们通常可以使用以下技术来找出哪些列具有空值。
注意:此方法改编自StackOverflow的一个很好的答案 。
from pyspark.
sql .
functions
import col
from pyspark.
sql .
functions
import
sum
as spark_sum
def count_null
( col_name
) :
return spark_sum
( col
( col_name
) .
isNull
(
) .
cast
(
'integer'
)
) .
alias
( col_name
)
# Build up a list of column expressions, one per column.
exprs
=
[ count_null
( col_name
)
for col_name
in logs_df.
columns
]
# Run the aggregation. The *exprs converts the list of expressions into
# variable function arguments.
logs_df.
agg
( *exprs
) .
show
(
)
检查哪些列具有空值。
好吧,好像我们在status列中缺少一个值,而其他所有内容都在content_size列中。 让我们看看是否可以找出问题所在!
在HTTP状态下处理null
我们对状态列的原始解析正则表达式为:
regexp_extract
(
'value'
, r
' \s ( \d {3}) \s '
,
1
) .
cast
(
'integer'
)
.
alias
(
'status'
)
可能还有更多的数字使我们的正则表达式错误吗? 还是数据点本身不好? 让我们找出答案。
注意 :在下面的表达式中,波浪号( ~ )
表示“不是”。
null_status_df
= base_df.
filter
(
~ base_df
[
'value'
] .
rlike
( r
' \s ( \d {3}) \s '
)
)
null_status_df.
count
(
)
1
让我们看看这个不良记录是什么样的:
null_status_df.show(truncate=False)
错误的记录,缺少信息。
看起来像一条记录,其中包含很多缺少的信息。 让我们通过日志数据解析管道进行传递:
bad_status_df
= null_status_df.
select
( regexp_extract
(
'value'
, host_pattern
,
1
) .
alias
(
'host'
)
,
regexp_extract
(
'value'
, ts_pattern
,
1
) .
alias
(
'timestamp'
)
,
regexp_extract
(
'value'
, method_uri_protocol_pattern
,
1
) .
alias
(
'method'
)
,
regexp_extract
(
'value'
, method_uri_protocol_pattern
,
2
) .
alias
(
'endpoint'
)
,
regexp_extract
(
'value'
, method_uri_protocol_pattern
,
3
) .
alias
(
'protocol'
)
,
regexp_extract
(
'value'
, status_pattern
,
1
) .
cast
(
'integer'
) .
alias
(
'status'
)
,
regexp_extract
(
'value'
, content_size_pattern
,
1
) .
cast
(
'integer'
) .
alias
(
'content_size'
)
)
bad_status_df.
show
( truncate
=
False
)
完整的错误日志记录,不包含任何信息和两个空条目。
看起来记录本身是不完整的记录,没有有用的信息,最好的选择是按以下方式删除该记录:
logs_df
= logs_df
[ logs_df
[
'status'
] .
isNotNull
(
)
]
exprs
=
[ count_null
( col_name
)
for col_name
in logs_df.
columns
]
logs_df.
agg
( *exprs
) .
show
(
)
删除的记录。
处理HTTP内容大小中的null
根据先前的正则表达式,我们对content_size列的原始解析正则表达式为:
regexp_extract
(
'value'
, r
' \s ( \d +)$'
,
1
) .
cast
(
'integer'
)
.
alias
(
'content_size'
)
我们原始数据集中的数据可能会丢失吗? 让我们找出答案。 我们首先在基本DataFrame中找到具有潜在缺失内容大小的记录:
null_content_size_df
= base_df.
filter
(
~ base_df
[
'value'
] .
rlike
( r
' \s \d +$'
)
)
null_content_size_df.
count
(
)
33905
这个数字似乎与我们处理的DataFrame中缺少的内容大小值的数量匹配。 让我们看一下缺少内容大小的数据帧的前十条记录:
null_content_size_df. take ( 10 )
![缺少内容大小的前10个数据框记录。 The top 10 dataframe records with missing content sizes.](https://opensource.com/sites/default/files/uploads/processing_and_wrangling_nasa_logs_figure_12_600.png)
缺少内容大小的前10个数据框记录。
很明显,不良的原始数据记录与错误响应相对应,在错误响应中,没有内容被发送回,并且服务器为content_size
字段发出了-
。 由于我们不想从分析中丢弃这些行,因此我们将其估算或填充为0。
使用空content_size修复行
最简单的解决方案是像我们前面讨论的那样,用0替换logs_df
的空值。 Spark DataFrame API提供了一组专门用于处理空值的函数和字段,其中包括:
-
fillna ()
,用指定的非空值填充空值。 -
na
,它返回一个DataFrameNaFunctions
对象,该对象具有用于对空列进行操作的许多函数。
有几种方法可以调用此功能。 最简单的方法就是用已知值替换所有空列。 但是,为了安全起见,最好传递一个包含(column_name, value)
映射的Python字典。 那就是我们要做的。 文档中的一个示例如下所示:
>>> df4.
na .
fill
(
{
'age' :
50
,
'name' :
'unknown'
}
) .
show
(
)
+---+------+-------+
|age|height| name|
+---+------+-------+
|
10 |
80 | Alice|
|
5 | null| Bob|
|
50 | null| Tom|
|
50 | null|unknown|
+---+------+-------+
现在,我们使用此函数用0填充content_size
字段中的所有缺失值:
logs_df
= logs_df.
na .
fill
(
{
'content_size' :
0
}
)
exprs
=
[ count_null
( col_name
)
for col_name
in logs_df.
columns
]
logs_df.
agg
( *exprs
) .
show
(
)
空值现在替换为零。
看看,没有缺失的价值!
处理时间字段(时间戳)
现在,我们有了一个干净的,已解析的DataFrame,我们必须将timestamp字段解析为实际的时间戳。 通用日志格式时间有些不标准。 用户定义函数(UDF)是解析它的最直接方法:
from pyspark.
sql .
functions
import udf
month_map
=
{
'Jan' :
1
,
'Feb' :
2
,
'Mar' :
3
,
'Apr' :
4
,
'May' :
5
,
'Jun' :
6
,
'Jul' :
7
,
'Aug' :
8
,
'Sep' :
9
,
'Oct' :
10
,
'Nov' :
11
,
'Dec' :
12
}
def parse_clf_time
( text
) :
""" Convert Common Log time format into a Python datetime object
Args:
text (str): date and time in Apache time format [dd/mmm/yyyy:hh:mm:ss (+/-)zzzz]
Returns:
a string suitable for passing to CAST('timestamp')
"""
# NOTE: We're ignoring the time zones here, might need to be handled depending on the problem you are solving
return
"{0:04d}-{1:02d}-{2:02d} {3:02d}:{4:02d}:{5:02d}" .
format
(
int
( text
[
7 :
11
]
)
,
month_map
[ text
[
3 :
6
]
]
,
int
( text
[
0 :
2
]
)
,
int
( text
[
12 :
14
]
)
,
int
( text
[
15 :
17
]
)
,
int
( text
[
18 :
20
]
)
)
现在让我们使用此函数来解析我们的DataFrame的time
列:
udf_parse_time
= udf
( parse_clf_time
)
logs_df
=
( logs_df.
select
(
'*'
, udf_parse_time
( logs_df
[
'timestamp'
]
)
.
cast
(
'timestamp'
)
.
alias
(
'time'
)
)
.
drop
(
'timestamp'
)
logs_df.
show
(
10
, truncate
=
True
)
![使用用户定义的函数(UDF)解析的时间戳。 The timestamp parsed with a User-Defined Function (UDF).](https://opensource.com/sites/default/files/uploads/processing_and_wrangling_nasa_logs_figure_14_600.png)
使用用户定义的函数(UDF)解析的时间戳。
情况看起来不错! 让我们通过检查DataFrame的架构来验证这一点:
logs_df. printSchema ( )
root
|-- host: string (nullable = true)
|-- method: string (nullable = true)
|-- endpoint: string (nullable = true)
|-- protocol: string (nullable = true)
|-- status: integer (nullable = true)
|-- content_size: integer (nullable = false)
|-- time: timestamp (nullable = true)
现在让我们缓存logs_df
因为我们将在本系列的第二部分中将其广泛用于数据分析部分。
logs_df. cache ( )
结论
在任何端到端数据科学或Analytics用例中,获取,处理和整理数据都是最重要的步骤。 大规模处理半结构化或非结构化数据时,事情开始变得越来越困难。 本案例研究为您提供了逐步实践的方法,以利用Python和Spark等开源工具和框架的功能来大规模处理和处理半结构化NASA日志数据。 准备好干净的数据集后,我们最终可以开始使用它来获取有关NASA服务器的有用见解。 单击本系列的第二篇文章,获得有关使用Python和Apache Spark分析和可视化NASA日志数据的动手教程 。
这篇文章最初出现在Medium的Towards Data Science频道上,经许可重新发布。
翻译自: https://opensource.com/article/19/5/log-data-apache-spark