BigtableとSmalltable
App Engineによる設計手法でひとつ私が実案件で試してなかなかうまくいったと思ったのは、「Smalltable」って私が勝手に呼んでいるアーキテクチャです。簡単にいうと、「複数クライアントのローカルのSQLite間をDatastoreを介して同期する」仕組みです(こういうの一般に何パターンと言うのでしょう…教えてください!)。
- クライアントはHTML5やAIR、iPhone、Android等のリッチクライアントで(実際に実装したのはAIRとiPhoneです)、SQLite等の小規模RDB(以下、Smalltable)をローカルに持つことが前提
- Smalltableは、Datastoreが保持するすべてのデータのうち、そのユーザーが常時使用するデータのみ保持するサブセット
- アプリケーションの大半のロジックをリッチクライアント内のSmalltableだけで実装する
- すべてのレコードにはクライアント側でGUID/UUIDを割り振り、またサーバーと同期した時のタイムスタンプを持たせる
- サーバーと同期がとれてないdirtyなレコードはマーク
- SmalltableとサーバーのDatastore間を適切なタイミングで同期する
- サーバー側ではOracleのSCNに相当するグローバルなタイムスタンプを取得。同期時点のタイムスタンプをTnowとする
- サーバー側にはクライアント側と同じスキーマでテーブル(カインド)を用意しておく
- サーバー→クライアントの同期
- クライアントの前回の同期時点ToldからTnowまでに更新されたレコードをクライアントに送信する
- クライアント→サーバーの同期
- Smalltable上のdirtyなレコードをサーバーに送信する
- クライアントから受信したdirtyなレコードすべてにTnowを付けてDatastoreに保存する。ただし、同じGUIDのレコードがDatastoreに存在し、それがTold以降のタイムスタンプを持つ場合は、他クライアントと競合しているので同期しない(楽観排他)
- 他txとの競合が検出されなければ、同期が成功したレコードのGUID一覧をクライアントに返す
- クライアントでは同期済みGUID一覧に基づいてdirtyマークを外してTnowを記録する。dirtyなまま残ったレコード(=競合したレコード)はユーザーに明示的にマージさせる
この方法のメリット:
- アプリケーションロジックの大半は従来通りSQLやjoinバリバリ使って書ける。Datastoreの制限に悩む局面が少なくなる
- サービス全体はDatastoreのスケーラビリティや低コスト性、高可用性をそのまま享受できる
- アプリケーション機能の大半がオフラインでも使える
- アプリケーションのロジックはSQLiteに律速されるため、ネットやサーバーの状況に依存せずさくさく動く
- ローカルのSQLiteをトランザクショナルな分散キャッシュとして使える
- 一般的なWebアプリに比べてサーバーへのリクエスト数が1ケタ以上少なくなる(同期リクエストのみ)
一方デメリットは:
- ユーザーがアクセスするデータ対象を特定範囲に絞れるような用途にのみ有効(たとえばtwitterのTLとか)
- 「サービス上の全データを対象に検索」等のロジックはサーバー側での実装が不可欠
- 更新の競合が発生しやすい用途、頻繁な排他が必要な用途(例えば在庫管理、連番の採番、ユニーク制約とか)も同様
- Smalltableはスケールしない。ローカルのデータが大きくなると遅くなる
- Smalltableのスキーマ変更が面倒
- クライアントアプリの再インストール等でSmalltableにデータを一括ダウンロードするのに時間がかかる
- 削除は論理削除のみでデータがどんどんたまる
…って感じです。HTML5/AIR/iPhone/Android等のリッチクライアントでApp Engine連携する場合はなかなか便利な仕組みと思います。
Smalltableはトランザクショナルな分散キャッシュ(12/27追記)
古橋さんの「RDBに代わるスケーラブルなデータモデルの必要性」を読んで思いましたが、Smalltableには「トランザクショナル(=ACID特性を備えた)な分散キャッシュ」という側面もありますね。もちろんSQLiteはmemcached等に比べてけた違いに遅い(のでAIR実装ではさらにDAOでキャッシュとかしてる)ですが、インターネットの向こうにあるサーバーに対するクライアント側のキャッシュとして大きな効果があります。実際にこの方式で実装したアプリのレスポンス性は、ネットワーク接続状況等に依らずつねに一定しており、またサーバーへのリクエスト数も一般的なWebアプリより1ケタ以上少ないです(同期リクエストのみ)。
また普通のメモリベースのキャッシュでは、当然アプリを落とすとキャッシュ内容は失われ、またロジックやキャッシュの障害時や更新の集中時にはキャッシュ内容が不整合になる可能性もあります(STM等を除く)。Smalltableの場合はアプリ再起動後もキャッシュ内容は維持され、またアプリケーションロジック内でcommit/rollbackを記述したりtx排他を実装できるトランザクショナルなキャッシュになっています。