Kengo's blog

Technical articles about original projects, JVM, Static Analysis and TypeScript.

AppEngine-MapReduceをGAE/Jで使ってみた

f:id:eller:20110206115003p:image*1
ということで@twistoireの管理用にAppEngine-MapReduceのrevision150を使ってみた。
用意されている公式ドキュメント読めばわりと問題なく使えるが、まだまだ洗練されてはいないので細かいところで苦労する感じ。以下に使うまでの流れをまとめる。

jarを作る

trunkからコード落としてantでjarを作るだけ。6つjarができるので、projectのWEB-INF/libに突っこんでCLASSPATHに追加してやる。

# for Mac OS X 10.6.6
cd /tmp
svn co http://appengine-mapreduce.googlecode.com/svn/trunk/java mapreduce
cd mapreduce
ant
open dist/lib

Mapperを実装する

公式サンプルを参考にすれば良い。自分は以下3点に引っかかった。Datastoreの低レベルAPIに慣れてればほぼ問題ないはず。

  • AppEngineMapper<Key, Entity, NullWritable, NullWritable>を継承する。
    • Key, Entityは入力として使用しているDatastoreInputFormatによって決定されているものなので、変更しない。
    • 後半2つはReduceが実装されたら使えるようになるっぽい?
  • @twistoireはEntityの扱いにJPAを使っているが、MapReduceは低レベルAPIを使う必要がある。
    • int[]のフィールドをEntity#getProperty(String)するとList<Integer>が返ってくる。
  • CounterGroupはTreeMapとして実装されているためか、たくさんのグループに少ないカウンタが属するよりも、少ないグループにたくさんのカウンタが属する方が管理画面表示が高速になる。

@twistoireではユーザのカウントとフォロー/リムーブ回数の合計に使用している。実装イメージは以下のとおり。

public class UserCountMapper extends AppEngineMapper<Key, Entity, NullWritable, NullWritable> {
	private final Logger logger = Logger.getLogger(this.getClass().getName());

	@Override
	public void map(Key key, Entity value, Context context) {
		if (!value.getKind().equals("User")) {
			logger.warning("unknown entity kind");
			return;
		}

		context.getCounter("user", "all").increment(1);
		if (Boolean.TRUE.equals(value.getProperty("notifyFollow"))) {
			context.getCounter("user", "notifyFollow").increment(1);
		}
	}
}

今まではbulk loaderでCSVをダウンロードしてからExcelで調べる方法しか無かったので、結構便利。CPUをかなり使うので1日に10数回も実行するのは難しそうだ。

XMLに設定を書く

web.xmlServletを叩くためのMappingを書いてやる。

<servlet>
  <servlet-name>mapreduce</servlet-name>
  <servlet-class>com.google.appengine.tools.mapreduce.MapReduceServlet</servlet-class>
</servlet>
<servlet-mapping>
  <servlet-name>mapreduce</servlet-name>
  <url-pattern>/mapreduce/*</url-pattern>
</servlet-mapping>

MapReduceは高負荷だし見せたくないデータも扱うので、管理者権限でのみ実行できるように制限するといい。

<security-constraint>
  <web-resource-collection>
    <!-- 略 -->
    <url-pattern>/mapreduce/*</url-pattern>
  </web-resource-collection>
  <auth-constraint>
    <role-name>admin</role-name>
  </auth-constraint>
</security-constraint>


次はMapReduce独自の設定。mapreduce.xmlをWEB-INFに作り、公式サンプルを参考に書いてやる。
コピペしてMapperとKindの名前だけ変えてやれば問題ない。Mapperに独自の設定を渡したい場合はXML

<property>
  <name human="my option">option</name>
  <value template="optional">10</value>      
</property>

とした上でMapperに

String myOption = context.getConfiguration().get("option");

と書いてやれば良い。


私はさらにappengine-web.xmlに設定を書いて、appengineの管理画面からアクセスできるようにした。

<admin-console>
  <page name="mapreduce" url="/mapreduce/status"/>
</admin-console>

こうすると管理画面の左ペインにリンクが追加される。

実行

まずはローカルで試す。

ローカルではshardが1つしか使えないので並列処理にはならないが、とりあえず走るかどうかはわかる。例外が出ているときはそれを参考にコードを修正すること。

既知の問題

  • 処理が完了するとJobのStatusがUnknownになるのは既知の不具合らしい。優先度高い割に半年も放置されているのが気にはなる。
  • たまにDatastore操作関連の例外が投げられるが再現条件不明。
    • ApiProxy$RequestTooLargeException: The request to API call datastore_v3.Put() was too large.
    • 各種データがKind(ShardState やMapReduceState)のBLOBプロパティに記録されるので、これが大きすぎたのかもしれない。

*1:引用画像は[http://code.google.com/intl/en/edu/parallel/mapreduce-tutorial.html]から