https://www.youtube.com/watch?v=GnhPAdFreX4
1 comment | 0 points | by WazanovaNews 4日前 edited
ウォルマートカナダの開発を担当したKevin Webberが、エンタープライズ向けの開発を前提としたPlayフレームワーク + Scalaの利用について、講演しています。
まずUIまわりのアーキテクチャについて、PlayをAPIとして利用するパターンと、Playを複数のSPA (Single Page Web Application)のホストとするパターンの二つを紹介。
1) UIを物理的に分ける
構成図例(ビデオ 6分40秒時点)
- PlayフレームワークをRESTful APIとして使う。Playにはフロント側のコードを置かない。各UIは、Play APIのクライアントという位置づけになる。
- この場合のUIの選択肢は、
- JavaScriptフレームワーク、HTML5/CSS/Javascript etc.
- ネイティブモバイルアプリ
- 標準的なwebプロトコルで通信できるものであれば何でもあり
- またUIのコンポーネントごとに、個別のレポジトリ / ビルド & デプロイプロセス / アセットパイプライン / テクノロジーの選択 / チーム にわけることができる。
メリット
- クライアントとサーバが、概念的にも、論理的にも、物理的にも、きれいに分かれる。
- UIは単なるAPIのコンシューマーの位置づけであり、新しいインターフェースの追加が簡単。
- デザイナー、フロントエンド開発者、バックエンド開発者が、それぞれの得意な領域に集中できる。
デメリット
- Playのパワフルなルーティング機能を使えない。
- マルチテナントのアプリや、プログレッシブにページをビルドするアプリ(CMSアプリの動的なURIなど)には最適なのに、活かせない。
- データを事前にレンダリングする機能が使えない。
- 動的なコンテンツは最初のページのレンダリング後にリクエストする必要がある。
- サーバへのリクエスト頻度が高いアプリになる。
2) UIをロジカルにわける
構成図例(ビデオ 12分50秒時点)
- 名付けるとすると、mSPAs(Multiple Single Page Web Applications)
- PlayフレームワークはmSPAsのホストの役割。物理的にはバンドルされているが、ロジカルには分離している。
- UIからPlayへのREST
- ほとんどの動的なデータは、UIからJavaScriptを経由してリクエストされる
- レポジトリ、デプロイ & ビルドプロセスなどは共有
- Scalaのviewテンプレは、mSPAs(ビデオの例の場合はAngularJS)をプラグインできるコンテナとして利用される
- JavaScriptやCSSなどの静的なアセットは、Playフレームワークではなく、nginxやCDNに配置する。
なぜ、単なるSPAにしないのか?
- エンタープライズ向けの大規模プロジェクトになると、決済チーム、商品情報チーム、アドミンチームなど、複数のチームに管轄がわかれる場合がある。
- UIのコンポーネントごとにユースケースも様々。アドミンページは特定の社員20名だけが使う機能だが、ホームページはサーバサイドにイベントにあわせて更新される仕組みかもしれない。
- モジュール化されたSPAは、レスが早く、リッチなユーザ体験を提供できる
- 単一の巨大なSPAだと、大規模チームでレポやビルドプロセスを共有して進めるのは相当面倒になる可能性あり。mSPAsだと、概念的なサイトのUI区分に沿って、小さなチームに分割して作業ができる。
シンプルなHTTPリクエストの例(概念図 ビデオ16分40秒時点)
- /admin や /admin/* は、views.html.admin を返す。
- /admin/* の子viewへのルーティングは、AngulaJSが担当する。
AngularJS adminアプリへのルート
- ルートは順に宣言される必要あり。
- /admin と /admin/loginが、Scalaのviewテンプレをレンダリングするのに使われる。
- /admin/*any は、URIがブックマークされてなければ、クライアント側のルーティングにインターセプトされる。
AdminController.scalaにおいては、
- indexとangularWildcardは同じテンプレをレンダリングする。
- angularWildcardは、ユーザが /admin/*の子(例えば、/admin/setting )をブックマークしたときだけ、呼び出される。
- 概ね、AngulaJSは、SPA内のナビゲーションを全て担当する。
admin-template,scala.htmlにおいて、
- Scalaのテンプレは、Playのコントローラーでレンダリングされる
- 最初のページのレンダリングの後に、AngularコントローラーによってHTMLはng-viewに挿入される。
メリット
- Playルーティングのパワーを利用できる。
- 特定のSPAへのリクエストをPlayを使ってルーティングする。
- Playをいつものような使い方ができる。例えば、アドミンSAPを読込む前に認証/認可を行うとか。
- ものすごくレスポンシブにすべきUIには、SPAレベルで、AngularJSのルーティングとコントローラーを使う。
- Scalaのテンプレを引き続き使える。
- 一部のデータを事前にレンダリングし、HTTPトラフィックを減らす。
- サーバエンジニアがアプリのアーキテクチャ全体を担当できる。
- フロントエンジニアは、自分のコンポーネントをプラグインするかたちの開発になる。
デメリット
- 複雑さが増す。
- URIパススルーのために、Playで余計なルートの設定が発生する。
- HTMLとサーバサイドのコードが混在する。
- クライアントとサーバサイドのコードが、レポジトリやデプロイ & ビルドプロセスを共有することになる。
- モデルのclassとScalaのviewの定義がオーバーラップする。
Kevinは、「デメリットは面倒だが、乗り越えられないほどの問題ではない。」ということで、2) の方がよいと考えている様子。
3) システムごとの特性にあった開発方針を選択する
次に、「エンタープライズの開発においては、システムのコンポーネントごとに、スピード / 信頼度 / 重要性の意味付けがあり、その組み合わせを考慮して、開発方針を選択すべき。」としています。
例えば、ウォルマートカナダのケースでは、
- 在庫管理: スピードは早くないが、信頼度が高く、重要度の高いシステム。
- ユーザレビュー & コメント: スピードは早いが、サードパーティにホスティングされているサービスなので信頼度は低く、オプション的な位置づけのシステム。
- 注文管理: スピードが早く、信頼度が高く、重要度の高いシステム。
シナリオ1: もし、スピードと信頼度が重要な要素であれば、
- スピードが遅く and/or 不安定サービスからデータをキャッシュすべき
- キャッシュで正確性が担保できないかもしれないケースがあることは留意すること。無効になった「在庫なし」というステータスを表示してしまうかも。
- もし、キャッシュしないという選択をするなら、スピードが遅いサービスの呼び出しは非同期処理とし、ページ全体がブロックされるのを避ける
シナリオ2: 信頼度が最も重要だとすると、
- データを非同期に読込む
- 遅いサービスからの正確なデータもブロックされることなく取り込める。
- 非同期処理でデータが期待する時間内に返ってくる保証がない。返ってこないかも。
- 事前のレンダリングを慎重にする必要がある
- 事前にレンダリングするデータは、キャッシュもしくはかなり正確性の高いサービスなど、スピードと信頼度を両立できるソースから取得する必要がある。
4) キャッシュの留意点
- Playインスタンス単位のキャッシュは危険。
- サーバ xはサーバyと違う価格を返してくるかも。
- キャッシュバスターも検討してみる。
- サーバサイドでキャッシュを無効にするAPI
- 静的にキャッシュしたアセットにはフィンガープリントをつけておく
5) スピードと信頼度が様々な外部システムに対するあるべきアプローチ(優先順)
- スピードの早いサービスからリアルタイムで正確なデータを事前にレンダリングする。
- 遅い方のサービスからのリアルタイムデータで、プログレッシブにUIを向上させる。
- 信頼度の低いサービスから、事前に取得/キャッシュしたコンテンツをレンダリングする。
- 事前に取得とかキャッシュをしないくらいデータの正確性が重要であれば、ページの該当箇所を表示しないようにする。
- ページごともしくはSPAのモジュール単位で、障害ページを表示。
6) 障害耐性をあげる
分散システムでの安全を担保し、障害の雪崩現象化を防ぐために、サーキットブレーカーを使う。不可避な障害の可能性があるところ、例えば、外部システムは全て信頼度が低いと判断し、そこへのコールはラップする。エンタープライズシステムでは慎重に慎重を期すこと。一方、モジュール化した設計を考慮することも大切。例えば、支払機能がダウンしていても、ユーザがアイテムを閲覧できることは担保するとか。また、テスト実施時には障害を想定したテストもすること。
実行コンテキスト: スレッド & スレッドプールに対する抽象化。渡されたタスクを実行する。デフォルトでは、fork-join-executor。各スレッドがキューをもつ。キューがなくなれば、スレッドは他のキューからタスクを取ってくる。コードが完全にノンブロッキングなら、デフォルトの実行コンテキストを使えば良い。でなければ、チューニングする。かなり、トライ & エラーが必要。
- 利用可能なスレッド数は
- パフォーマンス(CPU)のために十分あり
- Out of memoryやクラッシュを起こさない程度に少ない
- メモリアロケーションを調整するケースは
- 大量のデータ処理。例えば、サービスから大量のJSONドキュメントを取得
- 設定は処理しているデータの型次第
- 作業のタイプにあわせてそれぞれプールを用意する
- intenseなプロセスとメモリ。小さめの数のスレッド
- ブロッキングI/Oなら大きめの数のスレッド
- パフォーマンスとメモリが両立できるまで調整
#playframework #scala