KotlinでgRPC。実運用にも活かせるWEBアプリケーション構成で試してみた。
KotlinでgRPCを試していきます。protocol buffersがkotlinに対応していないのでjavaに生成したものを使います。次のようなアプリケーション構成でKotlinを使ったgRPC通信を試してみました。
アプリケーション構成
- エンドクライアントからのアクセスは
Gateway Server
が窓口となりHTTP/1.1
で通信を行います。 Gateway Server
のバックエンドにいるgRPC Server
とはgRPC(HTTP/2)
で通信を行います。monitoring tool
はGateway Server
とgRPC Server
の監視を行いHTTP/1.1
で通信を行います。
モチベーション
何度かgRPCについてのエントリをまとめてきました。kotlinでgRPCを試してみたいと感じていたのとSpring Framework 5.0のリリースを控えた状況でSpring Framework 5 on Kotlinを試してみたい欲求がありました。 そのためアプリケーション構成図にあるとおりGateway Server
にはRouter機能を試したいので spring-webflux
でアプリケーションを作りました。
次のエントリではSpring Framework 5.0でReactive Programmingを活用しながらkotlinらしいコードの紹介がされています。
またgRPC Server
ではHTTP/1.1
とgRPC(HTTP2)
の2つの通信方式を有効にしたいです。Spring Bootでどのように実現するのか?この課題についても理解を深める必要がありました。
そして実戦に向けて実運用をイメージしたアプリケーション構成を構築する必要がありました。
ここからは構築にいたるまでの勘所や課題などについてまとめていきます。
gRPC Server
まずはgRPC Serverからです。
ここでの課題はHTTP/1.1
とgRPC(HTTP2)
の2つの通信方式を有効にすることです。
monitoring tool
からはヘルスチェックなどの監視リクエストに応えるためにHTTP/1.1
で通信を行いたいGateway Server
との通信にはgRPC(HTTP2)
で通信を行いたい
この課題を解決するために次のspring-boot-starterを使いました。
こちらを使えばgRPC Serverを実装したクラスに@GRpcService
をつけるだけでgRPC SeverをSpring Boot上に起動できます。またSpring Boot(spring-boot-starter-web)で起動していますのでHTTP/1.1
の通信も有効です。
@GRpcService class EchoServer : EchoServiceGrpc.EchoServiceImplBase() { override fun echoService(request: EchoMessage?, responseObserver: StreamObserver<EchoMessage>?) { val msg = EchoMessage.newBuilder().setMessage("echo \\${request?.message}/").build() responseObserver?.onNext(msg) responseObserver?.onCompleted() } }
レポジトリのREADMEにあるとおりinterceptor
の提供(ログ差し込んだり)やServerビルド定義もカスタマイズできます。
$ ./gradlew clean generateProto bootRun ・・・ 2017-04-13 14:48:45.479 INFO 30602 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http) 2017-04-13 14:48:45.482 INFO 30602 --- [ main] o.l.springboot.grpc.GRpcServerRunner : Starting gRPC Server ... 2017-04-13 14:48:45.528 INFO 30602 --- [ main] o.l.springboot.grpc.GRpcServerRunner : 'app.grpc.server.EchoServer' service has been registered. 2017-04-13 14:48:45.531 INFO 30602 --- [ main] o.l.springboot.grpc.GRpcServerRunner : 'app.grpc.server.GreetServer' service has been registered. 2017-04-13 14:48:45.765 INFO 30602 --- [ main] o.l.springboot.grpc.GRpcServerRunner : gRPC Server started, listening on port 50051. ・・・
8080ポート(HTTP/1.1)と50051ポート(gRPC/HTTP2)の両方が起動ログで確認できます。これでgRPC Serverの課題は解決です。
Gateway Server
次にGateway Serverです。
ここではspring-web-fluxを使いreactor coreベースでアプリケーションが動いています。そしてエンドクライアントからリクエストをgRPC Serverへ渡すgatewayの役割を担います(gRPCクライアントの役割)。
spring-web-fluxを使うとnon-blockingなservletが起動するためgRPCクライントには okhttp
を使います。gRPCのクライアントは標準でnettyが使われるためokhttp
を指定します。これをしないとreactor coreのアプリケーションとgRPC Serverのnon-blokingなところがバッティングしてしまうようです。
private fun getChannel() = OkHttpChannelBuilder.forAddress(appProperties.grpc.server.hostname, appProperties.grpc.server.port!!) // for testing .usePlaintext(true) .build()
もしGateway Server
とエンドクライアント
との通信にgRPCを使いたい場合はGateway ServerにgRPC Server
を置く必要がありバッティング問題を解消しなくてはなりません。これに関してはSpring Framework5.0の正式リリースやマイルストーンの動きを見て試していく必要があり課題として残りました。
protobuf-gradle-plugin
protoclo bufferの生成には次のgradleプラグインを使っています。
起動時に.proto
からprotocol bufferを生成するようにgradleに次ような設定をしています。
protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.2.0' } plugins { grpc { artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" } } generateProtoTasks { ofSourceSet('main').each { task -> task.builtins { java { outputSubDir = 'protoGen' } } task.plugins { grpc { outputSubDir = 'protoGen' } } } } generatedFilesBaseDir = "$projectDir/src/" } task cleanProtoGen { doFirst{ delete("$projectDir/src/main/protoGen") } } clean.dependsOn cleanProtoGen
上記の定義により./gradlew clean generateProto
を実行することでprotocol bufferを再生成できます。起動時に最新の.proto
からprotocol bufferを使われるように起動コマンドは./gradlew clean generateProto bootRun
を使っています。
まとめ
- 現バージョンの
Spring Boot 1.5.2
ではLogNet/grpc-spring-boot-starter
を使うことでHTTP/1.1
とgRPC(HTTP2)
の共存課題は解決できました。 - Spring Framework 5ではreactor coreの採用からgRPC Serverとの共存には課題が残りました。今後のjavaのエコシステムなどを使いながら課題解決に取り組みます。
コードを公開しています
今回のKotlinでgRPCを試したコードのすべてはgithubに公開しています。 起動して確認するにはgithubのコードからできますのでREADMEを参照してください。
supersetをシュッと起動できるDockerfile(認証方式をGoogle API OAuth2に変更)をつくってみた、あと触ってみた所感など
ダッシュボードツールのsupersetをシュッと起動できるDockerfileを作りました。といってもsupersetのDockerfileはgithub.comに見かけるので認証方式をGoogle API OAuth2.0に変更したDockerfileを作りました。あとsupersetを触ってみての感想など導入に向けての所感をまとめたエントリです。
認証方式をOAuthに変更する方法
supersetは認証方式を変更できます。チームに最適な認証方式を選択できます。標準はDBにID/パスワードを登録する方式になっています。これをOAuthに変更する方法をまとめます。
コンテナ内の環境変数 SUPERSET_HOME
にセットしたディレクトリ配下にsuperset_config.py
を置いてsupersetの環境変数を上書きします。次のように認証方式をAUTH_OAUTH
に設定し認証プロバイダの詳細設定を記述します。
import os from flask_appbuilder.security.manager import AUTH_OAUTH basedir = os.path.abspath(os.path.dirname(__file__)) AUTH_TYPE = AUTH_OAUTH OAUTH_PROVIDERS = [ {'name':'google', 'icon':'fa-google', 'token_key':'access_token', 'remote_app': { 'consumer_key':'{GOOGLE_AUTH_CLIENT_ID}', 'consumer_secret':'{GOOGLE_AUTH_SECRET_KEY}', 'base_url':'https://www.googleapis.com/plus/v1/', 'request_token_params':{ 'scope': 'https://www.googleapis.com/auth/userinfo.email' }, 'request_token_url':None, 'access_token_url':'https://accounts.google.com/o/oauth2/token', 'authorize_url':'https://accounts.google.com/o/oauth2/auth'} } ]
認証方式をAUTH_OAUTH
に設定した状態でsupersetを起動するとログイン画面で認証するサービスにGoogle
が表示されます。
次に認証情報を作成したGoogle Developer ConsoleでリダイレクトURL
をhttp://localhost:8088/oauth-authorized/google
に設定するとGoogle Accountで認証ができます。またGoogle+ Api
からアカウント情報を取得しますのでDeveloper ConsoleでGoogle+ Api
を有効にします。
最後に認証させたいアカウントをfabmaneger
を使って作成します。
docker exec -it superset \ fabmanager create-admin --app superset \ --username 'Google+ アカウントの表示名(displayName)' \ --firstname '任意の名' \ --lastname '任意の姓' \ --email 'Google アカウントのメールアドレス' \ --password '任意のパスワード'
Google以外にもTwitterやFacebookなどの認証サービスを追加することができます。
詳しくはgithubのレポジトリに公開していますので合わせて確認できます。
supersetが参照するDBを標準のsqliteからmysqlに変更する
標準ではsupersetが参照するDBはsqliteでOS内の$DATA_DIR
にデータが格納されます。
これだとコンテナを削除するとダッシュボードの登録設定が消えてしまうのでsupersetのコンテナではない外のmysqlコンテナを起動させて参照させました。
Compose化して次のようにSQLALCHEMY_DATABASE_URI
の環境変数を変更しています。
SQLALCHEMY_DATABASE_URI = 'mysql://root@mysql:3306/app?charset=utf8mb4'
mysqlのコンテナではマルチバイト文字列も扱えるようにutf8mb4
の文字コードを有効にしています。
supersetアプリが参照するデータベースURLの末尾に?charset=utf8mb4
をつければダッシュボードの名前にマルチバイト文字列が使えるようになります。
Dockerfileの使い方
次のレポジトリのDockerfileでコンテナを起動させるとsupersetが使えるようになります。
superset-demo/superset at master · nsoushi/superset-demo · GitHub
supersetだけを起動したい場合はsuperset-init.sh
内の次の行をコメントアウトしてください。
SQLALCHEMY_DATABASE_URI = '${SUPERSET_DB_URI}'
mysqlのコンテナとセットで動かしたい場合はレポジトリのREADMEを参考にdocker-copomseでsupersetとmysqlのコンテナを起動してください。
GitHub - nsoushi/superset-demo: This repository contains demo using Superset. After begging containers, you can try Superset right now.
supersetを触ってみた感想など
最後にsupersetを触ってみた感想をまとめます。
初めてsupersetを触りましたが次のようなダッシュボードを作成することができました。
mysqlが提供するworldデータベースをデータソースにして人口やGNPの数値をグラフ化しました。
- 人口の総数(VisualizationType: BigNumber)
- 大陸ごとの人口総数(VisualizationType: Distribution - Bar Chart)
- 大陸ごとの人口総数(VisualizationType: Distribution - Pie Chart)
- 言語ごとのGNP(VisualizationType: Word Cloud)
- 大陸ごとのGNP(VisualizationType: Treemap)
worldデータベースには時系列のデータがありませんが、時系列のデータがあれば集計条件に取得範囲時間を設定してダッシュボードを定期的に更新して定点観測することもできます。
グラフの作成手順
特にヘルプなどを見なくても直感的にグラフ作成まで進めます。
グラフ作成手順は次のような流れです。
- データベースを登録する
- 登録したデータベースからテーブルを登録する
- テーブルからグラフ化するカラムを登録する
- グラフ化に必要なメトリクスを登録する
- データの総数が必要な場合は
Count(*)
、カラム値の総数が必要な場合(人口の総数)はSum(Population)
などを登録する
- データの総数が必要な場合は
- 登録したテーブルからスライスを登録する
- グラフを選択する
- メトリクスとGroup Byするカラムを組み合わせる
- 例)'Asia',‘Europe'などの大陸ごとに人口総数を出す場合は、メトリクスに
Sum(Population)
を登録してContinent
カラムをGroup Byする - 登録したグラフをダッシュボードへ登録する
柔軟にテーブルを定義できる
柔軟にグラフ化したデータテーブルを定義できます。
- DBにあるテーブルをつかう
- 複数のテーブルをJoinさせた結果をテーブルとしてつかう
SQL Lab
(SQLクエリを実行できる)で実行した結果からダイレクトにグラフ化に進む
ただ、SQL Lab
からダイレクトにグラフ化に進む方法は手元のバージョン(0.17.3)ではエラーとなりIssueとしても登録されていました。
SQL Lab
SQL Labでは作成したクエリを実行できます。
実行したクエリは履歴として残ります。後から再度実行できたり、実行結果から直接グラフを作ることもできます。
クエリを書ける人であればSQL Labでグラフ化したいデータの条件でSQLを作りメトリクスとグラフ作成に進むほうが効率が良さそうです。
機能権限とセキュリティ
Admin, Alpha, Gamma, sql_labなどのロールが用意されていてテーブルの登録権限、スライスの登録権限、SQL Labだけを使える権限などがあります。
http://airbnb.io/superset/security.html
機能権限に加えてユーザの操作ログや各種メニューへのアクセス権限などを設定することも可能です。
ここらへんはBIツールに必要そうな機能をサポートする姿勢が伺えます。
まとめ
- RedashはSQLクエリの作成を起点としてダッシュボードを整える流れに比べて、supersetは予め準備されたデータソースを選択します。次に総数や平均など、どんなメトリクスでグラフを作るかを考えダッシュボードを整えます。データソースの選択からダッシュボード登録まで全て画面UIとして提供されているのでクエリを理解していなくても簡単にグラフを作成できます。
- 必要なデータソースを予め準備するエンジニアと解析する人で役割を分ける運用ができます。解析者はエンジニアが準備してくれたデータソースをもとにメトリクスを作成して長所を活かした役割分担ができます。
- ロールとセキュリティも担保されているので情報の公開範囲に注意しながら運用できます。
- グラフの種類が豊富で定期的にグラフが更新されるダッシュボードが作れるので実行したSQL結果をエクセルに持っていきプレゼンしているような状況であれば利用の検討ができそうです。
コードを公開しています
コード全体はgitbubで確認できます。
go-grpc-prometheusでgRPCのmetricsをPrometeusとGrafanaでモニタリングしてみた
gRPC Ecosystemの1つにgo-grpc-prometheus
があります。今回は「gRPC Ecosystem
のgo-grpc-prometheus
を試してみました」エントリです。
go-grpc-prometheus
go-grpc-prometheus
はgRPCのmetricsをPrometheusでモニタリングできるログ出力をサポートするインターセプターを提供します。
取得できるmetricsはレポジトリのREADMEにまとまっています。
GitHub - grpc-ecosystem/go-grpc-prometheus: Prometheus monitoring for your gRPC Go servers.
gRPC Goはインターセプターをサポートしていますので次のようにClientとServerそれぞれに設定します。
- Serverでは次のように。https://github.com/grpc-ecosystem/go-grpc-prometheus#server-side
- Clientでは次のように。https://github.com/grpc-ecosystem/go-grpc-prometheus#client-side
PrometheusでモニタリングしたmetricsをGrafanaでもモニタリングしてみる
go-grpc-prometheus
でgRPCのmetricsが取得できるようになります。Prometheusを起動すればmetricsをモニタリングできるようになります。合わせてPrometheusでモニタリングしているmetricsをGrafanaでもモニタリングしてみます。
シンプルなEchoサービスを作る
unary RPCs
を利用してシンプルなEchoサービスを作ります。
proto
syntax = "proto3"; option go_package = "protobuf"; package proto; service EchoService { rpc EchoService (Message) returns (Message) {} } message Message { string message = 1; }
Server Side
Server sideはgRPCのClientからのリクエストに応えるServer-side of gRPC
の役割とPrometeusのためのMetrics
を出力する役割の2つが必要です。
1つのPortでHTTP/2 (gRPC)
とHTTP/1.1
のリクエストを解釈する必要があるのでsoheilhy/cmux
を使います。
ほぼ素の使い方ですがServer-sideのソースは次のようになりました。(コード抜粋。詳細はnsoushi/go-grpc-prometheus-demoにあります。)
func main() { // Create the main listener. s, err := net.Listen("tcp", fmt.Sprintf(":%s", os.Getenv("GRPC_SERVER_PORT"))) if err != nil { log.Fatal(err) } // Create a cmux. m := cmux.New(s) // Match connections in order: grpcL := m.Match(cmux.HTTP2HeaderField("content-type", "application/grpc")) httpL := m.Match(cmux.HTTP1Fast()) // gRPC server grpcS := grpc.NewServer( grpc.UnaryInterceptor(grpc_prometheus.UnaryServerInterceptor), grpc.StreamInterceptor(grpc_prometheus.StreamServerInterceptor), ) pb.RegisterEchoServiceServer(grpcS, newGrpcServer()) // prometheus metrics server grpc_prometheus.Register(grpcS) httpS := &http.Server{ Handler: promhttp.Handler(), } go grpcS.Serve(grpcL) go httpS.Serve(httpL) m.Serve() }
※ unary RPCs
のみなのでgrpc.StreamInterceptor
は必要ないですがデモのため入れています。
Client Side
Client Sideはブラウザからリクエストを受け取りgRPCのServer-sideへリクエストを送ってくれるエンドポイントとPrometeusのためのmetricsを出力するエンドポイントの2つを用意します。
Client-sideのソースは次のようになりました。(コード抜粋。詳細はnsoushi/go-grpc-prometheus-demoにあります。)
func main() { //gRPC connection var err error conn, err = grpc.Dial( fmt.Sprintf("%s:%s", os.Getenv("GRPC_SERVER_HOST"), os.Getenv("GRPC_SERVER_PORT")), grpc.WithInsecure(), grpc.WithBackoffMaxDelay(time.Second), grpc.WithUnaryInterceptor(grpc_prometheus.UnaryClientInterceptor), grpc.WithStreamInterceptor(grpc_prometheus.StreamClientInterceptor), ) if err != nil { log.Error("Connection error: %v", err) } defer conn.Close() // handle http http.Handle("/metrics", promhttp.Handler()) http.HandleFunc("/echo", echoHandler) http.HandleFunc("/", indexHandler) // serve http http.ListenAndServe(fmt.Sprintf(":%s", os.Getenv("GRPC_CLIENT_PORT")), nil) }
Prometheusでmetricsを確認する
PrometheusはDockerで起動しました。Dockerで起動するとprometheus.ymlのtargetsにlocalhost
としてもgRPCのServer-sideとClient-sideのホストへはアクセスできないのでdocker-composeを使いコンテナ構成をまとめてホスト解決を行います。
version: "3" services: grpcserver: container_name: grpcserver build: ./server ports: - 8080:8080 environment: GRPC_SERVER_HOST: grpcserver GRPC_SERVER_PORT: 8080 grpcclient: container_name: grpcclient build: ./client ports: - 8081:8081 environment: GRPC_SERVER_HOST: grpcserver GRPC_SERVER_PORT: 8080 GRPC_CLIENT_HOST: grpcclient GRPC_CLIENT_PORT: 8081 prometheus: container_name: prometheus build: ./prometheus ports: - 9090:9090 depends_on: - grpcserver - grpcclient grafana: image: grafana/grafana ports: - "3000:3000" depends_on: - prometheus - grpcserver - grpcclient
Prometheusのコンテナを起動してhttp://localhost:9090/graph
へアクセスするとgRPCのmetricsが insert metric at cursor
のメニューに追加されているのが確認できます。
Grafanaでmetricsを確認する
Grafanaのコンテナもdocker-composeに入れましたのでhttp://localhost:3000/login
へアクセスするとGrafanaのダッシュボードを確認できます。Data Sourceにprometheusを追加してDashboardを作成します。
次のようなServer-sideのダッシュボードを作成しました。
gRPCのServer-sideのレスポンス送信数、クライアントからの受信数をGrafanaに設定しました。
※ nsoushi/go-grpc-prometheus-demoのgrafana
フォルダにServer-sideとClient-sideのダッシュボード設定をエクスポートしたJSONがあります。このJSONをインポートするとダッシュボードが簡単に作れます。詳細はレポジトリのREADMEを参照してください。
まとめ
go-grpc-prometheus
をつかってgRPCのmetricsをPrometheusとGrafanaでモニタリングしました。go-grpc-prometheus
の導入はインターセプターを入れるだけなので簡単ではありますが複数のClient-sideとServer-sideの条件での検証、負荷検証などサービスへの導入検証が必要。
コードを公開しています
コード全体はgitbubで確認できます。