木曜日, 11月 19, 2009

【Google App Engine】 疎結合とバージョニングについて このエントリーを含むはてなブックマーク


ぶいてく流スケーラブル設計3大要素

 私たちがスケーラブルなアプリを作る際に重要だと考えている要素は、疎結合、バージョニング、非同期の3つである。
 今回は、疎結合、特にバージョニングについて詳しく述べる。非同期(TaskQueue)は次回の予定。

疎結合

 GAEといったスケーラブルなプラットフォームを利用することで、単純なWebアプリでもスケーラビリティを得られるわけだが、さらにそれをRESTfulなWebサービスにすることで、より柔軟なスケーラビリティを享受できる。マッシュアップアプリがいい例で、ワンソース・マルチビューを実現できる。それは、この記事や、実装例で示してきたとおりである。これらはReflexやReflexGaeフレームワークにより、EntityからJSONやXML等に変換することで実現している。

バージョニング

この記事の最後の一文は、なんのこっちゃ!?と思った方も多いと思う。

 「ただし、これをやるには、レコードをRevision番号で管理するという前提で考える必要があり、更新において新しいRevision番号でレコードを追加する必要がある。」

 要はバージョニングのことなのだが、その目的は、楽観的排他によるスケーラビリティの向上と、履歴管理によるトレーサビリティの向上の2つである。削除したデータも保存しておくことでUndo機能もつけることができる。
 楽観的排他はロックを起こさないのでスケーラビリティ向上につながる。つまり、更新の際にRevison番号を比較することで不整合を起こしていないかチェックする仕組みで、それをアプリレベルで実装しようというものである。
 HTML5のDataStorageのように、疎結合、非同期を前提とした「いまどき」のアプリでは、ブラウザで何かアクションを起こしても、すぐにはサーバに問い合わせにはいかない。そうなると不整合を起こす可能性も大きいわけだが、レコードがバージョニングされてさえいれば、整合性を解決することはそれほど難しくない。Revison番号が同じレコードは常に同じ内容であることが重要で、また、サーバのDatastoreやMemcache、あるいはクライアントのStorageなど、どこにあろうと同じものとして扱われることになる。もし同じRevisionで異なる情報があったとしたらDatastoreの内容を正とすることで整合性は保たれる。したがって、クライアントに存在する情報とサーバにある情報は時間差こそあるものの、全体的に見ると常に整合性が取れた状態とみなすことができる。(これが私が理解している、Eventually Consistencyの概念だ)
 というわけなので、オフライン時はクライアントのStorageを読みこめばいいし、オンラインでもMemcacheにデータが残っていればそれを返せばいい。そのとき最新のRevisionがDatastoreにあったとしても、神経質にならずに、Memcacheのタイムアウトまでは古い情報であっても返しちゃえばいいのだ。もちろん、在庫引当や座席予約など、このやり方が通用しない要件はあるが、ほとんどは「遅延」を許容できるアプリと思うので、このように実装することでスケーラブルになることは間違いないと思われる。(そういえば、DatastoreのIndex作成も遅延が起きるような・・appengineはRead Committed相当だがcommit()には2つのマイルストーンがあることを忘れてはいけない
 関連として、レコードにシーケンス番号をつけることも挙げておく。これは、Pagingや件数管理のために使用する。シーケンス番号をつけることで、最新レコードを取得できるようになる。また、大量のレコードを分割してTaskQueueで並列処理させたい場合には、シーケンス番号で範囲指定を行える。
 件数管理は、GAEは件数を数えるのが苦手で数十万件以上になるとタイムアウトを起こして検索できなくなるので、最新レコードで常に管理しておこうというアイデアである。
 最新レコードのQueryは件数に影響されずにそこそこ高速に検索できる(約0.6s)ことを利用している。
 削除済みデータなど、不必要に思える(むしろ消すべきと思える)ものを残して履歴管理する第一の理由は、データ量がパフォーマンスに影響しないということ。その点は特に強調したい。

 以下に、バージョニングのロジックを示す。大福帳管理モデルというのは私が勝手につけた名前で、あまり人に吹聴するとイタイ目にあうかも。multidimensionalの方が近い意味だと思う。
 
大福帳管理モデル(別名:カーボンコピーモデル 英語名:multidimensional)

  • 大福帳管理モデル(multidimensional,カーボンコピーモデル)(勝手に命名)
    • 楽観的排他でスケーラビリティ向上
    • 履歴管理によるトレーサビリティ向上、Undo機能
    • 件数管理によるパフォーマンス向上
    • すべてのレコードは、Key+Revison番号で管理される。
      • Key(※)+Revisonが同じであれば、同じ内容のレコードであるとみなす。ただし、削除は例外扱い。(※このKeyはアプリのキーでありDatastoreのKeyではない。詳細は、ココのKeyに何を入れるべきかを参照のこと)
    • 追加では、レコードが存在しなければ、Revisonを0にセットして追加する。レコードが存在していれば、エラーか強制登録を実行する。強制登録は、削 除フラグが立っていることを確認して、revisonを+1したうえで追加する。このとき削除フラグが立っていなければエラーとする。この処理は GET/PUTによるトランザクションで実行されなければならない。
      •  ただし、強制登録では一度削除されたものと同じKeyでの再登録をすることになるので、それを許すかどうかはアプリの要件による。(Forceオプションか何かで区別すべき)
    • 更新では、現在のレコードがあることを確認して、更新レコードのRevisonと同じであるかチェックする。NGであれば楽観的排他エラーとする。OKで あればRevisonを+1して更新すると同時に現在のレコードには削除フラグを立てる。この処理はGET/PUTによるトランザクションで実行されなけ ればならない。また、現在のレコードに削除フラグが立っていてもエラーとする。(これで既に削除されているときの楽観的排他ができる)
    • 検索ではRevison番号は更新されない
    • 削除は論理削除であり実際には消さない。削除フラグに削除された日時を入れて更新することで削除されていることを示す。Revison番号は変わらない。
      •  削除レコードを追加するという考え方ではなく、削除フラグに日時を入れることで対応する。その理由は、古いRevisonレコードに削除フラグが立たないと検索対象に含まれてしまって不都合だから。また、削除フラグのみの更新であれば、元の情報が消されることもない。
  • 情報の鮮度(強弱)
    • 同一KeyのレコードではRevisonの大きいものが最新となるが、同一Revisonで削除フラグが立っているものがある場合には、それが最新となる。
      • Rev.3(削除なし)>Rev.2(削除あり)>Rev.2(削除なし)>Rev.1(削除ありなし)


大福帳管理モデル 更新のサンプルとパフォーマンス

 以下にサンプルコードを示す。これはカウンタをもつEntityGroupを構成するタイプで、最新レコードに件数をもつタイプではない。

 拙作のReflexGaeを利用している。

 ReflexGaeの特長は、JDOのEntityをLow Level APIを使ってGET/PUTできるところ。なんのこっちゃ!?と思うかもしれないが、要はJDOがヘタレなのでLow Level APIでラッパーを作った。その際、EntityはJDOと完全に互換性をもたせるようにした。(JDOがダメダメと言い出したのはたぶん私が最初だと思うが、概念というか、Entityの構造まで否定したつもりはなく、実はこれはこれで気に入っていたりする)

 ReflexGaeには、KeyUtils、EntityConverter、FieldMapperというものがあり、以下のようなことができる。
  1.KeyUtilsを使って、EntityGroupを作成する
  2.EntityConverter().convert(RECORD_KIND_CLASS, entity); で、EntityクラスからJDOクラスに変換する
  3.fieldMapper.setValue(current, target); で、JDOの@Persistent項目で更新があるものだけをセットする

 Entity変換ではReflectionを使っているが高速である。

速度比較
  • 登録(登録 + カウンタ更新・・EntityGroup更新)
    • JDO : 0.251s
    • ReflexGAE : 0.163s

  • 更新(1レコード登録 + 1レコード(旧Revision)・・同一Kind更新)
    • JDO : 2.790s
    • ReflexGAE : 0.250s

検索ではJDOは1万件以上でタイムアウトになるので単純に比較できないが、ReflexGaeは次の通り。
  • keyによるget : 0.030s
  • query : 0.622s (92300件対象にSort条件をつけて1件取得)

 ちなみに、Memcacheへの登録参照はココによれば、0.015s程度とのこと。


0 件のコメント:

 
© 2006-2015 Virtual Technology
当サイトではGoogle Analyticsを使ってウェブサイトのトラフィック情報を収集しています。詳しくは、プライバシーポリシーを参照してください。