最近考えていることを話す機会があったので文章にしてまとめてみる。
疎結合
昨今の複雑化するウェブアプリケーションを効率的に開発するにあたって、疎結合な設計にすることは開発/保守効率を上げるためには必須の条件となることは経験上嫌というほど皆が経験している。(このへんの感覚がわからない人は一度疎結合なアプリケーションを書いてみると良い)
疎結合な設計をすることで問題の切り分けが容易になり、自動化されたユニットテスト・コンポーネントテスト・ UAT が手元の MacBook で実行でき、高いカバレッジに助けられて臆すること無くコードに手を入れられる環境を入手でき、開発する上でストレスなく障害も少ないというメリットが享受できる。
話を戻して、疎結合な設計の例をみると、ウェブサーバとアプリケーションを分離することであるとか、 MVC であるとか、良質かつ単体で動く小さなモジュールを組み合わせて書くといった例があげられ、また最近ではWebサービスのAPIを組み合わせるマッシュアップも台頭し、これも一種の疎結合な設計に含めて考えても良い。この概念に近いものとして MVC にサービス層を含めるような設計をすることでモデル層を抽象化し、コンポーネントテストを書きやすくする設計もチラホラ見かける。
MVC と JSON API
「MVC + サービス層」の設計でアプリケーションを書くほど抽象化するのであれば、それはWebサービスのAPIを提供することとやっていることの本質はさほど変わらないのではないかと思う。そこでいっそのこと振り切って考えて、サービス層に当たる部分に JSON を返せるだけの薄い Controller をのせ、 HTTP サーバとして投入することで設計をより一層疎結合なものへと変更できる。
こうすることでブラウザからリクエストを受けるアプリサーバの Controller は、認証等が済んだ状態で JSON API を叩きレスポンスを View に渡す(あるいは JSON をそのまま通過させる)だけの機能にまで落とし込める。
オーバーヘッド
ここまでやってしまうと気になることが API を叩くために HTTP を経由させることで発生するオーバーヘッド。リクエストを飛ばすことによる CPU 負荷や直列化されたネットワーク IO がボトルネックになりそうなことは容易に想像できる。 CPU 負荷に関してはより高速なライブラリを選ぶなどしてチューニングすべきではあるが…。(他にも内部のトラフィックが増大する可能性もあり、 API サーバは deflate を使って通信を圧縮する必要が有りそうではあるがここでは無視する。インフラの人ごめんなさい)
非同期
直列化されたネットワーク IO がレスポンスタイムに対して大きな比率を占めることは容易に想像できるが、近年のブームを見ているとここは非同期化して解決する方法がわりと簡単に使えるのではないかと思う。リクエストを受けるサーバとして node.js が頭に浮かぶが、 node.js は自由が効かないので選択肢から除外する。(ポイントとなるのは API に対するリクエストの並列化であって、ブラウザからのリクエストに対して全体を非同期化することではない)
非同期化の対象を Controller 内での API に対するリクエストに局所化する理由
- prefork モデルの方が運用上のノウハウが豊富にある
- callback地獄を回避する
- レスポンスタイムが問題になるほど大量にリクエストしている箇所だけをチューニングできる(=最小の努力で最大の効果を得る)
- 全てが非同期で処理される場合かつ一人の開発者がブロックするコードを混ぜてしまった場合、プロセス全体のパフォーマンスを悪化させてしまう(これだけの設計が必要な規模になるとコミットが大量になるはずで、現実的に考えてデバッグは不可能に近い、気がする)
全体を非同期化することで得られるメリットはこれらを無視してでも手にすべきほど魅力的なものか、現状では考えにくい。
pseudocode
package MyApp::Controller::MyPage; use strict; use warnings; use parent 'MyApp::Handler'; use AE; use MyApp::UserAgent::AE; my $ua = MyApp::UserAgent::AE->new; sub show { my ($self, $ctx) = @_; my $id = $ctx->request->session->{user_id}; my $params = {}; my $cv = AE::cv; $cv->begin; $ua->get("friends/list", { user_id => $id }, sub { my ($res) = @_; $params->{friends_list} = $res; $cv->end; }); $cv->begin; $ua->get("activity", { user_id => $id }, sub { my ($res) = @_; $params->{activity} = $res; $cv->end; }); $cv->begin; $ua->get("notifications", { user_id => $id }, sub { my ($res) = @_; $params->{notifications} = $res; $cv->end; }); # ... $cv->recv; $self->render("mypage", $params); } 1;
こんな感じに設計できたらいいよねーというお話でした。
疎結合になるとインフラの人も色々と手を入れやすくなるのでデメリットばっかりじゃないよ><