Blog
最近光麺にハマっている太田です。
グーグル、分散処理のためにデザインされた言語「Sawzall」をオープンソースで公開 ? Publickeyで紹介されている、並列ログ解析向け言語「Sawzall」を試してみました。動かし方のドキュメントが少なかったので、紹介エントリを書いてみます。
Sawzallについては、5年前に論文が発表されており一部概要を知ることは出来ましたが、先日実装がオープンソースで公開されました。論文の第一著者はUNIXやPlan9の開発者で知られるRob Pike氏です。
MapReduceのOSS実装として「Hadoop」が良く知られていますが、Hadoop向けの言語としてはHiveやPig等が有名です。
Sawzallとは?
Sawzallは一言で言うと、大規模なログデータから統計解析するための言語です。Sawzallの構文は、覚えやすいように意図的にC言語に似せて作られています。
しかし、C言語と異なり大量のデータを並列で処理するために1つ大きな制約がかかっています。それは、「全体のログを扱うのではなく、ログの1レコードに対する処理を記述する」という点です。
例えばRubyでログデータを処理するためには、以下のような処理が必要です。
open("log.txt").each_line { |record| # 各recordに対する処理を記述 }
しかし、Sawzallではeachの中の各recordに対する処理のみを記述します。外側のレコードに対するループは言語環境によって自動的に行われます。
これにより、大量のレコードを並列分散して処理出来るようになります。より具体的には、MapReduceフレームワーク上で処理実行する事が出来るようになります。
では具体的にSawzallのプログラムを記述して行きます。
例1: Hello World
まずはHello Worldの例です。
emit stdout <- "Hello World!"
プログラムの実行には、szlコマンドを使用します。
$ szl hello.szl Hello World!
例2: ワードカウント処理
次にMapReduceプログラムではHello Worldのような存在のワードカウント処理を行なってみます。以下のようなテキストファイルを用意し、ファイル内で各単語が何回出現したのかをカウントしてみます。
$ cat input.txt foo foo foo hoge hoge fuga fuga kzk
集計プログラムは以下のようになります。
fields: array of string = sawzall(string(input), "[^ tn]+"); wc: table sum[word: string] of int; for (i: int = 0; i < len(fields); i++) { s: string = fields[i]; emit wc[s] <- 1; }
少し複雑ですが一行ずつ解説していきます。
1行目
Sawzallでは”input”というシンボルが予約語になっており、プログラムの入力が渡されます。通常は入力ファイルの1行がバイト列で入っています。
ここではsawzall関数を用い、空白・タブ文字・改行で分割を行い、それをfieldsに代入しています。たとえば一行目は”foo foo foo”なので、1行目処理時には fields は [“foo”, “foo”, “foo”] の3要素の文字列配列になります。
注意したいのは、ここに記述しているのは1レコードに対する処理だということです。複数レコード(この場合は複数行)に対するループは、sawzall側で自動的にループを回してくれます。
2行目
2行目では、”table”を定義しています。tableとは、最終的な出力をaggregation(まとめ上げる)ための機能です。この場合は、”sum”タイプのテーブルを使用しています。他にもmaximum, minimum, top, unique等、様々なtableが用意されており良く有る集計処理のパターン化が行われています。
この行では、文字列(word)をテーブルのキーとし、それぞれのエントリは一つのint値を持っておりその値は足されるというテーブルを定義しています。”足される”の意味は後の行で分かります。
3 – 6行目
3-6行目ではループを回し各単語について処理を行っています。emit関数で先ほど定義したテーブルに対するデータの挿入を行っています。
プログラムを処理すると、以下のような順番でemitが行われます。
$ szl wc.szl input.txt emit wc["foo"] <- 1; emit wc["foo"] <- 1; emit wc["foo"] <- 1; emit wc["hoge"] <- 1; emit wc["hoge"] <- 1; emit wc["fuga"] <- 1; emit wc["fuga"] <- 1; emit wc["kzk"] <- 1;
第二引数に入力ファイルを渡すとデフォルトではemitのログが出力されます。しかし、ここで最終的に欲しい結果はテーブルwcの集計結果です。その場合は以下のように –table_output オプションを指定します。
$ szl wc.szl input.txt --table_output wc wc[fuga] = 2 wc[kzk] = 1 wc[foo] = 3 wc[hoge] = 2
出力を見てみると、各単語が出現した回数がカウント出来ているのが分かります。またemit時に1を入れましたが、こちらが足し合わされているのが分かります。
例3: 上位検索クエリ算出処理
ワードカウントだけだと面白く無いので、手元に有る検索ログデータを使用して、一番検索されている単語を出す処理を書いてみました。検索ログのフォーマットは以下のようになっています。日時と実際の検索クエリがタブで区切られています。
$ cat qlog.txt [2010-01-30 02:00:39] 黒子のバスケ [2010-01-30 02:00:39] 黒子のバスケ [2010-01-30 02:00:39] 黒子のバスケ [2010-01-30 02:00:40] ワンピース [2010-01-30 02:00:40] ワンピース [2010-01-30 02:00:40] ナルト
ここから各検索語のカウントを行なうプログラムは以下のようになります。
topwords: table top(30) of word: string weight count: int; fields: array of string = saw(string(input), skip `[`, `[0-9-]+`, skip `[ t]+`, `[0-9-:]+`, skip `[ t]+`, `([^ t])+`); if (len(fields) >= 3) { s: string = fields[2]; emit topwords <- s weight 1; }
最初の行はtopwordsというテーブルの定義です。今回はtopタイプのテーブルを用いています。このテーブルを用いると、上位N件データを出すことが出来ます。
2行目はsaw関数と正規表現を用いて地道にレコードをフィールドに分割しています。その後topwordsテーブルに、単語を重み1でemitしていきます。emitの履歴を見ると以下のようになります。
$ szl qrank.szl qlog.txt emit topwords <- "黒子のバスケ" weight 1; emit topwords <- "黒子のバスケ" weight 1; emit topwords <- "黒子のバスケ" weight 1; emit topwords <- "ワンピース" weight 1; emit topwords <- "ワンピース" weight 1; emit topwords <- "ナルト" weight 1;
最終的にtopwordsを出力すると以下に様になります。検索回数が多い順に並んでいるのが分かります。
$ szl qrank.szl qlog.txt --table_output topwords topwords[] = 黒子のバスケ, 3, 0 topwords[] = ワンピース, 2, 0 topwords[] = ナルト, 1, 0
まとめ
Sawzallは並列ログ解析の為の言語です。C++/JavaでMapReduceプログラムを書くのに比べて非常に短い行数で処理を記述出来ます。速度は論文にC++の場合の約1/10と書いてありました。
Adhocな解析を多々行う場合には非常に便利と思われますが、現状公開されているsawzall処理系ではMapReduceによる並列/分散処理を行なう事が出来ません。
が、ソースを読んでみた所Hadoopには意外と簡単に繋ぎこめそうでしたので今後に期待です。個人的にはPigよりC言語に近く書きやすいのと、日付周り・テーブル周りの機能が豊富なのが良いなと思いました。
より詳しく触ってみたい方は、ドキュメントとソース中に含まれるサンプルコードが参考になるかと思います。
Tag