日誌分析方法概述 & Web日誌挖掘分析的方法
本文討論的日誌處理方法中的日誌,僅指Web日誌。其實並沒有精確的定義,可能包括但不限於各種前端Web伺服器——apache、lighttpd、tomcat等產生的使用者訪問日誌,以及各種Web應用程式自己輸出的日誌。
在Web日誌中,每條日誌通常代表著使用者的一次訪問行為,例如下面就是一條典型的apache日誌:
211.87.152.44 – - [18/Mar/2005:12:21:42 +0800] “GET / HTTP/1.1″ 200 899 “http://www.baidu.com/” “Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; Maxthon)”
從上面這條日誌中,我們可以得到很多有用的資訊,例如訪問者的IP、訪問的時間、訪問的目標網頁、來源的地址以及訪問者所使用的客戶端的UserAgent資訊等。如果需要更多的資訊,則要用其它手段去獲取:例如想得到使用者螢幕的解析度,一般需要使用js程式碼單獨傳送請求;而如果想得到諸如使用者訪問的具體新聞標題等資訊,則可能需要Web應用程式在自己的程式碼裡輸出。
為什麼要分析日誌
毫無疑問,Web日誌中包含了大量人們——主要是產品分析人員會感興趣的資訊,最簡單的,我們可以從中獲取網站每類頁面的PV值(PageView,頁面訪問量)、獨立IP數(即去重之後的IP數量)等;稍微複雜一些的,可以計算得出使用者所檢索的關鍵詞排行榜、使用者停留時間最高的頁面等;更復雜的,構建廣告點選模型、分析使用者行為特徵等等。
既然這些資料是如此的有用,那麼當然已經有無數現成的工具可以幫助我們來分析它們,例如awstats、Webalizer,都是專門用於統計分析Web伺服器日誌的免費程式。
另外還有一類產品,它們不分析直接日誌,而是通過讓使用者在頁面中嵌入js程式碼的方式來直接進行資料統計,或者說我們可以認為它是直接讓日誌輸出到了它們的伺服器。典型的代表產品——大名鼎鼎的Google Analytics,另外還有國內的cnzz、百度統計等。
很多人可能會說,既然如此,我們為什麼還需要自己來分析日誌,有必要嗎?當然有。我們的使用者(產品分析人員)需求是無窮盡的,上面說的這幾類工具雖然很好很強大,但顯然沒辦法滿足全部的需求。
無論是本地分析的工具,還是線上的分析服務,它們雖然提很豐富的的統計分析功能,可以做一定程度的配置,但是依然很有限的。要進行稍複雜點的分析,或者要做基於日誌的資料探勘,依然需要自己來完成。
另外絕大多數日誌分析工具都是隻能用於單機的,資料量稍大就沒轍了。同時那些提供線上分析的服務對於單個站點通常也都有最大流量的限制——這是很容易理解的,他們也需要考慮伺服器的負載。
所以,很多時候還是得靠自己。
怎麼進行日誌分析
這並不是一個簡單的問題。即使我們把“日誌”限定為Web日誌,依然包含了成千上萬種可能的格式和資料,而是“分析”更是難以定義,也許是簡單的統計值的計算,也許是複雜的資料探勘演算法。
下面並不打算討論這些複雜的問題,而只是籠統的討論如何構建進行日誌分析工作的基礎。有了這些基礎會讓基於日誌的簡單統計分析變得很簡單,並讓複雜的分析挖掘等變得可行。
少量資料的情況
先考慮最簡單的情況,在資料規模比較小的時候,也許是幾十MB、幾百MB或者幾十GB,總之就是在單機處理尚能忍受的時候。一切都很好辦,現成的各種Unix/Linux工具——awk、grep、sort、join等都是日誌分析的利器,如果僅僅是想知道某個頁面的PV,一個wc+grep就能搞定。如果有稍複雜的邏輯,那就使用各種指令碼語言,尤其是perl,配合偉大的正則表示式,基本就可以解決所有的問題。
例如,我們想從上面提到的apache日誌中得到訪問量最高前100個IP,實現很簡單:
cat logfile | awk ‘{a[$1]++} END {for(b in a) print b”\t”a[b]}’|sort -k2 -r|head -n 100
不過當我們需要頻繁去分析日誌的時候,上面的做法在一段時間之後可能就會讓我們頭疼如何進行各種日誌檔案、用於分析的指令碼檔案、crontab檔案等等的維護,並且可能會存在大量重複的程式碼來做資料格式的解析和清洗,這個時候也許就需要更合適的東西,比如——資料庫。
當然,要使用資料庫來進行日誌分析還是需要一些代價的,最主要的就是如何將各種異構的日誌檔案匯入的資料庫中——這個過程通常稱為ETL(Extraction-Transformation-Loading)。幸好依然有各種現成的開源、免費的工具來幫助我們做這件事情,並且在日誌種類不太多的時候,自己寫幾個簡單的指令碼來完成這項工作也並不困難。例如可以將上面的日誌去掉不必要的欄位,然後匯入如下的資料庫中:
現在需要考慮一下用什麼資料庫來儲存這些資料。MySQL是一個很經典的開源資料庫,它的傳統引擎(MyISAM或者InnoDB,行儲存)也許並不非常的適合日誌資料的儲存,但是在小資料量的時候還是很夠用的。而且,在這方面現在已經有了更好的選擇,例如開源且免費的Infobright、Infinidb,都是專門為資料倉庫應用而進行了優化的資料引擎,採用列儲存,有良好的資料壓縮,處理幾百GB的資料基本上不是問題。
使用資料庫的好處之一就是,偉大的SQL可以幫我們很簡單的完成絕大部分的統計分析工作——PV只需要SELECT+COUNT,計算搜尋詞排行只需要SELECT+COUNT+GROUP+ORDER+LIMIT。此外,資料庫本身的結構化儲存模式也讓日誌資料的管理變的更簡單,減少運維代價。
同樣還是上面的那個例子,簡單的一個SQL就可以搞定:
SELECT * FROM (SELECT ip, COUNT(*) AS ip_count FROM apache_log GROUP BY ip) a ORDER BY ip_count DESC LIMIT 100
至於效能問題,資料庫的索引和各種優化機制通常會讓我們的統計分析工作變得更快,並且上面提到的Infobright和Infinidb都專門為類似SUM、COUNt之類的聚集應用做了優化。當然也不是絕對的會快,例如在資料庫中進行LIKE操作,通常會比grep一個檔案還要慢很多。
更進一步的,使用基於資料庫的儲存,可以很容易的進行OLAP(聯機分析處理)應用,從日誌中挖掘價值會變的更加簡單。
更多的資料怎麼辦
一個好的資料庫似乎會讓事情變的很簡單,但是別忘了前面提到的都是單機資料庫。一臺單機在儲存容量、併發性上毫無疑問都是有很大限制的。而日誌資料的特點之一就是隨時間持續增長,並且由於很多分析過程往往需要歷史資料。短時間內的增長也許可以通過分庫、分表或者資料壓縮等來解決,不過很顯然並不是長久之計。
想要徹底解決資料規模增長帶來的問題,很自然的會想到使用分散式技術,結合上面的結論,也許使用某個分散式資料庫是一個好選擇,那麼對終端使用者就可以完全透明瞭。這個的確是很理想的情況,不過現實往往是殘酷的。
首先,實現比較完美的分散式資料庫(受限於CAP原則)是一個非常複雜的問題,因此在這裡並不像單機資料庫那樣,有那麼多開源的好東西可以用,甚至於商用的也並不是太多。當然,也並非絕對,如果有錢,還是可以考慮一下Oracle RAC、Greenplum之類東西。
其次,絕大多數分散式資料庫都是NoSQL的,所以想繼續用上SQL的那些優點基本上是沒指望,取而代之的都是一些簡單、難以使用的介面。單從這點看來,使用這些資料庫的價值已經降低很多了。
所以,還是先現實一點,先退一步考慮如何解決的超大規模的日誌的分析問題,而不是想如何讓它變的像在小資料規模時那樣簡單。單單想做到這點,目前看來並不是太難,並且依然有免費的午餐可以吃。
Hadoop是偉大的Apache基金會下面的一套分散式系統,包括分散式檔案系統(HDFS)、MapReduce計算框架、Hbase等很多元件——這些基本都是Google的GFS/MapReduce/BigTable的克隆產品。
Hadoop經過數年的發展,目前已經很成熟了,尤其是其中的HDFS和MapReduce計算框架元件。數百臺機器的叢集已經被證明可以使用,可以承擔PB級別的資料。
Hadoop專案中的HBase是一個按列儲存的NoSQL分散式資料庫,它提供的功能和介面都非常簡單,只能進行簡單的K-V查詢,因此並不直接適用於大多數日誌分析應用。所以一般使用Hadoop來做日誌分析,首先還是需要將日誌儲存在HDFS中,然後再使用它提供的MapReduce API編寫日誌分析程式。
MapReduce是一種分散式程式設計模型,並不難學習,但是很顯然使用它來處理日誌的代價依然遠大於單機指令碼或者SQL。一個簡單的詞頻統計計算可能都需要上百程式碼——SQL只需要一行,另外還有複雜的環境準備和啟動指令碼。
例如同樣還是上面的例子,實現就要複雜的多,通常需要兩輪MapReduce來完成。首先要在第一輪的mapper中計算部分ip的訪問次數之和,並以ip為key輸出:
//遍歷輸入,並聚合結果
foreach(record in input) {
ip = record.ip;
dict[ip]++;
}
//用emit輸出,第一個引數為key,用於reduce的分發
foreach(<ip, count> in dict) {
emit(ip, count);
}
然後在第一輪的reduce中就可以得到每個ip完整的計數,可以順便排個序,並且只保留前100個。
count = 0;
//對於每個key(ip),遍歷所有的values(count),並累加
while(input.values.hasNext()) {
count += input.values.next();
}
//插入到大小為100的堆中
heap_insert(input.key, count);
在reduce結束的時候輸出:
//輸出當前reduce中count最高的100個ip
foreach(<ip, count> in dict) {
emit(ip, count);
}
由於reduce一般會有很多個,所以最後還需要將所有reduce的輸出進行合併、再排序,並得到最終的前100個IP以及對應的訪問量。
所以,使用Hadoop來做日誌分析很顯然不是一件簡單事情,它帶來了很多的額外的學習和運維成本,但是至少,它讓超大規模的日誌分析變成了可能。
怎樣變得更簡單
在超大規模的資料上做任何事情都不是一件容易的事情,包括日誌分析,但也並不是說分散式的日誌分析就一定要去寫MapReduce程式碼,總是可以去做進一步的抽象,在特定的應用下讓事情變得更簡單。
也許有人會很自然的想到如果能用SQL來操作Hadoop上的資料該有多好。事實上,不僅僅只有你一個人會這麼想,很多人都這麼想,並且他們實現了這個想法,於是就有了Hive。
Hive現在也是Hadoop專案下面的一個子專案,它可以讓我們用SQL的介面來執行MapReduce,甚至提供了JDBC和ODBC的介面。有了這個之後,Hadoop基本上被包裝成一個數據庫。當然實際上Hive的SQL最終還是被翻譯成了MapReduce程式碼來執行,因此即使最簡單的SQL可能也要執行好幾十秒。幸好在通常的離線日誌分析中,這個時間還是可以接受的。更重要的是,對於上面提到的例子,我們又可以用一樣的SQL來完成分析任務了。
當然Hive並不是完全的相容SQL語法,而且也不能做到完全的對使用者遮蔽細節。很多時候為了執行效能的優化,依然需要使用者去了解一些MapReduce的基本知識,根據自己的應用模式來設定一些引數,否則我們可能會發現一個查詢執行很慢,或者壓根執行不出來。
另外,很顯然Hive也並不能覆蓋所有的需求,所以它依然保留插入原始MapReduce程式碼的介面,以便擴充套件。
更多的問題
即使有了Hive這樣一個類似於資料庫的東西,我們依然還有很多事情需要做。例如時間久了,可能會有越來越多的需要例行執行的SQL,而這些SQL中,也許有一些是做了重複的事情;也許有一些的執行效率非常低下,一個複雜的SQL就佔滿了所有的計算資源。這樣的系統會變得越來越難以維護的,直到有一天例行的SQL終於跑不完了。而終端使用者往往不會去關心這些事情,他們只關心自己提交的查詢是不是能即時得到響應,怎麼樣才能儘快的拿到結果。
舉個簡單的例子,如果發現在使用apache_log的所有查詢中,幾乎沒有人用其中的user_agent欄位,那麼我們完全可以把這個欄位去除掉,或者拆分成兩張表,以減少多數查詢的IO時間,提高執行的效率。
為了系統化的解決這些問題,我們可能需要引入例行任務的排程機制,可能需要去分析所有的SQL來發現哪些是可以合併的、哪些的效能需要優化,使用的資料表是不是需要做水平或者垂直分表等等。根據實際情況的不同,這時事情可能是人工來完成,也可能是寫程式來自動分析並調整。
再者隨著日誌型別、分析需求的不斷增長。使用者會越來越多的抱怨很難找到想要的資料在哪份日誌裡,或者跑的好好的查詢因為日誌格式的變化而突然不能用了。另外上面提到的ETL過程也會變得複雜,簡單的轉換匯入指令碼很可能已經解決不了問題。這時候可能需要構建一個數據管理系統,或者乾脆考慮建立一個所謂的資料倉庫。
總之,隨著日誌資料量、日誌型別、使用者數量、分析需求等等的不斷增長,越來越多的問題會逐漸浮現出來,日誌分析這件事情可能就不再像我們最初想的那麼簡單,會變得越來越有價值,也越來越有挑戰。