🌉

Airbnbの宣言的なGraphQL Schema Stitchingがめちゃ便利そう

2021/07/16に公開

https://medium.com/airbnb-engineering/taming-service-oriented-architecture-using-a-data-oriented-service-mesh-da771a841344

https://www.youtube.com/watch?v=xxk9MWCk7cM

上記の記事、動画で紹介されているAirbnbのGraphQLを適用したデータ指向のサービスメッシュの話がすごかったのでまとめます。

昨年のEnterprise GraphQL Confでの発表されたのがこの記事・動画で、主旨としてはサービスメッシュにGraphQLを持ち込んでそれまでのサービスメッシュを手続き指向的とするならデータ指向的なアーキテクチャを構築したというものです。

サービスメッシュは専門外なのであまり深く触れませんが、その発表の一部として紹介されていた宣言的Schema Stitching[1]とでも言うような手法がめちゃくちゃ理にかなってると感じました。

Schema Stitching

Schema Stitchingは複数のGraphQLスキーマを一つに統合するための手法で、マイクロサービスのアグリゲーションような用途で使われてると思います。
https://www.apollographql.com/blog/backend/graphql-schema-stitching/

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と組み合わせた運用としてとても参考になりそうです。

脚注
  1. 動画内でその手法に明示的に名前が付いてる感じがしなかったので勝手に自分で読んでる名称です ↩︎

Discussion