Home > 日記 > 日記2011後期 Archive

日記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ののコードで作る方法あたりを書きたいと思います。

間違いを見つけたらどんどん指摘してくれると嬉しいです(・ω・)ノ

Home > 日記 > 日記2011後期 Archive

Search
Feeds

Return to page top