第一部分:項目介紹
一、項目背景與數據情況
1.1 項目來源
本次要實踐的數據日志來源於國內某技術學習論壇,該論壇由某培訓機構主辦,匯聚了眾多技術學習者,每天都有人發帖、回帖,如圖1所示:
圖1 項目來源網站-技術學習論壇
本次實踐的目的就在於通過對該技術論壇的apache common日志進行分析,計算該論壇的一些關鍵指標,供運營者進行決策時參考。
PS:開發該系統的目的是為了獲取一些業務相關的指標,這些指標在第三方工具中無法獲得的;
1.2 數據情況
該論壇數據有兩部分:
(1)歷史數據約56GB,統計到2012-05-29。這也說明,在2012-05-29之前,日志文件都在一個文件里邊,采用了追加寫入的方式。
(2)自2013-05-30起,每天生成一個數據文件,約150MB左右。這也說明,從2013-05-30之后,日志文件不再是在一個文件里邊。
圖2展示了該日志數據的記錄格式,其中每行記錄有5部分組成:訪問者IP、訪問時間、訪問資源、訪問狀態(HTTP狀態碼)、本次訪問流量。
圖2 日志記錄數據格式
二、關鍵指標KPI
2.1 瀏覽量PV
(1)定義:頁面瀏覽量即為PV(Page View),是指所有用戶瀏覽頁面的總和,一個獨立用戶每打開一個頁面就被記錄1 次。
(2)分析:網站總瀏覽量,可以考核用戶對於網站的興趣,就像收視率對於電視劇一樣。但是對於網站運營者來說,更重要的是,每個欄目下的瀏覽量。
計算公式:記錄計數,從日志中獲取訪問次數,又可以細分為各個欄目下的訪問次數。
2.2 注冊用戶數
該論壇的用戶注冊頁面為member.php,而當用戶點擊注冊時請求的又是member.php?mod=register的url。
計算公式:對訪問member.php?mod=register的url,計數。
2.3 IP數
(1)定義:一天之內,訪問網站的不同獨立 IP 個數加和。其中同一IP無論訪問了幾個頁面,獨立IP 數均為1。
(2)分析:這是我們最熟悉的一個概念,無論同一個IP上有多少電腦,或者其他用戶,從某種程度上來說,獨立IP的多少,是衡量網站推廣活動好壞最直接的數據。
計算公式:對不同的訪問者ip,計數
2.4 跳出率
(1)定義:只瀏覽了一個頁面便離開了網站的訪問次數占總的訪問次數的百分比,即只瀏覽了一個頁面的訪問次數 / 全部的訪問次數匯總。
(2)分析:跳出率是非常重要的訪客黏性指標,它顯示了訪客對網站的興趣程度:跳出率越低說明流量質量越好,訪客對網站的內容越感興趣,這些訪客越可能是網站的有效用戶、忠實用戶。
PS:該指標也可以衡量網絡營銷的效果,指出有多少訪客被網絡營銷吸引到宣傳產品頁或網站上之后,又流失掉了,可以說就是煮熟的鴨子飛了。比如,網站在某媒體上打廣告推廣,分析從這個推廣來源進入的訪客指標,其跳出率可以反映出選擇這個媒體是否合適,廣告語的撰寫是否優秀,以及網站入口頁的設計是否用戶體驗良好。
計算公式:①統計一天內只出現一條記錄的ip,稱為跳出數;②跳出數/PV;
2.5 板塊熱度排行榜
(1)定義:版塊的訪問情況排行。
(2)分析:鞏固熱點版塊成績,加強冷清版塊建設。同時對學科建設也有影響。
計算公式:按訪問次數統計排序;
三、開發步驟
3.0 需要用到的技術
(1)Linux Shell編程
(2)HDFS、MapReduce
(3)HBase、Hive、Sqoop框架
3.1 上傳日志文件至HDFS
把日志數據上傳到HDFS中進行處理,可以分為以下幾種情況:
(1)如果是日志服務器數據較小、壓力較小,可以直接使用shell命令把數據上傳到HDFS中;
(2)如果是日志服務器數據較大、壓力較大,使用NFS在另一台服務器上上傳數據;
(3)如果日志服務器非常多、數據量大,使用flume進行數據處理;
3.2 數據清洗
使用MapReduce對HDFS中的原始數據進行清洗,以便后續進行統計分析;
3.3 統計分析
使用Hive對清洗后的數據進行統計分析;
3.4 分析結果導入MySQL
使用Sqoop把Hive產生的統計結果導出到mysql中;
3.5 提供視圖工具
提供視圖工具供用戶使用,指標查詢mysql、明細則查詢Hbase;
四、表結構設計
4.1 MySQL表結構設計
這里使用MySQL存儲關鍵指標的統計分析結果。
4.2 HBase表結構設計
這里使用HBase存儲明細日志,能夠利用ip、時間查詢。
后面,我們就開始具體的實戰了,本篇作為介紹就到此為止!
第二部分:數據清洗
一、數據情況分析
1.1 數據情況回顧
該論壇數據有兩部分:
(1)歷史數據約56GB,統計到2012-05-29。這也說明,在2012-05-29之前,日志文件都在一個文件里邊,采用了追加寫入的方式。
(2)自2013-05-30起,每天生成一個數據文件,約150MB左右。這也說明,從2013-05-30之后,日志文件不再是在一個文件里邊。
圖1展示了該日志數據的記錄格式,其中每行記錄有5部分組成:訪問者IP、訪問時間、訪問資源、訪問狀態(HTTP狀態碼)、本次訪問流量。
圖1 日志記錄數據格式
本次使用數據來自於兩個2013年的日志文件,分別為access_2013_05_30.log與access_2013_05_31.log,下載地址為:http://pan.baidu.com/s/1pJE7XR9
1.2 要清理的數據
(1)根據前一篇的關鍵指標的分析,我們所要統計分析的均不涉及到訪問狀態(HTTP狀態碼)以及本次訪問的流量,於是我們首先可以將這兩項記錄清理掉;
(2)根據日志記錄的數據格式,我們需要將日期格式轉換為平常所見的普通格式如20150426這種,於是我們可以寫一個類將日志記錄的日期進行轉換;
(3)由於靜態資源的訪問請求對我們的數據分析沒有意義,於是我們可以將"GET /staticsource/"開頭的訪問記錄過濾掉,又因為GET和POST字符串對我們也沒有意義,因此也可以將其省略掉;
二、數據清洗過程
2.1 定期上傳日志至HDFS
首先,把日志數據上傳到HDFS中進行處理,可以分為以下幾種情況:
(1)如果是日志服務器數據較小、壓力較小,可以直接使用shell命令把數據上傳到HDFS中;
(2)如果是日志服務器數據較大、壓力較大,使用NFS在另一台服務器上上傳數據;
(3)如果日志服務器非常多、數據量大,使用flume進行數據處理;
這里我們的實驗數據文件較小,因此直接采用第一種Shell命令方式。又因為日志文件時每天產生的,因此需要設置一個定時任務,在第二天的1點鍾自動將前一天產生的log文件上傳到HDFS的指定目錄中。所以,我們通過shell腳本結合crontab創建一個定時任務techbbs_core.sh,內容如下:
#!/bin/sh
#step1.get yesterday format string
yesterday=$(date --date='1 days ago' +%Y_%m_%d)
#step2.upload logs to hdfs
hadoop fs -put /usr/local/files/apache_logs/access_${yesterday}.log /project/techbbs/data
結合crontab設置為每天1點鍾自動執行的定期任務:crontab -e,內容如下(其中1代表每天1:00,techbbs_core.sh為要執行的腳本文件):
* 1 * * * techbbs_core.sh
驗證方式:通過命令 crontab -l 可以查看已經設置的定時任務
2.2 編寫MapReduce程序清理日志
(1)編寫日志解析類對每行記錄的五個組成部分進行單獨的解析
static class LogParser {
public static final SimpleDateFormat FORMAT = new SimpleDateFormat(
"d/MMM/yyyy:HH:mm:ss", Locale.ENGLISH);
public static final SimpleDateFormat dateformat1 = new SimpleDateFormat(
"yyyyMMddHHmmss");/**
* 解析英文時間字符串
*
* @param string
* @return
* @throws ParseException
*/
private Date parseDateFormat(String string) {
Date parse = null;
try {
parse = FORMAT.parse(string);
} catch (ParseException e) {
e.printStackTrace();
}
return parse;
}
/**
* 解析日志的行記錄
*
* @param line
* @return 數組含有5個元素,分別是ip、時間、url、狀態、流量
*/
public String[] parse(String line) {
String ip = parseIP(line);
String time = parseTime(line);
String url = parseURL(line);
String status = parseStatus(line);
String traffic = parseTraffic(line);
return new String[] { ip, time, url, status, traffic };
}
private String parseTraffic(String line) {
final String trim = line.substring(line.lastIndexOf("\"") + 1)
.trim();
String traffic = trim.split(" ")[1];
return traffic;
}
private String parseStatus(String line) {
final String trim = line.substring(line.lastIndexOf("\"") + 1)
.trim();
String status = trim.split(" ")[0];
return status;
}
private String parseURL(String line) {
final int first = line.indexOf("\"");
final int last = line.lastIndexOf("\"");
String url = line.substring(first + 1, last);
return url;
}
private String parseTime(String line) {
final int first = line.indexOf("[");
final int last = line.indexOf("+0800]");
String time = line.substring(first + 1, last).trim();
Date date = parseDateFormat(time);
return dateformat1.format(date);
}
private String parseIP(String line) {
String ip = line.split("- -")[0].trim();
return ip;
}
}
(2)編寫MapReduce程序對指定日志文件的所有記錄進行過濾
Mapper類:
static class MyMapper extends
Mapper<LongWritable, Text, LongWritable, Text> {
LogParser logParser = new LogParser();
Text outputValue = new Text();
protected void map(
LongWritable key,
Text value,
org.apache.hadoop.mapreduce.Mapper<LongWritable, Text, LongWritable, Text>.Context context)
throws java.io.IOException, InterruptedException {
final String[] parsed = logParser.parse(value.toString());
// step1.過濾掉靜態資源訪問請求
if (parsed[2].startsWith("GET /static/")
|| parsed[2].startsWith("GET /uc_server")) {
return;
}
// step2.過濾掉開頭的指定字符串
if (parsed[2].startsWith("GET /")) {
parsed[2] = parsed[2].substring("GET /".length());
} else if (parsed[2].startsWith("POST /")) {
parsed[2] = parsed[2].substring("POST /".length());
}
// step3.過濾掉結尾的特定字符串
if (parsed[2].endsWith(" HTTP/1.1")) {
parsed[2] = parsed[2].substring(0, parsed[2].length()
- " HTTP/1.1".length());
}
// step4.只寫入前三個記錄類型項
outputValue.set(parsed[0] + "\t" + parsed[1] + "\t" + parsed[2]);
context.write(key, outputValue);
}
}
Reducer類:
static class MyReducer extends
Reducer<LongWritable, Text, Text, NullWritable> {
protected void reduce(
LongWritable k2,
java.lang.Iterable<Text> v2s,
org.apache.hadoop.mapreduce.Reducer<LongWritable, Text, Text, NullWritable>.Context context)
throws java.io.IOException, InterruptedException {
for (Text v2 : v2s) {
context.write(v2, NullWritable.get());
}
};
}
(3)LogCleanJob.java的完整示例代碼(見附錄)
(4)導出jar包,並將其上傳至Linux服務器指定目錄中
2.3 定期清理日志至HDFS
改寫剛剛的定時任務腳本,將自動執行清理的MapReduce程序加入腳本中,內容如下:
#!/bin/sh
#step1.get yesterday format string
yesterday=$(date --date='1 days ago' +%Y_%m_%d)
#step2.upload logs to hdfs
hadoop fs -put /usr/local/files/apache_logs/access_${yesterday}.log /project/techbbs/data
#step3.clean log data
hadoop jar /usr/local/files/apache_logs/mycleaner.jar /project/techbbs/data/access_${yesterday}.log /project/techbbs/cleaned/${yesterday}
這段腳本的意思就在於每天1點將日志文件上傳到HDFS后,執行數據清理程序對已存入HDFS的日志文件進行過濾,並將過濾后的數據存入cleaned目錄下。
2.4 定時任務測試
(1)因為兩個日志文件是2013年的,因此這里將其名稱改為2015年當天以及前一天的,以便這里能夠測試通過。
(2)執行命令:techbbs_core.sh 2014_04_26
控制台的輸出信息如下所示,可以看到過濾后的記錄減少了很多:
15/04/26 04:27:20 INFO input.FileInputFormat: Total input paths to process : 1
15/04/26 04:27:20 INFO util.NativeCodeLoader: Loaded the native-hadoop library
15/04/26 04:27:20 WARN snappy.LoadSnappy: Snappy native library not loaded
15/04/26 04:27:22 INFO mapred.JobClient: Running job: job_201504260249_0002
15/04/26 04:27:23 INFO mapred.JobClient: map 0% reduce 0%
15/04/26 04:28:01 INFO mapred.JobClient: map 29% reduce 0%
15/04/26 04:28:07 INFO mapred.JobClient: map 42% reduce 0%
15/04/26 04:28:10 INFO mapred.JobClient: map 57% reduce 0%
15/04/26 04:28:13 INFO mapred.JobClient: map 74% reduce 0%
15/04/26 04:28:16 INFO mapred.JobClient: map 89% reduce 0%
15/04/26 04:28:19 INFO mapred.JobClient: map 100% reduce 0%
15/04/26 04:28:49 INFO mapred.JobClient: map 100% reduce 100%
15/04/26 04:28:50 INFO mapred.JobClient: Job complete: job_201504260249_0002
15/04/26 04:28:50 INFO mapred.JobClient: Counters: 29
15/04/26 04:28:50 INFO mapred.JobClient: Job Counters
15/04/26 04:28:50 INFO mapred.JobClient: Launched reduce tasks=1
15/04/26 04:28:50 INFO mapred.JobClient: SLOTS_MILLIS_MAPS=58296
15/04/26 04:28:50 INFO mapred.JobClient: Total time spent by all reduces waiting after reserving slots (ms)=0
15/04/26 04:28:50 INFO mapred.JobClient: Total time spent by all maps waiting after reserving slots (ms)=0
15/04/26 04:28:50 INFO mapred.JobClient: Launched map tasks=1
15/04/26 04:28:50 INFO mapred.JobClient: Data-local map tasks=1
15/04/26 04:28:50 INFO mapred.JobClient: SLOTS_MILLIS_REDUCES=25238
15/04/26 04:28:50 INFO mapred.JobClient: File Output Format Counters
15/04/26 04:28:50 INFO mapred.JobClient: Bytes Written=12794925
15/04/26 04:28:50 INFO mapred.JobClient: FileSystemCounters
15/04/26 04:28:50 INFO mapred.JobClient: FILE_BYTES_READ=14503530
15/04/26 04:28:50 INFO mapred.JobClient: HDFS_BYTES_READ=61084325
15/04/26 04:28:50 INFO mapred.JobClient: FILE_BYTES_WRITTEN=29111500
15/04/26 04:28:50 INFO mapred.JobClient: HDFS_BYTES_WRITTEN=12794925
15/04/26 04:28:50 INFO mapred.JobClient: File Input Format Counters
15/04/26 04:28:50 INFO mapred.JobClient: Bytes Read=61084192
15/04/26 04:28:50 INFO mapred.JobClient: Map-Reduce Framework
15/04/26 04:28:50 INFO mapred.JobClient: Map output materialized bytes=14503530
15/04/26 04:28:50 INFO mapred.JobClient: Map input records=548160
15/04/26 04:28:50 INFO mapred.JobClient: Reduce shuffle bytes=14503530
15/04/26 04:28:50 INFO mapred.JobClient: Spilled Records=339714
15/04/26 04:28:50 INFO mapred.JobClient: Map output bytes=14158741
15/04/26 04:28:50 INFO mapred.JobClient: CPU time spent (ms)=21200
15/04/26 04:28:50 INFO mapred.JobClient: Total committed heap usage (bytes)=229003264
15/04/26 04:28:50 INFO mapred.JobClient: Combine input records=0
15/04/26 04:28:50 INFO mapred.JobClient: SPLIT_RAW_BYTES=133
15/04/26 04:28:50 INFO mapred.JobClient: Reduce input records=169857
15/04/26 04:28:50 INFO mapred.JobClient: Reduce input groups=169857
15/04/26 04:28:50 INFO mapred.JobClient: Combine output records=0
15/04/26 04:28:50 INFO mapred.JobClient: Physical memory (bytes) snapshot=154001408
15/04/26 04:28:50 INFO mapred.JobClient: Reduce output records=169857
15/04/26 04:28:50 INFO mapred.JobClient: Virtual memory (bytes) snapshot=689442816
15/04/26 04:28:50 INFO mapred.JobClient: Map output records=169857
Clean process success!
(3)通過Web接口查看HDFS中的日志數據:
存入的未過濾的日志數據:/project/techbbs/data/
存入的已過濾的日志數據:/project/techbbs/cleaned/
第三部分:統計分析
一、借助Hive進行統計
1.1 准備工作:建立分區表
為了能夠借助Hive進行統計分析,首先我們需要將清洗后的數據存入Hive中,那么我們需要先建立一張表。這里我們選擇分區表,以日期作為分區的指標,建表語句如下:(這里關鍵之處就在於確定映射的HDFS位置,我這里是/project/techbbs/cleaned即清洗后的數據存放的位置)
hive>CREATE EXTERNAL TABLE techbbs(ip string, atime string, url string) PARTITIONED BY (logdate string) ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t' LOCATION '/project/techbbs/cleaned';
建立了分區表之后,就需要增加一個分區,增加分區的語句如下:(這里主要針對20150425這一天的日志進行分區)
hive>ALTER TABLE techbbs ADD PARTITION(logdate='2015_04_25') LOCATION '/project/techbbs/cleaned/2015_04_25';
1.2 使用HQL統計關鍵指標
(1)關鍵指標之一:PV量
頁面瀏覽量即為PV(Page View),是指所有用戶瀏覽頁面的總和,一個獨立用戶每打開一個頁面就被記錄1 次。這里,我們只需要統計日志中的記錄個數即可,HQL代碼如下:
hive>CREATE TABLE techbbs_pv_2015_04_25 AS SELECT COUNT(1) AS PV FROM techbbs WHERE logdate='2015_04_25';
(2)關鍵指標之二:注冊用戶數
該論壇的用戶注冊頁面為member.php,而當用戶點擊注冊時請求的又是member.php?mod=register的url。因此,這里我們只需要統計出日志中訪問的URL是member.php?mod=register的即可,HQL代碼如下:
hive>CREATE TABLE techbbs_reguser_2015_04_25 AS SELECT COUNT(1) AS REGUSER FROM techbbs WHERE logdate='2015_04_25' AND INSTR(url,'member.php?mod=register')>0;
(3)關鍵指標之三:獨立IP數
一天之內,訪問網站的不同獨立 IP 個數加和。其中同一IP無論訪問了幾個頁面,獨立IP 數均為1。因此,這里我們只需要統計日志中處理的獨立IP數即可,在SQL中我們可以通過DISTINCT關鍵字,在HQL中也是通過這個關鍵字:
hive>CREATE TABLE techbbs_ip_2015_04_25 AS SELECT COUNT(DISTINCT ip) AS IP FROM techbbs WHERE logdate='2015_04_25';
(4)關鍵指標之四:跳出用戶數
只瀏覽了一個頁面便離開了網站的訪問次數,即只瀏覽了一個頁面便不再訪問的訪問次數。這里,我們可以通過用戶的IP進行分組,如果分組后的記錄數只有一條,那么即為跳出用戶。將這些用戶的數量相加,就得出了跳出用戶數,HQL代碼如下:
hive>CREATE TABLE techbbs_jumper_2015_04_25 AS SELECT COUNT(1) AS jumper FROM (SELECT COUNT(ip) AS times FROM techbbs WHERE logdate='2015_04_25' GROUP BY ip HAVING times=1)e;
PS:跳出率是指只瀏覽了一個頁面便離開了網站的訪問次數占總的訪問次數的百分比,即只瀏覽了一個頁面的訪問次數 / 全部的訪問次數匯總。這里,我們可以將這里得出的跳出用戶數/PV數即可得到跳出率。
(5)將所有關鍵指標放入一張匯總表中以便於通過Sqoop導出到MySQL
為了方便通過Sqoop統一導出到MySQL,這里我們借助一張匯總表將剛剛統計到的結果整合起來,通過表連接結合,HQL代碼如下:
hive>CREATE TABLE techbbs_2013_05_30 AS SELECT '2013_05_30', a.pv, b.reguser, c.ip, d.jumper FROM techbbs_pv_2013_05_30 a JOIN techbbs_reguser_2013_05_30 b ON 1=1 JOIN techbbs_ip_2013_05_30 c ON 1=1 JOIN techbbs_jumper_2013_05_30 d ON 1=1;
二、使用Sqoop導入到MySQL
2.1 准備工作:在MySQL中創建結果匯總表
(1)Step1:創建一個新數據庫:techbbs
mysql> create database techbbs;
Query OK, 1 row affected (0.00 sec)
(2)Step2:創建一張新數據表:techbbs_logs_stat
mysql> create table techbbs_logs_stat(
-> logdate varchar(10) primary key,
-> pv int,
-> reguser int,
-> ip int,
-> jumper int);
Query OK, 0 rows affected (0.01 sec)
2.2 導入操作:通過export命令
(1)Step1:編寫導出命令
sqoop export --connect jdbc:mysql://hadoop-master:3306/techbbs --username root --password root --table techbbs_logs_stat --fields-terminated-by '\001' --export-dir '/hive/techbbs_2013_05_30'
這里的--export-dir是指定的hive目錄下的匯總表所在位置,我這里是/hive/techbbs_2015_04_25。
(2)Step2:查看導出結果
三、改寫Linux定時任務
剛剛我們已經借助Hive進行了關鍵指標的統計分析,並且借助Sqoop導出到了MySQL,后續可以借助JSP或者ASP.NET開發指標瀏覽界面供決策者進行瀏覽分析。但是剛剛這些操作都是我們自己手工操作的,我們需要實現自動化的統計分析並導出,於是我們改寫前一篇中提到的定時任務腳本文件。
3.1 加入分區、統計與導出操作
重寫techbbs_core.sh文件,內容如下,step4~step8為新增內容:
#!/bin/sh
......
#step4.alter hive table and then add partition
hive -e "ALTER TABLE techbbs ADD PARTITION(logdate='${yesterday}') LOCATION '/project/techbbs/cleaned/${yesterday}';"
#step5.create hive table everyday
hive -e "CREATE TABLE hmbbs_pv_${yesterday} AS SELECT COUNT(1) AS PV FROM hmbbs WHERE logdate='${yesterday}';"
hive -e "CREATE TABLE hmbbs_reguser_${yesterday} AS SELECT COUNT(1) AS REGUSER FROM hmbbs WHERE logdate='${yesterday}' AND INSTR(url,'member.php?mod=register')>0;"
hive -e "CREATE TABLE hmbbs_ip_${yesterday} AS SELECT COUNT(DISTINCT ip) AS IP FROM hmbbs WHERE logdate='${yesterday}';"
hive -e "CREATE TABLE hmbbs_jumper_${yesterday} AS SELECT COUNT(1) AS jumper FROM (SELECT COUNT(ip) AS times FROM hmbbs WHERE logdate='${yesterday}' GROUP BY ip HAVING times=1) e;"
hive -e "CREATE TABLE hmbbs_${yesterday} AS SELECT '${yesterday}', a.pv, b.reguser, c.ip, d.jumper FROM hmbbs_pv_${yesterday} a JOIN hmbbs_reguser_${yesterday} b ON 1=1 JOIN hmbbs_ip_${yesterday} c ON 1=1 JOIN hmbbs_jumper_${yesterday} d ON 1=1;"
#step6.delete hive tables
hive -e "drop table hmbbs_pv_${yesterday};"
hive -e "drop table hmbbs_reguser_${yesterday};"
hive -e "drop table hmbbs_ip_${yesterday};"
hive -e "drop table hmbbs_jumper_${yesterday};"
#step7.export to mysql
sqoop export --connect jdbc:mysql://hadoop-master:3306/techbbs --username root --password admin --table techbbs_logs_stat --fields-terminated-by '\001' --export-dir '/hive/hmbbs_${yesterday}'
#step8.delete hive table
hive -e "drop table techbbs_${yesterday};"
3.2 分離日期獲取操作
(1)改寫techbbs_core.sh腳本文件:
#!/bin/sh
#step1.get yesterday format string
#yesterday=`date --date='1 days ago' +%Y_%m_%d`
yesterday=$1
這里將日期字符串作為參數傳入,將該步驟轉移到了其他腳本文件中;
(2)新增techbbs_daily.sh腳本文件:
#!/bin/sh
yesterday=`date --date='1 days ago' +%Y_%m_%d`
hmbbs_core.sh $yesterday
這里獲取日期並作為參數傳遞給techbbs_core.sh文件;
(3)改寫crontab定時任務配置:crontab -e
* 1 * * * /usr/local/files/apache_logs/techbbs_daily.sh
這里每天凌晨1點自動執行的就變為techbbs_daily.sh腳本文件了;從此,我們只需定期查看mysql數據庫中的匯總結果表進行瀏覽即可;
3.3 初始化任務操作
當一個網站已經生成了很多天的日志,而我們的日志分析系統卻一直沒上線,一直等到了某天才上線。這時,我們需要寫一個初始化腳本任務,來對之前的每天的日志進行統計分析與導出結果。這里,我們新增一個techbbs_init.sh腳本文件,內容如下:
#!/bin/sh
#step1.create external table in hive
hive -e "CREATE EXTERNAL TABLE techbbs(ip string, atime string, url string) PARTITIONED BY (logdate string) ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t' LOCATION '/project/techbbs/cleaned';"
#step2.compute the days between start date and end date
s1=`date --date="$1" +%s`
s2=`date +%s`
s3=$((($s2-$s1)/3600/24))
#step3.excute techbbs_core.sh $3 times
for ((i=$s3; i>0; i--))
do
logdate=`date --date="$i days ago" +%Y_%m_%d`
techbbs_core.sh $logdate
done
四、小結
通過三部分的介紹,該網站的日志分析工作基本完成,當然還有很多沒有完成的東西,但是大體上的思路已經明了,后續的工作只需要在此基礎上稍加分析即可完成。當然,我們還可以通過JSP或ASP.NET讀取MySQL或HBase中的分析結果表來開發關鍵指標查詢系統,供網站運營決策者進行查看和分析。
import java.net.URI; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.conf.Configured; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.LongWritable; import org.apache.hadoop.io.NullWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Job; import org.apache.hadoop.mapreduce.Mapper; import org.apache.hadoop.mapreduce.Reducer; import org.apache.hadoop.mapreduce.lib.input.FileInputFormat; import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; import org.apache.hadoop.util.Tool; import org.apache.hadoop.util.ToolRunner; public class LogCleanJob extends Configured implements Tool { public static void main(String[] args) { Configuration conf = new Configuration(); try { int res = ToolRunner.run(conf, new LogCleanJob(), args); System.exit(res); } catch (Exception e) { e.printStackTrace(); } } @Override public int run(String[] args) throws Exception { final Job job = new Job(new Configuration(), LogCleanJob.class.getSimpleName()); // 設置為可以打包運行
job.setJarByClass(LogCleanJob.class); FileInputFormat.setInputPaths(job, args[0]); job.setMapperClass(MyMapper.class); job.setMapOutputKeyClass(LongWritable.class); job.setMapOutputValueClass(Text.class); job.setReducerClass(MyReducer.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(NullWritable.class); FileOutputFormat.setOutputPath(job, new Path(args[1])); // 清理已存在的輸出文件
FileSystem fs = FileSystem.get(new URI(args[0]), getConf()); Path outPath = new Path(args[1]); if (fs.exists(outPath)) { fs.delete(outPath, true); } boolean success = job.waitForCompletion(true); if(success){ System.out.println("Clean process success!"); } else{ System.out.println("Clean process failed!"); } return 0; } static class MyMapper extends Mapper<LongWritable, Text, LongWritable, Text> { LogParser logParser = new LogParser(); Text outputValue = new Text(); protected void map( LongWritable key, Text value, org.apache.hadoop.mapreduce.Mapper<LongWritable, Text, LongWritable, Text>.Context context) throws java.io.IOException, InterruptedException { final String[] parsed = logParser.parse(value.toString()); // step1.過濾掉靜態資源訪問請求
if (parsed[2].startsWith("GET /static/") || parsed[2].startsWith("GET /uc_server")) { return; } // step2.過濾掉開頭的指定字符串
if (parsed[2].startsWith("GET /")) { parsed[2] = parsed[2].substring("GET /".length()); } else if (parsed[2].startsWith("POST /")) { parsed[2] = parsed[2].substring("POST /".length()); } // step3.過濾掉結尾的特定字符串
if (parsed[2].endsWith(" HTTP/1.1")) { parsed[2] = parsed[2].substring(0, parsed[2].length() - " HTTP/1.1".length()); } // step4.只寫入前三個記錄類型項
outputValue.set(parsed[0] + "\t" + parsed[1] + "\t" + parsed[2]); context.write(key, outputValue); } } static class MyReducer extends Reducer<LongWritable, Text, Text, NullWritable> { protected void reduce( LongWritable k2, java.lang.Iterable<Text> v2s, org.apache.hadoop.mapreduce.Reducer<LongWritable, Text, Text, NullWritable>.Context context) throws java.io.IOException, InterruptedException { for (Text v2 : v2s) { context.write(v2, NullWritable.get()); } }; } /* * 日志解析類 */
static class LogParser { public static final SimpleDateFormat FORMAT = new SimpleDateFormat( "d/MMM/yyyy:HH:mm:ss", Locale.ENGLISH); public static final SimpleDateFormat dateformat1 = new SimpleDateFormat( "yyyyMMddHHmmss"); public static void main(String[] args) throws ParseException { final String S1 = "27.19.74.143 - - [30/May/2013:17:38:20 +0800] \"GET /static/image/common/faq.gif HTTP/1.1\" 200 1127"; LogParser parser = new LogParser(); final String[] array = parser.parse(S1); System.out.println("樣例數據: " + S1); System.out.format( "解析結果: ip=%s, time=%s, url=%s, status=%s, traffic=%s", array[0], array[1], array[2], array[3], array[4]); } /** * 解析英文時間字符串 * * @param string * @return * @throws ParseException */
private Date parseDateFormat(String string) { Date parse = null; try { parse = FORMAT.parse(string); } catch (ParseException e) { e.printStackTrace(); } return parse; } /** * 解析日志的行記錄 * * @param line * @return 數組含有5個元素,分別是ip、時間、url、狀態、流量 */
public String[] parse(String line) { String ip = parseIP(line); String time = parseTime(line); String url = parseURL(line); String status = parseStatus(line); String traffic = parseTraffic(line); return new String[] { ip, time, url, status, traffic }; } private String parseTraffic(String line) { final String trim = line.substring(line.lastIndexOf("\"") + 1) .trim(); String traffic = trim.split(" ")[1]; return traffic; } private String parseStatus(String line) { final String trim = line.substring(line.lastIndexOf("\"") + 1) .trim(); String status = trim.split(" ")[0]; return status; } private String parseURL(String line) { final int first = line.indexOf("\""); final int last = line.lastIndexOf("\""); String url = line.substring(first + 1, last); return url; } private String parseTime(String line) { final int first = line.indexOf("["); final int last = line.indexOf("+0800]"); String time = line.substring(first + 1, last).trim(); Date date = parseDateFormat(time); return dateformat1.format(date); } private String parseIP(String line) { String ip = line.split("- -")[0].trim(); return ip; } } }
完整文檔:
https://pan.baidu.com/s/1PymOnCZ1Ytv9BYKjZwNVfg