appengine ja night #14

48
APP ENGINE 上の 大量データ処理について appengine ja night #14 @int128

Upload: hidetake-iwata

Post on 27-May-2015

1.972 views

Category:

Technology


1 download

DESCRIPTION

AppEngine MapReduceと大量データ処理について

TRANSCRIPT

Page 1: appengine ja night #14

APP ENGINE上の 大量データ処理について

appengine ja night #14

@int128

Page 2: appengine ja night #14

Introduction

いわてぃ

@int128

http://d.hatena.ne.jp/int128/

•本業

• SIerでお仕事しています。

•プライベート

• Google App Engine/Java

• 自宅サーバ

Page 3: appengine ja night #14
Page 4: appengine ja night #14

本日の内容

• App Engineとバッチ処理

•タスクチェーン

•シャーディング

• App Engine MapReduce (Java)の仕組み

•デモ:Twitterのタイムラインで遊んでみる

http://goo.gl/iibHl

Page 5: appengine ja night #14

APP ENGINEとバッチ処理

Page 6: appengine ja night #14

App Engineのインフラ

•バッチサーバはない。

• SQLやストアドはない。

• HTTPリクエスト処理の延長上にバッチ処理を考える必要がある。

• Task Queueが用意されている。

•長時間の処理は細かい単位に分けて処理する。

• Googleインフラを使うので資源は無尽蔵にある。

• お金はかかります。

Page 7: appengine ja night #14

Task Queueの制約

•タスクは10分以内に終了する必要がある。

• 通常のリクエストは30秒以内。

• 以前は30秒制限があったことを考えると、事実上の無制限になったといえる。

•タスクに渡すパラメータは10kB以内に抑える必要がある。

•タスクは必ず実行されるが、2回以上実行されてしまうかもしれない。

• 処理は冪等であるべき。

Page 8: appengine ja night #14

大量データの処理場面

•データストア上のエンティティを処理したいケースを考える。

1. 定時処理

• メール配信、データ取得など

2. 集約プロパティの更新

3. 集計処理

4. スキーマ変更(データ移行)

Page 9: appengine ja night #14

タスクチェーン

• Task Queueを引き継いで長時間の処理を行う。

•タスクの30秒制限があった時は必須だった。

•タスクの完了は保証されていないため、制限がなくなってもタスクチェーンが望ましい。

• 10秒以内にリクエストを返した方がいい?

リクエスト リクエスト リクエスト

リクエスト リクエスト

Page 10: appengine ja night #14

実験1

• 30秒間かかる処理をどの程度のタスクに分割すると最も早く完了するか?

•タスクの分割数を変化させて所要時間を測定する。

• ここでは、処理は自由に分割できると考える。

• 例:3秒×10タスク

enqueue

task

task

task

Thread.sleep() を実行する

enqueue を開始してから すべてのタスクが完了するまでの時間を測定する。

Page 11: appengine ja night #14

回目 タスク数 wait [ms] 処理量 [ms] 所要時間 [ms]

3 50 200 10,000 1,274

2 50 200 10,000 1,489

5 20 500 10,000 1,549

4 20 500 10,000 1,551

3 20 500 10,000 1,587

4 50 200 10,000 1,673

2 20 500 10,000 1,744

1 50 200 10,000 1,785

4 10 1,000 10,000 2,029

3 5 2,000 10,000 2,035

2 10 1,000 10,000 2,039

5 5 2,000 10,000 2,041

5 10 1,000 10,000 2,043

3 10 1,000 10,000 2,044

4 5 2,000 10,000 2,047

2 5 2,000 10,000 2,098

1 20 500 10,000 2,622

1 10 1,000 10,000 4,208

1 5 2,000 10,000 6,236

5 50 200 10,000 7,932

2 100 100 10,000 10,949

5 100 100 10,000 14,729

3 100 100 10,000 20,002

4 100 100 10,000 20,110

1 100 100 10,000 20,158

10秒の処理 • 100 ms×100 tasks

• 200 ms×50 tasks

• 500 ms×20 tasks

• 1,000 ms×10 tasks

• 2,000 ms×5 tasks

上記5条件を5セット実行した。

Page 12: appengine ja night #14

回目 タスク数 wait [ms] 処理量 [ms] 所要時間 [ms]

4 50 400 20,000 2,896

5 20 1,000 20,000 3,042

2 20 1,000 20,000 3,050

1 20 1,000 20,000 3,055

4 20 1,000 20,000 3,060

3 20 1,000 20,000 3,117

4 25 800 20,000 3,240

3 25 800 20,000 3,250

5 25 800 20,000 3,254

2 25 800 20,000 3,258

5 50 400 20,000 3,311

1 25 800 20,000 3,720

1 10 2,000 20,000 4,024

3 10 2,000 20,000 4,030

5 10 2,000 20,000 4,036

4 10 2,000 20,000 4,037

2 10 2,000 20,000 4,042

4 100 200 20,000 9,431

2 50 400 20,000 11,990

2 100 200 20,000 12,724

3 100 200 20,000 13,011

5 100 200 20,000 20,204

1 100 200 20,000 20,209

1 50 400 20,000 20,432

3 50 400 20,000 36,946

20秒の処理 • 200 ms×100 tasks

• 400 ms×50 tasks

• 1,000 ms×20 tasks

• 2,000 ms×10 tasks

• 4,000 ms×5 tasks

上記5条件を5セット実行した。

Page 13: appengine ja night #14

回目 タスク数 wait [ms] 処理量 [ms] 所要時間 [ms]

4 50 600 30,000 3,887

2 30 1,000 30,000 4,061

4 30 1,000 30,000 4,066

3 30 1,000 30,000 4,150

1 30 1,000 30,000 4,203

5 50 600 30,000 4,306

5 20 1,500 30,000 4,539

1 20 1,500 30,000 4,549

4 20 1,500 30,000 4,558

2 20 1,500 30,000 4,559

3 20 1,500 30,000 4,563

5 30 1,000 30,000 5,057

1 10 3,000 30,000 6,022

5 10 3,000 30,000 6,022

2 10 3,000 30,000 6,026

3 10 3,000 30,000 6,034

4 10 3,000 30,000 6,040

5 100 300 30,000 11,019

3 50 600 30,000 13,613

4 100 300 30,000 20,304

2 100 300 30,000 20,305

1 100 300 30,000 20,312

3 100 300 30,000 20,577

1 50 600 30,000 20,607

2 50 600 30,000 20,623

30秒の処理 • 300 ms×100 tasks

• 600 ms×50 tasks

• 1,000 ms×30 tasks

• 1,500 ms×20 tasks

• 3,000 ms×10 tasks

上記5条件を5セット実行した。

Page 14: appengine ja night #14

実験1のまとめ

•タスク当たりの処理時間を 1,000 ms 程度にするとよい?

• 50以上に分割すると効率が悪化する。

•インスタンス数は22まで増えた。

•今回の実験は一例にすぎない。

• 測定結果が安定しないことを付記しておきます。

Page 15: appengine ja night #14

キーの分割手法

•データストア上のエンティティをいくつかの固まりに分解したい。

1. 既知のキー集合を分割する。

2. Scatterプロパティで分割する。

3. カーソルチェーンで処理する。

Page 16: appengine ja night #14

既知のキー集合

•キーの集合があらかじめ分かっている場合、一定のルールに基づいてキーを分ける。

•例:範囲の決まっているID

• n等分する。

• ID mod nを使う。

•例:日付キー

• 月別に処理する。

• 親キーから子キーを取得して処理する。

Page 17: appengine ja night #14

Scatterプロパティ

• App Engine 1.4.0(時期から推測)で追加された。

• Release Notesには書かれていない?

• AppEngine MapReduceで採用されている。

•新しいエンティティが保存される際、 0.8%の割合でScatterプロパティが付加される。

• 付加されるかどうかはキーによって決まる。

• 付加分のデータ量は課金されない。

• ShortBlob型にキーのハッシュ値が入っている。

• ただし、リザーブドプロパティなので取得できない。

http://code.google.com/p/appengine-mapreduce/wiki/ScatterPropertyImplementation

Page 18: appengine ja night #14

Scatterプロパティ (cont.)

• Scatterプロパティを持つキーを取り出すと、キー集合を分割する中間点が得られる。

• 取り出すキーの数に関係なく、一様に分布する中間点が得られることが期待される。

• プロパティの内容がハッシュ値であるため。

•中間点を得るにはScatterプロパティでソートする。

キーの 集合

List<Key> scatterKeys = Datastore.query(m) .sort(Entity.SCATTER_RESERVED_PROPERTY, SortDirection.ASCENDING) .asKeyList();

Page 19: appengine ja night #14

Scatterによる分割

•シャードの区間キーをタスクに渡して処理する。

• Scatterで得られるシャード数は多いため、流量の調整が必要になる。

• 例:100,000エンティティに対して800 Scatter(0.8%)

Processor Task Processor Task

プロセッサ プロセッサ

Scatter Task

Processor Task

プロセッサ

シャード シャード シャード シャード

Processor Task

プロセッサ

Page 20: appengine ja night #14

memcache memcache

カーソルチェーン

•クエリの結果を少しずつ処理する。

• ProducerとConsumerが独立して動くようにする。

Processor Task Processor Task

Query Task

結果リストをmemcacheに 書き込み、カーソルのみを プロセッサに渡す。 (memcacheのexpire対策)

プロセッサ プロセッサ

結果リスト 結果リスト

カーソル カーソル クエリ

Page 21: appengine ja night #14

ElShardフレームワーク

• ElShardというフレームワークを作っています。

•開発中です...

•タスクチェーン

• TaskChainController

•カーソルチェーン

• QueryProcessorController<M>

Page 22: appengine ja night #14

比較実験(参考)

• 27,000件のエンティティコピー

• AppEngine-Mapper:68秒(1.33 CPU hours)

• カーソルチェーン:32秒(1.29 CPU hours)

• 27,000件のエンティティ削除

• AppEngine-Mapper:57秒(2.24 CPU hours)

• カーソルチェーン:31秒(1.10 CPU hours)

Page 23: appengine ja night #14

APPENGINE-MAPREDUCEの 仕組み Java版のコードリーディング風

Page 24: appengine ja night #14

AppEngine MapReduceとは

http://code.google.com/p/appengine-mapreduce/

• Google App Engineで動くMapReduceフレームワーク。

• 2010年のGoogle I/Oで発表された。

• Python版とJava版がある。

•依然としてReducerは発表されていない。

• Issueにpatchが上がっている…

Page 25: appengine ja night #14

AppEngine MapReduceでできること

•すべてのエンティティにアクセスする処理が極めて簡単に書ける。

•集約カウンタが使える。

•できないこと

• 12時間のバッチが30分になります

• Reducerは発表されていない。

• 管理コンソールで日本語が表示されない。

Page 26: appengine ja night #14

アーキテクチャ

• Task Queueの上に構築されたフレームワーク。

• アプリケーションからは Hadoop API が見える。

• 実際は AppEngineMapper などの独自クラスが多く使われているため、Hadoop とは別物である。

Mapper(自分で定義する)

AppEngine MapReduce

Datastore / Blobstore Task Queue

Hadoop MapReduce API

Page 27: appengine ja night #14

AppEngine MapReduceの使い方

• SVNからチェックアウトする。 $ svn co http://appengine-mapreduce.googlecode.com/svn/trunk/java

•ビルドする。 $ ant

• ソースコードをEclipseにインポートしてもおk

• Mapperクラスを定義する。

•パラメータを定義する。

•管理コンソールからMapperを実行する。

http://code.google.com/p/appengine-mapreduce/wiki/GettingStartedInJava

Page 28: appengine ja night #14

Mapperの定義

public class ParseTweet

extends AppEngineMapper<Key, Entity, NullWritable, NullWritable>

{

@Override

public void map(Key key, Entity entity, Context context)

{

TweetMeta m = TweetMeta.get();

Tweet tweet = m.entityToModel(entity);

context.getCounter(COUNTER_GROUP_SURFACE, tweet.getUser())

.increment(1);

}

}

必ずAppEngineMapperを継承する

キーとエンティティがmap()に渡される

ModelMeta#entityToModel()で Slim3のモデルに変換可能

Page 29: appengine ja night #14

エンティティを put/delete する場合

•プーリングの仕組みが用意されている。

• delete()は100件ずつ処理される。

• put()はPBが256kBを越えたら処理される。

public void map(Key key, Entity value, Context context)

{

getAppEngineContext(context).getMutationPool().delete(key);

}

public void map(Key key, Entity value, Context context)

{

TweetMeta m = TweetMeta.get();

Tweet tweet = m.entityToModel(entity);

getAppEngineContext(context).getMutationPool().put(m.modelToEntity(tweet));

}

Page 30: appengine ja night #14

カウンタの使い方

• Hadoop Counters が使われている。

• Context#getCounter(カウンタグループ名, カウンタ名)

• Mapperの中でカウンタ値を参照しても途中経過は得られない。集約結果は最後に得られる。

public void map(Key key, Entity entity, Context context) {

String userId = entity.getProperty(“userId”);

context.getCounter(“user”, userId).increment(1);

}

Page 31: appengine ja night #14

パラメータの定義 <configuration name="CountTweet"> <property> <name>mapreduce.map.class</name> <value>org.hidetake.elshard.demo.mapper.tweet.CountTweet</value> </property> <property> <name>mapreduce.inputformat.class</name> <value>com.google.appengine.tools.mapreduce.DatastoreInputFormat</value> </property> <property> <name>mapreduce.mapper.inputformat.datastoreinputformat.entitykind</name> <value template="optional">Tweet</value> </property> <property> <name>mapreduce.mapper.shardcount</name> <value template="optional">16</value> </property> <property> <name>mapreduce.mapper.inputprocessingrate</name> <value template="optional">10000</value> </property> </configuration>

Mapperクラス

対象のカインド

シャード数 (デフォルト4)

処理エンティティ/秒の上限値 (デフォルト1,000)

Page 32: appengine ja night #14

ジョブの開始

com.google.appengine.tools.mapreduce.MapReduceServlet#handleStart(Configuration, String, HttpServletRequest)

MapReduce 管理コンソール

/mapreduce/command/start_job

Ajax GET

MapReduceServlet#handleStartJob()

MapReduceServlet#handleStart()

1. InputSplitリストの取得

2. Controllerタスクのスケジュール

3. ShardStateの初期化

4. Mapperタスクのスケジュール

ジョブ開始処理は、 サーブレットハンドラに ベタ書きされている。

Page 33: appengine ja night #14

ジョブの開始(プログラムから)

public Navigation run() throws Exception {

Configuration configuration = new Configuration();

configuration.set("mapreduce.map.class", Mapper.class.getName());

configuration.set("mapreduce.inputformat.class",

"com.google.appengine.tools.mapreduce.DatastoreInputFormat");

configuration.set("mapreduce.mapper.inputformat.datastoreinputformat.entitykind",

TweetMeta.get().getKind());

Queue queue = QueueFactory.getDefaultQueue();

TaskOptions task = TaskOptions.Builder

.withMethod(TaskOptions.Method.POST)

.url("/mapreduce/start")

.param("configuration", ConfigurationXmlUtil.convertConfigurationToXml(configuration));

queue.add(task);

return null;

} /mapreduce/start にXMLをPOSTする

※これ以外の方法をご存じでしたら教えてください。

Page 34: appengine ja night #14

タスクの実行制御

• Controllerタスク:Mapperの流量を制御する。

• Mapperタスク:エンティティを処理する。

Controller

start_job

2秒 Mapper Mapper Mapper

最長10秒

Mapper Mapper Mapper

Controller

2秒間隔で 実行される

2秒

シャード数

map()

map()

map()

map()

map()

map()

map()

map()

map()

com.google.appengine.tools.mapreduce.MapReduceServlet

Page 35: appengine ja night #14

タスクの実行制御 (cont.)

•短時間に大量のクォータを消費するのを防ぐため、スループットの上限値を設けている。

• デフォルトは 1,000 エンティティ/秒

Controller Mapper

Quota Manager

Memcache

refillQuotas()

com.google.appengine.tools.mapreduce.QuotaManager

consume() put()

Datastore

QuotaConsumer

生産する側(?) 消費する側

Page 36: appengine ja night #14

Mapperの入力

•ジョブの開始時にエンティティが分割される。

•分割結果は ShardState に保存され、Mapperタスクに渡される。

Mapper

Datastore Input

Format

DatastoreInputSplit

Start Key

Shard State

End Key

DatastoreInputSplit

Start Key

End Key

Shard State

Shard State

Mapper

Mapper

com.google.appengine.tools.mapreduce.MapReduceServlet#scheduleShards()

Page 37: appengine ja night #14

ShardState カインド

プロパティ 型 内容

countersMap Blob Counters のシリアライズデータ

inputSplit Blob InputSplit のシリアライズデータ

inputSplitClass String デシリアライズ用クラス名

jobId String ジョブID

recordReader Blob RecordReader のシリアライズデータ

recordReaderClass String デシリアライズ用クラス名

status (Enum) 状態(ACTIVE/DONE)

statusString String メッセージ?

updateTimestamp Long 最終更新時間

Page 38: appengine ja night #14

キー分割アルゴリズム

• Scatterプロパティからシャード頂点を生成する。

キーの昇順

com.google.appengine.tools.mapreduce.DatastoreInputFormat#getSplits(JobContext)

キーの 集合

シャード1

シャード2

シャード4

Scatterプロパティで 得られる頂点

シャードの頂点 DatastoreInputSplit オブジェクト

シャード3

Page 39: appengine ja night #14

キー分割アルゴリズム (cont.)

•オーバーサンプリング

• シャード数×32のScatterからシャード頂点を生成する。

• シャードに対してScatterが不足する場合は、Scatterがシャード頂点になる。

キーの 集合

シャード1

シャード2

シャード3

キーの昇順

com.google.appengine.tools.mapreduce.DatastoreInputFormat#getSplits(JobContext)

Page 40: appengine ja night #14

キー分割アルゴリズム (cont.)

•開発環境ではScatterプロパティが存在しないため、すべてのキーを同一のシャードに割り当てる。

• Productionでも起こり得るのか不明

キーの昇順

キーの 集合

シャード1

com.google.appengine.tools.mapreduce.DatastoreInputFormat#getSplits(JobContext)

Page 41: appengine ja night #14

キー分割の例

DatastoreInputFormat getSplits: Getting input splits for: Tweet DatastoreInputFormat getSplits: Requested 128 scatter entities. Got 75 so using oversample factor 18 DatastoreInputFormat getSplitsFromSplitPoints: Added DatastoreInputSplit DatastoreInputSplit@4ab40a Tweet(14814807863) Tweet(29077523019) DatastoreInputFormat getSplitsFromSplitPoints: Added DatastoreInputSplit DatastoreInputSplit@721965 Tweet(29077523019) Tweet(15299850388119552) DatastoreInputFormat getSplitsFromSplitPoints: Added DatastoreInputSplit DatastoreInputSplit@e14ebc Tweet(15299850388119552) Tweet(24434595889946625) DatastoreInputSplit write: Writing DatastoreInputSplit DatastoreInputSplit@4ab40a Tweet(14814807863) Tweet(29077523019) DatastoreInputSplit write: Writing DatastoreInputSplit DatastoreInputSplit@721965 Tweet(29077523019) Tweet(15299850388119552) DatastoreInputSplit write: Writing DatastoreInputSplit DatastoreInputSplit@e14ebc Tweet(15299850388119552) Tweet(24434595889946625) DatastoreInputSplit write: Writing DatastoreInputSplit DatastoreInputSplit@303a60 Tweet(24434595889946625) null

4個のシャード設定 128個のScatterを希望

↓ 75個のScatterを取得できた

↓ 18個のScatterごとに1シャードを構成する

Page 42: appengine ja night #14

以前のキー分割アルゴリズム(参考)

•キーのID/Nameを元に分割点を生成していた。 • Sharding is currently done by splitting the space of keys lexicographically. For instance,

suppose you have the keys 'a', 'ab', 'ac', and 'e' and you request two splits. The framework will find that the first key is 'a' and the last key is 'e'. 'a' is the first letter and 'e' is the fifth, so the middle is 'c'. Therefore, the two splits are ['a'...'c') and ['c'...), with the first split containing 'a', 'ab', and 'ac', and the last split only containing 'e'. http://code.google.com/p/appengine-mapreduce/wiki/UserGuideJava

•最初と最後のキーを取得し、その間にある文字列空間を分割していた(IDの場合は整数空間)

•キーの降順インデックスが必要だった。

• Scatterプロパティの導入により廃止された。

• リビジョン142 (2010/12/22) 以降

Page 43: appengine ja night #14

カウンタの集約

•各シャードのカウンタは定期的に集約される。

• Mapperタスクの終わりに ShardState が永続化される。これによりシャードのカウンタが更新される。

• Controllerタスクではすべての ShardState のカウンタが集約され、MapReduceState に書き込まれる。

Mapper 1 Mapper 2

Controller

Shard State 1

MapReduce State

map() map()

Shard State 2

map() map()

com.google.appengine.tools.mapreduce.MapReduceServlet#aggregateState(MapReduceState, List<ShardState>)

Page 44: appengine ja night #14

MapReduceState カインド

プロパティ 型 内容

activeShardCount Long 実行中のシャード数

chart Text シャードグラフのURL

configuration Text Configuration XML

countersMap Blob Counters のシリアライズデータ

lastPollTime Long Controllerタスクの最終実行日時

name String Mapperの名前

progress Double 進捗率

shardCount Long シャード数

startTime Long 開始日時

status (Enum) ステータス

Page 45: appengine ja night #14

後続処理

• Mapperの完了後、任意の処理を実行できる。

• 指定したURLの TaskQueue が実行される。

• ジョブIDがPOSTで渡ってくるので、ジョブに対応するMapReduceState を取得できる。

• Mapperの前後にジョブを配置できる。

Mapperジョブ

Controller start_job

Mapper

後続ジョブ 先行ジョブ

Configuration ジョブID

Page 46: appengine ja night #14

デモ Twitterのタイムラインで遊んでみる

Page 47: appengine ja night #14

タイムラインの解析

•ユーザのツイートを形態素解析し、よくつぶやいている単語を調べる。

•ツイート取得:タスクチェーン

•形態素解析とカウント:AppEngine MapReduce

形態素解析とカウント

Controller start_job

Mapper

カウンタの保存

ツイートの取得

ツイート 単語数

Page 48: appengine ja night #14

ご清聴ありがとうございました。

2011.2.22 ajn#14