日記2011後期 Archive
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
Hadoopアドベントカレンダー2011 17日目 - Hive 0.8 について -
こんにちは、タムタムです。
久しぶりに記事を書きます。 Hadoopアドベントカレンダー2011の17日目という事で書かせていただきます。
私のような底辺エンジニアでも集合論の知識のみで簡単にデータ集計処理が書けるのがHiveの良いところだと思います。
試しに某ゲームのモンスター討伐のログを、数日分、全ワールド、Zoneエリア、モンスター単位で集計したら100秒くらいで結果が出ました。(その時はレコードが1億程度と少ないという事もありますが)
※ちなみに私はマイニングエンジニアではありません。
というわけでHive0.8に関する事を書きたいと思います。
Hive0.8ではBitmapインデックスやtimestamp型が追加されたというのが大きなトピックらしいです。さっそく新機能を試してみたいと思います。また細かい構文が追加されているようなので、それもおいおい試してみたいと思います。今回はtimestamp型について書きます。
ちなみに、まだリリースされていないのでソースを持ってきてビルドする必要があります。ビルドの方法は次の通りです。
svn co http://svn.apache.org/repos/asf/hive/branches/branch-0.8/ hive-0.8 cd hive-0.8 ant clean package
build/dist配下にパッケージが出来ていますので、build/distをHIVE_HOMEに設定してbin/hiveを実行します。
※(追記)Hive 0.8は12/16にリリースされてましたw
今まで日付データをhiveで扱うにはstringとして保持してWHERE句にUDFで日付に戻して計算をするとか、bigintでunixtimeとして保持したりしていました。timestamp型についてまずはどんな操作ができるのか調べてみます。
hive> show functions;
の結果を0.7と0.8で比較し、0.8で追加されたUDFを列挙しました。 (descriptionはコマンド打っても出てこなかったものがあるので書くのをやめましたw)
| UDF | Description |
|---|---|
| ewah_bitmap | |
| ewah_bitmap_and | |
| ewah_bitmap_empty | |
| ewah_bitmap_or | |
| from_utc_timestamp | |
| map_keys | |
| map_values | |
| named_struct | |
| timestamp | |
| to_utc_timestamp |
またJIRAのチケットによると、datetimeに関する様々なUDFがtimestampをサポートしたようです。
例えば以下のような操作ができます。
サンプルデータを1件用意します。書式はyyyy-MM-dd HH:mm:ss か yyyy-MM-dd HH:mm:ss.fffffffffのみです。
2011-12-25 09:00:00
create table ts1(t1 timestamp);
load data local inpath '/mnt/iscsi/test1/data4.txt' overwrite into table ts1;
select cast(t1 as float), cast(t1 as double) from ts1 limit 1;
1.3247712E9 1.3247712E9
select cast(t1 as bigint), cast(t1 as int), cast(t1 as boolean), cast(t1 as tinyint), cast(t1 as smallint) from ts1 limit 1;
1324771200 1324771200 true -128 26496
select cast(t1 as string) from ts1 limit 1;
2011-12-25 09:00:00
select cast('2011-12-25 9:00:00.000000000' as timestamp) from ts1 limit1;
2011-12-25 09:00:00
select
unix_timestamp(t1),
year(t1),
month(t1),
day(t1),
dayofmonth(t1),
weekofyear(t1),
hour(t1),
minute(t1),
second(t1),
to_date(t1)
from ts1;
1324771200 2011 12 25 25 51 9 0 0 2011-12-25
select date_add(t1, 5), date_sub(t1, 5) from ts1 limit 1;
2011-12-30 2011-12-20
select datediff(t1, t1), datediff(t1, '2011-12-10'), datediff('2011-12-20 9:00:00', t1) from ts1 limit 1;
0 15 -5
新しいUDFも試してみましょう。
select from_utc_timestamp(t1, 'JST') from ts1 limit 1; 2011-12-25 18:00:00 select from_utc_timestamp(t1, 'UTC') from ts1 limit 1; 2011-12-25 09:00:00 select from_utc_timestamp(t1, 'PST') from ts1 limit 1; 2011-12-25 01:00:00 select to_utc_timestamp(t1, 'JST') from ts1 limit 1; 2011-12-25 00:00:00 select to_utc_timestamp(t1, 'UTC') from ts1 limit 1; 2011-12-25 09:00:00 select to_utc_timestamp(t1, 'PST') from ts1 limit 1; 2011-12-25 17:00:00
うまく動いているように見えます。が、以下のクエリを発行すると正しい結果が得られません。
SELECT句に複数記述するのはNGなのかな?
select from_utc_timestamp(t1, 'JST'), from_utc_timestamp(t1, 'UTC'), from_utc_timestamp(t1, 'PST') from ts1 limit 1; 2011-12-25 10:00:00 2011-12-25 10:00:00 2011-12-25 10:00:00 select to_utc_timestamp(t1, 'JST'), to_utc_timestamp(t1, 'UTC'), to_utc_timestamp(t1, 'PST') from ts1 limit 1; 2011-12-25 08:00:00 2011-12-25 08:00:00 2011-12-25 08:00:00
ちなみに、2011-12-25 09:00:00はどのTimeZoneの9時なのか?という事が気になると思います。チケットを見ると、「hive.time.default.timezone」こういう設定が一瞬だけあったようですが、「Removed references to a "default" timezone. All times are treated as UTC」という事で無かったことになっています。実際は上の結果を見る限り(castとか)TimeZone.getDefault()が使われているように見えます。(すいませんソース追っていません)。なのでOracleVMの場合はVMプロパティのuser.timezoneで指定できますが、システムプロパティなので他に影響を及ぼしそうで怖いです。
Javaのコードで確認したら似たような動作したので、たぶんDefaultのTimeZoneを使っていると思います。grepしたらUTCとかいっぱい出てきましたけどw
(ていうか、Hiveのソース読めよって話ですよね)
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdf.setTimeZone(TimeZone.getTimeZone("JST"));
System.out.println(sdf.parse("2011-12-25 09:00:00").getTime());
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
System.out.println(sdf.parse("2011-12-25 09:00:00").getTime());
1324771200000 1324803600000
個人的には、時間に関する型をサポートするならSimpleDateFormatの書式をtableの設定で出来ると嬉しいんですけどね。
というわけで、timestamp型に関する事を書いてみました。
もしもネタが足りなければ列指向のRCFileとSequenceFileの性能の違いやRCFileをJavaののコードで作る方法あたりを書きたいと思います。
間違いを見つけたらどんどん指摘してくれると嬉しいです(・ω・)ノ