🐇

Scala で gRPC をやるときに候補になりそうなフレームワーク3選

2020/12/01に公開

こんばんは、Scala Advent Calendar 2020 の一日目の記事です。

今年に入ってからお仕事で gRPC を扱うようになりました。スキーマ駆動開発かなりいい感じです。

ということで今回は Scala で gRPC アプリケーションを開発する際に候補になりそうなフレームワークを3つ紹介します。

文中のサンプルコードは GitHub に公開してますのでお手元でどうぞ。

お題.proto

紹介するにも何かお題があったほうが使い勝手を比較しやすかったりするので、以下のようなチュートリアルでよく見かける Protobuf を題材することにします。

greeter.proto
syntax = "proto3";

option java_multiple_files = true;
option java_package = "example.myapp.helloworld.grpc";
option java_outer_classname = "HelloWorldProto";

package helloworld;

service GreeterService {
    rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
    string name = 1;
}

message HelloReply {
    string message = 1;
}

Akka-gRPC

1つ目は、Lightbend 提供の Akka-gRPC です。今年の6月にメジャーバージョンリリースがあり、現在お仕事ではこちらを使っています。[1]

SBTセットアップ

sbt-akka-grpc プラグインが必要です。また、他の akka ライブラリに依存してるので関連ライブラリを全部追加する必要があります

// plugins.sbt
addSbtPlugin("com.lightbend.akka.grpc" % "sbt-akka-grpc" % "1.0.2")

// build.sbt
libraryDependencies ++= Seq(
    "com.typesafe.akka" %% "akka-http" % "10.2.1",
    "com.typesafe.akka" %% "akka-stream" % "2.6.10",
    "com.typesafe.akka" %% "akka-discovery" % "2.6.10",
    "com.typesafe.akka" %% "akka-protobuf" % "2.6.10",
    "com.typesafe.akka" %% "akka-http2-support" % "10.2.1",
    "ch.qos.logback" % "logback-classic" % "1.2.3"
)

enablePlugins(AkkaGrpcPlugin)

Code Generate

デフォルトだと src / main / protobuf に proto ファイルを配置します。

スクリーンショット 2020-12-01 18.03.31.png

コンパイルするとコード生成されるのでコレを使って実装していく形になります。

スクリーンショット 2020-12-01 18.05.53.png

実装

生成された GreeterService を実装してエンドポイントを構築します。

GreeterServiceImpl.scala
package example

import example.myapp.helloworld.grpc.{GreeterService, HelloReply, HelloRequest}

import scala.concurrent.{ExecutionContext, Future}

class GreeterServiceImpl(implicit ec: ExecutionContext) extends GreeterService {

  override def sayHello(in: HelloRequest): Future[HelloReply] =
    Future.successful(HelloReply(message = s"Hello ${in.name}"))

}

akka-grpc 自体はサーバーを立ち上げる仕組みはないので akka-http を使う必要があります。
ServerReflection.partial(...) にサービスを追加することをで Reflection を有効にすることができます。

Main.scala
package example

import akka.actor.ActorSystem
import akka.grpc.scaladsl.{ServerReflection, ServiceHandler}
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.{HttpRequest, HttpResponse}
import com.typesafe.config.ConfigFactory
import example.myapp.helloworld.grpc.{GreeterService, GreeterServiceHandler}

import scala.concurrent.{ExecutionContext, Future}

class Main(system: ActorSystem) {

  def run(): Future[Http.ServerBinding] = {
    implicit val sys: ActorSystem = system
    implicit val ec: ExecutionContext = sys.dispatcher

    val service: PartialFunction[HttpRequest, Future[HttpResponse]] =
      GreeterServiceHandler.partial(new GreeterServiceImpl())

    val reflection: PartialFunction[HttpRequest, Future[HttpResponse]] =
      ServerReflection.partial(List(GreeterService))

    val binding = Http().newServerAt(
      interface = "127.0.0.1",
      port = 8080
    ).bind(ServiceHandler.concatOrNotFound(service, reflection))

    binding.foreach { binding => println(s"gRPC server bound to: ${binding.localAddress}") }
    binding
  }

}

object Main {

  def main(args: Array[String]): Unit = {
    val conf = ConfigFactory.load()
    val system = ActorSystem("HelloWorld", conf)
    new Main(system).run()
  }

}

ここまでで Evans などで動作確認することができます。

echo '{"name":"World"}' | evans -r -p 8080 cli call helloworld.GreeterService.SayHello
{
  "message": "Hello World"
}

クライアントコードの実装については今回は割愛します。こちらを参照願います。

所感

他の Akka ライブラリに依存してるところが地雷になりそうでネックな感じがありますが、metadata や Reflection もサポートされていて業務に支障がでないくらいには使えています。

Play Framefwork

続いて我らが Play Framework です。gRPC をサポートしていく動きがあるようですがメジャーバージョンリリースもされてないので使うには勇気がいりそうです。

https://developer.lightbend.com/docs/play-grpc/current/index.html

SBTセットアップ

薄々勘付いてはいましたが裏では akka-grpc が動いているようです...。様々なプラグインやら設定やらを追加します。

// plugins.sbt
addSbtPlugin("com.lightbend.akka.grpc" % "sbt-akka-grpc" % "1.0.2")
resolvers += Resolver.bintrayRepo("playframework", "maven")
libraryDependencies += "com.lightbend.play" %% "play-grpc-generators" % "0.9.1"

// build.sbt
enablePlugins(PlayScala, PlayAkkaHttp2Support, AkkaGrpcPlugin)

libraryDependencies ++= Seq(
  guice,
  "com.lightbend.play" %% "play-grpc-runtime" % "0.9.1",
  "com.typesafe.akka" %% "akka-discovery" % "2.6.8",
)

import play.grpc.gen.scaladsl.PlayScalaServerCodeGenerator
akkaGrpcExtraGenerators += PlayScalaServerCodeGenerator

import play.grpc.gen.scaladsl.PlayScalaClientCodeGenerator
akkaGrpcExtraGenerators += PlayScalaClientCodeGenerator

Code Generate

Play の場合は app / protobuf に proto ファイルを配置します。

スクリーンショット 2020-12-01 18.43.36.png

実装

Server

play-scala-grpc-exampleを参考にしてみたところ、エンドポイントは Controller ではなく Router とするのが慣習のようです。生成された Abstract クラスを継承・実装して app / routers 配下に配置します。

package routers

import akka.actor.ActorSystem
import example.myapp.helloworld.grpc.{AbstractGreeterServiceRouter, HelloReply, HelloRequest}
import javax.inject.{Inject, Singleton}

import scala.concurrent.Future

@Singleton
class GreeterServiceRouter @Inject()(implicit actorSystem: ActorSystem)
  extends AbstractGreeterServiceRouter(actorSystem) {

  override def sayHello(in: HelloRequest): Future[HelloReply] =
    Future.successful(HelloReply(s"Hello, ${in.name}!"))

}

お馴染みの routes に追加します。
見覚えのない書き方です。

->         /                       routers.GreeterServiceRouter

サーバーの起動は Play がやってくれるので自分で実装する必要はありません。
また、あちこちドキュメントを漁りましたが Reflection を有効にする方法が見当たりませんでした(知ってる方いたら教えて下さい...)。

Client

Reflection を使い方がわからないので、クライアントを実装して動作検証してみます。

同様に play-scala-grpc-example を参考にしてみると、Controller で自動生成したクライアントコードをDIして利用できるようになるようです。

app / controllers に配置します。

GreeterController.scala
package controllers

import example.myapp.helloworld.grpc.GreeterServiceClient
import example.myapp.helloworld.grpc.HelloRequest
import javax.inject.Inject
import javax.inject.Singleton
import play.api.mvc.{AbstractController, Action, AnyContent, ControllerComponents}

import scala.concurrent.ExecutionContext

@Singleton
class GreeterController @Inject()(
  implicit greeterClient: GreeterServiceClient,
  cc: ControllerComponents,
  exec: ExecutionContext,
) extends AbstractController(cc) {

  def sayHello(name: String): Action[AnyContent] = Action.async {
    greeterClient
      .sayHello(HelloRequest(name))
      .map { reply =>
        Ok(s"response: ${reply.message}")
      }
  }

}

application.conf を以下のように追記します。

  • Callする先のサーバーの情報をクライアントに設定
  • クライアントモジュールの有効化
  • ClassicActorsystemProviderModule を disable (忘れてしまったけど確か akka-grpc だかが inject する actor と競合してしまうので)
application.conf
play.modules {
  enabled += example.myapp.helloworld.grpc.AkkaGrpcClientModule
  disabled += "play.grpc.ClassicActorsystemProviderModule"
}

akka.grpc.client {
  "helloworld.GreeterService" {
    host = "127.0.0.1"
    port = 9000
    use-tls = false
  }
}

routes に往年の記法でコードを追記します。

GET        /say_hello/:name        controllers.GreeterController.sayHello(name: String)

guice を使ってるのでお作法に従い Module.scala を追加します。

Module.scala
import com.google.inject.AbstractModule

class Module extends AbstractModule {

  override def configure(): Unit = (): Unit

}

ここまでで、サーバーを立ち上げるとクライアント経由で動作することを確認できます。

sbt run

# 別のタブを開いて
curl http://localhost:9000/say_hello/World
response: Hello, World!

所感

akka-grpc と比べてこれだけでいいの?ってくらいサーバーの実装コードが少ないのはいいですね。ですが Reflection や metadata が扱いが不明瞭だったのと、そもそもリリースされてないということで利用を見送りました。ドキュメントもまだまだ十分とは言えないように思えました。

今後に期待ですかね。

※lightbendのページにLagomっていうのも紹介されてたけどこちら見てないのでどなたかが紹介してくれることを期待してます。

Airframe gRPC

最後は、Airframe gRPC です。
なんとこちらは proto ファイル不要で、Scala でスキーマ定義が出来てしまうというものです(なんだと!!!)

(This is an experimental feature available since Airframe 20.8.0)

というもののこちらもまだ実験的機能とのことなので、それを前提に紹介します。

SBTセットアップ

プラグインに sbt-airframe を追加します。
build.sbt はそれぞれ以下のようなプロジェクト構成にします。

  • api: スキーマ定義
  • server: サーバー
  • client: クライアント
// plugins.sbt
addSbtPlugin("org.wvlet.airframe" % "sbt-airframe" % "20.8.0")

// build.sbt
val airframeGrpc = "org.wvlet.airframe" %% "airframe-http-grpc" % "20.11.0"

lazy val api = (project in file("api"))
  .settings(
    libraryDependencies += airframeGrpc
  )

lazy val server = (project in file("server"))
  .settings(libraryDependencies += airframeGrpc)
  .dependsOn(api)

lazy val client = (project in file("client"))
  .settings(
    libraryDependencies += airframeGrpc,
    airframeHttpClients := Seq("api:grpc")
  )
  .enablePlugins(AirframeHttpPlugin)
  .dependsOn(api)

スキーマ定義

注目のスキーマ定義を見てみます。
@RPCというアノテーションをつけることでスキーマ定義と認識されるようです。[2]

GreetingApi.scala
package api

import wvlet.airframe.http.RPC

@RPC
trait GreeterApi {
  def sayHello(message: String): String
}

実装

Server

作成した GreetingApi を継承してエンドポイントを実装します。

GreeterApiImpl.scala
package server

class GreeterApiImpl extends api.GreeterApi {
  def sayHello(message: String): String = s"Hello ${message}!"
}

サーバー起動の仕組みは airframe-http の grpc パッケージにあるようです。

package server

import wvlet.airframe.http.Router
import wvlet.airframe.http.grpc.gRPC

object Main {

  val router: Router = Router.add[GreeterApiImpl]

  def main(args: Array[String]): Unit = {
    gRPC.server
      .withRouter(router)
      .withPort(8080)
      .start { server =>
        server.awaitTermination
      }

  }
}

ここまでで、gRPCサーバーの実装ができました。

Clients

動作確認のために、Clients を実装していきます。

スキーマ定義ファイルを追加した段階でコンパイルを通すとクライアント実装で利用するコードが自動生成されていることが確認できます。

スクリーンショット 2020-12-01 20.49.43.png

クライアントの実装は以下のようになりました。
今回はドキュメントの gRPC Async Client を試しました。

Main.scala
package client

import api.ServiceGrpc
import io.grpc.ManagedChannelBuilder
import io.grpc.stub.StreamObserver

object Main {

  def main(args: Array[String]): Unit = {

    val channel =
      ManagedChannelBuilder
        .forTarget("localhost:8080")
        .usePlaintext()
        .build()

    val client = ServiceGrpc
      .newAsyncClient(channel)
    client.GreeterApi.sayHello(
      "Airframe gRPC",
      new StreamObserver[String] {
        def onNext(v: String): Unit = {
          println(s"""response is "$v""")
        }
        def onError(t: Throwable): Unit = {
          println("boom!!!")
        }
        def onCompleted(): Unit = {
          println("completed!!!")
        }
      }
    )
  }
}

ここまでで、動作確認を取ることができます。

sbt server/run
...
2020-12-01 20:54:23.638+0900  info [LifeCycleManager] [session:36bc4a54] Starting a new lifecycle ...
2020-12-01 20:54:23.644+0900  info [LifeCycleManager] [session:36bc4a54] ======== STARTED ========
2020-12-01 20:54:23.920+0900  info [GrpcServer] Starting gRPC server default at localhost:8080

# 別のタブで
sbt client/run
response is "Hello Airframe gRPC!
completed!!!

所感

Protobuf 無しで gRPC アプリケーションが作れてしまいました。何でも Scala でやりたい強いひとにとっては魅力的なフレームワークになりそうです。

ただ、現状はPlay と同様に Reflection や metadata に関連した機能はまだ?のようです(見落としてるだけかも)。あとは gRPC でマイクロサービスとかやるときに Protobuf を扱って他の言語で実装されたアプリケーションとのコラボレーションをどうするのかが気になるところでした。

今後に期待です。

最後に

Scala で gRPC をやるためのフレームワークを駆け足で紹介してきましたが、現状ですと akka-grpc が機能が揃っていて、他言語の gRPC 実装と同じ雰囲気で使えるので無難かなとは思います。

とはいえ、Scala での gRPC 利用はまだまだ発展途上なところはあると思っていますので、Play も Airframe gRPC のメジャーバージョンリリースも楽しみにしています。

かとじゅんさんにバトンタッチします。

脚注
  1. こちらで以前紹介しました。 ↩︎

  2. お題.proto と若干差異はあるんですけど、すみません。 ↩︎

Discussion