appengine ja night #14

2,219 views

Published on

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

Published in: Technology
0 Comments
3 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
2,219
On SlideShare
0
From Embeds
0
Number of Embeds
396
Actions
Shares
0
Downloads
11
Comments
0
Likes
3
Embeds 0
No embeds

No notes for slide

appengine ja night #14

  1. 1. APP ENGINE上の大量データ処理についてappengine ja night #14@int128
  2. 2. Introduction いわてぃ @int128 http://d.hatena.ne.jp/int128/ • 本業 • SIerでお仕事しています。 • プライベート • Google App Engine/Java • 自宅サーバ
  3. 3. 本日の内容• App Engineとバッチ処理• タスクチェーン• シャーディング• App Engine MapReduce (Java)の仕組み• デモ:Twitterのタイムラインで遊んでみるhttp://goo.gl/iibHl
  4. 4. APP ENGINEとバッチ処理
  5. 5. App Engineのインフラ• バッチサーバはない。• SQLやストアドはない。• HTTPリクエスト処理の延長上にバッチ処理を考える必要がある。• Task Queueが用意されている。• 長時間の処理は細かい単位に分けて処理する。• Googleインフラを使うので資源は無尽蔵にある。 • お金はかかります。
  6. 6. Task Queueの制約• タスクは10分以内に終了する必要がある。• 通常のリクエストは30秒以内。• 以前は30秒制限があったことを考えると、事実上の無 制限になったといえる。• タスクに渡すパラメータは10kB以内に抑える必要 がある。• タスクは必ず実行されるが、2回以上実行されて しまうかもしれない。• 処理は冪等であるべき。
  7. 7. 大量データの処理場面• データストア上のエンティティを処理したいケー スを考える。1. 定時処理 • メール配信、データ取得など2. 集約プロパティの更新3. 集計処理4. スキーマ変更(データ移行)
  8. 8. タスクチェーン• Task Queueを引き継いで長時間の処理を行う。• タスクの30秒制限があった時は必須だった。• タスクの完了は保証されていないため、制限がなくなってもタスクチェーンが望ましい。 • 10秒以内にリクエストを返した方がいい? リクエスト リクエスト リクエスト リクエスト リクエスト
  9. 9. 実験1• 30秒間かかる処理をどの程度のタスクに分割する と最も早く完了するか?• タスクの分割数を変化させて所要時間を測定する。• ここでは、処理は自由に分割できると考える。• 例:3秒×10タスク task Thread.sleep() を実行する task enqueue … task enqueue を開始してから すべてのタスクが完了する までの時間を測定する。
  10. 10. 回目 タスク数 wait [ms] 処理量 [ms] 所要時間 [ms]10秒の処理 3 2 50 50 200 200 10,000 10,000 1,274 1,489 5 20 500 10,000 1,549• 100 ms×100 tasks 4 20 500 10,000 1,551 3 20 500 10,000 1,587• 200 ms×50 tasks 4 50 200 10,000 1,673 2 20 500 10,000 1,744• 500 ms×20 tasks 1 50 200 10,000 1,785 4 10 1,000 10,000 2,029• 1,000 ms×10 tasks 3 5 2,000 10,000 2,035 2 10 1,000 10,000 2,039• 2,000 ms×5 tasks 5 5 2,000 10,000 2,041 5 10 1,000 10,000 2,043 3 10 1,000 10,000 2,044上記5条件を5セット実 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
  11. 11. 回目 タスク数 wait [ms] 処理量 [ms] 所要時間 [ms]20秒の処理 4 5 50 20 400 1,000 20,000 20,000 2,896 3,042 2 20 1,000 20,000 3,050• 200 ms×100 tasks 1 20 1,000 20,000 3,055 4 20 1,000 20,000 3,060• 400 ms×50 tasks 3 20 1,000 20,000 3,117 4 25 800 20,000 3,240• 1,000 ms×20 tasks 3 25 800 20,000 3,250 5 25 800 20,000 3,254• 2,000 ms×10 tasks 2 25 800 20,000 3,258• 4,000 ms×5 tasks 5 50 400 20,000 3,311 1 25 800 20,000 3,720 1 10 2,000 20,000 4,024上記5条件を5セット実 3 5 10 10 2,000 2,000 20,000 20,000 4,030 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
  12. 12. 回目 タスク数 wait [ms] 処理量 [ms] 所要時間 [ms]30秒の処理 4 2 50 30 600 1,000 30,000 30,000 3,887 4,061 4 30 1,000 30,000 4,066• 300 ms×100 tasks 3 30 1,000 30,000 4,150 1 30 1,000 30,000 4,203• 600 ms×50 tasks 5 50 600 30,000 4,306 5 20 1,500 30,000 4,539• 1,000 ms×30 tasks 1 20 1,500 30,000 4,549 4 20 1,500 30,000 4,558• 1,500 ms×20 tasks 2 20 1,500 30,000 4,559 3 20 1,500 30,000 4,563• 3,000 ms×10 tasks 5 30 1,000 30,000 5,057 1 10 3,000 30,000 6,022 5 10 3,000 30,000 6,022上記5条件を5セット実 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
  13. 13. 実験1のまとめ• タスク当たりの処理時間を 1,000 ms 程度にする とよい?• 50以上に分割すると効率が悪化する。• インスタンス数は22まで増えた。• 今回の実験は一例にすぎない。 • 測定結果が安定しないことを付記しておきます。
  14. 14. キーの分割手法• データストア上のエンティティをいくつかの固まりに分解したい。1. 既知のキー集合を分割する。2. Scatterプロパティで分割する。3. カーソルチェーンで処理する。
  15. 15. 既知のキー集合• キーの集合があらかじめ分かっている場合、一定のルールに基づいてキーを分ける。• 例:範囲の決まっているID• n等分する。• ID mod nを使う。• 例:日付キー• 月別に処理する。• 親キーから子キーを取得して処理する。
  16. 16. Scatterプロパティ • App Engine 1.4.0(時期から推測)で追加された。 • Release Notesには書かれていない? • AppEngine MapReduceで採用されている。 • 新しいエンティティが保存される際、 0.8%の割合でScatterプロパティが付加される。 • 付加されるかどうかはキーによって決まる。 • 付加分のデータ量は課金されない。 • ShortBlob型にキーのハッシュ値が入っている。 • ただし、リザーブドプロパティなので取得できない。http://code.google.com/p/appengine-mapreduce/wiki/ScatterPropertyImplementation
  17. 17. Scatterプロパティ (cont.)• Scatterプロパティを持つキーを取り出すと、キー集合を分割する中間点が得られる。 • 取り出すキーの数に関係なく、一様に分布する中間点 が得られることが期待される。 • プロパティの内容がハッシュ値であるため。• 中間点を得るにはScatterプロパティでソートする。 List<Key> scatterKeys = Datastore.query(m) キーの .sort(Entity.SCATTER_RESERVED_PROPERTY, 集合 SortDirection.ASCENDING) .asKeyList();
  18. 18. Scatterによる分割• シャードの区間キーをタスクに渡して処理する。• Scatterで得られるシャード数は多いため、流量の調整が必要になる。 • 例:100,000エンティティに対して800 Scatter(0.8%) Scatter Task シャード シャード シャード シャードProcessor Task Processor Task Processor Task Processor Task プロセッサ プロセッサ プロセッサ プロセッサ
  19. 19. カーソルチェーン• クエリの結果を少しずつ処理する。• ProducerとConsumerが独立して動くようにする。Query Task クエリ カーソル カーソル 結果リスト 結果リスト memcache memcache 結果リストをmemcacheに 書き込み、カーソルのみを Processor Task Processor Task プロセッサに渡す。 (memcacheのexpire対策) プロセッサ プロセッサ
  20. 20. ElShardフレームワーク• ElShardというフレームワークを作っています。• 開発中です...• タスクチェーン • TaskChainController• カーソルチェーン • QueryProcessorController<M>
  21. 21. 比較実験(参考)• 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)
  22. 22. APPENGINE-MAPREDUCEの仕組みJava版のコードリーディング風
  23. 23. AppEngine MapReduceとはhttp://code.google.com/p/appengine-mapreduce/• Google App Engineで動くMapReduceフレーム ワーク。• 2010年のGoogle I/Oで発表された。• Python版とJava版がある。• 依然としてReducerは発表されていない。 • Issueにpatchが上がっている…
  24. 24. AppEngine MapReduceでできること• すべてのエンティティにアクセスする処理が極め て簡単に書ける。• 集約カウンタが使える。• できないこと• 12時間のバッチが30分になります• Reducerは発表されていない。• 管理コンソールで日本語が表示されない。
  25. 25. アーキテクチャ• Task Queueの上に構築されたフレームワーク。 • アプリケーションからは Hadoop API が見える。 • 実際は AppEngineMapper などの独自クラスが多く使わ れているため、Hadoop とは別物である。 Mapper(自分で定義する) Hadoop MapReduce API AppEngine MapReduce Datastore / Blobstore Task Queue
  26. 26. 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
  27. 27. Mapperの定義 必ずAppEngineMapperを継承するpublic class ParseTweetextends AppEngineMapper<Key, Entity, NullWritable, NullWritable>{ キーとエンティティがmap()に渡される @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); } ModelMeta#entityToModel()で Slim3のモデルに変換可能}
  28. 28. エンティティを 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));}
  29. 29. カウンタの使い方• 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); }
  30. 30. パラメータの定義<configuration name="CountTweet"><property><name>mapreduce.map.class</name><value>org.hidetake.elshard.demo.mapper.tweet.CountTweet</value> Mapperクラス</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> (デフォルト4)</property><property><name>mapreduce.mapper.inputprocessingrate</name><value template="optional">10000</value> 処理エンティティ/秒の上限値</property> (デフォルト1,000)</configuration>
  31. 31. ジョブの開始 Ajax MapReduce GET /mapreduce/command/start_job 管理コンソール MapReduceServlet#handleStartJob() MapReduceServlet#handleStart() 1. InputSplitリストの取得 ジョブ開始処理は、 2. Controllerタスクのスケジュール サーブレットハンドラに ベタ書きされている。 3. ShardStateの初期化 4. Mapperタスクのスケジュールcom.google.appengine.tools.mapreduce.MapReduceServlet#handleStart(Configuration, String, HttpServletRequest)
  32. 32. ジョブの開始(プログラムから)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する※これ以外の方法をご存じでしたら教えてください。
  33. 33. タスクの実行制御 • Controllerタスク:Mapperの流量を制御する。 • Mapperタスク:エンティティを処理する。 シャード数 start_job 2秒 Mapper Mapper Mapper Controller map() map() map() map() map() map() 最長10秒 2秒 map() map() map() Controller 2秒間隔で Mapper Mapper Mapper 実行されるcom.google.appengine.tools.mapreduce.MapReduceServlet
  34. 34. タスクの実行制御 (cont.) • 短時間に大量のクォータを消費するのを防ぐため、 スループットの上限値を設けている。 • デフォルトは 1,000 エンティティ/秒 生産する側(?) 消費する側 Controller Mapper put() Quota consume() refillQuotas() QuotaConsumer Manager Memcache Datastorecom.google.appengine.tools.mapreduce.QuotaManager
  35. 35. Mapperの入力 • ジョブの開始時にエンティティが分割される。 • 分割結果は ShardState に保存され、Mapperタス クに渡される。 DatastoreInputSplit Shard Start End Mapper State Key Key Datastore Shard Input … Mapper State Format DatastoreInputSplit Shard Start End Mapper State Key Keycom.google.appengine.tools.mapreduce.MapReduceServlet#scheduleShards()
  36. 36. ShardState カインドプロパティ 型 内容countersMap Blob Counters のシリアライズデータinputSplit Blob InputSplit のシリアライズデータinputSplitClass String デシリアライズ用クラス名jobId String ジョブIDrecordReader Blob RecordReader のシリアライズデータrecordReaderClass String デシリアライズ用クラス名status (Enum) 状態(ACTIVE/DONE)statusString String メッセージ?updateTimestamp Long 最終更新時間
  37. 37. キー分割アルゴリズム • Scatterプロパティからシャード頂点を生成する。 キーの昇順 シャード1 シャード2 キーの 集合 シャード3 シャード4 Scatterプロパティで シャードの頂点 得られる頂点 DatastoreInputSplit オブジェクトcom.google.appengine.tools.mapreduce.DatastoreInputFormat#getSplits(JobContext)
  38. 38. キー分割アルゴリズム (cont.) • オーバーサンプリング • シャード数×32のScatterからシャード頂点を生成する。 • シャードに対してScatterが不足する場合は、Scatterが シャード頂点になる。 キーの昇順 シャード1 キーの 集合 シャード2 シャード3com.google.appengine.tools.mapreduce.DatastoreInputFormat#getSplits(JobContext)
  39. 39. キー分割アルゴリズム (cont.) • 開発環境ではScatterプロパティが存在しないため、 すべてのキーを同一のシャードに割り当てる。 • Productionでも起こり得るのか不明 キーの昇順 キーの シャード1 集合com.google.appengine.tools.mapreduce.DatastoreInputFormat#getSplits(JobContext)
  40. 40. キー分割の例DatastoreInputFormat getSplits: Getting input splits for: TweetDatastoreInputFormat getSplits: Requested 128 scatter entities. Got 75 so usingoversample factor 18DatastoreInputFormat getSplitsFromSplitPoints: Added DatastoreInputSplitDatastoreInputSplit@4ab40a Tweet(14814807863) Tweet(29077523019)DatastoreInputFormat getSplitsFromSplitPoints: Added DatastoreInputSplitDatastoreInputSplit@721965 Tweet(29077523019) Tweet(15299850388119552) 4個のシャード設定DatastoreInputFormat getSplitsFromSplitPoints: Added DatastoreInputSplitDatastoreInputSplit@e14ebc Tweet(15299850388119552)Tweet(24434595889946625) 128個のScatterを希望 ↓DatastoreInputSplit write: Writing DatastoreInputSplitDatastoreInputSplit@4ab40a Tweet(14814807863) Tweet(29077523019) 75個のScatterを取得できたDatastoreInputSplit write: Writing DatastoreInputSplit ↓DatastoreInputSplit@721965 Tweet(29077523019) Tweet(15299850388119552)DatastoreInputSplit write: Writing DatastoreInputSplit 18個のScatterごとに1シャードを構成するDatastoreInputSplit@e14ebc Tweet(15299850388119552)Tweet(24434595889946625)DatastoreInputSplit write: Writing DatastoreInputSplitDatastoreInputSplit@303a60 Tweet(24434595889946625) null
  41. 41. 以前のキー分割アルゴリズム(参考)• キーの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) 以降
  42. 42. カウンタの集約 • 各シャードのカウンタは定期的に集約される。 • Mapperタスクの終わりに ShardState が永続化される。 これによりシャードのカウンタが更新される。 • Controllerタスクではすべての ShardState のカウンタが 集約され、MapReduceState に書き込まれる。 Mapper 1 Mapper 2 map() map() Controller map() map() MapReduce Shard Shard State State 1 State 2com.google.appengine.tools.mapreduce.MapReduceServlet#aggregateState(MapReduceState, List<ShardState>)
  43. 43. MapReduceState カインドプロパティ 型 内容activeShardCount Long 実行中のシャード数chart Text シャードグラフのURLconfiguration Text Configuration XMLcountersMap Blob Counters のシリアライズデータlastPollTime Long Controllerタスクの最終実行日時name String Mapperの名前progress Double 進捗率shardCount Long シャード数startTime Long 開始日時status (Enum) ステータス
  44. 44. 後続処理• Mapperの完了後、任意の処理を実行できる。• 指定したURLの TaskQueue が実行される。• ジョブIDがPOSTで渡ってくるので、ジョブに対応する MapReduceState を取得できる。• Mapperの前後にジョブを配置できる。先行ジョブ Mapperジョブ 後続ジョブ Configuration start_job Controller ジョブID Mapper
  45. 45. デモTwitterのタイムラインで遊んでみる
  46. 46. タイムラインの解析• ユーザのツイートを形態素解析し、よくつぶやい ている単語を調べる。• ツイート取得:タスクチェーン• 形態素解析とカウント:AppEngine MapReduce ツイートの 形態素解析とカウント カウンタの 取得 保存 ツイート start_job Controller 単語数 Mapper
  47. 47. ご清聴ありがとうございました。2011.2.22 ajn#14

×