Airbnbの宣言的なGraphQL Schema Stitchingがめちゃ便利そう
上記の記事、動画で紹介されているAirbnbのGraphQLを適用したデータ指向のサービスメッシュの話がすごかったのでまとめます。
昨年のEnterprise GraphQL Confでの発表されたのがこの記事・動画で、主旨としてはサービスメッシュにGraphQLを持ち込んでそれまでのサービスメッシュを手続き指向的とするならデータ指向的なアーキテクチャを構築したというものです。
サービスメッシュは専門外なのであまり深く触れませんが、その発表の一部として紹介されていた宣言的Schema Stitching[1]とでも言うような手法がめちゃくちゃ理にかなってると感じました。
Schema Stitching
Schema Stitchingは複数のGraphQLスキーマを一つに統合するための手法で、マイクロサービスのアグリゲーションような用途で使われてると思います。
Schema Stitchingの短所としてはスキーマを統合するロジックを書く必要があり、データをくっつけたりする退屈なボイラープレート的なコードがスキーマの規模に比例して増加し、そのメンテナンスが大変そうなイメージです。似た用途で後から出てきたApollo Federationという技術もあり、その動機はStitchingのロジックを集約的にやりたくないことがあると思います。
Apollo Federationの登場で古い手法になるかと思われていたSchema Stitchingですが、最近も記事やOSSなどが更新されて見直されているようです。
宣言的Schema Stitching
記事では触れられていませんが、動画の方で以下のようなGraphQLスキーマ(一部を抜粋)が紹介されていました。GraphQLのディレクティブを使ってStithcingに必要なロジックを最大限宣言的に定義しています。
type User implements Node
@scope(scopes: ["public", "internal", "user-block"])
@serviceBackedNode(
service: "user-block"
methodName: "loadUsers"
)
@owners(list: "airbnb/user-block")
{
id: ID!
firstName: String
lastname: String
email: String @key(path: "emailAddress")
isHost: Boolean
@derivedField(
classPath: "com.airbnb.viaduct.fields.IsHostProvider"
)
# ...
}
GraphQL typeとそれを提供するサービスの紐付け
type User implements Node
@serviceBackedNode(
service: "user-block"
methodName: "loadUsers"
)
まず注目したいのがここで、 @serviceBackedNode
ディレクティブで User
typeが user-block
と呼ばれるサービスの loadUsers
メソッドで得られることが示されています。AirbnbではThriftがマイクロサービス間の通信で使われているそうなのでGraphQLとThriftの両者のスキーマから静的にそのサービスコールが成り立つことが検証できると思います。
ちょっとしたデータ変換ロジック
email: String @key(path: "emailAddress")
Thriftだと emailAddress
で定義されているフィールドをGraphQL上では email
で扱いたいくらいのロジックは @key
ディレクティブを書くだけでいけるみたいです。便利!!
isHost: Boolean
@derivedField(
classPath: "com.airbnb.viaduct.fields.IsHostProvider"
)
そのユーザーの所有物件が0件以上なら(ownedListings
の件数が0以上だったら) isHost = true
のような、そのtypeのデータから別の値を算出するみたいなちょっとしたロジックは @derivedField
ディレクティブでJavaのクラスパスを書くことでそのロジックが走るようになっているようです。AirbnbではKotlinがそのロジックを記述する言語として使われているそうです。
fragment IsHostProvider on User {
ownedListings {
pageInfo {
totalCount
}
}
}
interface DerivedFieldProvider<T> {
fun getFragment(vars): Fragment
fun resolve(context): T
}
override fun resolve(context: ViaductDerivedFieldProviderContext): Boolean {
val source = context.souceObj
val totalListingCount = source.ownedListings?.pageInfo.totalCount
return totalListingCount ?: 0 > 0
}
他の普通に便利そうなやつ
type User implements Node
@scope(scopes: ["public", "internal", "user-block"])
@owners(list: "airbnb/user-block")
{
@scope
で外部へ露出するものなのか、内部のマイクロサービス間でのみ使用するのかなどを指定しています。 @owners
でそのTypeの障害が起きたときなどにレポートするチームを指定しています。
まとめ
マイクロサービスがgRPCやThriftなRPCで、クライアントサイドにはGraphQLというのはよくある構成だと思いますがGraphQLスキーマの表現力を活用してボイラープレート的なコードを書かなくてよくするとても良さそうな仕組みだと思いました。 @scope
や @owners
あたりのディレクティブは規模の小さいサービスでも適用できそうですし、各サービスの事情に応じたロジックをディレクティブとしてスキーマに埋め込むのはすぐにでも真似できそうです。
このGraphQLとサービスメッシュを組み合わせた仕組みをViaductと彼らは呼んでいて、AirbnbのThrift・Kotlinという技術スタックに密結合っぽくてすぐにOSSというわけにはいかないと思いますが、マイクロサービスとGraphQLと組み合わせた運用としてとても参考になりそうです。
-
動画内でその手法に明示的に名前が付いてる感じがしなかったので勝手に自分で読んでる名称です ↩︎
Discussion