Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

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

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

Hidetake Iwata

February 22, 2011
Tweet

More Decks by Hidetake Iwata

Other Decks in Technology

Transcript

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

    View full-size slide

  2. Introduction
    いわてぃ
    @int128
    http://d.hatena.ne.jp/int128/
    • 本業
    • SIerでお仕事しています。
    • プライベート
    • Google App Engine/Java
    • 自宅サーバ

    View full-size slide

  3. 本日の内容
    • App Engineとバッチ処理
    • タスクチェーン
    • シャーディング
    • App Engine MapReduce (Java)の仕組み
    • デモ:Twitterのタイムラインで遊んでみる
    http://goo.gl/iibHl

    View full-size slide

  4. APP ENGINEとバッチ処理

    View full-size slide

  5. App Engineのインフラ
    • バッチサーバはない。
    • SQLやストアドはない。
    • HTTPリクエスト処理の延長上にバッチ処理を考
    える必要がある。
    • Task Queueが用意されている。
    • 長時間の処理は細かい単位に分けて処理する。
    • Googleインフラを使うので資源は無尽蔵にある。
    • お金はかかります。

    View full-size slide

  6. Task Queueの制約
    • タスクは10分以内に終了する必要がある。
    • 通常のリクエストは30秒以内。
    • 以前は30秒制限があったことを考えると、事実上の無
    制限になったといえる。
    • タスクに渡すパラメータは10kB以内に抑える必要
    がある。
    • タスクは必ず実行されるが、2回以上実行されて
    しまうかもしれない。
    • 処理は冪等であるべき。

    View full-size slide

  7. 大量データの処理場面
    • データストア上のエンティティを処理したいケー
    スを考える。
    1. 定時処理
    • メール配信、データ取得など
    2. 集約プロパティの更新
    3. 集計処理
    4. スキーマ変更(データ移行)

    View full-size slide

  8. タスクチェーン
    • Task Queueを引き継いで長時間の処理を行う。
    • タスクの30秒制限があった時は必須だった。
    • タスクの完了は保証されていないため、制限がな
    くなってもタスクチェーンが望ましい。
    • 10秒以内にリクエストを返した方がいい?
    リクエスト リクエスト リクエスト
    リクエスト リクエスト

    View full-size slide

  9. 実験1
    • 30秒間かかる処理をどの程度のタスクに分割する
    と最も早く完了するか?
    • タスクの分割数を変化させて所要時間を測定する。
    • ここでは、処理は自由に分割できると考える。
    • 例:3秒×10タスク
    enqueue
    task
    task
    task

    Thread.sleep()
    を実行する
    enqueue を開始してから
    すべてのタスクが完了する
    までの時間を測定する。

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  13. 実験1のまとめ
    • タスク当たりの処理時間を 1,000 ms 程度にする
    とよい?
    • 50以上に分割すると効率が悪化する。
    • インスタンス数は22まで増えた。
    • 今回の実験は一例にすぎない。
    • 測定結果が安定しないことを付記しておきます。

    View full-size slide

  14. キーの分割手法
    • データストア上のエンティティをいくつかの固ま
    りに分解したい。
    1. 既知のキー集合を分割する。
    2. Scatterプロパティで分割する。
    3. カーソルチェーンで処理する。

    View full-size slide

  15. 既知のキー集合
    • キーの集合があらかじめ分かっている場合、一定
    のルールに基づいてキーを分ける。
    • 例:範囲の決まっているID
    • n等分する。
    • ID mod nを使う。
    • 例:日付キー
    • 月別に処理する。
    • 親キーから子キーを取得して処理する。

    View full-size slide

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

    View full-size slide

  17. Scatterプロパティ (cont.)
    • Scatterプロパティを持つキーを取り出すと、キー
    集合を分割する中間点が得られる。
    • 取り出すキーの数に関係なく、一様に分布する中間点
    が得られることが期待される。
    • プロパティの内容がハッシュ値であるため。
    • 中間点を得るにはScatterプロパティでソートする。
    キーの
    集合
    List scatterKeys = Datastore.query(m)
    .sort(Entity.SCATTER_RESERVED_PROPERTY,
    SortDirection.ASCENDING)
    .asKeyList();

    View full-size slide

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

    View full-size slide

  19. memcache memcache
    カーソルチェーン
    • クエリの結果を少しずつ処理する。
    • ProducerとConsumerが独立して動くようにする。
    Processor Task
    Processor Task
    Query Task
    結果リストをmemcacheに
    書き込み、カーソルのみを
    プロセッサに渡す。
    (memcacheのexpire対策)
    プロセッサ プロセッサ
    結果リスト 結果リスト
    カーソル カーソル
    クエリ

    View full-size slide

  20. ElShardフレームワーク
    • ElShardというフレームワークを作っています。
    • 開発中です...
    • タスクチェーン
    • TaskChainController
    • カーソルチェーン
    • QueryProcessorController

    View full-size slide

  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)

    View full-size slide

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

    View full-size slide

  23. AppEngine MapReduceとは
    http://code.google.com/p/appengine-mapreduce/
    • Google App Engineで動くMapReduceフレーム
    ワーク。
    • 2010年のGoogle I/Oで発表された。
    • Python版とJava版がある。
    • 依然としてReducerは発表されていない。
    • Issueにpatchが上がっている…

    View full-size slide

  24. AppEngine MapReduceでできること
    • すべてのエンティティにアクセスする処理が極め
    て簡単に書ける。
    • 集約カウンタが使える。
    • できないこと
    • 12時間のバッチが30分になります
    • Reducerは発表されていない。
    • 管理コンソールで日本語が表示されない。

    View full-size slide

  25. アーキテクチャ
    • Task Queueの上に構築されたフレームワーク。
    • アプリケーションからは Hadoop API が見える。
    • 実際は AppEngineMapper などの独自クラスが多く使わ
    れているため、Hadoop とは別物である。
    Mapper(自分で定義する)
    AppEngine MapReduce
    Datastore / Blobstore Task Queue
    Hadoop MapReduce API

    View full-size slide

  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

    View full-size slide

  27. Mapperの定義
    public class ParseTweet
    extends AppEngineMapper
    {
    @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のモデルに変換可能

    View full-size slide

  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));
    }

    View full-size slide

  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);
    }

    View full-size slide

  30. パラメータの定義


    mapreduce.map.class
    org.hidetake.elshard.demo.mapper.tweet.CountTweet


    mapreduce.inputformat.class
    com.google.appengine.tools.mapreduce.DatastoreInputFormat


    mapreduce.mapper.inputformat.datastoreinputformat.entitykind
    Tweet


    mapreduce.mapper.shardcount
    16


    mapreduce.mapper.inputprocessingrate
    10000


    Mapperクラス
    対象のカインド
    シャード数
    (デフォルト4)
    処理エンティティ/秒の上限値
    (デフォルト1,000)

    View full-size slide

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

    View full-size slide

  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する
    ※これ以外の方法をご存じでしたら教えてください。

    View full-size slide

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

    View full-size slide

  34. タスクの実行制御 (cont.)
    • 短時間に大量のクォータを消費するのを防ぐため、
    スループットの上限値を設けている。
    • デフォルトは 1,000 エンティティ/秒
    Controller Mapper
    Quota
    Manager
    Memcache
    refillQuotas()
    com.google.appengine.tools.mapreduce.QuotaManager
    consume()
    put()
    Datastore
    QuotaConsumer
    生産する側(?) 消費する側

    View full-size slide

  35. 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()

    View full-size slide

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

    View full-size slide

  37. キー分割アルゴリズム
    • Scatterプロパティからシャード頂点を生成する。
    キーの昇順
    com.google.appengine.tools.mapreduce.DatastoreInputFormat#getSplits(JobContext)
    キーの
    集合
    シャード1
    シャード2
    シャード4
    Scatterプロパティで
    得られる頂点
    シャードの頂点
    DatastoreInputSplit
    オブジェクト
    シャード3

    View full-size slide

  38. キー分割アルゴリズム (cont.)
    • オーバーサンプリング
    • シャード数×32のScatterからシャード頂点を生成する。
    • シャードに対してScatterが不足する場合は、Scatterが
    シャード頂点になる。
    キーの
    集合
    シャード1
    シャード2
    シャード3
    キーの昇順
    com.google.appengine.tools.mapreduce.DatastoreInputFormat#getSplits(JobContext)

    View full-size slide

  39. キー分割アルゴリズム (cont.)
    • 開発環境ではScatterプロパティが存在しないため、
    すべてのキーを同一のシャードに割り当てる。
    • Productionでも起こり得るのか不明
    キーの昇順
    キーの
    集合
    シャード1
    com.google.appengine.tools.mapreduce.DatastoreInputFormat#getSplits(JobContext)

    View full-size slide

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

    View full-size slide

  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) 以降

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  44. 後続処理
    • Mapperの完了後、任意の処理を実行できる。
    • 指定したURLの TaskQueue が実行される。
    • ジョブIDがPOSTで渡ってくるので、ジョブに対応する
    MapReduceState を取得できる。
    • Mapperの前後にジョブを配置できる。
    Mapperジョブ
    Controller
    start_job
    Mapper
    後続ジョブ
    先行ジョブ
    Configuration ジョブID

    View full-size slide

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

    View full-size slide

  46. タイムラインの解析
    • ユーザのツイートを形態素解析し、よくつぶやい
    ている単語を調べる。
    • ツイート取得:タスクチェーン
    • 形態素解析とカウント:AppEngine MapReduce
    形態素解析とカウント
    Controller
    start_job
    Mapper
    カウンタの
    保存
    ツイートの
    取得
    ツイート 単語数

    View full-size slide

  47. ご清聴ありがとうございました。
    2011.2.22 ajn#14

    View full-size slide