[TOC]
## 概況
### 背景
這個項目的背景是起源于,我有一個2G左右的網站訪問日志。我想看看訪問網站的人都來自哪里,于是我想開始想辦法來分析這日志。當時正值大數據火熱的時候,便想拿著Hadoop來做這樣一件事。
### ShowCase
最后的效果如下圖如示:

Demo
這是一個Web生成的界面,通過Elastic.js向搜索引擎查詢數據,將再這些數據渲染到地圖上。
### Hadoop + Pig + Jython + AmMap + ElasticSearch
我們使用的技術棧有上面這些,他們的簡介如下:
* Hadoop是一個由Apache基金會所開發的分布式系統基礎架構。用戶可以在不了解分布式底層細節的情況下,開發分布式程序。充分利用集群的威力進行高速運算和存儲。
* Pig 是一個基于Hadoop的大規模數據分析平臺,它提供的SQL-LIKE語言叫Pig Latin,該語言的編譯器會把類SQL的數據分析請求轉換為一系列經過優化處理的MapReduce運算。
* Jython是一種完整的語言,而不是一個Java翻譯器或僅僅是一個Python編譯器,它是一個Python語言在Java中的完全實現。Jython也有很多從CPython中繼承的模塊庫。
* AmMap是用于創建交互式Flash地圖的工具。您可以使用此工具來顯示您的辦公室地點,您的行程路線,創建您的經銷商地圖等。
* ElasticSearch是一個基于Lucene 構建的開源,分布式,RESTful 搜索引擎。 設計用于云計算中,能夠達到搜索實時、穩定、可靠和快速,并且安裝使用方便。
## 步驟
總的步驟并不是很復雜,可以分為:
* 搭建基礎設施
* 解析access.log
* 轉換IP為GEO信息
* 展示數據到地圖上
### Step 1: 搭建基礎設施
在這一些系列的實戰中,比較麻煩的就是安裝這些工具,我們需要安裝上面提到的一系列工具。對于不同的系統來說,都有相似的安裝工具:
* Windows上可以使用Chocolatey
* Ubuntu / Mint上可以使用aptitude
* CentOS / OpenSUSE上可以使用yum安裝
* Mac OS上可以使用brew安裝
如下是Mac OS下安裝Hadoop、Pig、Elasticsearch、Jython的方式
~~~
brew install hadoop
brew install pig
brew install elasticsearch
brew install jython
~~~
對于其他操作系統也可以使用相似的方法來安裝。接著我們還需要安裝一個Hadoop的插件,用于連接Hadoop和ElasticSearch。
下載地址:[https://github.com/elastic/elasticsearch-hadoop](https://github.com/elastic/elasticsearch-hadoop)
復制其中的`elasticsearch-hadoop-*.jar`、`elasticsearch-hadoop-pig-*.jar`到你的pig庫的目錄,如我的是:`/usr/local/Cellar/pig/0.14.0`。
由于我使用提早期的版本,所以這里我的文件名是:`elasticsearch-hadoop-2.1.0.Beta3.jar`、`elasticsearch-hadoop-pig-2.1.0.Beta3.jar`。
下面我們就可以嘗試去解析我們的日志了。
### Step 2: 解析access.log
在開始解析之前,先讓我們來看看幾條Nginx的日志:
~~~
106.39.113.203 - - [28/Apr/2016:10:40:31 +0000] "GET / HTTP/2.0" 200 0 "https://www.phodal.com/" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36" -
66.249.65.119 - - [28/Apr/2016:10:40:51 +0000] "GET /set_device/default/?next=/blog/use-falcon-peewee-build-high-performance-restful-services-wordpress/ HTTP/1.1" 302 5 "-" "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" -
~~~
而上面的日志實際上是有對應的格式的,這個格式寫在我們的Nginx配置文件中。如下是上面的日志的格式:
~~~
log_format access $remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" $http_x_forwarded_for';
~~~
在最前面的是訪問者的IP地址,然后是訪問者的當地時間、請求的類型、狀態碼、訪問的URL、用戶的User Agent等等。隨后,我們就可以針對上面的格式編寫相應的程序,這些代碼如下所示:
~~~
register file:/usr/local/Cellar/pig/0.14.0/libexec/lib/piggybank.jar;
register file:/usr/local/Cellar/pig/0.14.0/libexec/lib/elasticsearch-hadoop-pig-2.1.0.Beta3.jar;
RAW_LOGS = LOAD 'data/access.log' USING TextLoader as (line:chararray);
LOGS_BASE = FOREACH RAW_LOGS GENERATE
FLATTEN(
REGEX_EXTRACT_ALL(line, '(\\S+) - - \\[([^\\[]+)\\]\\s+"([^"]+)"\\s+(\\d+)\\s+(\\d+)\\s+"([^"]+)"\\s+"([^"]+)"\\s+-')
)
AS (
ip: chararray,
timestamp: chararray,
url: chararray,
status: chararray,
bytes: chararray,
referrer: chararray,
useragent: chararray
);
A = FOREACH LOGS_BASE GENERATE ToDate(timestamp, 'dd/MMM/yyyy:HH:mm:ss Z') as date, ip, url,(int)status,(int)bytes,referrer,useragent;
--B = GROUP A BY (timestamp);
--C = FOREACH B GENERATE FLATTEN(group) as (timestamp), COUNT(A) as count;
--D = ORDER C BY timestamp,count desc;
STORE A INTO 'nginx/log' USING org.elasticsearch.hadoop.pig.EsStorage();
~~~
在第1~2行里,我們使用了自定義的jar文件。接著在第4行,載入了log文件,并其值賦予RAW_LOGS。隨后的第6行里,我們取出RAW_LOGS中的每一個值 ,根據下面的正則表達式,取出其對應的值到對象里,如`- -`前面的(\S+)對應的是ip,最后將這些值賦給LOGS_BASE。
接著,我們就可以對值進行一些特殊的處理,如A是轉化時間戳后的結果。B是按時間戳排序后的結果。最后,我們再將這些值存儲到ElasticSearch對應的索引`nginx/log`中。
### Step 3: 轉換IP為GEO信息
在簡單地完成了一個Demo之后,我們就可以將IP轉換為GEO信息了,這里我們需要用到一個名為pygeoip的庫。GeoIP是一個根據IP地址查詢位置的API的集成。它支持對國家、地區、城市、緯度和經度的查詢。實際上,就是在一個數據庫中有對應的國家和地區的IP段,根據這個IP段,我們就可以獲取對應的地理位置。
由于使用Java來實現這個功能比較麻煩,這里我們就使用Jython來實現。大部分的過程和上面都是一樣的,除了注冊了一個自定義的庫,并在這個庫里使用了解析GEO的方法,代碼如下所示:
~~~
register file:/usr/local/Cellar/pig/0.14.0/libexec/lib/piggybank.jar;
register file:/usr/local/Cellar/pig/0.14.0/libexec/lib/elasticsearch-hadoop-pig-2.1.0.Beta3.jar;
register utils.py using jython as utils;
RAW_LOGS = LOAD 'data/access.log' USING TextLoader as (line:chararray);
LOGS_BASE = FOREACH RAW_LOGS GENERATE
FLATTEN(
REGEX_EXTRACT_ALL(line, '(\\S+) - - \\[([^\\[]+)\\]\\s+"([^"]+)"\\s+(\\d+)\\s+(\\d+)\\s+"([^"]+)"\\s+"([^"]+)"\\s+-')
)
AS (
ip: chararray,
timestamp: chararray,
url: chararray,
status: chararray,
bytes: chararray,
referrer: chararray,
useragent: chararray
);
A = FOREACH LOGS_BASE GENERATE ToDate(timestamp, 'dd/MMM/yyyy:HH:mm:ss Z') as date, utils.get_country(ip) as country,
utils.get_city(ip) as city, utils.get_geo(ip) as location,ip,
url, (int)status,(int)bytes,referrer,useragent;
STORE A INTO 'nginx/log' USING org.elasticsearch.hadoop.pig.EsStorage();
~~~
在第三行里,我們注冊了`utils.py`并將其中的函數作為utils。接著在倒數第二行里,我們執行了四個utils函數。即:
* get_country從IP中解析出國家
* get_city從IP中解析出城市
* get_geo從IP中解析出經緯度信息
其對應的Python代碼如下所示:
~~~
#!/usr/bin/python
import sys
sys.path.append('/Users/fdhuang/test/lib/python2.7/site-packages/')
import pygeoip
gi = pygeoip.GeoIP("data/GeoLiteCity.dat")
@outputSchema('city:chararray')
def get_city(ip):
try:
city = gi.record_by_name(ip)["city"]
return city
except:
pass
@outputSchema('country:chararray')
def get_country(ip):
try:
city = gi.record_by_name(ip)["country_name"]
return city
except:
pass
@outputSchema('location:chararray')
def get_geo(ip):
try:
geo = str(gi.record_by_name(ip)["longitude"]) + "," + str(gi.record_by_name(ip)["latitude"])
return geo
except:
pass
~~~
代碼相應的簡單,和一般的Python代碼也沒有啥區別。這里一些用戶自定義函數,在函數的最前面有一個`outputSchema`,用于返回輸出的結果。
### Step 4: 展示數據到地圖上
現在,我們終于可以將數據轉化到可視化界面了。開始之前,我們需要幾個庫
* jquery 地球人都知道
* elasticsearch.jquery即用于搜索功能
* ammap用于制作交互地圖。
添加這些庫到html文件里:
~~~
<script src="bower_components/jquery/dist/jquery.js"></script>
<script src="bower_components/elasticsearch/elasticsearch.jquery.js"></script>
<script src="bower_components/ammap/dist/ammap/ammap.js" type="text/javascript"></script>
<script src="bower_components/ammap/dist/ammap/maps/js/worldLow.js" type="text/javascript"></script>
<script src="bower_components/ammap/dist/ammap/themes/black.js" type="text/javascript"></script>
<script src="scripts/latlng.js"></script>
<script src="scripts/main_ammap.js"></script>
~~~
生成過程大致如下所示:
* 獲取不同國家的全名,用于解析出全名,如US -> “United States”
* 查找ElasticSearch搜索引擎中的數據,并計算訪問量
* 再將數據渲染到地圖上
對應的main文件如下所示:
~~~
var client = new $.es.Client({
hosts: 'localhost:9200'
});
// 創建ElasticSearch搜索條件
var query = {
index: 'nginx',
type: 'log',
size: 200,
body: {
query: {
query_string: {
query: "*"
}
},
aggs: {
2: {
terms: {
field: "country",
size: 200,
order: {
_count: "desc"
}
}
}
}
}
};
// 獲取到country.json后生成數據
$(document).ready(function () {
$.ajax({
type: "GET",
url: "country.json",
success: function (data) {
generate_info(data)
}
});
});
// 根據數據中的國家名,來計算不同國家的訪問量大小。
var generate_info = function(data){
var mapDatas = [];
client.search(query).then(function (results) {
$.each(results.aggregations[2].buckets, function(index, bucket){
var mapData;
$.each(data, function(index, country){
if(country.name.toLowerCase() === bucket.key) {
mapData = {
code: country.code,
name: country.name,
value: bucket.doc_count,
color: "#eea638"
};
}
});
if(mapData !== undefined){
mapDatas.push(mapData);
}
});
create_map(mapDatas);
});
};
var create_map = function(mapData){
var map;
var minBulletSize = 3;
var maxBulletSize = 70;
var min = Infinity;
var max = -Infinity;
AmCharts.theme = AmCharts.themes.black;
for (var i = 0; i < mapData.length; i++) {
var value = mapData[i].value;
if (value < min) {
min = value;
}
if (value > max) {
max = value;
}
}
map = new AmCharts.AmMap();
map.pathToImages = "bower_components/ammap/dist/ammap/images/";
map.areasSettings = {
unlistedAreasColor: "#FFFFFF",
unlistedAreasAlpha: 0.1
};
map.imagesSettings = {
balloonText: "<span style='font-size:14px;'><b>[[title]]</b>: [[value]]</span>",
alpha: 0.6
};
var dataProvider = {
mapVar: AmCharts.maps.worldLow,
images: []
};
var maxSquare = maxBulletSize * maxBulletSize * 2 * Math.PI;
var minSquare = minBulletSize * minBulletSize * 2 * Math.PI;
for (var i = 0; i < mapData.length; i++) {
var dataItem = mapData[i];
var value = dataItem.value;
// calculate size of a bubble
var square = (value - min) / (max - min) * (maxSquare - minSquare) + minSquare;
if (square < minSquare) {
square = minSquare;
}
var size = Math.sqrt(square / (Math.PI * 2));
var id = dataItem.code;
dataProvider.images.push({
type: "circle",
width: size,
height: size,
color: dataItem.color,
longitude: latlong[id].longitude,
latitude: latlong[id].latitude,
title: dataItem.name,
value: value
});
}
map.dataProvider = dataProvider;
map.write("mapdiv");
};
~~~
我們可以看到比較麻煩的地方就是生成地圖上的數量點,也就是create_map函數。
### 練習建議