appengine java night #3

57
appengine java night #3 実際に作ってわかった App Engine の困ったところ source: http://www.flickr.com/photos/katemonkey/122489910/

Upload: bluerabbit777jp

Post on 05-Jul-2015

2.336 views

Category:

Technology


3 download

DESCRIPTION

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

TRANSCRIPT

Page 1: appengine java night #3

appenginejava night #3

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

source: http://www.flickr.com/photos/katemonkey/122489910/

Page 2: appengine java night #3

自己紹介

はてなID:bluerabbittwitterID:bluerabbit777jp

Page 3: appengine java night #3

内容「雨の日め〜る」というサービスを作りました。 実際にApp Engineで作るにあたって困ったことをどのように回避したかをお話します。

Page 4: appengine java night #3

雨の日め〜るとは会社帰りに・・・ 「あれっ。今日って雨だったの?」と傘を忘れた経験がある。そんなあなたのためのサービスです。 傘忘れを防止する為に作りました。

Page 5: appengine java night #3

仕組み「雨の日め〜る」は天気が雨の場合に天気予報メールを送信する。 実現するための機能は下記3つ。

天気予報を取得する天気予報をメールするユーザ登録

Page 6: appengine java night #3

天気予報を取得する天気予報をUrlFetch APIを用いて取得天気予報は朝(6:00)に取得利用者のお住まいの地域は142用意142の地域の天気予報をDatastoreに保存ユーザの指定した時間にメールする

これらをバッチ処理で行う。

Page 7: appengine java night #3

しかし

Page 8: appengine java night #3

立ちはだかるApp Eengineの制約

Page 9: appengine java night #3

制約その1

1 リクエストは30秒以内に処理すべしHardDeadlineExceededError

http://code.google.com/intl/ja/appengine/docs/whatisgoogleappengine.html

Page 10: appengine java night #3

App Engineではバッチ処理も30秒以内

天気予報を取得するために次のようにした。 Cronで1分毎に実行各地域の天気予報を取得

取得済みの地域はスキップ天気予報をDatastoreへ保存

※当時はTaskQueueがリリースされていなかった。

Page 11: appengine java night #3

処理イメージ

Page 12: appengine java night #3

処理時間30分orz...

Page 13: appengine java night #3

TaskQueueで高速化TaskQueueを使って並列処理1地域毎に1タスク、142のTaskで実行するCronは指定時間にQueueを追加するだけ

for (Location location : Location.getAll()) {    QueueFactory.getDefaultQueue(). add(TaskOptions.Builder .url("/crawler") .param("locationID",location.getId())); }

Page 14: appengine java night #3

処理イメージ

Page 15: appengine java night #3

処理時間3分

Page 16: appengine java night #3

App Engineでのバッチ処理

バッチ処理でも30秒以内に処理結果的にTaskQueueを使う必要ありキューを使うことで非同期、並列処理となる非同期、並列処理の知識と経験が必要既存プログラムをApp Engineに移行する場合にバッチ処理は処理方式を変更する必要に迫られる

Page 17: appengine java night #3

こんなバッチの場合はどうする?

1Taskが30秒以内に終わらない バッチが終わったことを知りたい

Page 18: appengine java night #3

1Taskが30秒以内に終わらない

機能を分割する。1機能を30秒以内に分割当該アプリの例だと1Taskの機能は下記

FetchParseInsert

機能別にTaskを実行するようにする

Page 19: appengine java night #3

TaskQueueをチェインする

Fetch処理の最後にParseのキューを追加Parse処理の最後にInsertのキューを追加Insert処理を実行してDatastoreに登録する

Page 20: appengine java night #3

処理イメージ

Page 21: appengine java night #3

バッチが終わったことを知りたい

処理件数で把握する複数リクエスト(TaskQueue)間で連番を作成する連番の処理件数がキューを追加した件数と同じだったらバッチ終了と判断する

Page 22: appengine java night #3

カウンターSharding Counter

書き込みが集中しないように複数のエンティティに分散して書き込みし集計する

Memcache CounterMemcacheを用いた簡易カウンター 

Memcache Counterを紹介

Page 23: appengine java night #3

Memcache Counter

MemcacheServiceのjava doc

MemcacheのLow Level APIMemcacheService#incrementはアトミックに実行されるTaskQueueなどで複数のスレッドが同時にアクセスしても連番が補償される

Page 24: appengine java night #3

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

Page 25: appengine java night #3

天気予報をメールする天気予報が雨かを判断する。特定の時間になったらメールする。

これらをバッチ処理で行う。 しかし、ここにも制約が存在する。

Page 26: appengine java night #3

制約その2

MailAPI の呼出回数は24時間あたり7000件(1分間に32件)までにすべし

http://code.google.com/intl/ja/appengine/docs/quotas.html#Mail

Page 27: appengine java night #3

Mailの回数を制御するメール送信はDatastoreを用いた自作Queueを使用する。メール送信する場合はMailQueueのKind(テーブル)にEntity(データ)を保存する。MailQueueの送信はCronにて1分毎にMailQueueに未送信があればメールするようにする。

Page 28: appengine java night #3

処理イメージ

Page 29: appengine java night #3

この処理には2つの誤りがある

1.ユーザ数が増加した場合に  _____しない。

2.エラーが発生した場合に  ________の危険性がある

スケール

メール二重送信

Page 30: appengine java night #3

制約その3

Datastore は定期的にエラーが出ることを許容すべし

DatastoreTimeoutException ApiProxy$UnknownExceptionCapabilityDisabledException

Page 31: appengine java night #3

ユーザ登録下記のユーザ情報を登録する

受信するメールアドレス受信する時間受信する曜日 お住まいの地域

メールでユーザ登録の確認をする

MailQueueを作成する

上記の2つのEntityを登録する

Page 32: appengine java night #3

制約その4

トランザクションは設計する必要があるRDB のように使えないことを許容すべし

Page 33: appengine java night #3

(案1) EntityGroupユーザとメールキューをEntityGroup関係にする

App Engine のEntityGroupを理解しよう

※説明のため、意図的にJDOのイメージで記載しています。

Page 34: appengine java night #3

(案2) 非正規化1リクエストで複数のEntityを登録しない。1つのEntityですべて処理する

Page 35: appengine java night #3

(案3) TaskQueue

1リクエストで複数のEntityを登録しない。1リクエストは1Entityのみ登録する。MailQueueはTaskQueueで登録する。

Page 36: appengine java night #3

(案4) 考慮しないエラーがたまに出ることを前提とする一時的に不整合になることを許容する 偶発的に起こる事象に対して柔軟に対応できるように備える

エラー、不整合を早急に発見する方法を作りこむ

Page 37: appengine java night #3

(案5) 補償トランザクショントランザクションをプログラムで補償するInsert時

Userの登録は正常終了MailQueueが異常終了異常を検知してUserをロールバックする(Userを削除する)

Update時Userを更新する前にバックアップを作成する(Userをシリアライズして保存)失敗した場合はバックアップから戻す

※30秒制限があるため実装は困難です。しかし、タスクキューを使えば出来なくもありません。

Page 38: appengine java night #3

どれが最適な案?決め手はなに?

案1)EntityGroup案2)非正規化案3)TaskQueue案4)考慮しない案5)補償トランザクション

Page 39: appengine java night #3

Entity Groupって何?全てのEntityはEntity Groupに所属Entity Group内ではトランザクションをサポート

全ての操作が成功か失敗かになるEntityを作成するときに、別のEntityを新しいEntityの「親」に指定することができる新しいEntityに対して親を指定することで、その新しいEntityは親Entityと同じEntity Groupに入る親を持たないEntityはルートエンティティとなるEntityの親はEntityの作成時に定義され、後で変更することはできないEntity Group全体に対してトランザクションの排他処理は実行される

Page 40: appengine java night #3

ルートエンティティ

KEY KindUser(1) User

String kind = "User";Key userKey = KeyFactory.createKey(kind, 1);Entity user = new Entity(userKey);DatastoreService ds = DatastoreServiceFactory.getDatastoreService();ds.put(user);

Page 41: appengine java night #3

UserにMailQueueを追加

KEY KindUser(1) UserUser(1)/MailQueue(1) MailQueue

String kind = "MailQueue";Key mailKey = KeyFactory.createKey(userKey, kind, 1);Entity mail = new Entity(mailKey);DatastoreService.put(mail);

Page 42: appengine java night #3

EntityGroupはKeyで構成

KEY KindUser(1) UserUser(1)/MailQueue(1) MailQueueUser(1)/MailQueue(2) MailQueueUser(1)/Book(8) Book※ルートエンティティが子エンティティ

を保持している訳ではない

Page 43: appengine java night #3

同一Kindでも構成可能

KEY KindBank(1) BankBank(1)/Bank(2) Bank

Bank(1)/Bank(3) Bank

Bank(1)/Bank(4) Bank

※注意:排他はEntityGroup全体

Page 44: appengine java night #3

EntityGroupの排他

tx = ds.beginTransaction() ;口座C = ds.get(tx, keyC);

 口座C -2000 口座D +2000

// ConcurrentModificationExceptiontx.commit();

tx = ds.beginTransaction() ;

 口座A -1000円 口座B +1000円

tx.commit();

※口座A、B、C、DはEntityGroupです。

Page 45: appengine java night #3

トランザクション内の分離レベルは SERIALIZABLE

tx = ds.beginTransaction() ; 口座A -1000円 口座B +1000円tx.commit();

tx = ds.beginTransaction() ;口座A = ds.get(tx, keyA); 口座A 残高照会 1000円

口座B = ds.get(tx, keyB); 口座B 残高照会 0円

※リクエスト前は口座Bの残高は0円です。

Page 46: appengine java night #3

その他困ったこと

Page 47: appengine java night #3

制約その5

App Engine のDatastoreにはユニークキー制約がつけられない

Page 48: appengine java night #3

ユニークキー制約がないのでこんなミス

TaskQueueで以下の登録処理を実行した1. パラメータでEntityの登録値を取得2. Datastoreに新規登録 3. 終了処理 

 2.の処理後にエラーが出たらリトライされて2重登録された

Page 49: appengine java night #3

対応策TaskQueueで登録処理する場合は事前にキーを作成する 

TaskQueueの追加処理    1. キューで登録するキーを作成する2. キーをTaskのパラメータに設定する

登録処理1. キーのパラメータを取得してキーが登録

されているかを確認する2. 存在しない場合は登録処理をする

Page 50: appengine java night #3

処理イメージ(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

Page 51: appengine java night #3

処理イメージ(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) { // 未登録 // → 登録処理を行う}

Page 52: appengine java night #3

対応策(2)Keyにユニークな名前をつける

TaskQueueの追加処理特に処理なし

登録処理1. ユニークになるようにcreateKeyする

例えば、当アプリはLocationIdと日付2. キーが既に登録されているかを確認する3. 存在しない場合は登録処理をする

Page 53: appengine java night #3

処理イメージ // 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

Page 54: appengine java night #3

まとめ制約,エラーを寛大な心で受け入れる制約ではなくルールルールを守りながらプログラムするゲームこのゲームは必ず開発者を成長させる

Page 55: appengine java night #3

Let's Enjoy Cloud Programming!!

Page 56: appengine java night #3

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

Page 57: appengine java night #3

Questions?