appengine java night #3

2,677 views

Published on

実際に作ってわかったApp Engineの困ったところ

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

No Downloads
Views
Total views
2,677
On SlideShare
0
From Embeds
0
Number of Embeds
504
Actions
Shares
0
Downloads
20
Comments
0
Likes
4
Embeds 0
No embeds

No notes for slide

appengine java night #3

  1. 1. appengine java night #3 実際に作ってわかった App Engine の困ったところ source: http://www.flickr.com/photos/katemonkey/122489910/
  2. 2. 自己紹介 はてなID:bluerabbit twitterID:bluerabbit777jp
  3. 3. 内容 「雨の日め〜る」というサービスを作りま した。 実際にApp Engineで作るにあたって困っ たことをどのように回避したかをお話しま す。
  4. 4. 雨の日め〜るとは 会社帰りに・・・ 「あれっ。今日って雨だったの?」と 傘を忘れた経験がある。 そんなあなたのためのサービスです。   傘忘れを防止する為に作りました。
  5. 5. 仕組み 「雨の日め〜る」は天気が雨の場合に 天気予報メールを送信する。 実現するための機能は下記3つ。 天気予報を取得する 天気予報をメールする ユーザ登録
  6. 6. 天気予報を取得する 天気予報をUrlFetch APIを用いて取得 天気予報は朝(6:00)に取得 利用者のお住まいの地域は142用意 142の地域の天気予報をDatastoreに保存 ユーザの指定した時間にメールする これらをバッチ処理で行う。
  7. 7. しかし
  8. 8. 立ちはだかる App Eengineの制約
  9. 9. 制約その1 1 リクエストは30秒以内に処理すべし HardDeadlineExceededError http://code.google.com/intl/ja/appengine/docs/whatisgoogleappengine.html
  10. 10. App Engineではバッチ 処理も30秒以内 天気予報を取得するために次のようにした。 Cronで1分毎に実行 各地域の天気予報を取得 取得済みの地域はスキップ 天気予報をDatastoreへ保存 ※当時はTaskQueueがリリースされていなかった。
  11. 11. 処理イメージ
  12. 12. 処理時間30分orz...
  13. 13. TaskQueueで高速化 TaskQueueを使って並列処理 1地域毎に1タスク、142のTaskで実行する Cronは指定時間にQueueを追加するだけ for (Location location : Location.getAll()) {    QueueFactory.getDefaultQueue(). add(TaskOptions.Builder .url("/crawler") .param("locationID",location.getId())); }
  14. 14. 処理イメージ
  15. 15. 処理時間3分
  16. 16. App Engineでの バッチ処理 バッチ処理でも30秒以内に処理 結果的にTaskQueueを使う必要あり キューを使うことで非同期、並列処理となる 非同期、並列処理の知識と経験が必要 既存プログラムをApp Engineに移行する場合 にバッチ処理は処理方式を変更する必要に迫 られる
  17. 17. こんなバッチの場合は どうする? 1Taskが30秒以内に終わらない バッチが終わったことを知りたい
  18. 18. 1Taskが30秒以内に 終わらない 機能を分割する。1機能を30秒以内に分割 当該アプリの例だと1Taskの機能は下記 Fetch Parse Insert 機能別にTaskを実行するようにする
  19. 19. TaskQueueを チェインする Fetch処理の最後にParseのキューを追加 Parse処理の最後にInsertのキューを追加 Insert処理を実行してDatastoreに登録する
  20. 20. 処理イメージ
  21. 21. バッチが終わったことを 知りたい 処理件数で把握する 複数リクエスト(TaskQueue)間で連番を作 成する 連番の処理件数がキューを追加した件数 と同じだったらバッチ終了と判断する
  22. 22. カウンター Sharding Counter 書き込みが集中しないように複数のエン ティティに分散して書き込みし集計する Memcache Counter Memcacheを用いた簡易カウンター  Memcache Counterを紹介
  23. 23. Memcache Counter MemcacheのLow Level API MemcacheService#incrementはアトミックに 実行される TaskQueueなどで複数のスレッドが同時にア クセスしても連番が補償される MemcacheServiceのjava doc
  24. 24. APIの使用例 MemcacheService s = MemcacheServiceFactory.getMemcacheService(); if (!s.contains("MemcacheCounter")) { s.put("MemcacheCounter", 1); // 初期化は1 } else { // 2回目以降は値に+1する s.increment("MemcacheCounter", 1); } // 実行のたびに1,2,3,4,5になる System.out.println(s.get("MemcacheCounter")); http://d.hatena.ne.jp/bluerabbit/20091008/1255007854
  25. 25. 天気予報をメールする 天気予報が雨かを判断する。 特定の時間になったらメールする。 これらをバッチ処理で行う。 しかし、ここにも制約が存在する。
  26. 26. 制約その2 MailAPI の呼出回数は24時間あたり 7000件(1分間に32件)までにすべし http://code.google.com/intl/ja/appengine/docs/quotas.html#Mail
  27. 27. Mailの回数を制御する メール送信はDatastoreを用いた自作Queue を使用する。 メール送信する場合はMailQueueのKind (テーブル)にEntity(データ)を保存する。 MailQueueの送信はCronにて1分毎に MailQueueに未送信があればメールするよう にする。
  28. 28. 処理イメージ
  29. 29. この処理には2つの 誤りがある 1.ユーザ数が増加した場合に   _____しない。 スケール 2.エラーが発生した場合に   ________の危険性がある メール二重送信
  30. 30. 制約その3 Datastore は定期的にエラーが出る ことを許容すべし DatastoreTimeoutException ApiProxy$UnknownException CapabilityDisabledException
  31. 31. ユーザ登録 下記のユーザ情報を登録する 受信するメールアドレス 受信する時間 受信する曜日 お住まいの地域 メールでユーザ登録の確認をする MailQueueを作成する 上記の2つのEntityを登録する
  32. 32. 制約その4 トランザクションは設計する必要がある RDB のように使えないことを許容すべし
  33. 33. (案1) EntityGroup ユーザとメールキューをEntityGroup関係 にする ※説明のため、意図的にJDOのイメージで記載しています。 App Engine のEntityGroupを理解しよう
  34. 34. (案2) 非正規化 1リクエストで複数のEntityを登録しない。 1つのEntityですべて処理する
  35. 35. (案3) TaskQueue 1リクエストで複数のEntityを登録しない。 1リクエストは1Entityのみ登録する。 MailQueueはTaskQueueで登録する。
  36. 36. (案4) 考慮しない エラーがたまに出ることを前提とする 一時的に不整合になることを許容する 偶発的に起こる事象に対して柔軟に対応で きるように備える エラー、不整合を早急に発見する方法を 作りこむ
  37. 37. (案5) 補償トランザクション トランザクションをプログラムで補償する Insert時 Userの登録は正常終了 MailQueueが異常終了 異常を検知してUserをロールバックする (Userを削除する) Update時 Userを更新する前にバックアップを作成 する(Userをシリアライズして保存) 失敗した場合はバックアップから戻す ※30秒制限があるため実装は困難です。しかし、タスクキューを使えば出来なくもありません。
  38. 38. どれが最適な案? 決め手はなに? 案1)EntityGroup 案2)非正規化 案3)TaskQueue 案4)考慮しない 案5)補償トランザクション
  39. 39. Entity Groupって何? 全てのEntityはEntity Groupに所属 Entity Group内ではトランザクションをサポート 全ての操作が成功か失敗かになる Entityを作成するときに、別のEntityを新しいEntity の「親」に指定することができる 新しいEntityに対して親を指定することで、その新し いEntityは親Entityと同じEntity Groupに入る 親を持たないEntityはルートエンティティとなる Entityの親はEntityの作成時に定義され、後で変更 することはできない Entity Group全体に対してトランザクションの排他処 理は実行される
  40. 40. ルートエンティティ String kind = "User"; Key userKey = KeyFactory.createKey(kind, 1); Entity user = new Entity(userKey); DatastoreService ds = DatastoreServiceFactory. getDatastoreService(); ds.put(user); KEY Kind User(1) User
  41. 41. UserにMailQueueを追加 String kind = "MailQueue"; Key mailKey = KeyFactory.createKey(userKey, kind, 1); Entity mail = new Entity(mailKey); DatastoreService.put(mail); KEY Kind User(1) User User(1)/MailQueue(1) MailQueue
  42. 42. EntityGroupはKeyで構成 KEY Kind User(1) User User(1)/MailQueue(1) MailQueue User(1)/MailQueue(2) MailQueue User(1)/Book(8) Book ※ルートエンティティが子エンティティ を保持している訳ではない
  43. 43. 同一Kindでも構成可能 KEY Kind Bank(1) Bank Bank(1)/Bank(2) Bank Bank(1)/Bank(3) Bank Bank(1)/Bank(4) Bank ※注意:排他はEntityGroup全体
  44. 44. EntityGroupの排他 tx = ds.beginTransaction() ;  口座A -1000円  口座B +1000円 tx = ds.beginTransaction() ; 口座C = ds.get(tx, keyC); tx.commit();  口座C -2000  口座D +2000 // ConcurrentModificationException tx.commit(); ※口座A、B、C、DはEntityGroupです。
  45. 45. トランザクション内の分離レベルは SERIALIZABLE tx = ds.beginTransaction() ; 口座A = ds.get(tx, keyA);  口座A 残高照会 1000円 tx = ds.beginTransaction() ;  口座A -1000円  口座B +1000円 tx.commit(); 口座B = ds.get(tx, keyB);  口座B 残高照会 0円 ※リクエスト前は口座Bの残高は0円です。
  46. 46. その他 困ったこと
  47. 47. 制約その5 App Engine のDatastoreには ユニークキー制約がつけられない
  48. 48. ユニークキー制約が ないのでこんなミス TaskQueueで以下の登録処理を実行した 1. パラメータでEntityの登録値を取得 2. Datastoreに新規登録  3. 終了処理   2.の処理後にエラーが出たら リトライされて2重登録された
  49. 49. 対応策 TaskQueueで登録処理する場合は事前にキー を作成する  TaskQueueの追加処理    1. キューで登録するキーを作成する 2. キーをTaskのパラメータに設定する 登録処理 1. キーのパラメータを取得してキーが登録 されているかを確認する 2. 存在しない場合は登録処理をする
  50. 50. 処理イメージ(1) // キーを作成する。 DatastoreService service = DatastoreServiceFactory.getDatastoreService(); KeyRange keys = service.allocateIds("Kind", 1); String key = KeyFactory.keyToString(keys.getStart()); ); // キューのパラメータにキーを設定する QueueFactory.getDefaultQueue(). add(TaskOptions.Builder.url("/insert"). param("key", key)); DatastoreServiceのjava doc
  51. 51. 処理イメージ(2) // DatastoreService#get(Key)で登録有無をチェック String keyString = (String) request.getAttribute("key"); Key key = KeyFactory.stringToKey(keyString); try { DatastoreService service = DatastoreServiceFactory.getDatastoreService(); Entity e = service.get(key); // 登録済み } catch (EntityNotFoundException e) { // 未登録 // → 登録処理を行う }
  52. 52. 対応策(2) Keyにユニークな名前をつける TaskQueueの追加処理 特に処理なし 登録処理 1. ユニークになるようにcreateKeyする 例えば、当アプリはLocationIdと日付 2. キーが既に登録されているかを確認する 3. 存在しない場合は登録処理をする
  53. 53. 処理イメージ // Keyを作成 String keyName = "001" + "20091204"; Key key = KeyFactory.createKey("Kind", keyName); DatastoreService ds = DatastoreServiceFactory.getDatastoreService(); try { ds.get(key); } catch (EntityNotFoundException e) { Entity entity = new Entity(key); // 作成キーで登録 ds.put(entity); // 存在しないときにのみ登録 } KeyFactoryのjava doc
  54. 54. まとめ 制約,エラーを寛大な心で受け入れる 制約ではなくルール ルールを守りながらプログラムするゲーム このゲームは必ず開発者を成長させる
  55. 55. Let's Enjoy Cloud Programming!!
  56. 56. ご清聴ありがとうございました
  57. 57. Questions?

×