Projektabschluss Pageviews

So ist es recht. Kurz vorm naechsten Urlaub konnte dieses Projekt abgeschlossen werden. Inspiriert  durch eine grosse Anzeigetafel in der Firmenzentrale eines Mobilfunkanbieters in England und auf der Suche nach sinnvollen Anwendungen fuer Hadoop und nosql

Posted by eumel8 on February 14, 2011 · 9 mins read

So ist es recht. Kurz vorm naechsten Urlaub konnte dieses Projekt abgeschlossen werden. Inspiriert  durch eine grosse Anzeigetafel in der Firmenzentrale eines Mobilfunkanbieters in England und auf der Suche nach sinnvollen Anwendungen fuer Hadoop, nosql a ka  Hbase und REST stellte sich mir die Aufgabe, die Pageviews einer Webseite darzustellen - in Echtzeit.


Ueblicherweise werden Zugriffe auf Webseiten von der verwendeten Applikation wie Apache Httpd mitgeloggt. Es entstehen Logfiles mit systematischem Aufbau wie Uhrzeit, IP-Adresse, aufgerufene Seite, uebertragene Daten, Browserversion usw. Das Sammeln dieser Daten ist zwar umstritten, aber nicht Thema dieser Abhandlung.  Die Auswertung der Daten erfolgt mit Tools wie Webalizer oder Awstats einmal im Monat oder maximal taeglich. Von den vielen Ergebnissen, die so eine Auswertung bietet, interessiert uns nur ein Wert. Die Pageviews, also die Anzahl der angerufenen Webseiten ist der Index fuer Webmarketing und Ranking (www.alexa.com).

Jetzt haette man diese Zahlen auch durch Google-Analytics (oder Omniture etc.) berechnen koennen oder auf jeder Webseite ein Blind-Gif einbauen, um diese dann etwa als CGI irgendwo zu zaehlen, aber da muss man ja jede Webseite anfassen (auch die dynamisch generierten) und ist erstmal ewig beschaeftigt und hat vor allem keine sinnvolle Anwendung fuer Hadoop ;-)

Dessen Zusammenspiel habe ich weiter hinten schon beschrieben. Fuer dieses Projekt brauchen wir

  • Einen funktionierenden Hadoop-Cluster mit Datanodes und Map&Reduce-Nodes
  • Hbase
  • Stargate fuer Hbase
  • pig (mit der library piggybank)
  • Einen Webserver mit PHP und Curl

Los gehts:

Die Logfiles liegen ueblicherweise gezipt auf den Applikationsservern oder werden auf einen Fileserver archiviert. Es gibt zwar Ansaetze mit Log2MySQL, die Daten gleich in eine relationale Datenbank zu schreiben, aber zum einen ist das nicht vhost-faehig und zum anderen  performed das nicht. Wenn die Datenbank haengt, handelt man sich wahrscheinlich sogar ein Bottleneg auf dem Webserver ein.

Schauen wir uns zuerst das Herzstueck unserer Web 2.0 Anwendung an - das pig-Skript

# vi /usr/local/pig/apacheAccessLogAnalysis_pageview.pig 
-- Registriere piggybank fuer Logformat-Schema
register /usr/local/pig-0.8.0/contrib/piggybank/java/piggybank.jar;

DEFINE LogLoader org.apache.pig.piggybank.storage.apachelog.CombinedLogLoader();

-- Lade Logs ins LogLoader Schema

logs = LOAD '$LOGS' USING LogLoader
as (remoteAddr, remoteLogname, user, time, method, uri, proto, status, bytes,
referer, userAgent);

-- Filter Logrequests nach Methoden, Groesse, Pageviews und Statuscodes
logs = FILTER logs BY method == 'GET'
AND bytes != '-'
AND (NOT (uri matches '.*(css|js|gif|png|jpg|bmp|ico|swf|jpeg|class)$'))
AND status >= 200 AND status < 300;

logsfull = FILTER logs BY method == 'GET'
AND bytes != '-'
AND status >= 200 AND status < 300;

-- Generiere Report fuer uri, status und bytes
logs = FOREACH logs GENERATE uri, status, bytes;

groupedByCount = GROUP logs ALL;

-- Summe aller PageViews bestimmen
sumCounts = FOREACH groupedByCount GENERATE COUNT(logs.$0);

-- Ausgabe aller Werte ins HDFS
STORE sumCounts INTO 'sum_counts';

piggybank brauchen wir, weil es schon vordefinierte Parser fuer Apache-Logfiles hat. Wir muessen nur drauf achten, dass unser Logformat auch stimmt. Dann filtern wir unsere Zugriffe nach Gueltigkeit (Statuscode), Groesse (mindestens > 0 byte) und keine Bilder oder andere eingebettete Objecte. Die letzte Anweisung gleicht dem select count(*) einer Datenbankanweisung.

Unser Hbase liegt natuerlich im selben Hadoop-Cluster, aber ich habe keine Moeglichkeit gefunden, die Daten direkt aus Hadoop fuers Web auszulesen. Eventuell wuerde das im zunehmenden Masse unuebersichtlich werden. Also muessen die  Ergebnisse aus Pig von Hadoop ins Hbase. Da wir spaeter REST verwenden, schreiben wir die Daten auch mit REST ins Hbase. Dazu brauchen wir aber einen REST-Server. Den bekommen wir mit Stargate. Das ist einfach eine Java-Klasse, die wir aus unserem Hbase heraus starten, womit sofort eine Webschnittstelle fuer unsere nosql Datenbank zur Verfuegung steht:

cd /usr/local/hbase/; bin/hbase org.apache.hadoop.hbase.stargate.Main -p 60050

Unser REST-Server laucht auf Port 60050. Und wir brauchen ein Schema:

cd /usr/local/hbase/; bin/hbase shell
HBase Shell; enter 'help' for list of supported commands.
Version: 0.20.4, r941076, Tue May 4 15:23:44 PDT 2010
hbase(main):001:0> create "pageviews", "web"</pre> </blockquote>

Auf http://hdp-cl01.eumel.de:60050/ sollte "pageviews" stehen. So funktioniert unser REST-Server.

Da wir spaeter PHP als Skriptsprache einsetzen, habe ich mich zum Sammeln der Daten und Starten des Pig auch fuer PHP von der Kommandozeile entschieden:

# vi /usr/local/scripts/pagewrite.php

// Erstellung Pageview Statistik

putenv("JAVA_HOME=/usr/java/jre1.6.0_20/");

$pageview= 0;
$hadoop="/usr/local/hadoop/bin/hadoop";
$pig="/usr/local/pig/bin/pig";
$tod = date("Ymd");


exec("$hadoop dfs -copyFromLocal /logs/www01/combined_log.".$tod."*.gz /weblogs/www01/");
exec("$hadoop dfs -copyFromLocal /logs/www02/combined_log.".$tod."*.gz /weblogs/www02/");
exec("$hadoop dfs -copyFromLocal /logs/www03/combined_log.".$tod."*.gz /weblogs/www03/");
exec("$hadoop dfs -copyFromLocal /logs/www04/combined_log.".$tod."*.gz /weblogs/www04/");
exec("$hadoop dfs -copyFromLocal /logs/www05/combined_log.".$tod."*.gz /weblogs/www05/");
exec("$hadoop dfs -copyFromLocal /logs/www06/combined_log.".$tod."*.gz /weblogs/www06/");
exec("$hadoop dfs -copyFromLocal /logs/www07/combined_log.".$tod."*.gz /weblogs/www07/");
exec("$hadoop dfs -copyFromLocal /logs/www08/combined_log.".$tod."*.gz /weblogs/www08/");
exec("$hadoop dfs -rmr /user/root/sum_counts");
exec("$pig -x mapreduce -f /usr/local/pig/apacheAccessLogAnalysis_pageview.pig \
-param LOGS='/weblogs/www*/combined_log.".$tod."*.gz'");

exec("$hadoop dfs -cat /user/root/sum_counts/part*", $output);

while(list(,$line) = each($output)) {
$pageview=$pageview + $line;
}

$row = base64_encode('www.eumel.de');
$col = base64_encode('web:'.$tod);
$val = base64_encode($pageview);
$time = time();


$xml = '


'.$val.'


';

$session = curl_init("http://hdp-cl01.eumel.de:60050/pageviews/www.arcor.de");

curl_setopt($session, CURLOPT_POST, 0);
curl_setopt($session, CURLOPT_VERBOSE, 0);
curl_setopt($session, CURLOPT_HEADER, 0);
curl_setopt($session, CURLOPT_POSTFIELDS, $xml);
curl_setopt($session, CURLOPT_HTTPHEADER, array("Content-Type: text/xml;charset=UTF-8"));
curl_setopt($session, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($session);
curl_close($session);

echo $response;

Unser PHP-Shell-Skript fungiert also als REST-Client. Es gibt zwar unzaehlige Implementierungsversuche (am liebsten auch in CodeIgniter), aber zum Schluss verwenden die meisten dann doch Curl. Dort kann man einfach HTTP-Header manipulieren. Am schwierigsten war, das Script davon zu ueberzeugen, doch bitteschoen text/xml zum Server zu senden und nicht text/html, wie es es default tut. Das XML selbst ist minimal mit einem Wert (Value) fuer eine Zeile (Column) in einer Spalte (Row). Das Abstrakte hierbei ist das Dreidimensionale, mit dem ich immer mal wieder Probleme habe: Jeder Wert in jeder Zeile und Zelle hat eine Versionierung, die sich ueber den Zeitstempel definiert. Wenn ich diesen mit abfrage, habe ich beliebig viele Werte fuer diese eine Zelle in der Tabelle. Ansonsten wird immer der neueste Wert ausgegeben.

Fragen wir doch unsere Pageviews ab, nachdem wir das pagewrite.php zyklisch per Cron aufrufen lassen.

# vi pageview.php

$tod = date("Ymd");

if(isset($_GET['useday'])) {
$useday = $_GET['useday'];
} else {
$useday = $tod;
}

if(isset($_GET['hour'])) {
$hour = $_GET['hour'];
} else {
$hour = "0";
}

if ((!ereg("^[0-9]+$", $useday) || (strlen($useday) > 8) || (strlen($useday) > 8))){
echo "Falsche Eingabe"; exit;}

if ((!ereg("^[0-9]+$", $hour) || (strlen($hour) > 2) )){
echo "Falsche Eingabe"; exit;}

$xmlUrl = "http://hdp-cl01.eumel.de:60050/pageviews/www.arcor.de/web:".$useday;

if(!isset($xmlUrl)) exit;
$xmlStr = @file_get_contents($xmlUrl);

if ($xmlStr === false)
{
exit;
} else {

$xmlObj = simplexml_load_string($xmlStr);
$arrXml = objectsIntoArray($xmlObj);
// print_r ($arrXml);
$res = base64_decode($arrXml['Row']['Cell'][$hour]);
if( $res == 0) {
$res = base64_decode($arrXml['Row']['Cell']);
}
echo $res;
}

function objectsIntoArray($arrObjData, $arrSkipIndices = array())
{
$arrData = array();

// if input is object, convert into array
if (is_object($arrObjData)) {
$arrObjData = get_object_vars($arrObjData);
}

if (is_array($arrObjData)) {
foreach ($arrObjData as $index => $value) {
if (is_object($value) || is_array($value)) {
$value = objectsIntoArray($value, $arrSkipIndices); // recursive call
}
if (in_array($index, $arrSkipIndices)) {
continue;
}
$arrData[$index] = $value;
}
}
return $arrData;
}

?>


Die Funktion ist einfach dem php-Manual vom xmlparser uebernommen. Es schreibt im Prinzip die XML-Werte in ein Array. Unser PHP-Script vertraegt 2 Eingabevariablen

  • useday - Datum der Pageview-Statistik im Format "20110214"
  • hour - Stunde (Version) der Pageview-Statistik (0 = neueste ... 24 = aelteste)

Die Variablen werden auf Eingabemaengel geprueft (sollte man sowieso immer tun) und als Ausgabe erhalte ich einfach die Pageviews als Zahl. Diese kann dann in mein Praesentationslayer eingebunden werden.

 

[1] http://wiki.apache.org/pig/PiggyBank

[2] http://wiki.apache.org/hadoop/Hbase/Stargate
[3] http://php.net/manual/de/book.curl.php
[4] http://pig.apache.org
[5] http://php.net/manual/de/book.simplexml.php
[6] http://hbase.apache.org