Google App Engine用に作ったDatastoreMapを高速化する

以前に、Google App Engine for Java(以下GAE/J)のデータストアにHashMapのような感覚で書き込む(または読み込む)ためのDatastoreMapというクラスを作成しましたが、より現実的な利用を考えて高速化を行いました。

java.util.Mapのメソッドを仕様に忠実に実装すると、putやremoveの際に置換・削除前の古いデータを返さなければなりませんこれらの戻り値はほとんどのケースで利用されることがないと思うのですが、そのためだけにトランザクションを使用して古いデータを取得し、それから値を置換・削除するというのは、パフォーマンス上のデメリットが大きすぎます。戻り値が使用されないという前提に立てば、トランザクションの使用と古いデータの取得を省略できます。同様のことは(これも以前に作成した)MemcacheMapにも言えます。

今回は、上記のようにputとremoveの戻り値の取得を省略することで、処理を高速化したFastDatastoreMap、FastMemcacheMap、また、データストア操作時に透過的にキャッシュを行うFastCachedDatastoreMapの実装を行いました。これで、Low-Level APIを叩いたのと同程度の速さで読み書きが行えるはずです。

サンプルコード

使い方はDatastoreMap等とまったく同じです。クラス名にFastがついただけです。

// 「名前」に対応した「年齢」を格納する"people"というkindのMapオブジェクトを生成。
Map<String, Integer> map = new FastDatastoreMap<String, Integer>("people");

// Mapにデータ(「名前」と「年齢」)を格納。
map.put("Tom", 27);
map.put("Mary", 21);
map.put("John", 23);

// 「年齢」を取得。
int ageOfMary = map.get("Mary");

// 「名前」が”Tom"のデータを削除。
map.remove("Tom");

ダウンロードと使い方

JARファイルとソースを下記からダウンロードできます。Apache License 2.0にて公開します。

GitHub - koher/Koherent-App-Engine-Library-for-Java: Koherent App Engine Library is a library for Google App Engine for Java. It includes "DatastoreMap" which enable to operate Datastores like java.util.HashMap, "DatastoreOutputStream" which enable to write data to Datastores like java.util.FileOutputStream and so on.
(画面右上あたりのDownload Sourcesをクリック→ZipかTarの形式を選択)

ダウンロードした圧縮ファイルを解凍し、中にあるJARファイルを/WEB-INF/libに放り込んでパスを通せばOKです(Eclipseなら、プロジェクトを右クリック→Properties→Java Build Path→Libraries→Add JARs→/WEB/INF/libの中に入れたJARファイルを選択)。

その他

配布ファイル内には、データストアにFileInputStream/FileOutputStreamのように書き込みを行うためのクラス、DatastoreInputStream/DatastoreOutputStreamも含んでいます。また、実はFileReader/FileWriterに対応するDatastoreReader/DatastoreWriterもこっそりと実装していてそれらも含んでいます。

Google App Engine上にFileOutputStreamを実装する

Google App Engine for Java(以下GAE/J)で、FileOutputStreamのような感覚でデータストアに書き込むためのOutputStreamを実装しました。

背景

GAE上ではFileOutputStreamを始めとしたファイル書き込みのためのクラスを利用することができません。代わりに、永続化するデータはGAEの提供するデータストアに書き込むことになっています。

GAE/Jでデータストアにデータを書き込む手段として、公式に提供されているJDOやJPA、Low-Level API(以下LLAPI)、ひがやすをさんのslim3などがあります。先日このブログに書いたように、僕もデータストアをHashMapのように扱えるDatastoreMapを実装してみました。また、最近ではBlobstoreという、ファイルを格納するための仕組みも公式に提供されています。

このようにデータストアに書き込む手段はたくさん提供されているのですが、OutputStreamを継承したクラスを用いてFileOutputStreamのようにデータを書き込みたいというケースもあるのではないかと思い、データストアに書き込むためのOutputStreamを実装してみました。タイトルの「FileOutputStreamを実装する」は少し言い過ぎかもしれません。正確には、「FileOutputStreamのような感覚で使用し、データを永続化できるOutputStreamを実装する」です。データストアのエンティティのサイズの制限である1MBを超えるデータでも書き込むことができます

サンプルコード

下記のように、FileOutputStreamを用いてファイル書き込みを行うのと同じようにデータストアに書き込めます*1。書き込みにはDatastoreOutputStream、読み込みにはDatastoreInputStreamを使います。

書き込み

BufferedWriter writer = null;
try {
	// DatastoreOutputStreamを用いてBufferedWriterを作成
	// FileOutputStreamと同じように利用することが可能
	writer = new BufferedWriter(new OutputStreamWriter(
			new DatastoreOutputStream("sample")));

	// データの書き込み
	writer.write("Hello");
	writer.newLine();
	writer.write("World");
	writer.newLine();

	writer.flush();
} catch (IOException e) {
	e.printStackTrace();
} finally {
	if (writer != null) {
		writer.close();
	}
}

読み込み

BufferedReader reader = null;
try {
	// DatastoreInputStreamを用いてBufferedReaderを作成
	// FileInputStreamと同じように利用することが可能
	reader = new BufferedReader(new InputStreamReader(
			new DatastoreInputStream("sample")));

	// データの読み込み
	String line;
	while ((line = reader.readLine()) != null) {
		System.out.println(line);
	}
} catch (IOException e) {
	e.printStackTrace();
} finally {
	if (reader != null) {
		reader.close();
	}
}

上記サンプルコード中にはありませんが、DatastoreOutputStreamの第2引数appendにtrueを渡すことで、データの追記が可能になります。

ダウンロードと使い方

JARファイルとソースを下記からダウンロードできます。Apache License 2.0にて公開します。

GitHub - koher/Koherent-App-Engine-Library-for-Java: Koherent App Engine Library is a library for Google App Engine for Java. It includes "DatastoreMap" which enable to operate Datastores like java.util.HashMap, "DatastoreOutputStream" which enable to write data to Datastores like java.util.FileOutputStream and so on.
(画面右上あたりのDownload Sourcesをクリック→ZipかTarの形式を選択)

ダウンロードした圧縮ファイルを解凍し、中にあるJARファイルを/WEB-INF/libに放り込んでパスを通せばOKです(Eclipseなら、プロジェクトを右クリック→Properties→Java Build Path→Libraries→Add JARs→/WEB/INF/libの中に入れたJARファイルを選択)。

FileOutputStream、FileInputStreamとの相違点

DatastoreOutputStream、DatastoreInputStreamはできるだけFileOutputStream、FileInputStreamと同じような挙動をするように作っていますが、実装上バッファリングを行う必要がありました。そのため、下記の三点を留意して下さい。ただ、FileOutputStream、FileInputStreamを使用する場合も、通常はBufferedOutputStream、BufferedInputStream等を用いてバッファリングを行うので、差異はほぼないと考えられます。

  • DatastoreOutputStream、DatastoreInputStreamではバッファリングが行われるため、BufferedOutputStream、BufferedInputStream等でラップする必要がない。
  • データ読み込み時に同時に書き込みが行われた場合、すでにInputStreamのバッファに読み込まれたデータに関しては変更が反映されない。
  • バッファが溢れるかflushされるまではデータが書き込まれない。

注意点として、BufferedWriterを利用した際に、BufferedWriterで指定したバッファサイズを超える書き込みを行っても、DatastoreOutputStreamのバッファサイズ(およそ1MBです)を超えない限り書き込みが行われないことが挙げられます(もちろんflushすれば書き込みが行われます)。

同時アクセス時の挙動

FileOutputStream、FileInputStream同様に、読み込み時にはロックを行わず、書き込み時にはWrite Lockを行うようにできています。FileOutputStream、FileInputStream同様、書き込み途中の読み込みには、書き込まれたデータが反映されるので注意が必要です。

Write Lockによってデータ書き込みが拒否された場合には、WriteLockExceptionがスローされます

WEBでの利用という前提を考えればSerializableに扱えた方が便利かとも思いましたが、とりあえずFileOutputStream、FileInputStreamに合わせて実装しました。

なお、データ書き込み途中にエラーが発生してロックが残ってしまうと問題があるので、ロックは取得から30秒*2で自動的に解除されるように実装しています。

Datastoreへの格納の仕組み

データストアの各エンティティには1MBまでしかデータを格納できないため、DatastoreOutputStream、DatastoreInputStreamでは約1MBごとにデータを分割して複数のエンティティにデータを保持しています。エンティティのnameには連番が振られており、順番に書き込み/読み込みを行います。

エンティティごとに1MBぎりぎりまでデータを格納しているのは、データストアへの読み書きの回数を減らすためです。ランダムアクセスを行う場合や一部だけを読み書きする場合はこれが効率的であるとは限りませんが、OutputStream、InputStreamを用いてそのような操作を行うケースは少ないと思うので。

上書きを行った場合、古いデータのエンティティは上書きされますが削除はされません。つまり、古いデータよりも小さいデータで上書きを行った場合、データストアにゴミが残る可能性があります。(close時にゴミエンティティを削除する実装も考えましたが、同期的に削除を行うと巨大データを小さなデータで上書いたときのオーバーヘッドが大きいですし、Task Queueに入れて非同期的に削除するか、cronで定期的にゴミデータを削除するかが現実的でしょうか。)

雑感

DatastoreMapと比べると簡単だろうと思い、また思いついてから勢いで実装しましたが、並列処理を考えると結構面倒でした。FileOutputStreamが排他ロックを、FileInputStreamが共有ロックを取得しないという実装は意外でしたが、何か理由があるのでしょうか?

バグや改善点などあればご連絡いただけると幸いです。

*1:このプログラムのDatastoreOutputStreamをFileOutputStreamに、DatastoreInputStreamをFileInputStreamに書き換えるだけで動作します。

*2:GAEでのプログラムの最大実行時間です。誤差を考慮して、実際には最大ロック時間を31秒に設定しています。

Google App EngineのDatastoreをMapでラップする

Google App Engine for Javaで、ちょっとしたデータを永続化するためだけにDatastoreを触るのは面倒です。そこで、Datastoreをjava.util.Mapでラッピングしたクラスを作ってみました。(もうすでに誰かが作ってそうなにおいがぷんぷんするんですが軽く調べてみると見つからなかったので。)

他にもMemcacheのラッパークラス等も作りました。下記が作ったもの一覧です。

  • DatastoreMap: Datastoreをjava.util.Mapでラッピングしたクラス。
  • MemcacheMap: Memcacheをjava.util.Mapでラッピングしたクラス。
  • CachedDatastoreMap: オブジェクトを透過的にMemcacheにキャッシュするDatastoreMap。

App Engineにあまり詳しいわけではないので、おかしなところがあればご指摘いただければ幸いです。

サンプルコード

百聞は一見にしかず、ということでまずはサンプルコードです。下記のようにまるでHashMapでも触っているかのような感じでDatastoreを操作するプログラムを書くことができます。

// 「名前」に対応した「年齢」を格納する"people"というkindのMapオブジェクトを生成。
Map<String, Integer> map = new DatastoreMap<String, Integer>("people");

// Mapにデータ(「名前」と「年齢」)を格納。
map.put("Tom", 27);
map.put("Mary", 21);
map.put("John", 23);

// 「年齢」を取得。
int ageOfMary = map.get("Mary");

// 「名前」が”Tom"のデータを削除。
map.remove("Tom");

用途

BigTableがKey-ValueストアでRDBMSより機能が少ないとはいえ、さすがにMapよりは多くのことができます。当然のことながら、DatastoreMapを通してDatastoreにアクセスすると様々な機能が使えなくなります。

上記を踏まえて、次のような用途が考えられます。

  • LLAPIやSlim3、JDO*1などで複雑なデータを扱っているが、ちょっとしたデータを永続化したいとき。
  • インデックスやレンジスキャンが必要ないような簡単なアプリケーションを作成するとき。
  • App Engineを触ったことのない人が、とりあえず動かしてみたいとき(慣れ親しんだMapのようにストレージを操作できる)*2
  • LLAPIを勉強したいときの資料として(Mapならどんな機能があるか分かるので、実装がどうなっているのか覗きやすいのでは)。

ダウンロードと使い方

JARファイルとソースを下記からダウンロードできます。Apache License 2.0にて公開します。

GitHub - koher/Koherent-App-Engine-Library-for-Java: Koherent App Engine Library is a library for Google App Engine for Java. It includes "DatastoreMap" which enable to operate Datastores like java.util.HashMap, "DatastoreOutputStream" which enable to write data to Datastores like java.util.FileOutputStream and so on.
(画面右上あたりのDownload Sourcesをクリック→ZipかTarの形式を選択)

ダウンロードした圧縮ファイルを解凍し、中にあるJARファイルを/WEB-INF/libに放り込んでパスを通せばOKです(Eclipseなら、プロジェクトを右クリック→Properties→Java Build Path→Libraries→Add JARs→/WEB/INF/libの中に入れたJARファイルを選択)。

できること・できないこと

DatastoreMapとCachedDatastoreMapは、Mapに出来ることは一通りできます。entrySet()メソッドで取得したEntryオブジェクトを通してDatastoreを更新する等も可能です。ただし、keySet()メソッドとentrySet()メソッドを利用するためにはコンストラクタにキーのParserを渡してやる必要があります(詳細は下記の「Datastoreへの格納の仕組みとキーとParser」)。また、DatastoreMapとCachedDatastoreMapはアトミックな更新を実現するためのupdateメソッドを持つUpdatableMapインタフェースを実装しています。

MemcacheMapに関しては、Memcacheの機能上の問題でいくつか実装できなかったメソッドがあります。それらを呼び出すとUnsupportedOperationExceptionがスローされます。

できること

  • java.util.MapでDatastoreおよびMemcacheを操作(MemcacheMapは一部メソッドを未サポート)
  • キー、値ともにnull値の使用
  • 自作クラスのインスタンスをキーにしてデータを格納(ただし、toString()メソッドを実装する必要あり(下記))
  • 自作クラスのインスタンスを値にしてデータを格納(ただし、Serializableインタフェースを実装する必要あり)

DatastoreMap、CachedDatastoreMapのみ

  • put、remove等のメソッドを利用して更新、削除とデータの取得をアトミックに実行
  • updateメソッドを利用した既存データに対するアトミックな更新
  • keySet()、entrySet()等のメソッドで取得したSetやEntryを介したDatastoreの更新(ただし、インスタンス生成時にキーのParserを渡す必要あり)
  • put、remove等のメソッドでデータ取得→更新、削除の同時実行でConcurrentModificationExceptionが発生した際のリトライ回数の指定

CachedDatastoreMapのみ

  • 透過的にMemcacheにキャッシュを行いながらDatastoreを操作

できないこと

  • インデックスを利用したレンジスキャン
  • キー以外のプロパティによる検索
  • Limit、Offset、ソート順等の指定

MemcacheMapのみ

  • アトミックなインクリメント(値をLongにした場合しかサポートできないため)
  • containsValue、entrySet、keySet、valuesの各メソッド(Memcacheの機能上実装できなかったため)

アトミックな更新

DatastoreMapとCachedDatastoreMapでは、updateメソッドを用いてアトミックな更新が可能です。

// ユーザー別のカウンター
Map<String, Integer> counters = new DatastoreMap("counters");

// アトミックな更新
map.update(userId, new Updater<Integer>() {
	@Override
	public String update(Integer object) {
        // カウンターの値を1増加
		return object++;
	}
});

Datastoreへの格納の仕組みとキーとParser

Datastoreに格納する際には、キーとなるインスタンスのtoString()メソッドを利用してStringオブジェクトを取得し、Datastoreのキーとしています。このため、自作クラスのインスタンスをキーとしてデータを格納するためにはtoString()メソッドがインスタンスにとって一意な値を返すようにオーバーライドする必要があります。具体的には、toString()メソッドは、equalsメソッドがtrueを返す二つのオブジェクトが同じ値を、falseを返すオブジェクト間では違う値を返すようにする必要があります

keySet()およびentrySet()メソッドを利用するには、String化されたキーを復元する必要があります。このため、これらのメソッドを利用するにはStringをパースしてキーを作成するParserをmapに渡してやる必要があります。プリミティブに対するParser(IntegerParser、LongParser等)とStringParserはライブラリに含まれています(これらのクラスはSingletonパターンを採用しており、コンストラクタでnewするのではなく、StringParser.getInstance()でインスタンスを取得します)。

キーがnullの場合はそのままではDatastoreに格納できないため、他のデータと衝突しないようにLong 1Lをキーとして格納します。

課題

時間がないので後で書きます。

雑感

一昨日に「DatastoreをMapのように操作したいなぁ」と思い軽く探してみても見つからなかったので勢いで作ったのですが、LLAPIのいい勉強になりました。まだまだ理解が浅いのでおかしなところがあれば容赦なくつっこんでいただけるとありがたいです。

簡単なテストはしましたが、まだ十分にテストできてないです。色々バグがあるかも。Javadocもまともに書けてませんが、所詮Mapなんでとりあえずいいかと(ry

これから出かけてそのまま夜行バスで東京に帰ります。

追記(2010.03.07)

より高速なFastDatastoreMap等も実装したのでご利用下さい。putやremoveの戻り値を省略することで高速化しています。

*1:App EngineのJDOはいけてないみたいなので、LLAPIを直接叩くか、Slim3などのラッパーを利用するのがいいでしょう。

*2:初めてApp Engineに触れる人がどうやってこのライブラリを見つけるのか疑問ですが。

近況

箇条書きですが・・・。

  • 大学院復学(?)の凍結
  • はてなのアルバイト応募の凍結
  • システム開発の請負(この前はASでPapervision3D
  • AR(拡張現実感)関連のプロジェクトに参画予定
  • 2010年中に複数のWebアプリを企画・開発して収益化を目指す
  • 東京に戻る予定
  • 最近はGoogle App Engineいじったり

大学院はやはり今からドクターコース3年は時間的にも金銭的にも厳しいですし、勉強は自分でもできそうなので凍結しました。

はてなのアルバイトは当面の生活費を稼ぎながら勉強するのによさそうだと思っていましたが、そんなときにAR関連プロジェクトの話が降ってきたので、そちらに参加することにしました。

Google中国撤退(?)関連記事

昨日から追っかけてましたが、せっかくなので並べてみます。「」の中はてきとーなので元記事をご参照下さい。

事の発端となったGoogleの発表の日本語訳

Google「中国の検閲はもう許さん。場合によっては中国から撤退する!」
中国に対するあたらしい姿勢

それを扱った産経の記事

産経「Googleが↑みたいなこと言ってます。」
「これ以上、検閲を容認しない」グーグル、中国からの全面撤退も視野

所詮はビジネス上の理由という見方

TechCrunch「Googleは人権とか言ってるけど、本当はお金儲けのためのイメージアップ戦略でしょ。」
Googleにとっての中国: 人権うんぬんよりも世界でのビジネスが第一

未来予想図

「もしかしてGoogleと中国の全面戦争になるんじゃね?」
おまえらいい加減にせんと無検閲のgoogle.com見せちゃうぞ!

早速中国で起こったこと

Google「規制解除だ!」、中国国民「Google出て行かないで。」、Google中国社員「どうなってしまうんだろう。」
グーグル中国版、自主規制解除か 「天安門事件」も表示

Google中国の様子

Google「システムを調べるから有給取っといてね。」Google中国社員「社内システムにもアクセスできないよー。」
Google、社内システムを精査中―中国では社員全員に休暇を取らせる

今回の決断が下された経緯

サーゲイ・ブリン「道徳的にこれ以上の検閲はもう我慢できない!」
エリック・シュミット「検閲があっても中国で事業を続けることこそ中国の自由化につながるんだ!」
米グーグル、中国撤退をめぐりトップの意見が衝突

所感

色々言われてるけど、サーゲイ・ブリンとラリー・ページは道徳的な視点で言ってるんじゃないかなぁ、と思います。シュミットの「自由化につながる」ってのはやっぱり建前で、ビジネス的な視点で見てるんじゃないかなぁ(CEOとしてそうあるべきとも言えますし)。

ともあれ、Google中国の社員はかわいそうですね・・・。

Pure-Perl RDBMSのKoherent::DBを公開しました

http://www.koherent.org/db/にてモジュールを公開しています。インストール方法などはリンク先から。

Perlの練習用に作った上にまだα版でテストも全然できていないのでバグだらけだと思いますが・・・。