appengine java night #3
DESCRIPTION
実際に作ってわかったApp Engineの困ったところTRANSCRIPT
appenginejava night #3
実際に作ってわかったApp Engine の困ったところ
source: http://www.flickr.com/photos/katemonkey/122489910/
自己紹介
はてなID:bluerabbittwitterID:bluerabbit777jp
内容「雨の日め〜る」というサービスを作りました。 実際にApp Engineで作るにあたって困ったことをどのように回避したかをお話します。
雨の日め〜るとは会社帰りに・・・ 「あれっ。今日って雨だったの?」と傘を忘れた経験がある。そんなあなたのためのサービスです。 傘忘れを防止する為に作りました。
仕組み「雨の日め〜る」は天気が雨の場合に天気予報メールを送信する。 実現するための機能は下記3つ。
天気予報を取得する天気予報をメールするユーザ登録
天気予報を取得する天気予報をUrlFetch APIを用いて取得天気予報は朝(6:00)に取得利用者のお住まいの地域は142用意142の地域の天気予報をDatastoreに保存ユーザの指定した時間にメールする
これらをバッチ処理で行う。
しかし
立ちはだかるApp Eengineの制約
制約その1
1 リクエストは30秒以内に処理すべしHardDeadlineExceededError
http://code.google.com/intl/ja/appengine/docs/whatisgoogleappengine.html
App Engineではバッチ処理も30秒以内
天気予報を取得するために次のようにした。 Cronで1分毎に実行各地域の天気予報を取得
取得済みの地域はスキップ天気予報をDatastoreへ保存
※当時はTaskQueueがリリースされていなかった。
処理イメージ
処理時間30分orz...
TaskQueueで高速化TaskQueueを使って並列処理1地域毎に1タスク、142のTaskで実行するCronは指定時間にQueueを追加するだけ
for (Location location : Location.getAll()) { QueueFactory.getDefaultQueue(). add(TaskOptions.Builder .url("/crawler") .param("locationID",location.getId())); }
処理イメージ
処理時間3分
App Engineでのバッチ処理
バッチ処理でも30秒以内に処理結果的にTaskQueueを使う必要ありキューを使うことで非同期、並列処理となる非同期、並列処理の知識と経験が必要既存プログラムをApp Engineに移行する場合にバッチ処理は処理方式を変更する必要に迫られる
こんなバッチの場合はどうする?
1Taskが30秒以内に終わらない バッチが終わったことを知りたい
1Taskが30秒以内に終わらない
機能を分割する。1機能を30秒以内に分割当該アプリの例だと1Taskの機能は下記
FetchParseInsert
機能別にTaskを実行するようにする
TaskQueueをチェインする
Fetch処理の最後にParseのキューを追加Parse処理の最後にInsertのキューを追加Insert処理を実行してDatastoreに登録する
処理イメージ
バッチが終わったことを知りたい
処理件数で把握する複数リクエスト(TaskQueue)間で連番を作成する連番の処理件数がキューを追加した件数と同じだったらバッチ終了と判断する
カウンターSharding Counter
書き込みが集中しないように複数のエンティティに分散して書き込みし集計する
Memcache CounterMemcacheを用いた簡易カウンター
Memcache Counterを紹介
Memcache Counter
MemcacheServiceのjava doc
MemcacheのLow Level APIMemcacheService#incrementはアトミックに実行されるTaskQueueなどで複数のスレッドが同時にアクセスしても連番が補償される
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
天気予報をメールする天気予報が雨かを判断する。特定の時間になったらメールする。
これらをバッチ処理で行う。 しかし、ここにも制約が存在する。
制約その2
MailAPI の呼出回数は24時間あたり7000件(1分間に32件)までにすべし
http://code.google.com/intl/ja/appengine/docs/quotas.html#Mail
Mailの回数を制御するメール送信はDatastoreを用いた自作Queueを使用する。メール送信する場合はMailQueueのKind(テーブル)にEntity(データ)を保存する。MailQueueの送信はCronにて1分毎にMailQueueに未送信があればメールするようにする。
処理イメージ
この処理には2つの誤りがある
1.ユーザ数が増加した場合に _____しない。
2.エラーが発生した場合に ________の危険性がある
スケール
メール二重送信
制約その3
Datastore は定期的にエラーが出ることを許容すべし
DatastoreTimeoutException ApiProxy$UnknownExceptionCapabilityDisabledException
ユーザ登録下記のユーザ情報を登録する
受信するメールアドレス受信する時間受信する曜日 お住まいの地域
メールでユーザ登録の確認をする
MailQueueを作成する
上記の2つのEntityを登録する
制約その4
トランザクションは設計する必要があるRDB のように使えないことを許容すべし
(案1) EntityGroupユーザとメールキューをEntityGroup関係にする
App Engine のEntityGroupを理解しよう
※説明のため、意図的にJDOのイメージで記載しています。
(案2) 非正規化1リクエストで複数のEntityを登録しない。1つのEntityですべて処理する
(案3) TaskQueue
1リクエストで複数のEntityを登録しない。1リクエストは1Entityのみ登録する。MailQueueはTaskQueueで登録する。
(案4) 考慮しないエラーがたまに出ることを前提とする一時的に不整合になることを許容する 偶発的に起こる事象に対して柔軟に対応できるように備える
エラー、不整合を早急に発見する方法を作りこむ
(案5) 補償トランザクショントランザクションをプログラムで補償するInsert時
Userの登録は正常終了MailQueueが異常終了異常を検知してUserをロールバックする(Userを削除する)
Update時Userを更新する前にバックアップを作成する(Userをシリアライズして保存)失敗した場合はバックアップから戻す
※30秒制限があるため実装は困難です。しかし、タスクキューを使えば出来なくもありません。
どれが最適な案?決め手はなに?
案1)EntityGroup案2)非正規化案3)TaskQueue案4)考慮しない案5)補償トランザクション
Entity Groupって何?全てのEntityはEntity Groupに所属Entity Group内ではトランザクションをサポート
全ての操作が成功か失敗かになるEntityを作成するときに、別のEntityを新しいEntityの「親」に指定することができる新しいEntityに対して親を指定することで、その新しいEntityは親Entityと同じEntity Groupに入る親を持たないEntityはルートエンティティとなるEntityの親はEntityの作成時に定義され、後で変更することはできないEntity Group全体に対してトランザクションの排他処理は実行される
ルートエンティティ
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);
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);
EntityGroupはKeyで構成
KEY KindUser(1) UserUser(1)/MailQueue(1) MailQueueUser(1)/MailQueue(2) MailQueueUser(1)/Book(8) Book※ルートエンティティが子エンティティ
を保持している訳ではない
同一Kindでも構成可能
KEY KindBank(1) BankBank(1)/Bank(2) Bank
Bank(1)/Bank(3) Bank
Bank(1)/Bank(4) Bank
※注意:排他はEntityGroup全体
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です。
トランザクション内の分離レベルは 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円です。
その他困ったこと
制約その5
App Engine のDatastoreにはユニークキー制約がつけられない
ユニークキー制約がないのでこんなミス
TaskQueueで以下の登録処理を実行した1. パラメータでEntityの登録値を取得2. Datastoreに新規登録 3. 終了処理
2.の処理後にエラーが出たらリトライされて2重登録された
対応策TaskQueueで登録処理する場合は事前にキーを作成する
TaskQueueの追加処理 1. キューで登録するキーを作成する2. キーをTaskのパラメータに設定する
登録処理1. キーのパラメータを取得してキーが登録
されているかを確認する2. 存在しない場合は登録処理をする
処理イメージ(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
処理イメージ(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) { // 未登録 // → 登録処理を行う}
対応策(2)Keyにユニークな名前をつける
TaskQueueの追加処理特に処理なし
登録処理1. ユニークになるようにcreateKeyする
例えば、当アプリはLocationIdと日付2. キーが既に登録されているかを確認する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
まとめ制約,エラーを寛大な心で受け入れる制約ではなくルールルールを守りながらプログラムするゲームこのゲームは必ず開発者を成長させる
Let's Enjoy Cloud Programming!!
ご清聴ありがとうございました
Questions?