タムタムです。Hadoop アドベントカレンダーの12/24分 を書かせていただきます。
それと、時々ログ解析飲み会というものをやっているという噂があるのですが、わたしも混ぜてください><
さて、まずはじめに・・。 ログが整形されているなんて都市伝説です。
自分が作るアプリは最初からログ設計をして整形して出力しているのですが、世の中そんなものばかりではありません。Hiveで集計するためにはある程度書式が整っていないとスマートに処理できません。
適当なスクリプトで処理するのも手ですが、もともと分散しないと処理できないほどの量なのに、それを分散環境で処理しないとか無いと思います・・。
となると、スクリプトを書いてHadoop Streamingでログを処理すればいいよねーとなるわけです。が、用途はある程度限られてしまいますが実はHiveでも出来ます。
例えば、以下のようなログがあるとします。
どこかで似たようなものを見たことある人も居るかもしれませんが気のせいです(=ω=)
2011-10-14 00:00:14 INFO - [XXX][MONS] (charaId:123456, tmpId:56789) start attack monster(MonsterAAA) at (85.9,199.2,-590.6) 2011-10-14 00:00:14 INFO - [XXX][MONS] (charaId:123456, tmpId:56789) start attack monster(MonsterAAA) at (85.9,199.2,-590.6) 2011-10-14 00:00:14 INFO - [XXX][MONS] (charaId:123456, tmpId:56789) start attack monster(MonsterAAA) at (85.9,199.2,-590.6) 2011-10-14 00:00:18 INFO - [XXX][ITEM] (charaId:123456, tmpId:56789) item can add ******* 2011-10-14 00:00:18 INFO - [XXX][ITEM] (charaId:123456, tmpId:56789) item add ****** 2011-10-14 00:00:18 INFO - [XXX][XXXX] ************* 2011-10-14 00:00:18 INFO - [XXX][MONS] (charaId:123456, tmpId:56789) kill monster(MonsterAAA) at (85.9,199.2,-590.6)
さて、このファイルにはモンスターの討伐記録のログがあります。 一番下のログです。
このログファイルからモンスター討伐数を集計したい場合、一番下の行には、誰が(charaId)いつ(時間)どこで(ワールド、エリア、XYZ座標)なにをした(MonsterAAAを倒した) が記録されており、まずはこの形式のログだけを抽出してテーブルに入れたいわけです。 (ワールドとエリアはログファイルの単位なので記録はされていないです。)
まずは元のgzログをtmp2テーブルに入れます。ログファイルはHDFS上にあります。
パーティションを適当に切ります。idは適当なIDを入れて作業がかぶらないようにする用です。OracleのPLAN_TABLEみたいなイメージです。別に無くてもいいです(=ω=)
create table tmp2(line string) partitioned by (id string, world string, area string); load data inpath '/test/log/neko/2011-10-14-dungeon01.txt.gz' overwrite into table tmp2 partition(id='aaa111', world='neko', area='dungeon01');
次にMap用のスクリプトを書きます。 (map2.pl)
#!/usr/bin/perl use warnings; use strict; while (<>) { my ($world, $area, $line) = split("\t", $_); if ($line =~ /^((\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})).*\(charaId:(\d+).*tmpId:(\d+)\).*kill monster\s*\((\w+)\)\s*at\s*\((.*),(.*),(.*)\)/) { my @array = ( $world, $area, $1, # datetime $4, # charaid $5, # tmpId $6, # monster-name $7, $8, $9 # x,y,z ); print join("\t", @array), "\n"; } }
わざわざ配列に入れてjoinする必要なんてありませんよね。ごめんなさい。コメントを見やすくする関係でこう書きました。
ちょっと確認したい場合は、以下のようにやればスクリプトの確認はできます。
zcat hogefuga.log.gz | awk '{print "world01\tdungeon01\t"$0}' | ./map2.pl
world01 dungeon01 2011-10-14 00:00:18 123456 56789 MonsterAAA 85.9 199.2 -590.6
試しにSELECTしてみます。
SELECT TRANSFORM(line) USING '/home/hadoop/map2.pl' AS ( dt timestamp, charaId bigint, tmpId bigint, monster_name string, x double, y double, z double ) FROM tmp2 WHERE id = 'aaa111' LIMIT 10;
2011-10-14 00:00:18 12345678 00000000 MonsterAAA 85.9 199.2 -590.6 2011-10-14 00:00:18 12345678 00000000 MonsterBBB -41.7 184.7 -261.3 2011-10-14 00:00:28 12345678 00000000 MonsterAAA 85.9 199.2 -590.6 2011-10-14 00:00:42 78901234 11111111 MonsterCCC 85.9 199.2 -590.6 2011-10-14 00:00:53 12345678 00000000 MonsterCCC 85.9 199.2 -590.6 2011-10-14 00:01:07 12345678 00000000 MonsterBBB 63.5 200.5 -550.9 2011-10-14 00:01:11 78901234 11111111 MonsterCCC 85.9 199.2 -590.6 2011-10-14 00:01:20 12345678 00000000 MonsterCCC 85.9 199.2 -590.6 2011-10-14 00:01:30 12345678 00000000 MonsterBBB -61.7 184.0 -258.4 2011-10-14 00:01:37 12345678 00000000 MonsterBBB 13.5 200.1 -570.1
(結果をダミーのものに書き換えるのめんどくさい・・・。でも本物出すわけにはいかないし・・。)
集計してみます。
SELECT world, area, monster_name, count(*) as cnt FROM ( SELECT TRANSFORM(world, area, line) USING '/home/hadoop/map2.pl' AS ( world string, area string, dt timestamp, charaId bigint, tmpId bigint, monster_name string, x double, y double, z double ) FROM tmp2 WHERE id = 'aaa111' ) tbl GROUP BY world, area, monster_name ORDER BY cnt desc;
neko dungeon01 Monster001 3215 neko dungeon01 Monster002 1733 neko dungeon01 Monster003 945 neko dungeon01 Monster004 736 neko dungeon01 Monster005 476 neko dungeon01 Monster006 471 neko dungeon01 Monster007 455 neko dungeon01 Monster008 407 neko dungeon01 Monster009 181 neko dungeon01 Monster010 168 neko dungeon01 Monster011 154 neko dungeon01 Monster012 22 neko dungeon01 Monster013 16 neko dungeon01 Monster014 8 neko dungeon01 Monster015 8
集計できました(・ω・)ノ
Tmpテーブルを経由しないといけないのがアレですが、externalなのでログの複製は行われないし、中間テーブル的な整形されたテーブルの実体も持たなくて良いのです。集計前にテーブルに1回入れる場合は普通に末尾にINSERT構文を書けばできます。まぁこういう方法もありますよと言うことで。
今回はMapしか書いてませんが、Reduceもスクリプトで書くことができます。
https://cwiki.apache.org/confluence/display/Hive/LanguageManual+Transform