Webアプリ開発における「内部APIモデル」
前回の話は、一回のエントリーでは書ききれない内容でした。。以下もうすこし詳しく書き直してみます。
Webアプリ開発における「内部APIモデル」とは、ネットワーク越しに外部サイトのWebAPIを呼び出すかのごとく、自サイト内のリソースに対して内部専用のWebAPIでアクセスする仕組みを導入し、分散処理を行うモデルのことです。典型的なWebアプリでは、データベースがここでいうリソースに該当するかと思います。
図にすると以下のようなイメージです。
今回、Lang-8で実際に「内部APIモデル」を導入してみたので、気づきの点などをこのエントリーにまとめてみました。
※導入のいきさつについては、前回のエントリーで触れています。
「内部APIモデル」を採用するメリット
Webアプリ開発において「内部APIモデル」を採用するメリットは2つあります。
(1)言語やフレームワークの選択自由度が上がる
現在運用中のWebアプリにおいて、PHP→Ruby(Rails)といった乗換えをする場合、通常は新しい言語で一からの作り直しを強いられることになり、なかなか踏み切れないことも多いでしょう。プロジェクトで最初に採用した言語やフレームワークにずっと拘束されてしまい、「Rails面白そうだな・・」と横目で眺めつつも、その移行コストに萎縮し、惰性で今までと同じ言語を使い続けてしまう。。。
「内部APIモデル」へ切り替えることで、フロントエンドとバックエンドのインターフェースを、言語やフレームワークと非依存に規定することができます。現在稼動中のコードから機能を削ぎ落として「フロントエンド化」あるいは「バックエンド化」することは、一からの作り直しと比較すれば圧倒的に低い労力で実現可能です。「内部APIモデル」は、移行コストに躊躇する状況から脱するひとつのモデルケースに成り得るのでは、と思います。
(2)スケールする
Webアプリ開発にフレームワークを導入する場合、サイト規模が一定以上になると、O/Rマッパの制約がスケール戦略に影響を与えることが多いかと思います。内部APIモデルを採用すると、パフォーマンスにダイレクトに影響するクエリのSQLを直接書き、WebAPIインターフェースでカプセル化することが可能になります。バックエンド内部で透過的に複数のDBへアクセスを分散させるようなことも簡単にできます。一方でパフォーマンスの要求がそれほど厳しくない機能については、生産性を優先してフレームワークのO/Rマッパ任せで実装すればよいわけです。
内部APIモデルとは、そういったアプリ設計を開発者に強制するインターフェースでもあるため、皆が勝手にDBから直接データを引っ張ってこないように、スケール戦略面で開発チームの足並みをそろえることが可能です。
「内部APIモデル」を導入する上で重要なポイント
今回、実際にLang-8で「内部APIモデル」を導入してみて、思うに、内部APIモデルを設計する上で、重要なポイントは4つあるかと。
以下、順に説明してみますね。
【1】APIの”粒度”を大きくせよ
たとえばAPIに日記データを要求する場合、APIは日記単体のデータを返すのではなく、日記についたコメントもまとめてリスト化して返すべきです。さらにコメントをつけたユーザの基本情報(ニックネームやサムネイル画像のファイルネーム)もくっつけたほうがいいでしょう。このように粒度を大きくしておかないと、個別取得を繰り返していたら日記ページを表示するたびに合計で1+2×(コメント数)のAPIコールが生じてしまい、許容可能な遅延幅を超えてしまいます。まぁMemcachedのget_multiのようにパイプライン風に実装して、一セッションの中で複数のレスポンスをまとめて返してももいいのかもしれませんが・・
ちなみにLang-8の場合、上の方針でAPIの粒度を高めた結果、1PVあたりの内部API平均コール数は、およそ4。後述するように、フロントキャッシュのヒット率が50%強なので、実際にバックエンドに生じる問い合わせは1PVあたり2リクエスト程度です。
【2】バックエンドAPIはRESTfulに実装せよ
クライアントのセッション維持はフロントエンドの仕事にするのがよいでしょう(図中※1)。バックエンドAPIはRESTfulの思想に忠実に設計します。SNSでは例えば「自分の友達にのみ日記を公開」するなど、各種リソースに対してアクセスコントロールが細かく定められることが多いわけですが、Lang-8では、こういったリソースへのアクセスの際に、APIコールに用いるURIのQueryStringに閲覧者情報を埋め込むことで、セッションに頼らずにアクセスコントロールを踏まえた応答を返しています。セッション維持の仕事をバックエンドから排除することで、バックエンドの設計がシンプルになりスケールが容易になるとともに、リソースをGETする上で必要な情報がすべてURIに含まれているので、APIレスポンスのフロントでのキャッシュ処理が容易になります。
【3】高速なserialize手段を準備せよ - PHP最強?
内部APIモデルを使用する上で、バックエンドからフロントエンドへのレスポンスをどのフォーマットで提供するかという一大問題があるわけですが、最も重要視すべきなのはエンコード/デコード速度です。「内部API」モデルは比較的大量のデータを、フロントエンド-バックエンド間でやり取りすることになるので、エンコード/デコード処理に遅いライブラリを使ってしまうと、そのオーバーヘッドが許容できないレベルになります。
Lang-8では、各種言語でのサポートが充実しており、且つデータサイズもコンパクトで、エンコード/デコードも高速という特徴から、JSONを採用しました。エンコード性能については以下が詳しいけれど、PHPに関しては、PECLに含まれているjson_encode/decodeは、PHP標準関数のserialize()とほぼ同等の速度でのエンコードを実現していることが分かります。この辺、JSON周りのエンコード速度が十分に考慮されたMVCフレームワークがあるとバックエンドの構築が楽になっていいのですが(PerlやJavaの事情はよく知らないけど、CatalystとかStrutsとかはどうなんだろ)。
ちなみにRailsに関して言えば、JSONのdecodeを担うActiveSupport::JSON.decodeにUTF-8エンコーディングを正常に扱えないという問題があります。
この問題を回避するため、Lang-8では現状、JSONのデコードにSimple JSON parserを使用中。速度面で若干の問題を抱えています。
【4】バックエンドAPIのレスポンスはフロントでキャッシュせよ
Lang-8ではバックエンドへのAPIコールを軽減させるため、フロントエンドでAPIレスポンスのキャッシュを行っています(図中※2)。staleなデータが表示されないように、更新処理のたびにきめ細かなデータ破棄を行っており、また上記に挙げたようにAPIの粒度を高くしたため、キャッシュヒット率は50%〜60%程度と、低い水準で推移しています。
しかしたとえキャッシュヒット率が50%だったとしても、半分ヒットすればバックエンドノード数を半減させることができるので、フロントキャッシュは積極的に導入するべきです。なお、キャッシュ内容の整合性を保つために、フロントエンドノード間でキャッシュデータを共有する必要があります。よってフロントサイドでもMemcachedの導入が必須です。
バックエンドレスポンスのキャッシングについては、今回の経験を通して色々と思うところがありました。結論から言えばRESTfulにバックエンドAPIを実装したことで、フロントでのキャッシュ破棄アルゴリズムが非常に上手く機能しました。長くなるので詳細はまた次の機会にでも。
フロントエンドを二頭体制にする場合
現在稼動中のWebアプリにおいて、言語やフレームワークを移行させたいとき、開発体制などの面から、一斉移行が難しい場合があります。Lang-8でも、一度にRailsに移行することは無理と判断して、当分の間はフロントエンドとしてRailsとPHP(OpenPNE)が並走する二頭体制を選びました。(下図)
別々の言語で実装されたフロントエンドが二種類ある場合、以下のような独特の課題が生じます。
<1>セッションの共有
ユーザのセッション情報をすべてのフロントエンドで共有する必要があります。Lang-8がもともとベースにしていたOpenPNEは、セッション情報をDBに保存していましたが、そのデータフォーマットはPHPのserialize()でシリアライズされたデータ構造でした。Rails導入後も同じセッションデータを使い続けることにしましたが、PHPでシリアライズされたデータは、そのままではRailsで読めません。RubyでPHPシリアライズ文字列を読めるライブラリが無いかググってみたところ、そのものズバリのコードがありました。
Lang-8では、これをありがたく使わせていただいて、異種プラットフォーム間でセッションの共有を行っています。
<2>フロントキャッシュの共有
バックエンドAPIのレスポンスをフロントでキャッシュする場合、異なるフロントエンド間でデータを共有する必要があります。フロントエンドがPHPだけの時はPHPのserialize()を使えばよかったのですが、データをRailsと共有するため、フォーマットをJSONに切り替えました。その他、フレームワークからMemcacheを利用する際に、フレームワークが名前空間管理のためにキャッシュキーにprefixをつけてしまう場合があるので(ex. Rails ← これで数時間はまった)、この辺も確認しつつ、どちらのフロントエンドからも完全に同じキーでアクセスしていることを確認しなければいけません。