App Engine上の大量データ処理について

App Engine上の大量データ処理について

appengine ja night #14
https://api.atnd.org/events/12652

9054e8c93ca920a18b9822237c09f0a4?s=128

Hidetake Iwata

February 22, 2011
Tweet

Transcript

  1. 3.
  2. 4.

    本日の内容 • App Engineとバッチ処理 • タスクチェーン • シャーディング • App

    Engine MapReduce (Java)の仕組み • デモ:Twitterのタイムラインで遊んでみる http://goo.gl/iibHl
  3. 6.

    App Engineのインフラ • バッチサーバはない。 • SQLやストアドはない。 • HTTPリクエスト処理の延長上にバッチ処理を考 える必要がある。 •

    Task Queueが用意されている。 • 長時間の処理は細かい単位に分けて処理する。 • Googleインフラを使うので資源は無尽蔵にある。 • お金はかかります。
  4. 7.

    Task Queueの制約 • タスクは10分以内に終了する必要がある。 • 通常のリクエストは30秒以内。 • 以前は30秒制限があったことを考えると、事実上の無 制限になったといえる。 •

    タスクに渡すパラメータは10kB以内に抑える必要 がある。 • タスクは必ず実行されるが、2回以上実行されて しまうかもしれない。 • 処理は冪等であるべき。
  5. 11.

    回目 タスク数 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セット実 行した。
  6. 12.

    回目 タスク数 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セット実 行した。
  7. 13.

    回目 タスク数 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セット実 行した。
  8. 14.

    実験1のまとめ • タスク当たりの処理時間を 1,000 ms 程度にする とよい? • 50以上に分割すると効率が悪化する。 •

    インスタンス数は22まで増えた。 • 今回の実験は一例にすぎない。 • 測定結果が安定しないことを付記しておきます。
  9. 17.

    Scatterプロパティ • App Engine 1.4.0(時期から推測)で追加された。 • Release Notesには書かれていない? • AppEngine

    MapReduceで採用されている。 • 新しいエンティティが保存される際、 0.8%の割合でScatterプロパティが付加される。 • 付加されるかどうかはキーによって決まる。 • 付加分のデータ量は課金されない。 • ShortBlob型にキーのハッシュ値が入っている。 • ただし、リザーブドプロパティなので取得できない。 http://code.google.com/p/appengine-mapreduce/wiki/ScatterPropertyImplementation
  10. 19.

    Scatterによる分割 • シャードの区間キーをタスクに渡して処理する。 • Scatterで得られるシャード数は多いため、流量の 調整が必要になる。 • 例:100,000エンティティに対して800 Scatter(0.8%) Processor

    Task Processor Task プロセッサ プロセッサ Scatter Task Processor Task プロセッサ シャード シャード シャード シャード Processor Task プロセッサ
  11. 20.

    memcache memcache カーソルチェーン • クエリの結果を少しずつ処理する。 • ProducerとConsumerが独立して動くようにする。 Processor Task Processor

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

    比較実験(参考) • 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)
  13. 24.

    AppEngine MapReduceとは http://code.google.com/p/appengine-mapreduce/ • Google App Engineで動くMapReduceフレーム ワーク。 • 2010年のGoogle

    I/Oで発表された。 • Python版とJava版がある。 • 依然としてReducerは発表されていない。 • Issueにpatchが上がっている…
  14. 25.

    AppEngine MapReduceでできること • すべてのエンティティにアクセスする処理が極め て簡単に書ける。 • 集約カウンタが使える。 • できないこと •

    12時間のバッチが30分になります • Reducerは発表されていない。 • 管理コンソールで日本語が表示されない。
  15. 26.

    アーキテクチャ • Task Queueの上に構築されたフレームワーク。 • アプリケーションからは Hadoop API が見える。 •

    実際は AppEngineMapper などの独自クラスが多く使わ れているため、Hadoop とは別物である。 Mapper(自分で定義する) AppEngine MapReduce Datastore / Blobstore Task Queue Hadoop MapReduce API
  16. 27.

    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
  17. 28.

    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のモデルに変換可能
  18. 29.

    エンティティを 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)); }
  19. 30.

    カウンタの使い方 • 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); }
  20. 31.

    パラメータの定義 <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)
  21. 32.

    ジョブの開始 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タスクのスケジュール ジョブ開始処理は、 サーブレットハンドラに ベタ書きされている。
  22. 33.

    ジョブの開始(プログラムから) 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する ※これ以外の方法をご存じでしたら教えてください。
  23. 34.

    タスクの実行制御 • 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
  24. 35.

    タスクの実行制御 (cont.) • 短時間に大量のクォータを消費するのを防ぐため、 スループットの上限値を設けている。 • デフォルトは 1,000 エンティティ/秒 Controller

    Mapper Quota Manager Memcache refillQuotas() com.google.appengine.tools.mapreduce.QuotaManager consume() put() Datastore QuotaConsumer 生産する側(?) 消費する側
  25. 36.

    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()
  26. 37.

    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 最終更新時間
  27. 41.

    キー分割の例 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シャードを構成する
  28. 42.

    以前のキー分割アルゴリズム(参考) • キーの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) 以降
  29. 43.

    カウンタの集約 • 各シャードのカウンタは定期的に集約される。 • 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>)
  30. 44.

    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) ステータス
  31. 45.

    後続処理 • Mapperの完了後、任意の処理を実行できる。 • 指定したURLの TaskQueue が実行される。 • ジョブIDがPOSTで渡ってくるので、ジョブに対応する MapReduceState

    を取得できる。 • Mapperの前後にジョブを配置できる。 Mapperジョブ Controller start_job Mapper 後続ジョブ 先行ジョブ Configuration ジョブID