Scala で gRPC をやるときに候補になりそうなフレームワーク3選
こんばんは、Scala Advent Calendar 2020 の一日目の記事です。
今年に入ってからお仕事で gRPC を扱うようになりました。スキーマ駆動開発かなりいい感じです。
ということで今回は Scala で gRPC アプリケーションを開発する際に候補になりそうなフレームワークを3つ紹介します。
文中のサンプルコードは GitHub に公開してますのでお手元でどうぞ。
お題.proto
紹介するにも何かお題があったほうが使い勝手を比較しやすかったりするので、以下のようなチュートリアルでよく見かける Protobuf を題材することにします。
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 ファイルを配置します。
コンパイルするとコード生成されるのでコレを使って実装していく形になります。
実装
生成された GreeterService を実装してエンドポイントを構築します。
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 を有効にすることができます。
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 をサポートしていく動きがあるようですがメジャーバージョンリリースもされてないので使うには勇気がいりそうです。
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 ファイルを配置します。
実装
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
に配置します。
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 と競合してしまうので)
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 を追加します。
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]
package api
import wvlet.airframe.http.RPC
@RPC
trait GreeterApi {
def sayHello(message: String): String
}
実装
Server
作成した GreetingApi を継承してエンドポイントを実装します。
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 を実装していきます。
スキーマ定義ファイルを追加した段階でコンパイルを通すとクライアント実装で利用するコードが自動生成されていることが確認できます。
クライアントの実装は以下のようになりました。
今回はドキュメントの gRPC Async Client を試しました。
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 のメジャーバージョンリリースも楽しみにしています。
かとじゅんさんにバトンタッチします。
Discussion