副題: ビッグデータ時代の非ビッグデータ集計戦略

PHP と MySQL を使ってカジュアルに MapReduce する MyMR というものを作ってみました.
とても安直な名前ですね.

yuya-takeyama/mymr – GitHub

とりあえず試してみる

MyMR には, MapReduce のマナー (?) に従って, WordCount するためのサンプルコードとサンプルデータを同梱してみました.

map/reduce 関数は PHP で書かれています.
WordCount.php

MySQL のユーザ名・パスワード等は適宜置き換えて下さい.

見事, 入力テーブル内の単語の出現回数を集計することができました.

MyMR の特徴

  • データの入出力はいずれも MySQL のテーブル
  • 入力と出力のデータベースは同じでもいいし別でもいい
    (入力はプロダクションサービスの Slave サーバで, 出力はデータ集計用の別サーバ, とかいうこともできる)
  • map/reduce 関数を PHP で書く
  • MySQL を意識することは無く, PHP 標準の array にほげほげするだけ
  • 分散/冗長性/耐障害性などについては特に考えていないし, 考える予定も無い
    (エラー処理はちゃんとしたい)
  • 並列処理でなく直列処理
    (並列処理は出来た方がいいと思っている)

まだ「とりあえず動く」程度の状態なので, 色々足りてない状態ではありますが, ギガ/テラバイト級のデータを相手にするのであれば Hadoop とかを使うべきだと思いますし, MyMR はそのような問題を解決しようとはしていません.

モチベーション

  • MySQL に直接 MapReduce 処理を行いたい
  • GROUP BY よりはもっと複雑な処理がやりたい
  • map/reduce を LL で書きたい
  • プログラミングモデルとしての MapReduce を活用したい

DBMS 上で MapReduce をやりたいのであれば MongoDB や CouchDB という選択肢がありますし, map/reduce を LL で書きたいというのであれば Hadoop Streaming という選択肢があるでしょう.
そうなると, やはり一番のモチベーションは MySQL で MapReduce する ということに尽きるように思います.

MongoDB による MapReduce も JavaScript でカジュアルにできてとても便利です.
ですが, 自分の場合, 普段の仕事だと, そもそものデータはほとんど MySQL に入っているので,

  1. mysqldump で csv ファイルを作成
  2. mongoimport で MongoDB にインポート
  3. MongoDB 上で MapReduce

みたいな手順を踏む必要があり, 非常に面倒です.
場合によっては mongoexport で csv に出力してさらに MySQL 上にインポート, なんてことをやることもあって, 正直疲れました.

MySQL で ということの次に重要だと思うのが, プログラミングモデルとしての MapReduce です.
これについては書くと長くなりそうなのでやめておきますが, Haskell のような関数型言語をつまみ食いすることで, この辺りの魅力がわかってきたように思います.
(関数型言語における map 関数と Hadoop/MongoDB/MyMR における Map はちょっと違いますが)
伊藤直也さんの MapReduce::Lite なんかも, その辺りにモチベーションがあって作られたのではないか, と想像しています.

何故 PHP か

特に大した理由はありません.
強いていえば仕事で使いたいと思っていて, 周囲で使われているのが PHP である, というだけの話です.

ついでに挙げるなら, PHP では MySQL ドライバが標準で備わっている, というのもあります.
MyMR では PDO を使っており, 普通に構築された LAMP 環境であれば, まず問題無く使えると思います.

とはいえ, 例えば Bundler の使える環境であればそういった依存関係に悩まされることもありませんし, 正直 Ruby で書き直したい.

MyMR の仕組み

仕組みといえるほど複雑なことはやっていませんが…

1: 入力データの取得

これはとても単純で, mymr コマンドの -i (–input) オプションに渡したテーブルを全件 SELECT するのみです.

今の所全データをガバっとメモリ内に取り込むようになっているので, テーブルサイズに比例してメモリを食います.
非ビッグデータ向けのフレームワークとはいえ, これはさすがにあんまりなので, 何とかするつもりです.

あと, 未実装ですが, WHERE 条件なども指定できれば入力の段階でフィルタできて便利そうなので, やろうと思っています.

2: Map

SELECT で取得した全ての行に対して Map 処理を行います.

MyMR の map 関数では, その中で emit メソッドを実行することで, テンポラリテーブルにデータを INSERT していきます.
関数のプロトタイプは void map(array $record) というシンプルなもので, 1 レコードを表すハッシュ (PHP なので array) を受け取るだけです.

入力テーブルの定義についての制約は特にありません.
通常どんなテーブルでも入力として扱うことができます.

emit にはキーとそれに対応する値を渡すことで, 中間データがテンポラリテーブルに 1 行ずつ挿入されます.
本来は MySQL セッションが終了した時点でテーブルは消えるのですが, 先の WordCount の例でいうと以下のようなテーブルが作成されています.

emit に渡された値は自動的に JSON に変換されています.

JSON 化してるのは構造化データを扱えるようにするためで, この辺りは MongoDB の MapReduce にインスパイアされてます.

3: Shuffle/Sort (?)

Hadoop なんかで言うところの Shuffle/Sort 付近に該当する, と思っているのですが, あんまり自信ないです.
ぶっちゃけると Hadoop 自体を使ったことは無くて, ブログ記事や書籍でつまみ食いした程度の知識しかありません.
(り, りろんはしってる (知らない))

とにかく, ここでは Reduce フェーズの前の下準備として, キーが同一のものをグループ化しています.

MyMR ではカジュアルに GROUP BY と GROUP_CONCAT を使っています.

実際は GROUP_CONCAT の SEPARATOR には改行コードの LF を指定しており, 複数の JSON が改行区切りで連結されたものになります.
要するに複雑なパース無しで複数の JSON を分割できるデリミタであれば何でもよくて, 無難に LF を使っているというだけの話です.

ところで, ここで JSON の代わりに MessagePack を使えれば, それだけである程度高速化できると思うんですが, そのときのデリミタは何にすればいいんでしょう?

4: Reduce

前の Shuffle フェーズで実行したクエリの結果全行に対して, 1 行 1 行 Reduce 処理を行います.

Map と同様, reduce 関数に 1 行 1 行の値を渡していきます.
関数のプロトタイプは array reduce(string $key, array $values) となっています.
$values では GROUP_CONCAT で改行区切りとなっていた JSON を予めパースし, 値の配列として渡されます.

そして返り値のハッシュがそのまま 1 行として -o (–output) に指定されたテーブルに 1 行 1 行挿入されていきます.

また, map と違って単純な入出力モデルになっているので, テストが書きやすいという利点があります.

そしてこれも map と違って, 挿入するデータは JSON ではなく, ハッシュのキーをカラム名としてレコードにマッピングされます.
そのため, 出力テーブルの定義は reduce 処理に合わせておく必要があります.

これまた map と違って, 出力テーブルでは key という名前のカラムが, キーを格納するために予約されており, reduce 返すキーとしては避ける必要があります.
それ以外は自由ですが, key カラムにはユニークインデックスを設定しておくのが良いでしょう.

まとめ

MyMR について長々と書いてきましたが, この記事で一番主張したいのは「MySQL から直接 MapReduce できたら便利じゃないか」ということに尽きます.
そういう需要があってもよさそうなものですが, ググってみても意外とそれらしい記事は見つからず, じゃあ作ってみようということでできたのが MyMR です.

ほとんどプロトタイプみたいなもので, 作り込みはまだまだこれからですが, 「MySQL で MapReduce する」というアイディアを発表したくて, この記事を書いています.

また, 今回はたまたま MySQL でしたが, 入出力を抽象化して MapReduce をするための何かがあれば, いろいろ捗るんじゃないかなぁという妄想もあります.

ビッグデータは無くとも, ビッグデータ時代に生まれた知見を活かすことはできるんじゃないか, というお話でした.

See also

,

Zend Engine 初心者による, Zend Engine 初心者のための発表という感じで話してきました.

今年に入ってから, scribble というなぐり書き用のブログを始めました.
GitHub Pages 内で Jekyll というツールを使って書いているんですが, Markdown で書いて git commit して git push するだけでサクッと公開できるのでとても気に入っています.

今のところ主に PHP 本体や, そのコアであるところの Zend Engine のコードリーディングが中心で, 今回の発表はこれらの記事をまとめた内容です.

闇 PHP 勉強会でのその他の発表はというと, PHP 製のパーサコンビネータとか, OpenCL とか, 魔改造 PHP とか様々で, こんな中でハッシュテーブルの実装について話し手も釈迦に説法なんではないかとかいろいろ危惧しましたが, わかりやすかったという声ももらえたようでよかったです.

この発表では, 既に scribble でも紹介している hashtable_dump という PHP Extension についても紹介しています.

本当は HashDoS 問題を解決したという Perl や Ruby におけるハッシュテーブルの実装についても調べて発表したかったんですが, 今回は間に合いませんでした.
次回の闇 PHP 勉強会があれば是非それらについて発表できればと思っています.

最後に, 主催の @anatoo さん, 会場提供のアシアル株式会社さん, 楽しい勉強会をありがとうございました.

この記事は PHP5.4 Advent Calendar jp: 2011 の 20 日目です.

Travis CI とは

Travis CI は, Continuous Integration (CI: 継続的インテグレーション) を実行するクラウド環境です.
GitHub に push すると, Travis CI の VM 上に通知が行われ, GitHub リポジトリからのチェックアウトや, ユニットテストの実行が行われます.
ユニットテストの実行は成功/失敗の結果により通知が行われ, また, 履歴も Travis CI 上に残ります.

元々は Ruby 専用のサービスだったと思いますが, その後 Clojure や Node.js などをサポートし, 1 ヶ月ぐらい前に PHP までもサポートされました.

Travis CI を利用する PHP プロジェクト

これら以外にも Symfony 関連のライブラリやバンドルは多く登録されています.

ところで, 国産のものだと今のところは Diggin_Http_Charset ぐらいしか把握していません.
日本語の情報はまだほとんど無いに等しい状況なので, 日本国内の, 少なくとも PHP コミュニティにおいてはまだあまり認知されていないように思えます.

記事にするにはまだまだ調べが足りないのですが, とりあえず日本の PHP コミュニティにも広まって欲しいと思い, 紹介してみることにしました.

Travis CI で CI するまでの手順

何はともあれ, 実際に GitHub のリポジトリを登録してみましょう.

大まかに以下のような流れとなります.

  1. GitHub でのアカウント登録
  2. PHP プロジェクトのリポジトリを用意
  3. リポジトリ内に専用の設定ファイル .travis.yml を用意
  4. GitHub アカウントでの OAuth により Travis CI にサインイン
  5. Travis CI のアカウント情報を GitHub のリポジトリの Service Hooks に登録
  6. GitHub リポジトリに git push

まずは, Travis CI にサインインします.
GitHub のアカウントさえあれば, OAuth で簡単にサインインできます.

PHP のプロジェクトは何でもいいですが, とりあえずは PHPUnit のテストが書かれているものが必要です.
ちょっと試したいけど手頃なリポジトリが無い, という場合は, 以下のリポジトリを fork してやってみるのもいいでしょう.

Travis CI 用の設定ファイル .travis.yml ファイルは, とりあえず以下のようなものを用意しましょう.
これを, リポジトリ上のルートディレクトリに配置します.

設定項目は他にもたくさんあるので, 詳細は公式ドキュメントの Building a PHP Project を参照してください.

これで, git push 時に PHP5.3 と PHP5.4 の環境でテストが実行されます.
(そういえばこの記事は PHP5.4 Advent Calendar でした)

テストの実行には明示していませんが, デフォルトでは phpunit コマンドが実行されます.
PHPUnit ではデフォルトで phpunit.xml を設定ファイルとして読み込むので, これもリポジトリルートに置いておきましょう.
(ところで, よく phpunit.xml.dist という名前にしているものがありますけど, あれってどういう意味なんでしょう…)

リポジトリの Service Hooks の指定, つまりこれは push 等のイベントの発生時に Travis CI に通知を行う機能ですが, これは Travis CI 上の画面から簡単にできます.
フリックスイッチを ON にするだけです.

リポジトリの選択

リポジトリの選択

ですが, このリポジトリ一覧は最大で 30 しか表示されない問題があるようで, 登録したいリポジトリが表示されない場合は, 手動で登録する必要があります.
手動での登録は GitHub のリポジトリから Admin -> Service Hooks -> Travis と進み, 必要な情報を入力します.

Domain と User は通常空のままでいいので, Token のみ Travis CI の画面上からコピーし, Active にチェックを入れた上で, Update Settings します.

これで Travis CI で CI を行う準備が整いました.
あとは, git push すればそのタイミングでビルドが実行されるので, Travis CI の画面上で確認しましょう.
また, GitHub の Service Hooks の画面で Test Hook ボタンを押すことでもビルドが始まります.
なお, このときは GitHub 上でデフォルトで表示される設定に鳴っているブランチ (デフォルトは master) に対してビルドが行われるようです.

Travis CI のワーカの稼働状況によってはなかなか結果が反映されないこともあるので, ご注意ください.

以下は, 細かい設定について説明します.

vendor ディレクトリへの対応

開発しているプロジェクトが依存する外部ライブラリは, よく vendor というディレクトリに入れられています.
vendor ごとリポジトリに放り込むこともできますが, 通常は vendor は .gitignore に登録して, リポジトリには登録しないことが多いでしょう.

その場合, vendor ディレクトリに対象のライブラリを自動でインストールできる必要があります.

今回は, 最近開発中の Speciphy という BDD フレームワークを例に説明を行います.

今回は, Speciphy は PHPSpec に依存しているので, 事前に PHPSpec をインストールしておかないと, Travis CI 上でテストを実行することができません.
そこで, 以下のようなインストールスクリプトを, install-vendor.sh として用意します.

今回は, PHPSpec を pyrus.phar でインストールしています.
(pyrus.phar というのは次世代の pear コマンドです)
予め必要となる pear channel を登録しておくことで, 対話無しで全自動でインストールが完了します.

なお, Symfony2 だと git submodule を利用して vendor を管理していると聞きますが, これについてはあまり知らないので割愛します.
やり方はいろいろあると思うので, フレームワークに合わせたり, 自分の好きな方法を探してみてもいいでしょう.
(Composer を使うのも面白いですね)

そして, .travis.yml に, 先ほどのインストールスクリプトを, ビルドの前の準備として実行するよう, 追記しましょう.

pyrus による vendor 対応については以下の記事を参考にしました.

Behat によるテストも行う

デフォルトでは PHPUnit によるテストが実行されるのみですが, Behat によるテストも実行できるようにします.

テストとして実行されるコマンドは, .travis.yml で script という項目にコマンドを指定することで可能です.

ところで, ビルドの成功/失敗の判定は, コマンドの終了ステータスで行われているようです.
終了ステータスが 0 なら成功, それ以外なら失敗, という具合です.

つまり, 終了ステータスをきちんと指定しているテスティングフレームワークであれば, 基本的に何でも使えることになります.

そこで, テストを実行するためのスクリプトとして, 以下のような run-test.php を用意します.

環境変数 TESTING_FRAMEWORK が PHPUnit なら PHPUnit を実行し, Behat なら Behat を phar ファイルでダウンロードして実行する, というものです.
スクリプトの最後に終了ステータスを指定して exit しているので, テストの実行が成功/失敗のいずれでも適切に通知されます.

そして .travis.yml にも再度追記を行います.

今度は env という項目を追加しています.
これは, env を配列で複数指定することによって, その数だけテストを実行するというものです.

今回は PHP のバージョンに 5.3 と 5.4 の 2 種類, テスティングフレームワークに PHPUnit と Behat の 2 種類指定しているので, 2 x 2 で合計 4 パターンのビルドが行われます.
うまくいけば, 以下のような結果が得られます.
(設定は上記のものと多少異なります)

Travis CI 実行結果

Travis CI 実行結果

PHP の Travis CI を支える技術

Travis CI では, テストを実行する際の環境の実行に phpenv が使われています.
また, PHP のビルドには当初 phpfarm が, そして今後は php-build に切り替わるようです.

どのように使われているかは, このあたりを見れば何となくわかると思います.
(ただし, master がデプロイされているとは限らないので, これが最新の状態とは限らないので注意は必要ですが)

phpenv と php-build については以前に以下のような記事を書いていますので, これらが参考になれば幸いです.

蛇足: オレはこう思う

Travis CI がどういう組織で作られているのかはまだよくわかってないのですが, GitHub のアカウントページを見る限りはそれぞれ所属はバラバラですし, IT ベンチャーとかではなく, コミュニティベースの活動による成果, ということなのでしょうか.

Travis CI の PHP 対応にあたっては, phpenvphp-build の作者である CHH さん, それらを Travis CI に取り入れるべく多くのパッチを提供した loicfrering さんを初め, 何人もの開発者が関わっているようです.

こんな風にありもののツールと, 多方面からの強力が実を結び, 今までには無い手軽さで CI 環境が手に入れられるようになったことは大変素晴らしいことだと思います.

Travis CI 自体もオープンソース活動をさらに加速させるものだと思いますし, これからの PHP コミュニティにも希望が持てると感じます.

フレームワークやライブラリを開発している皆さんは, 導入を検討してみてはいかがでしょうか.

, , , , ,