はじめに
gRPCのJavaチュートリアルをやってみて、どういう仕組みで動いてるか気になったので色々調べた。
- gRPC-Javaの利用方法などはチュートリアルに記載してあるので細かい説明はしません
- どうやって通信しているのか、細かい内部の実装がどうなっているかについて、自分の中で理解した内容の整理です
確認バージョン
releases: gRPC-Java v1.16.1
- チュートリアルで使ったのはrouteguideパッケージのソース
- gradleの設定はこちら
compile 'io.grpc:grpc-netty-shaded:1.16.1'
compile 'io.grpc:grpc-protobuf:1.16.1'
compile 'io.grpc:grpc-stub:1.16.1'
gRPCについて
そもそもgRPCについて公式ドキュメントからポイントを抜粋。
-
Protocol Buffersを利用し、送受信するデータのシリアライズ化とRPCのインターフェース定義を行う
- Protocol Buffersの定義ファイルにはデータのシリアライズ定義のみだが、gRPCではそこにRPCのインターフェースも定義するように拡張している
- シリアライズするデータやインターフェースの大部分のコードは自動生成されるため、ビジネスロジックに集中して開発できる
- 通信はHTTP/2を利用する
- 通信の確立、ヘッダやデータの送受信はHTTP/2の形式
- 1つのTCPコネクションで双方向のストリーミング通信が可能であり、以下の4タイプのrpcメソッドを定義できる
type | 概要 |
---|---|
A simple RPC | 1リクエストに対して1レスポンスを返す |
A server-side streaming RPC | 1リクエストに対して複数レスポンスを返す |
A client-side streaming RPC | 複数リクエストに対して1レスポンスを返す |
A bidirectional streaming RPC | 複数リクエストと複数レスポンスを双方向にやりとりする |
gRPC-Javaについて
gRPC-Javaの実装のポイント。
-
サーバ側では通信制御部分はNettyを利用している
-
クライアント側はAndroidの場合はOkHttp、それ以外はNettyを利用している
- 今回はクライアント側の処理はあまり調べられていない
-
AndroidかどうかはServiceProviders#loadAll内で
ClassLoader
を利用して判別している。 -
Androidではない場合、ServiceProviders#getCandidatesViaServiceLoaderの中でjava.util.ServiceLoader#loadを利用してMETA-INF/services/io.grpc.ServerProviderに書かれているクラスを取得している
- java.util.ServiceLoader#loadって初めて知った。。
Nettyについて
gRPC-Javaで利用されているNettyとは、Javaでnon-blocking I/O(以下、NIO)のアプリケーションを作成できるフレームワークである。(Java サーブレットは使わない)
gRPCでの登場するスレッドやNIOのイベント名、Nettyの処理はこんな感じ。
- bossスレッドで
OP_ACCEPT
を検知し、OP_READ
イベントを登録 -
OP_READ
やOP_WRITE
の処理はworkerスレッドが実行 - workerスレッドはクライアントのリクエスト内容をreadして、gRPCのメソッド呼び出しが必要になったタイミングでexecutorスレッドを生成して処理を実行させる
登場人物 | 役割 |
---|---|
bossスレッド (ServerBootstrapのparentGroup) |
Nettyが利用するネットワークのI/Oを検知するスレッド。起動時にデフォルトでは1つ生成される。 |
workerスレッド (ServerBootstrapのchildGroup) |
Nettyが利用するI/Oイベントごとの処理をするスレッド。デフォルトはプロセッサの数x2。 |
executorスレッド | gRPCで定義したメソッドを実行するスレッド。メソッド呼び出しごとに生成される。 |
I/Oイベント名 | 概要 |
---|---|
OP_ACCEPT | クライアントからの接続 |
OP_READ | ネットワークI/Oの読み取り |
OP_WRITE | ネットワークI/Oの書き出し |
補足
Nettyについては「JJUG CCC 2018 Spring - I-7 (俺が)はじめての Netty」がすごく参考になった。
HTTP/2について
googleのIntroduction to HTTP/2より、gRPCを理解するためのポイントを抜粋。
HTTP/1.xでは改行で区切られたプレーンテキストが1つのリクエスト(またはレスポンス)とされているが、HTTP/2ではこれを1つのメッセージと表し、さらにメッセージは以下のフレーム単位に分割される
- HEADERSフレーム(HTTP/1.xのヘッダ)
- DATAフレーム(HTTP/1.xのボディ)
またフレームは1つのTCP接続で並列、双方向にやりとりができる。
つまりリクエスト毎にTCPコネクションを確立しなくて済み、一つのリクエストに対して複数のレスポンスや、サーバプッシュなどが可能となる。
gRPC-Javaの処理概要
これより先は「コードのどの部分でどんな処理を〜」みたいな話になるので、その前に一連の処理について自分の中で解釈した内容を要約。
- gRPCサーバは起動時にNettyが利用するbossスレッドを生成する
- gRPCクライアントがgRPCサーバへリクエストする
- bossスレッドはネットワークI/Oイベントの
OP_ACCEPT
を検知するとNettyのworkerスレッドを生成(もしくはプールから取得)してOP_READ
/OP_WRITE
の処理を委譲 - workerスレッドはTCPコネクション確立(3ウェイハンドシェイクなど)や、HTTP/2のHEADERSフレーム、DATAフレームなどI/Oイベントが発生するたびに呼び出される
- クライアントからgRPC呼び出しに必要なHTTP/2のメッセージ(複数のフレーム)の受け取りを完了したら、workerスレッドはgRPCメソッド実行用のexecutorスレッドを生成する
- Nettyとしてはbossスレッドとworkerスレッドのみで動作するイベントループの仕組みであるが、そこからgRPCメソッド呼び出しごとにスレッドが生成される
- そのためgRPCメソッド内でブロッキングな処理を実行してもNettyのスレッドはブロックされないようである
- executorスレッドはgRPCメソッドを実行し、レスポンスの
OP_WRITE
を登録 - workerスレッドが
OP_WRITE
を検知して、クライアントへレスポンスを返す - クライアントがまだ接続を続ける場合は再度TCPコネクションを確立する必要がないので、
5. ~ 7.
の処理を何度も並列で実行できる - クライアントが接続を切る場合はTCPの切断を行う
Server側のコードリーディング
TODO 自分のメモ用に書き殴ってるので、あとで追加とか整理する。(いろんなスレッドを生成したり、コールバックでさらに新たなスレッド生成したりとかで追うのが難しかったので間違っていたら教えてもらえると嬉しいです。。)
起動
- RouteGuideServer#mainを実行
- ServerBuilder#forPortでServerBuilderのインスタンスを取得する
- ServiceProviders#loadAllの中でClassLoaderを見てAndroidアプリかそうでないかを判別してる
- Androidでない場合は、java.utilパッケージのServiceLoader#loadの仕組みを使ってMETA-INF/services/io.grpc.ServerProviderからNettyServerProvider.javaのインスタンスを取得している
- NettyServerBuilder#addServiceで、自動生成されたxxxGrpc.xxxImplBaseを継承したクラスを渡す
- ここでProtocol Buffersで定義したメソッドがbindされる
- AbstractServerImplBuilder.java#L146で自動生成されたコードのbindServiceが呼ばれるため
- NettyServerBuilder#build
- ServerImplインスタンスを作成して返す
- この時AbstractServerImplBuilder#buildTransportServerでNettyServerのインスタンスを生成して引数に渡し、ServerImplの持っているInternalServer transportServerというフィールドへ
- Builderから取得したServerImpl#start内で、NettyServer#startを実行
- NettyServer#allocateSharedGroupsでNettyに必要なparent(boss)とchild(worker)のNioEventLoopGroupインスタンスを作成
- ServerBootstrapをnewしてNettyサーバを起動させる
- ChannelInitializerを実装し、ServerBootstrap#childHandlerに渡す
- io.netty.bootstrap.AbstractBootstrap#bind
- initAndRegisterでbossスレッドを起動
- io.netty.channel.nio.NioEventLoop.runで
OP_ACCEPT
イベントの待ち受け開始
- io.netty.channel.nio.NioEventLoop.runで
- initAndRegisterでbossスレッドを起動
- executorに
grpc-default-executor-%d
の名前のスレッドを生成するThreadFactory(ThreadFactoryBuilder$ThreadFactory)を持つThreadPoolExecutorを渡す- gRPCのメソッドは呼ばれるたびにここからスレッドを生成して実行する
- NettyServerを起動するとmainスレッドは、Runtime.getRuntime().addShutdownHook()を使って、終了時の処理を定義している
- さらにmainスレッドはRouteGuideServer.blockUntilShutdownを呼び出してWAIT状態へ
リクエスト受信
- workerスレッドがgRPCメソッドの呼び出しリクエストを受け付けると、NettyServerHandlerがServerImpl$ServerTransportListenerImpl#streamCreatedを実行する
- ServerImpl$ServerTransportListenerImpl#streamCreatedはContextRunnableを継承したStreamCreatedのインスタンスを、ThreadPoolExecutorをwrapしたSerializingExecutorに渡す
- このexecuteはThreadFactoryBuilder$ThreadFactoryで
grpc-default-executor-%d
というスレッド名で以下のRunnableタスクを実行する- StreamCreated#run->StreamCreated#runInContextが実行される
- そしてServerImpl#JumpToApplicationThreadServerStreamListener$MessagesAvailable#runInContextが呼ばれる
- さらにServerImpl$JumpToApplicationThreadServerStreamListener$1HalfClosed#runInContextHalfClosedgRPCのメソッドが実行される
- gRPCで定義した
getFeature
が呼ばれる時はこんな感じ
getFeature:130, RouteGuideServer$RouteGuideService (grpc.routeguide)
invoke:462, RouteGuideGrpc$MethodHandlers (grpc.routeguide)
onHalfClose:171, ServerCalls$UnaryServerCallHandler$UnaryServerCallListener (io.grpc.stub)
halfClosed:283, ServerCallImpl$ServerStreamListenerImpl (io.grpc.internal)
runInContext:710, ServerImpl$JumpToApplicationThreadServerStreamListener$1HalfClosed (io.grpc.internal)
run:37, ContextRunnable (io.grpc.internal)
run:123, SerializingExecutor (io.grpc.internal)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:748, Thread (java.lang)