whoami
- @qsona (Twitter, GitHub, Qiita)
- Node.js developer
- FiNC 2016/2 〜
- Ruby, Rails / MySQL
- love microservices!
- Microservices Meetup 主催
- 昨日3/30に開催: vol.5 (API Gateway & BFF)
BFF とは?
- Backends for Frontends の略
- クライアントとバックエンドの中間にサーバを置き、フロントエンド寄りの処理を行う
- Microservicesの文脈で語られることが多い
- 昨日の会長のスライド step by step BFF
GraphQL とは?
- クエリー型 Web API
- RESTful API において問題になりがちな点をカバーしている
- 仕様として定められている
- (RESTfulはあくまでAPI設計の指針)
Node.js + GraphQL でBFFを作った
- Node.js v6.x
- v7で
--harmony
オプションで async-await...!
は止められましたw - ※ v7.6でオプションなしでも使えるようになった
- v7で
- koa v2.x
構成図
Android iOS WebApp
| | | GraphQL API
Backends-for-Frontends (Node.js)
| | | RESTful API
Backend1 Backend2 Backend3
Rails Rails Node.js
※ 現状は、全てのリクエストでBFFを通しているわけではない
気をつけたこと
- 作り込み過ぎず、早めに試せることを重視した
- 価値検証を重視、ある程度失敗を許容する
- 役割をいきなり一手に引き受けすぎない
- BFFがドメインロジックを持つようになると悲劇
BFFの役割として選択したもの
- 複数のAPI呼び出しをまとめる (batch request)
- APIを順番に呼び出し、結果を合成する
- 例: ランキング
- ランキングのサービスはuser_idを含む配列を返す
- ユーザ情報を別のサービスにuser_idsで引く
- ユーザ認証
- Backendへのリクエストは user_id + サービスとしての認証トークン
- user_idはHTTPヘッダに入れた
BFFの役割として選択しなかったもの
- クライアント向けたレスポンスの整形
- バックエンドへのqueryingの仕方を持つ
- 例: 一覧画面でのフィルターやソートのかけ方
- 更新系をまとめる処理
BFFにドメインロジックが入る可能性を排除したかった
Why Node.js?
- (我々の)BFFのメインの役割は、複数のBackend APIを呼び出して返すこと
- 当然、並列に呼び出したい。非同期処理が大半になる
- フロントエンジニアに触りやすくしたかった
- 我々のクライアントサイドの言語: Swift, Java (Kotlin), JavaScript
- (そもそもSwiftやJavaはここの選択肢に入りにくい)
- JavaScriptはとっつきやすい(※個人の感想です)
- 重厚な言語(※個人の感想です)は選択肢に入れにくい
Why Node.js?
- qsonaがもともとNode畑だったので
- 手伝ってくれた人もJavaScript畑
- ReactのSSRを見越す
Why GraphQL?
GraphQLの特徴 (1)
複数のリソースを同時に取れる
query {
user(id: 10) {
name
age
}
groups(filter: "awesome") {
name
leader_name
}
}
{
"data": {
"user": { "name": "qsona", "age": 17 },
"groups": [
{ "name": "Node学園", "leader_name": "yosuke_furukawa" },
{ "name": "FiNC", "leader_name": "Yuji Mizoguchi" }
]
}
}
今回は batch requestと呼びます。
GraphQLの特徴 (2)
あるリソースのうち、必要なkeyのみを取得できる
query {
user(id: 10) {
name
// ageがない
}
}
{
"data": {
"user": { "name": "qsona" }
}
}
今回はfiltering queryと呼びます。
GraphQLの特徴 (3)
ネストされたリソースの、どの階層まで取るかを決められる
query {
user(id: 10) {
name
age
children {
name
age
}
}
}
{
"data": {
"user": {
"name": "qsona",
"age": 17,
"children": [
{ "name": "junki", "age": 4 },
{ "name": "sumire", "age": 2 }
]
}
}
}
今回は preload query と呼びます。
GraphQLの特徴 (4)
強力な型システムをもつ (GraphQLType)
それによって得られるもの
- リクエストのバリデーション
- レスポンスのバリデーション
- ドキュメントの自動生成
- cli (GraphiQL) の自動生成
- とても使いやすい
BFF導入前にあった問題点 (1)
Smart UIなAPI
- 画面に必要なものを全て返すようなAPI
- 画面が変わるたびに、1つのレスポンスにキーが追加される
- キーが増えすぎる
- 何が今でも使われているのか不透明
- メンテが大変
問題点 (1) への対応
- 前提: Backendをリソース指向に寄せていく
- それでも画面に必要なものを同時に取得できるように
- batch request
- preload query
BFF導入前にあった問題点 (2)
レスポンスのjsonの形が不安定 => クライアントが困る
- レスポンスのドキュメントがない
- DSLでリクエストパラメータを記述していた (grape)
- ここからドキュメント(swagger)を自動生成できる
- しかし、レスポンスのドキュメントはない
- リソースが定まってない
- ふわっとしたJSONシリアライザ ( rabl )
- 複数のAPI間で、jsonの形が微妙に違う
- Android/iOS間でのドメインの解釈も定まっていなかった
問題点 (2) への対応
- 前提: Backendをリソース指向に寄せていく
- GraphQLType
BFF導入前にあった問題点 (3)
- メインのサービスが、他のサービスのデータもまとめて返していた
- オーケストレーション
- 凝集度の低下
- 他サービスの変更時、メインのサービスまで変更しなければならない
- (メインのサービスは規模が大きく、変更・デプロイが大変)
問題点 (3) への対応
- 前提: Backendはリソース指向に寄せていく
- batch requestを利用
- 複数APIの集約はBFFの責務とする
(画像)
改善した
その後
BFF導入後の問題点 (1) クエリの管理
GraphQLの特徴 (2) で紹介した "filtering query" は特に何も解決していない
- あるリソースのうちの一部分だけ取得したい、というユースケースがほとんどなかった
- クライアントが、部分的なエンティティをわざわざ作らなければならない
- 部分で取得すると、キャッシュもヒットできなくなる
BFF導入後の問題点 (1) クエリの管理
- バックエンドにキーを追加するたびに、変更する必要がある
- iOSとAndroidでの共通化
- 本来は「iOSとAndroidで別々な方法でリソースを取れる」というメリットがある
- しかし、そのようなユースケースは今の所なかった
- クエリを共通管理することに
BFF導入後の問題点 (2) 改善の競合
- Backendサーバ (Rails) 側も問題を改善してきた
- JSON Hyper Schemaの導入
- 使うシリアライザの変更 (rabl => active_model_serializers)
- API設計レビュー、設計力の向上、設計の標準化
- クライアントも、ドメインモデルの手前にJSONをマップする層を持っている
- 腐敗防止層 (参考: @yanzm さんの記事 )
BFF導入後の問題点 (2) 改善の競合
- 役割が結構かぶる
- Server: JSON Schema / Serializer
- BFF: GraphQLType
- Client: 腐敗防止層
- 必要かどうかは、チームの距離感の問題でもある
- 結合度
- そこまで遠くする必要はない
落穂拾い
落穂拾い (1) 更新系はどうしている?
- ひとまず、更新系はBFFに持っていない
- 更新系のオーケストレーションは、今後もしない
- やるとしたら「このボタン(アクション)に対応するエンドポイントをBFFで持つ」
- GraphQLではちょっと難しい
落穂拾い (2) エラー処理
- GraphQLでは、レスポンスは errors または data のキーを返す
- 複数リソースを同時に取得しようとして、1つが失敗した場合、errorsだけ返すのが仕様
- BFFでは、成功したリソースは返しつつ、失敗したリソースはその理由を返す必要がある
- つまり errors は基本使えない
落穂拾い (2) エラー処理
{
"data": {
"user": {
"isError": false,
"data": {
"name": "qsona"
}
},
"groups": {
"isError": true,
"reason": "Server is broken"
}
}
}
落穂拾い (3)
- GraphQLInt型は32bitまでしか対応していない
- オーバーフローして障害が発生
振り返り
- GraphQLをBFFに採用するのは、少なくとも銀の弾丸ではない
- 資産が使えるので、やりたいことにマッチすればメリットがある
- 自由度が効きにくい面もある
振り返り
ハイブリッド策を取るとよかったかも
- BFF置くならやはり全部のリクエストを通したい。
- 初めはただのProxyでいい。 (step by step BFF 参照)
- GraphQLは、1つのエンドポイント (例えば
POST /v1/graphql
) を提供するだけ- 同じサーバで他のエンドポイントを提供しても問題ない
- エンドポイントは画面ごとに提供して、BFF内でGraphQLを完結させることもできる
- (それをやりたいかはともかく...)
Thank you!
We are hiring!!
- Microservices
- hot: Asynchronus architecture
- Node.js Server
- React, Redux
- GraphDB (TitanDB)
- Fulltext Search
- Ruby on Rails
- v5 / api mode
- Docker, Amazon ECS
- iOS Swift / Android Java, Kotlin
- Machine Learning