🐡

知っておきたいgRPCの通信の基本の仕組み

に公開

このブログの対象読者

  • gRPCというものが何かは知っている人
  • gRPCの通信の仕組みを知りたい人
  • gRPCを使用していて接続エラーなどで困ったことがある人

gRPCとは

この辺はある程度知っているという前提で簡単に説明。

gRPCはHTTP/2をベースにしたRPCフレームワーク。
Googleが開発していたStubbyというRPCインフラが前身。
ロードバランシングや認証、ヘルスチェック、流量制限といった機能と容易に統合できる設計を持ち、Kubernetesなどの周辺ツールと組み合わせて運用されることが多い。
gRPCはGoをはじめとする C++, Java, Python, JavaScript, Ruby, C#, Swift などの複数の言語で公式にサポートされており、異なる言語間でもシームレスな通信が可能。
通常は Protocol Buffers(Protobuf)を使用してRPCエンドポイント(サービス)とデータを定義する。
これにより、異なる言語で開発されたサーバー間でリクエスト/レスポンスの定義にずれを生じさせずに開発できる。

gRPCの通信の仕組み

前述の通りgRPCはHTTP/2がベースだが、フレームワークとしてどのようにこれを抽象化しているのかを掴むことが大事である。
まずはHTTP/2について簡単にみてみる。

HTTP/2

HTTP/2は、いってしまえばHTTP/1.1のパフォーマンスを向上させるために登場した技術。
HTTP/1.1は広く普及しているプロトコルだが、いくつかのパフォーマンス上の問題がある。

  • 単一のTCPコネクション(Keep-Alive前提)で複数のリクエストを並行に処理できない(Head-of-Line Blocking)
  • あまり変わることのないヘッダーでもテキスト形式で繰り返し送信
  • テキストベースのプロトコルのためデータ量が多い & パースコストがかかる

昨今のWebアプリケーションは見た目や機能もリッチになり、大量の通信も必要となってきたため、上記を改善するためにHTTP/2が登場した。
以下のような機能がある。

  • 単一TCPコネクションを利用したフレームとストリームによるマルチプレキシング
  • ヘッダ圧縮(HPACK)
  • ストリームの優先順位づけ
  • サーバプッシュ

このブログにおいて関係してくるのは、フレームとストリームによるマルチプレキシングの部分なので図で解説する。

http2

図のファイルの図形はフレームと呼ばれるHTTP2の通信単位である。
フレームはいくつか種類がある。ストリームを開始するためのHEADERSフレームや、データを表すDATAフレームなどなど。

続いて、ストリームは複数のフレーム(HEADERS, DATA など)によって構成される論理的な単位である
ストリームの実体はフレームである。
フレームにはHTTP/2コネクション全体に関する情報のやり取りとストリームに関する情報のやり取りがあり、後者のフレーム種別の場合はストリームIDを内部に含んでおり、これがストリームを構成する。
ストリームは従来のHTTP/1.1のリクエストとレスポンスとは異なり、複数のストリームが単一のTCPコネクションに存在しても良い。
また、ストリームはクライアントとサーバーのどちらからでも閉じることができる上に、どちらからも任意個数のフレームを送信できる。
gRPCのストリーム通信はこれによって実現されている。

図の解説に戻る。
HTTP/1.1の場合、単一のTCPコネクションではGET /users/bobGET /icons/bobは順番に送信されて処理される(Head-of-Line Blocking)。
2番目のリクエストであるGET /icons/bobの処理開始タイミングは最初のGET /users/bobにどれだけ時間がかかるかに左右される。
しかし、大量の通信をする必要がある現代のWebでは通信は並行に行われて欲しい。そうでないと、重い処理を伴うリクエストに他の処理が引っ張られる。
というわけで、HTTP/2を使用するとそれぞれのリクエストが図のように交互に多重化される。
こうすることにより、サーバーは両方のリクエストを並行に捌くことができるようになり、HTTPレベルのHead-of-Line Blockingを解決できる(TCPレベルでのHead-of-Line Blockingは生じる可能性あり)。

※ HTTP/2の詳細はRFC9113を参照。

gRPCはどのようにHTTP/2を抽象化しているのか

gRPCでは以下の概念を用いて通信を抽象化している。

  • Channel
  • Resolver
  • LoadBalancer
  • Subchannel
  • RPC
  • Message
  • Socket

以下はgrpc-goの場合の概念図。
他の言語の場合は実装が微妙に異なることもあるかもしれないが、登場する概念は同じかと。

gRPC2

Channel

gRPCクライアントが持つ、あるエンドポイントへの仮想的なコネクション。
エンドポイントはIPで指定することもあれば、ドメイン名で指定することもある。
ドメイン名で指定すると名前解決の結果として複数のIPが得られる可能性があるが、その場合、ChannelはすべてのIPに対してHTTP/2コネクションを持つことができる(後述のLoadBalancerのポリシー次第)。
その各IPに対するコネクションのことをSubchannelと呼ぶ。

チャネルによって、名前解決、TCP接続の確立、TLSハンドシェイク、エラー時の再接続などがカプセル化されている。

チャネルには以下のような接続ステータスがある。

状態 詳細内容
CONNECTING 名前解決、TCP接続の確立、TLSハンドシェイクなど、接続確立に必要な各ステップの進捗を待っている状態。チャネル生成時の初期状態
READY すべての接続手続き(名前解決、TCP接続、TLSハンドシェイク、HTTP/2などのプロトコルレベルの手続き)が完了し、以降の通信試行が成功している状態。通信中にエラーが発生していない、またはエラーが保留中である状態とも言える。
TRANSIENT_FAILURE 一時的な障害(例:TCPハンドシェイクのタイムアウトやソケットエラーなど)が発生した状態。一時的な失敗が起こった場合、チャネルは指数関数的バックオフを経て再接続を試み、最終的にCONNECTING状態へ移行する。
IDLE 新規または保留中のRPCが存在しないため、接続の維持や新たな接続確立を試みていない状態。新たなRPCが発生すると自動的にCONNECTING状態に戻る。一定期間(デフォルト5分間)RPC活動がない場合、READYやCONNECTING状態からIDLEに遷移する。
SHUTDOWN チャネルのシャットダウンが開始された状態。新規のRPCは即座に失敗し、保留中のRPCはキャンセルされるまで継続される。一度この状態に入ると、以降他の状態には遷移せず、チャネルは永久にこの状態となる。

また、各状態は次のように遷移する。

Channelとそのステータスを確認することで、アプリケーションで使用しているgRPCクライアントがどのような状態になっているのかを確認することができる。
その確認方法は後述する。

Resolver

gRPCクライアント生成時に渡されるエンドポイントを名前解決するコンポーネント。
取得したIPはすべてLoadBalancerへ引き渡される。

LoadBalancer

Resolverから接続情報を受け取り、Subchannelを生成・管理するコンポーネント。
名前の通り、クライアントのリクエストを各Subchannelに分配することでサーバーへの負荷を分散させることができる。
負荷分散にはポリシーが存在し、デフォルトではpick_firstポリシーが使用される。
このポリシーは実際には負荷分散を行わず、Resolverから取得した各アドレスを試行し、接続できる最初のアドレスを使用するというもの。
他にも順番にリクエストしていくround_robinなどもあるようだが、カスタムでポリシーを作成することもできる。

というわけで、図はround_robinなどの複数の接続先にリクエストを振り分ける場合のものであり、デフォルトのpick_firstの場合は最初に接続成功したサーバーへのコネクションを持つだけである。

Subchannel

ある1台のgRPCサーバーへの接続を表す。
各SubchannelもChannel同様の接続ステータスを持っている。
図では203.0.113.1203.0.113.2の2つの接続先があったため、それぞれの接続先用のSubchannelがLoadBalancerによって作成されている。

ちなみに、SubchannelはChannelとSubchannelを内部に持つことができるが、ソースコードを読んだらSubchannelが複数のアドレスを持つことはDeprecateされていそうな雰囲気だった(経緯までは調べてない)。

Each SubConn contains a list of addresses. gRPC will try to connect to the
addresses in sequence, and stop trying the remainder once the first
connection is successful. However, this behavior is deprecated. SubConns
should only use a single address.

RPC

Subchannelでやり取りされる実際のHTTP/2のStreamを表す。

Message

RPCでやり取りされるデータフレームのこと(図では省略されている)。
Protocol BuffersなどのIDLで定義されたメッセージのことでもある。

Socket

そのまんまソケットのこと。
書ききれなかったので図ではクライアント側のソケットは省略している。

Channelのdebug

Log Levelの変更

ログをデバッグモードで出すというお手軽な方法(Go以外の場合は各実装のリポジトリを参照。一応共通の環境変数はこれ)。
ここに書かれているように、環境変数を設定してアプリケーションを実行する。

GRPC_GO_LOG_VERBOSITY_LEVEL=99 GRPC_GO_LOG_SEVERITY_LEVEL=info your-app

こんな感じで、ログ出力にChannel、Subchannelなどに関する情報が表示されるようになる(gRPCクライアントをlocalhost:8000に向けて生成した時のログ)。

2025/03/16 21:03:46 INFO: [core] original dial target is: "localhost:8080"
2025/03/16 21:03:46 INFO: [core] [Channel #2]Channel created
2025/03/16 21:03:46 INFO: [core] [Channel #2]parsed dial target is: resolver.Target{URL:url.URL{Scheme:"dns", Opaque:"", User:(*url.Userinfo)(nil), Host:"", Path:"/localhost:8080", RawPath:"", OmitHost:false, ForceQuery:false, RawQuery:"", Fragment:"", RawFragment:""}}
2025/03/16 21:03:46 INFO: [core] [Channel #2]Channel authority set to "localhost:8080"
2025/03/16 21:03:46 INFO: [core] [Channel #2]Channel exiting idle mode
2025/03/16 21:03:46 INFO: [core] [Channel #2]Resolver state updated: {
  "Addresses": [
    {
      "Addr": "172.18.0.4:8080",
      "ServerName": "",
      "Attributes": null,
      "BalancerAttributes": null,
      "Metadata": null
    }
  ],
  "Endpoints": [
    {
      "Addresses": [
        {
          "Addr": "172.18.0.4:8080",
          "ServerName": "",
          "Attributes": null,
          "BalancerAttributes": null,
          "Metadata": null
        }
      ],
      "Attributes": null
    }
  ],
  "ServiceConfig": null,
  "Attributes": null
} (resolver returned new addresses)
...

Channelz

Channelに関するログを出力できるようにはしたが、今Channelがどうなっているのかが知りたい時がある。
Channelzは、ChannelのLive Debuggingをしやすくしようよということで誕生した機能。
詳細を知りたい場合は公式を見る方が早い。

ここではかいつまんで話す。

Channezは以下の4つのエンティティに順序づけた一意のIDを付与し、内部的に統計情報を収集・管理する。
ユーザはChannelz APIを叩くことでChannelzから統計情報を取得できる。

  • Channel
  • Subchannel
  • Server
  • Socket

各エンティティが扱っているデータをそれぞれ見ていく。

ChannelsとSubchannels

Channelsエンティティは、クライアント側で作成されたgRPC Channelの概要情報を提供する。各Channelは一意のIDとreferenceを持ち、接続状態、開始されたRPC呼び出し数、成功・失敗・キャンセルの統計、アクティブなストリーム数などのデータを収集している。また、Channelは内部で複数のSubchannelを管理している。Subchannelは、個々のサーバーへの接続ユニットとして機能し、それぞれが固有のIDとreferenceを保持する。Subchannelは、接続状態や再接続試行状況、関連するRPC呼び出しの統計情報、管理するSocketの一覧などを提供している。これにより、どのサブチャネルがどのバックエンドに接続しているかが明確になる。

Sockets

Socketsエンティティは、HTTP/2の基盤となるTCPソケットレベルの詳細な情報を提供する。ソケットエンティティでは、ローカルおよびリモートのIPアドレス、ポート番号、接続確立時刻、持続時間、最終の読み書きタイムスタンプ、送受信バイト数、パケット数、エラー件数などのネットワーク統計を収集している。これにより、ネットワークレベルでのパフォーマンスや障害の発生状況を把握できる。

Servers

Serversエンティティは、gRPCサーバーとしての内部状態を反映する。ここでは、受け入れた接続数、現在のアクティブな接続数、過去の接続履歴、サーバー上で処理されたRPC呼び出しの総数、成功/失敗/キャンセルされた呼び出し数などの統計情報を管理している。また、各サーバーに紐付くソケットの情報も取得可能であり、サーバー全体の健全性やパフォーマンスを評価することができる。

Channelzのクエリ

Channelz APIでは、各エンティティの状態や統計情報を問い合わせるためのクエリが用意されている。主なクエリは次の通りである。

  • GetChannel
    指定したChannel IDの詳細情報および統計情報を取得する。
  • GetSubchannel
    特定のSubchannelの状態、統計、関連するSocketの一覧などの情報を取得する。
  • GetSocket
    Socketエンティティの詳細情報(接続状態、IPアドレス、バイト数など)を取得する。
  • GetServer
    サーバーエンティティの統計情報および状態を取得する。
  • GetTopChannels
    トップレベルのChannel一覧を取得し、全体の接続状況を把握する。

これらのクエリを利用することで、運用中のgRPCシステムの内部状態をリアルタイムに監視し、デバッグやパフォーマンスチューニングに役立てることができる。

Channelzの活用

Channelzを活用するためには、ChannelzサービスをgRPCサーバーに登録する必要がある。
例えアプリケーション本来がgRPCサーバーとしての機能を持たない場合でも、Channelzの統計情報を取得するために、Channelzサービスを登録し、サーバーとして稼働させる必要がある。
Goの場合、以下のようにChannelzサービスを登録する。

import (
    "google.golang.org/grpc"
    cs "google.golang.org/grpc/channelz/service"
)

func main() {
    gs := grpc.NewServer()
    // 他のサービスの登録処理
    cs.RegisterChannelzServiceToServer(gs)
    lis, err := net.Listen("tcp", ":8080")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    log.Println("gRPC server with Channelz is listening on :8080")
    if err := gs.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

この設定により、Channelz用のエンドポイントが有効になり、外部ツールやAPIクライアントからChannelzクエリを実行して、内部状態を取得することができる。

最後に、Channelzのデータを可視化するためのツールも二つほど紹介する。
上記のChannelzサービス登録を実施したらこれらのツールを使用してみて欲しい。

grpcdebug

Go製のCLIツール。
細かい使い方はGitHubのリポジトリのREADMEに載っているので割愛するが、自分はこちらを使用してChannelzのデータを確認している。
go installするだけで使えるのでシンプルで扱いやすい。

gdebug

gRPC-experimentsの可視化ツール。EnvoyとAngularベースのWebアプリでdocker composeで稼働する。
ただし、メンテナンスが最後にされたのが6年前ということもあり、自分はローカルで動かしたらEnvoyが動かなかった。
ちなみに、READMEを読む限りこのリポジトリはそもそも実験的な機能を試すためのもののようなので、メンテされてないのも仕方ないことではある。

このリポジトリの目的は、gRPCの新機能の実験に関わるコードや様々なドキュメントをプッシュするための単一の場所を持つことです。このリポジトリはテストされていませんし、軽くキュレーションされるだけなので、ここにあるコードは実運用に耐えうるものであると考えるべきではないでしょう(全く機能しないかもしれません)。

最後に

ChannelとSubchannelのイメージを掴むのにやや苦労したのでこのブログが誰かの役にってくれると嬉しい。
ではまた。

参考

Discussion