16
10

More than 5 years have passed since last update.

gRPC-Javaの仕組みが気になったのでコードリーディングしてみた

Last updated at Posted at 2019-01-06

はじめに

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について

gRPC-Javaで利用されているNettyとは、Javaでnon-blocking I/O(以下、NIO)のアプリケーションを作成できるフレームワークである。(Java サーブレットは使わない)

gRPCでの登場するスレッドやNIOのイベント名、Nettyの処理はこんな感じ。

  • bossスレッドでOP_ACCEPTを検知し、OP_READイベントを登録
  • OP_READOP_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の処理概要

これより先は「コードのどの部分でどんな処理を〜」みたいな話になるので、その前に一連の処理について自分の中で解釈した内容を要約。

  1. gRPCサーバは起動時にNettyが利用するbossスレッドを生成する
  2. gRPCクライアントがgRPCサーバへリクエストする
  3. bossスレッドはネットワークI/OイベントのOP_ACCEPTを検知するとNettyのworkerスレッドを生成(もしくはプールから取得)してOP_READ/OP_WRITEの処理を委譲
  4. workerスレッドはTCPコネクション確立(3ウェイハンドシェイクなど)や、HTTP/2のHEADERSフレーム、DATAフレームなどI/Oイベントが発生するたびに呼び出される
  5. クライアントからgRPC呼び出しに必要なHTTP/2のメッセージ(複数のフレーム)の受け取りを完了したら、workerスレッドはgRPCメソッド実行用のexecutorスレッドを生成する
    1. Nettyとしてはbossスレッドとworkerスレッドのみで動作するイベントループの仕組みであるが、そこからgRPCメソッド呼び出しごとにスレッドが生成される
    2. そのためgRPCメソッド内でブロッキングな処理を実行してもNettyのスレッドはブロックされないようである
  6. executorスレッドはgRPCメソッドを実行し、レスポンスのOP_WRITEを登録
  7. workerスレッドがOP_WRITEを検知して、クライアントへレスポンスを返す
  8. クライアントがまだ接続を続ける場合は再度TCPコネクションを確立する必要がないので、5. ~ 7.の処理を何度も並列で実行できる
  9. クライアントが接続を切る場合は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を継承したクラスを渡す
  • 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イベントの待ち受け開始
  • executorにgrpc-default-executor-%dの名前のスレッドを生成するThreadFactory(ThreadFactoryBuilder$ThreadFactory)を持つThreadPoolExecutorを渡す
    • gRPCのメソッドは呼ばれるたびにここからスレッドを生成して実行する
  • NettyServerを起動するとmainスレッドは、Runtime.getRuntime().addShutdownHook()を使って、終了時の処理を定義している
  • さらにmainスレッドはRouteGuideServer.blockUntilShutdownを呼び出してWAIT状態へ

リクエスト受信

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)
16
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
10