近年来Web应用得到了空前的发展,Web用户显得越来越重要,用户对Web的要求也越来越高:Web需要有智能性,能快速、准确地帮助用户找到其需要的信息;同时,Web站点的运营方对Web用户的行为也越来越有兴趣。因此,对Web应用进行日志分析就具有价值。
分析Web日志数据的目的
Web日志由Web服务器产生,可能是Nginx、Apache、Tomcat等。目前越来越多的项目借助于服务器产生的Web日志来完成KPI分析,原因是服务器产生的日志并不依赖于业务系统生成,而是借助于服务器针对客户的HTTP请求参数属性来保存必要的信息,从数据生成和捕获两个方面都较为简单且不对业务系统产生侵入性,而且大多Web服务器支持自定义的日志格式以匹配通用或自定义的日志分析工具,而且服务器的日志文件中能够简单直接获取网络爬虫数据,也能够收集底层数据供反复分析。
了解Web用户的行为之后,可以为其提供更加精确的服务,如精确营销。另外,也可以根据用户的行为,调整Web的组织和结构,以吸引Web站点的不足之处,为Web站点的安全提供参考。
从Web日志中,可以获取网站每类页面的pv值、独立IP数;稍微复杂一些的,可以计算出用户所检索的关键词排行榜、用户停留时间最高的页面等;更复杂的,可以构建广告单机模型、分析用户行为特征等。根据Web日志的组成,其各项数据在网站数据统计和分析中都起到了重要作用,通过对服务器日志进行分析挖掘,得出用户的访问模式,它在网站个性化推荐、智能化服务上发挥着重要的作用。
重要的Web日志信息:请求(request)、状态码(status)、传输字节数(bytes)、来源页面(referrer)、用户代理(agent)、session和cookie
Web日志分析的典型应用场景
例如,某电子商务网站存在在线团购业务,通过日志分析后可以得到如下数据统计。
每日PV数450万,独立IP数25万。用户通常在工作日10:00~12:00和15:00 ~18:00访问量最大。
日间主要通过PC端浏览器访问,休息日及夜间通过移动设备访问较多。网站搜索流量占整个网站的80%,PC用户不足1%的用户会消费,移动用户有5%会消费。
通过简短的描述,可以粗略地看出电子商务网站的经营情况、愿意消费的用户来源、有哪些潜在的用户可以挖掘、网站是否存在倒闭风险等。
案例分析
本节将采用KPI对本案例的结构和分析方法进行分析。日志分析的KPI要围绕能够优化网站服务的目标来进行,分析结果要能够直接得出优化的指标建议。
Web常见的几个KPI如下:
- PV(PageView):页面访问量统计
- IP:页面独立IP的访问量统计
- Time:用户每小时PV的统计
- Source:用户来源域名的统计
- Browser:用户的访问设备统计
案例系统架构
日志是由业务系统产生的,可以设置Web服务器每天产生一个新的目录,目录下面会产生多个日志文件,每个日志文件128MB(Hddoop2之后的HDFS默认分块大小),设置系统定时器如CROD,每日0时后向HDFS导入头一天的日志文件。完成导入后,设置系统定时器,启动调度MapReduce程序,提取并计算统计指标。完成计算后,设置系统定时器,从HDFS导出数据到数据库,方便以后的实时查询。
系统结构如图所示。
日志分析方法
在Web日志中,每条日志通常代表用户的一次访问行为。如下是一条典型的Nginx日志。
222.68.172.190 - - [18/Sep/2013:06:49:57+00001] “GET/images/my.jpg HTTP/1.1” 200 19939 “http://???” “Mozilla/5.0 [Windows NT 6.1] AppleWebKit/537.36 [KHTML,like Gecko] Chrome/51.0.1547.66 Safarl/537.36”
这条日志可以通过分隔符拆解为变量数据供分析使用。
- remote_addr:记录客户端的IP地址,222.68.172.190。
- remote_user:记录客户端用户名称。
- time_local:记录访问时间与时区,[18/Sep/2013:06:49:57+00001]。
- request:记录请求的URL与HTTP,“GET/images/my.jpg HTTP/1.1”。
- status:记录请求状态,成功是200.
- body_bytes_sent:记录发送给客户端文件主题内容大小,19939.
- http_referer:记录从那个页面连接访问过来的。
- http_user_agent:记录客户浏览器的相关信息,“Mozilla/5.0 [Windows NT 6.1] AppleWebKit/537.36 [KHTML,like Gecko] Chrome/51.0.1547.66 Safarl/537.36”。
如果需要更多的信息,则要用其它手段获取。例如,通过javaScript代码单独发送请求,使用cookie记录用户的访问信息,使用业务代码主动记录自定义的业务操作日志。
由于Web的日志文件本身就是文件格式,因此无需对MapReduce默认数据的输入、输出格式进行自定义操作。在默认情况下,HDFS上的文件分块输入MapReduce流程后会被分割成单一的数据分片进行处理,而分片的依据就是换行符,即每一行作为一个输入分片调用一次Map操作,在Map操作中即可按照日志的格式分割规则(默认情况下空格分割)切分数据并获取需要的变量数据,构建成对应的键值对后交由Reduce处理汇总并保存最终的结果。
案例实现
1.定义日志相关属性字段
从Web日志的数据格式和分析要求来看,完成每个KPI分析的第一件事,就是将作为单条输入数据的日志中包含的不同的不同参数提取出来并保存在变量中供后续分析操作使用。因此,第一个步骤就是能够定义一个代表日志的类,类中定义的每一个属性字段都与日志文件中某个特定的属性参数匹配。
定义类com.Softi.mr.WEBlog.kpi.KPI,并在其中定义相关的属性字段。
//记录客户端的IP地址
private String remote_addr;
//记录客户端用户名称,忽略属性 "-"
private String remote_user;
//记录访问时间与时区
private String time_local;
//记录请求的UPL 与 HTTP
private String request;
//记录请求状态;成功是200
private String status;
//记录发送给客户端文件的主体内容大小
private String body_bytes_sent;
//用来记录从那个页面连接访问过来的
private String http_referer;
//记录客户端浏览器的相关信息
private String http_user_agent;
//判断数据是否合法:当valid变量取值为true时,该日志的数据将纳入分析的目标数据集合中,反之,该日志的数据在进行实际KPI指标分析时将被丢弃
private boolean valid=true;
2.解析日志
标准的单条Nginx日志的解析非常简单,参数属性之间使用了标准的空格作为分隔符,因此在Java代码中只需要使用空格将日志字符串转换为支付串数组,那么数组的每一个元素就按顺序保存对应参数属性取值,只需按照位置关系逐一将其保存到变量字段中即可。
private static KPI parser (String line) {
//System.out.println(line);
//构建用于保存日志信息的对象
KPI kpi = new KPI();
//利用标准的空格分割日志字符串(标准的Nginx格式)
String[] arr = line.split(" ");
//如果参数个数符合要求
if (arr.length > 11) {
//保存客户端的IP地址
kpi.setRemote_addr(arr[0]);
//保存客户端传递的用户数据
kpi.setremote_user(arr[1]);
//保存发起请求的时间和客户端的时区信息
kpi.setTime_local(arr[3].substring(1));
//保存本次请求对应的资源路径信息
kpi.setRequest(arr[6]);
//保存本次请求的响应状态
kpi.setStatus(arr[8]);
//保存本次请求向客户端发送内容的大小
kpi.setBody_bytes_sent(arr[9]);
//保存请求的来源信息(请求本资源的上一个资源路径)
kpi.setHttp_referer(arr[10]);
//如果存在附加的客户端代理信息
if (arr.length >12) {
//保存带有附加信息的客户端代理信息
kpi.setHttp_user_agent(arr[11] +" " + arr[12]);
}else{
//保存默认的客户端代理信息
kpi.setHttp_user_agent(arr[11]);
}
//如果出现Http错误(代码大于400)
if (Integer.parseInt(kpi.getStatus()) >= 400){
//本日志代表的请求不适用于分析
kpi.setValid(false);
}
}else{
//如果数据目录数不对,则本条日志代表的请求不适用于分析
kpi.setValid(false);
}
//返回解析后的结果
return kpi;
}
3.日志合法性过滤
本案例的合法性过滤采用"请求访问资源白名单" 的形式完成,即判定日志数据对应的请求资源目标是否位于一个白名单集合中。如果在,则本条日志数据在进行KPI指标分析时将作为有效数据参与分析,反之则在最后的数据分析过程中本条日志数据将被抛弃。
public static KPI filterPVs (String line) {
KPI kpi = parser(line);
//创建白名单集合
Set<String> pages = new HashSet<String>();
//在白名单集合中添加资源路径
pages.add("/about");
pages.add("/black-ip-list/");
pages.add("/cassandra-clustor/");
pages.add("finance-rhive-repurchase/");
pages.add("/hadoop-family-roadmap/");
pages.add("/hadoop-hive-intro/");
pages.add("/hadoop-zookeeper-intro/");
pages.add("/hadoop-mahout-roadmap/");
if (!pagas.contains(kpi.getRequest())) {
kpi.setValid(false);
}
return kpi;
}
4.页面访问量统计的实现
思路如下。
- Map: {key:$request,value:1}
- Reduce: {key:$request,value:求和(sum)}
(1)Map过程
public static class KPIPVMapper extends Mapper<Object, Text,Text, IntWritable> {
private IntWritable one = new IntWritable(1);
private Text word = new Text();
@Override
public void map(Object key, Text value, Context context) throws IOException, InterruptedException {
KPI kpi = KPI.filterPVs(value.toString());
if (kpi.isValid()) {
word.set(kpi.getRequest());
context.write(word,one);
}
}
}
(2)Reduce过程
public static class KPIPVReduce extends Reducer<Text, IntWritable,Text,IntWritable> {
private IntWritable result = new IntWritable();
@Override
public void reduce(Text key,Iterable<IntWritable> values,Context context) throws IOException, InterruptedException {
int sum = 0;
for (IntWritable value:values){
sum +=value.get();
}
result.set(sum);
context.write(key,result);
}
}
(3)Driver驱动类
public static void main(String[] args) throws IOException {
String input = "hdfs://hadoop:9000/WEBlog/logfile";
String output = "hdfs://hadoop:9000/WEBlog/kpi/pv";
Configuration conf = new Configuration();
Job job = Job.getInstance(conf,"KPIPV");
job.setJarByClass(com.Softi.mr.WEBlog.kpi.KPI.class);
job.setMapOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
job.setMapOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
job.setMapperClass(KPIPVMapper.class);
job.setCombinerClass(KPIPVReduce.class);
job.setReducerClass(KPIPVReduce.class);
FileInputFormat.addInputPath(job,new Path(input));
FileOutputFormat.setOutputPath(job,new Path(output));
if (!job.waitForCompletion(true)){
return;
}
}
5.页面独立IP访问量统计的实现
思路如下。
- Map: {key: r e q u e s t , v a l u e : request,value: request,value:remote_addr}
- Reduce: {key:$request,value:去重再求和(sum(unique))}
(1)Map操作
public static class KPIIPMapper extends Mapper<Object, Text,Text, Text> {
private Text ips = new Text();
private Text word = new Text();
@Override
public void map(Object key, Text value, Context context) throws IOException, InterruptedException {
KPI kpi = KPI.filterPVs(value.toString());
if (kpi.isValid()) {
word.set(kpi.getRequest());
ips.set(kpi.getRemote_addr());
context.write(word,ips);
}
}
}
(2)Reduce过程
Reduce过程需要首先完成集合数据去重操作,该操作可以利用Set集合元素不可重复的特性简化操作。
public static class KPIIPReduce extends Reducer<Text, Text,Text,Text> {
private Text result = new Text();
@Override
public void reduce(Text key,Iterable<Text> values,Context context) throws IOException, InterruptedException {
Set<String> count = new HashSet<String>();
for (Text value:values) {
count.add(value.toString());
}
result.set(String.valueOf(count.size()));
context.write(key,result);
}
}
(3)Driver驱动类
任务调度的过程与PV KPI 完全一致,只需要注意修改对应的输入、输出路径,MapReduce操作的类型,以及对应的输入参数/输出键值对的数据类型。