Hiveで整形されていないログを集計する方法

タムタムです。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

 

新しいサイトもよろしくお願いします!