appengine ja night #13 開発事例紹介 mixi Xmas 2010 株式会社あゆた 毛利真克
自己紹介 毛利真克 Twitter:@mouri45 会社:株式会社あゆた HP:http://www.ayuta.co.jp 最近の仕事:ソーシャルアプリの受託開発 「これから始める人のためのiPhoneアプリ開発勉強会」主催(http://bit.ly/cDT4x6 )
mixi Xmas 2010 mixi公式のmixiアプリ ピーク時は250万ユーザが参加
システム構成 PCアプリ makeRequest GAE/J (FW:Slim3) JSON http request クーポン在庫 モバイルアプリ http request クーポン在庫 及びタイアップ サーバ WebPage
Datastore使用量 Datastore使用サイズ: 95GB Datastore格納レコード数: 201,244,067件
効果的な施策 設定情報をstatic変数に保持 mixiアプリの制約に対する施策 spin-upに対する施策 その他
設定情報をstatic変数に保持 設定情報をstatic変数で保持しMemcacheへのアクセスを減らす。
設定情報をstatic変数に保持 Static変数 request Memcache Datastore request アップロード Static変数が空、または有効期限が切れている場合はMemcache or Datastoreから取得 Static変数 request Memcache Datastore request アップロード Static変数 設定 ファイル
mixiアプリの制約に対する施策 10秒制限 JOIN停止 PCアプリでmakeRequestで呼び出したWebAPI及びモバイルアプリの画面は10秒以内にレスポンスが返らなければタイムアウトとして扱われる。 JOIN停止 モバイルアプリでタイムアウトが一定時間に一定回数発生するとアプリに未登録のユーザが新規にアプリに参加することができなくなる。 GAE 10s 10s
Datastoreアクセスの遅延現象 他のアプリで通常の状態だと1reqにかかる時間は数百msだが、たまに遅くなることがあると報告を受けていた。 Datastoreアクセスの遅延が集中して起こることが原因。 この状態に陥るとレスポンスタイムが10秒を超えるものも多く発生しているためmixi側でタイムアウトとして扱われるようになり、JOIN停止になる懸念があった。
JOIN停止対策1 requestがqueueにたまる時間を短くする。 GAEにrequestが送られた際に処理可能なインスタンスが存在しない場合、requestはqueueに積まれ、処理可能なインスタンスができるまで待機する。この待機時間には制限時間があり、それを超えるとエラーとしてレスポンスされる。制限時間のデフォルト値は10秒であるため、制限時間に達してエラーになった場合は、mixi側でもタイムアウトとして扱われている。 mixiのJOIN停止の条件は、タイムアウトのみで、エラーなどは含まれない。 根本的な対策ではないが、queueにたまる制限時間を短くすることでGAE上で10秒かかるまえにエラーとしてレスポンスを返し、mixi側でタイムアウトとして扱われないようにすることで、JOIN停止を回避するように対応した。 ※設定するにはGoogleに依頼する必要がある
JOIN停止対策2 Datastoreアクセス時のdeadlineの指定。 Datastoreアクセスが遅延している状態では、アクセスに10秒以上要することがあり、その場合はmixi側でタイムアウトとして扱われる。 Datastoreアクセス時にDatastoreServiceConfigを使用することでDatastoreアクセスの制限時間を指定することが出来る。 こちらも根本的な解決策ではないが、エラーにしてしまうことでJOIN停止を回避するように対応した。
LowLevelAPIでの指定方法 DatastoreServiceConfig config = DatastoreServiceConfig.Builder .withReadPolicy(new ReadPolicy(Consistency.EVENTUAL)) .deadline(5.0); DatastoreService ds = DatastoreServiceFactory.getDatastoreService(config); ここはJavadocからひっぱってきたけどEVENTUALはデフォルトじゃないらしい ちなみにmixi Xmas2010ではSlim3つかったので、この部分は利用していない
Slim3での指定方法 アクセス毎に指定する場合 entity = Datastore.deadline(2.5).get(modelClass, key); デフォルト値を指定する場合 <?xml version="1.0" encoding="utf-8"?> <appengine-web-app xmlns="http://appengine.google.com/ns/1.0"> <application>app-name</application> <version>1</version> <precompilation-enabled>true</precompilation-enabled> <system-properties> <property name="slim3.datastoreDeadline" value="5.0" /> <property name="java.util.logging.config.file" value="WEB-INF/classes/logging.properties" /> </system-properties> <sessions-enabled>false</sessions-enabled> <inbound-services> <service>warmup</service> </inbound-services> </appengine-web-app>
Spin-upに対する施策 通常の運用をしている際は、それほど問題にならない。 デプロイ時(versionを毎回変えていたので実際はversion変更時)に同時にSpin-upが起こるため、JOIN停止になることがあった。 これは、インスタンスの起動時間+spin-up時のアプリの処理が遅いために発生していると考えられる。 Static変数をフラグとして使用することで現在のrequestがspin-upかどうか判定可能なため、spin-upであれば、レスポンスをすぐに返しクライアント側からrequestをリトライ(モバイルならredirectで)することで、mixi側のタイムアウトが発生しづらくなるためJOIN停止を回避することができる。 ※この施策については検証時間が取れなかったため実施しませんでした。
その他施策 Warm Up Requestsの利用。 queryよりbatch getを優先的に使用。 不要なsingle property indexが作られないようにする。
やらなかったこと 本来であれば、使用するべきだが、数億エンティティ・数百req/secという環境下で検証が十分行えなかったためやらなかったこと。 カスタムインデックスとマージジョイン task queueの多用
カスタムインデックスと マージジョイン 下記の問題について、検証する時間がとれなかったこと、及びどちらも、query用にプロパティを用意することでsingle property indexで代用できるため未使用とした。 5月ごろに別案件で、カスタムインデックスが作られないという事象が報告されたので念のため未使用。 (ただし、問題が報告されたのは、運用が始まって後からカスタムインデックスを設定したケースなので、最初から設定しておくケースについては特に問題ないかも) 同じく5月ごろ別案件で、件数が多いkindでマージジョイン利用時にカスタムインデックスの生成を促すエラーが報告されたので念のため未使用。 (こちらは、現在は問題ないかも。データさえたくさん登録すれば検証は簡単なので、お試しあれ) 今もダメな事がある
Task queueのデータのquotaは設定で変えられるとのこと 運用中、データリカバリのために利用したtask queueが1.4.0から入ったtask queueのデータのquota制限にかかっていたため、今回のアプリにおいては正しい判断であった。 Task queueのデータのquotaは設定で変えられるとのこと
失敗したこと GAEでPVの取得 Keyの命名
ログに関してはgoogleのほうで対策を検討中らしい GAEでPVの取得 ログに関してはgoogleのほうで対策を検討中らしい モバイルアプリのPVをサーバ側で取得する必要があったが、GAEのログは、一定以上量がたまると消えてしまうため、別の仕組みが必要と判断。(ログのダウンロードツールも取りこぼしが発生すると判断) 別のGAEアプリに対してimgタグを使ってリクエストを送りページ名を記録する方法をとった。 mixiアプリ用GAE Html <img … Jspでレスポンス生成時にhttp://別アプリ/{pageName}.gifにアクセスするimgタグを出力 http://.../pageName.gif PV用GAE Mixiアプリが稼動しているものとは別のGAEアプリを用意。リクエスト毎にページ名を記録することで後でPVを集計できるようにする。 透過gif
問題 予想以上に費用が高くついた…orz 最終的にEC2にApacheを立ててログを集計する方法に変更
key = Datastore.createKey(modelClass, keyName); entityのkeyの生成の際にnameを指定できるが、この文字列の先頭にタイムスタンプを利用している部分があった。(queryの結果が指定した条件以外の部分がkey順に取得されるのでその特性を利用しようとしていたため) key = Datastore.createKey(modelClass, keyName);
Indexとなるプロパティにタイムスタンプのような連続した値が格納されるケースも注意が必要(ただし、高アクセスな場合のみ) 問題 GAEのdatastoreは、BigTableを利用している。BigTable上でデータは複数のtablet serverにより管理されている。 ひとつのtablet serverにアクセスが集中するか、サイズが300MBを超えるとtablet serverの分割が起こり、その際にDatastoreアクセスのタイムアウトエラーが発生しやすくなる。(ただし、問題になるのは数百QPSぐらいアクセスがあるような場合) 今回の問題はkey文字列の先頭にタイムスタンプを使用していたためtablet serverにアクセスが集中してしまった。 http://bit.ly/hW2QG7 http://bit.ly/gqOVZO Indexとなるプロパティにタイムスタンプのような連続した値が格納されるケースも注意が必要(ただし、高アクセスな場合のみ)
解決策 key文字列の先頭にハッシュ値をつけることでアクセスがひとつのtablet serverに集中しないようにする。 アクセスが 集中する 12341 12342 12343 12344 12345 12346 12347 ABC_12311 ABC_12312 アクセスが分散される ABC_12345 DEF_12321 DEF_12322 DEF_12346 GHI_12347 GHI_12331 GHI_12332
GAEは、劇的なスピードで、機能追加・パフォーマンス改善が行われている。 GAEはソーシャルアプリに向いている。 過去断念した人ももう一度トライしてみよう! GAE/J+Slim3+Eclipseの生産性がすばらしい!
ご静聴ありがとうございました。