pixiv Sketchのフロントアーキテクチャ
(ピクシブ株式会社 Advent Calendar 2016 12日目の記事です)
こんにちは、エンジニアの id:geta6 です。普段は主にpixiv Sketchの開発などに携わっています。
さて、先日ISUCON6の本選が開催され、ISUketchというReactを使ったお絵かきサービスが話題になったかと思います。
この問題で採用されたアーキテクチャは『pixiv Sketch』のアーキテクチャを参考にしたものとなっています。
パフォーマンス等に関しましては本選の感想として良質な記事がいくつも執筆されていますのでそちらに譲るとして、今回はこのアーキテクチャの設計意図や利点について話します(2年前のリリース当初からずっとこの構成なので今さら感はありますが)。
どういうアーキテクチャか
pixiv SketchのWeb版は、2つのサーバーから構成されています。
1つ目はバックエンドでAPIを担当する『APIサーバー』で、Railsで作られています。APIサーバーはViewを持たず、主にJSONのみを出力します。
2つ目はフロントでViewのレンダリングを担当する『レンダリングサーバー』で、expressで作られています。httpクライアントを用いてAPIサーバーにリクエストを送り、かき集めた情報を元にReactをサーバーサイドレンダリングし、ユーザーへレスポンスを返します。
レンダリングサーバーのアプリケーションコードは、クライアントコードとしてそのまま利用されます。初回リクエスト以降はHistoryAPIを使ってリロード無しでURLを書き換え、APIサーバーから直接データを入手してViewを構築します。
バックエンドのAPIサーバーは、iOSアプリケーションやAndroidアプリケーションからも利用されます。iOSやAndroidのアプリケーションからフロントのレンダリングサーバーへリクエストが飛ぶことは基本的に無く、レンダリングサーバーは『WebBrowser利用者向けの1クライアント』という位置付けになっています。
設計の意図
『レンダリングサーバー』と『APIサーバー』を分離するというアーキテクチャは、主に3点の意図から設計されています。
マルチプラットフォーム前提
1点目は、pixiv Sketchでは開発開始当初からiOS版やAndroid版など、複数のプラットフォームからサービスを提供する予定があった点です。
Web版の開発開始時点で「Web版とiOS版を同時にリリースする」という目標があったこと、また「いずれAndroid版もリリースする」という予定があったため、設計時点でAPIベースの設計を強く意識しました。具体的には『Web版とiOS版が同時かつ新規に開発できる』という要件を重視していました。
「テンプレートを部分的にレンダリングしたhtmlをRailsから返し、DOMに挿入する」というアイデアもあったのですが、やはりiOS版の開発を同時かつ新規に行えるという要件から『ViewもAPIを利用することにして、データのインターフェースを統一した方がよい』という結論に至りました。
テンプレートの二重実装を防ぐ
2点目は、過去にRails側のテンプレートとJavaScript側のテンプレートを二重に実装する必要に迫られ、非常に辛い思いをしたことがあった点です。
Railsから出力されたView上で『ユーザーの入力内容に応じて内容が変化したり、要素が増減してプレビューができる』という機能を実装したことがあったのですが、JSが要素を挿入する際、Railsで実装したテンプレートとは別途にJSが認識できるテンプレートを実装する必要ができてしまいました。
このつらみを回避するため『ViewはView、ロジックはロジック、アーキテクチャーレベルで2者を完全に分離し、テンプレート実装を一度のみにしたい』という強い思いがありました。
個人的にやりたかった
3点目は、Node.jsを使ったReactのサーバーサイドレンダリングを実運用でやってみたかった、という個人的な理由によるものです。今までNode.jsをメインに据えたプロダクトは、期間を限定して公開したプロダクトを含めても数えるほどしかなく、よい挑戦の機会になりました。
設計の利点
この設計にしてよかったな、と思った代表的な利点を3つ挙げます。
コードの責任範囲が有限になる
アーキテクチャを分離しているので、どんなに頑張ってもViewがロジックを持つことはできません。「なんかテンプレートだと使えるけどAPIには乗っていない値があるらしい」といったような怪しさ満点の運用もできなくなります。
また、Viewへの変更はAPIサーバーのコードとは無関係なため、例えばエラーを含んだコードをデプロイしてしまいレンダリングサーバー全体が応答できなくなったとしても、APIサーバーの安定性に影響を及ぼしません。つまり、他のプラットフォームに影響を及ぼす可能性を限りなく低くすることができます。
コードの責任範囲が限定的であることは、コードレビューを効率化するという側面もあります。レンダリングサーバーのコードレビューは『js的に悪い書き方をしていないか・パフォーマンスは悪くなっていないか』などといった、単純なクライアントコードとしての良し悪しに集中して費やされることになるからです。
メンバーと共通の言語で会話できる
MVCが一体となったアプリケーションを開発していた時と比べて、例えば同じプロジェクトのiOSエンジニアと「このデータってどうやって表現する?」というような会話が生まれやすくなりました(もちろん、こちらから聞くこともあります)。
イテレートするオブジェクトをどのように保持するか・データ単位をどこで切るか・モジュールにどう命名するか・デザインにどう落とし込むかなど、会話は多岐に渡ります。
Viewの実装者が『JSONのスキーマ』という他プラットフォームのエンジニアと共通の言語を得ることで、同じデータを似たように扱うことになり、同じような悩みにぶつかりやすくなり、実装を先行した人に悩みを共有できる環境が整ったことによる効用だと思っています。
プラットフォームで足並みを揃える必要がない
新たに機能を開発するとき、APIの実装さえ終わっていれば全てのプラットフォームで実装を始めることができ、実装が完了すればリリースすることができます。取り決めさえしておけば、APIの実装完了を待たずにレスポンスをモック化して実装を始めることもできます。
重要なのは、Web版の実装が最も遅れた実装になっていても開発上で問題が起きないという点です(ユーザーやプロジェクトとしては問題ですが)。
『Web版もクライアントの1つ』というスタンスで開発されているため『テンプレートのみで実現されている機能』が存在しません。他プラットフォームのエンジニアの言葉を借りれば、「すでにWeb版で提供されているが、APIとしては存在しないので実装を待つしかない機能」が存在しません。
ある機能がプラットフォーム利用者にいつ提供されるかは各プラットフォームのエンジニア自身にかかっており、高いモチベーションの維持につながっています。
やっててよかったこと
このアーキテクチャでは複数のプラットフォーム・プログラミング言語から同一のAPIを頻繁に参照することになるため、APIドキュメントの整備が必須になると思います。
pixiv SketchではSwaggerを利用したドキュメントの自動生成を行なっており、あるエンドポイントから提供されるJSONのスキーマやモデル定義をいつでも参照できるようになっています。APIの実装が完了したら「pullして、読んで」と伝えるだけで各実装者が動けるようになるため、非常に有用です。これが無かったらリリースはもっと遅れていて、バグももっと多かったかもしれません。
APIドキュメントが開発の初期段階から整備済であったことはこのアーキテクチャにとって非常に大きな助けとなりました(実際に提案から導入までやってくれたのは id:walf443 でした、ありがとうございます)。
まとめ
ここまでアーキテクチャの利点を話しましたが、もちろん全てのアプリケーションや場面に対して汎用的に適用できるものではありません。例えば『Webで表示するだけなのにリポジトリを2つ管理しなければならない』『Node.jsとRailsと2つのインスタンスを立ち上げる必要がある』等のデメリットもあります。
ISUCON6では「問題として出題する」という前提があったため、アーキテクチャに対して『とっつきにくい』『めんどくせぇ』といった印象を持たれた方もいるかもしれませんが、条件が合致すれば非常に快適な開発パフォーマンスを提供してくれます。
ちなみに、現在の構成だとAPIサーバーはJSONを返すだけのサーバーになっています。当初は開発速度やメンバーの知見の有無を優先してRailsが採用されましたが、処理速度等を優先してGo言語など、より高速な言語を選定するのもアリかもしれません。
考案してから2年間運用して高い開発パフォーマンスを維持できているので、本当にこの構成を思いついてよかったと思います。
この構成を考えついた時の俺は非常に神がかっていたと思うので、参考にしていただければ幸いです。
明日は新卒JSマン期待の星、RaggがJavaScriptかなんかの話をするみたいです、お楽しみに。