Read OSS

gRPC コアアーキテクチャ:世界で最も使われている RPC フレームワークを読み解くための地図

中級

前提知識

  • C++ の基礎知識(クラス、テンプレート、スマートポインタ)
  • RPC の概念と gRPC が高レベルで何をするかの理解
  • ビルドシステム(Bazel または CMake)の基本的な理解

gRPC コアアーキテクチャ:世界で最も使われている RPC フレームワークを読み解くための地図

grpc/grpc リポジトリは、インフラ領域で最も重要なオープンソース C++ コードベースの1つです。コード量は200万行を超え、Python や Go のラッパーから Ruby、PHP に至るまで、ほぼすべてのプログラミング言語の gRPC を支える共有 C++ コアライブラリが収められています。このコードベースに貢献したり、内部を理解したりするには、まず全体像を把握することが欠かせません。本記事はその「地図」となることを目指しています。

レイヤー構造、ディレクトリ構成、すべてのコンポーネントを結びつける CoreConfiguration シングルトン、初期化ライフサイクル、そして新機能の段階的な有効化を管理する実験システムについて順を追って説明します。読み終えた後は、gRPC コアのどこに何があるかがわかるようになるでしょう。

リポジトリの概要とレイヤー構造

gRPC C++ コアは、厳格なレイヤー構造を採用しています。最上位には公開 C++ API(include/grpcpp/)があり、その下に公開 C API(include/grpc/)が続きます。これらの API の下には「サーフェス層」があり、不透明な C 型と内部の C++ 実装を橋渡しする役割を担っています。サーフェス層の下にあるのがコアライブラリ本体で、チャネル、コール、フィルター、リゾルバー、ロードバランサー、Promise ベースの非同期ランタイムが含まれます。最下層には、トランスポート実装(HTTP/2、chaotic_good)と I/O 抽象化レイヤー(EventEngine、および旧来の iomgr シム)があります。

flowchart TD
    A["C++ API<br/>(include/grpcpp/)"] --> B["C API<br/>(include/grpc/)"]
    B --> C["Surface Layer<br/>(src/core/lib/surface/)"]
    C --> D["Core Libraries<br/>Channels, Calls, Filters,<br/>Resolvers, LB Policies"]
    D --> E["Transports<br/>chttp2, chaotic_good"]
    E --> F["EventEngine / iomgr<br/>I/O, DNS, Timers, Thread Pool"]

include/grpc/grpc.h で宣言されている C API は、「上位レベルのライブラリによってラップされることを意図した低レベルライブラリ」と説明されています。これが安定した ABI の境界です。C++ ラッパーである grpcpp は、慣用的な RAII を提供しており、C++ アプリケーションが直接やり取りするのはほぼこちらです。

このレイヤー構造には明確な意図があります。Python、Ruby、PHP などの言語バインディングは C API に対してコンパイルされ、Go と Java は独自の純粋実装を持っています。C++ コアは gRPC プロトコルのリファレンス実装という位置づけです。

ディレクトリ構造のウォークスルー

これだけの規模のコードベースでは、「どこに何があるか」を把握することが理解の半分を占めます。重要なトップレベルパスのディレクトリマップを以下に示します。

ディレクトリ 役割
include/grpc/ 公開 C API ヘッダー(安定 ABI)
include/grpcpp/ 公開 C++ API ヘッダー
src/core/ 共有 C++ コアライブラリ
src/cpp/ C++ 言語バインディングの実装
src/python/ Python バインディングのグルーコード
test/ テスト(core、cpp、end2end)
tools/ ビルドツール、コード生成、CI スクリプト
doc/ プロトコル仕様、設計ドキュメント
BUILD 自動生成されたルート Bazel BUILD ファイル(約166K行)
CMakeLists.txt 自動生成された CMake ファイル(約2.4M行)

src/core/ の内部はドメイン駆動のレイアウトになっています。

サブディレクトリ 役割
src/core/lib/surface/ チャネル、コール、コンプリーションキューのサーフェス
src/core/lib/promise/ ポールベースの Promise ランタイム(Party、コンビネータ)
src/core/lib/transport/ 抽象トランスポートインターフェース
src/core/lib/channel/ チャネル引数、チャネルスタック
src/core/config/ CoreConfiguration シングルトン
src/core/client_channel/ ClientChannel、DirectChannel、サブチャネル、リトライ
src/core/resolver/ 名前解決(DNS、sockaddr、fake)
src/core/load_balancing/ LB ポリシー(pick_first、round_robin、ring_hash など)
src/core/ext/transport/chttp2/ HTTP/2 トランスポート
src/core/ext/transport/chaotic_good/ 実験的な chaotic_good トランスポート
src/core/call/ CallSpine、CallFilters、InterceptionChain
src/core/server/ サーバー実装
src/core/tsi/ トランスポートセキュリティインターフェース
src/core/credentials/ チャネルおよびコールのクレデンシャル
src/core/filter/ 認証フィルター
src/core/ext/filters/ 組み込みフィルター(compression、message_size など)
src/core/lib/experiments/ 機能フラグの YAML 定義
src/core/plugin_registry/ BuildCoreConfiguration の配線

ヒント: コードベースを調べるときは、まず src/core/plugin_registry/grpc_plugin_registry.cc を確認しましょう。すべてのサブシステム登録関数が一覧できるため、「どんなコンポーネントが存在するか」を把握するための最良のインデックスになっています。

CoreConfiguration:配線図

gRPC のモジュール性の核にあるのが CoreConfiguration です。これはすべてのコンポーネントレジストリを保持するグローバルシングルトンで、システム内のすべてのリゾルバー、ロードバランサー、ハンドシェイカー、クレデンシャル型、フィルターはこのオブジェクトを通じて登録されます。

このクラスは src/core/config/core_configuration.h で定義されています。Builder パターンを採用しており、スコープは2種類あります。永続的なビルダー(設定がビルドされるたびに適用され、環境への適応に使用)と、一時的なビルダー(ビルドのたびに破棄され、テストに最適)です。

flowchart LR
    subgraph Builder["CoreConfiguration::Builder"]
        A[channel_args_preconditioning]
        B[channel_init]
        C[handshaker_registry]
        D[resolver_registry]
        E[lb_policy_registry]
        F[channel_creds_registry]
        G[service_config_parser]
        H[endpoint_transport_registry]
    end
    Builder -->|Build| CC["CoreConfiguration singleton"]
    CC -->|Get| Consumers["Channels, Servers, etc."]

Builder は各レジストリへのアクセサメソッドを公開しています。resolver_registry()lb_policy_registry()handshaker_registry() などが該当します。シングルトンは std::atomic<CoreConfiguration*> によるレイジー初期化を採用し、初回アクセス時にロックフリーの compare-and-swap を行います(191〜197行目)。

実際の配線は grpc_plugin_registry.ccBuildCoreConfiguration() で行われます。この単一の関数がシステム内のすべての登録関数を呼び出しています。

flowchart TD
    BC["BuildCoreConfiguration()"] --> H["Handshakers<br/>TCP, HTTP CONNECT, Endpoint Info"]
    BC --> T["Transports<br/>chttp2"]
    BC --> LB["LB Policies<br/>pick_first, round_robin, ring_hash,<br/>weighted_round_robin, priority, etc."]
    BC --> R["Resolvers<br/>DNS, sockaddr, fake"]
    BC --> F["Filters<br/>HTTP, message_size, compression,<br/>security, fault_injection"]
    BC --> CC["Client Channel Configuration"]
    BC --> S["Security<br/>Handshaker factories, auth filters"]

98〜100行目 にある順序に関するコメントに注目してください。「ハンドシェイカーの登録順序はここで非常に重要です。TCP 接続ハンドシェイカーが最後に登録されることで、ハンドシェイカーリストの先頭に追加されます」と記されています。ハンドシェイカーでは登録順序が重要ですが、フィルターは明示的な順序制約を持つ別のトポロジカルソートを使用します。

テスト用途には、WithSubstituteBuilder ヘルパー(154〜186行目)がグローバル設定を一時的に置き換えます。共有プロセス内で独立したテストを実行するために欠かせない仕組みです。

初期化ライフサイクルとチャネル引数

gRPC は参照カウント方式の初期化を採用しています。grpc_init() の呼び出しには、必ず対応する grpc_shutdown() が必要です。src/core/lib/surface/init.cc の実装はシンプルです。

sequenceDiagram
    participant App as Application
    participant Init as grpc_init()
    participant Counter as g_initializations
    participant IO as iomgr
    participant DNS as DNS Resolver

    App->>Init: grpc_init()
    Init->>Counter: ++g_initializations
    alt First initialization (count == 1)
        Init->>IO: grpc_iomgr_init()
        Init->>DNS: Initialize DNS (c-ares or EventEngine)
        Init->>IO: grpc_iomgr_start()
    end
    Note over App: ...use gRPC...
    App->>Init: grpc_shutdown()
    Init->>Counter: --g_initializations
    alt Last shutdown (count == 0)
        Init->>IO: Shutdown iomgr, DNS, timers
    end

grpc_init() の初回呼び出しでは、gpr_once_init を介して do_basic_init が実行され、ミューテックス、条件変数、ロギング、実験フラグ、フォークハンドラー、トレーシングのセットアップが行われます。2回目以降の呼び出しはカウンターをインクリメントするだけです。

シャットダウンはもう少し複雑です。呼び出しスレッドが EventEngine スレッド(またはタイマーマネージャースレッド)の場合、デッドロックを避けるためにクリーンアップ処理がデタッチされたスレッドで実行されます(179〜189行目)。

64〜68行目 にも注目してください。デフォルトの CoreConfiguration ビルダーは g_initializations の静的初期化時、つまり grpc_init() が一度も呼ばれる前に設定されます。これは巧妙なパターンで、明示的な init 呼び出しがなくても BuildCoreConfiguration がデフォルトビルダーとして確実に登録されることを保証しています。

チャネル引数はシステム全体を通じて受け渡される、イミュータブルなキーバリュー形式の設定ストアです。src/core/lib/channel/channel_args.h で定義されており、セキュリティコネクターから channelz の設定まであらゆる情報を保持します。チャネル引数は使用前に ChannelArgsPreconditioningCoreConfiguration 経由で登録)によって前処理され、EventEngine などのコンポーネントがデフォルト値を注入できるようになっています。

ビルドシステムと実験フレームワーク

gRPC は Bazel をプライマリビルドシステムとして採用しています。ルートの BUILD ファイルは約166,000行に及びますが、tools/buildgen/ の YAML テンプレートから自動生成されたものです。CMake ファイルはさらに大きく約240万行で、こちらも自動生成です。これらのファイルを直接編集してはいけません。

実験/機能フラグシステムは、gRPC チームが新機能を段階的にリリースするための仕組みです。実験は src/core/lib/experiments/experiments.yaml に構造化された形式で定義されます。

各実験が持つフィールドは以下のとおりです。

  • name(例:event_engine_dnschaotic_good_framing_layer
  • description:何をゲートしているかの説明
  • expiry date:定期的なレビューを強制する有効期限
  • owner:担当者のメールアドレス
  • test_tags:その実験を対象とする CI テストを制御するタグ
  • オプションのフラグ:allow_in_fuzzing_configrequires(他の実験への依存関係)

ロールアウトの状態は別ファイルの src/core/lib/experiments/rollouts.yaml で管理されます。各実験のデフォルト値は以下のいずれかです。

  • broken:無効化され、すべてのプラットフォームでテストされない
  • false:すべての環境で無効
  • debug:デバッグビルドでは有効、リリースビルドでは無効
  • true:すべての環境で有効

デフォルト値はプラットフォームごとに設定することもでき(iOS、Windows、POSIX)、ターゲットを絞ったロールアウトが可能です。

flowchart LR
    YAML["experiments.yaml<br/>(definitions)"] --> Gen["Code Generation"]
    ROLL["rollouts.yaml<br/>(per-platform defaults)"] --> Gen
    Gen --> H["experiments.h<br/>(compiled flags)"]
    H --> Runtime["Runtime checks<br/>IsXxxEnabled()"]
    ENV["GRPC_EXPERIMENTS env var"] --> Runtime

実行時には、GRPC_EXPERIMENTS 環境変数で実験の設定を上書きできます。PrintExperimentsList() 関数は do_basic_init の中で呼び出され、どの実験が有効かをログに記録します。

ヒント: gRPC のコードを読んでいて IsXxxEnabled() という呼び出しに遭遇したら、experiments.yaml でその実験名を検索して何をゲートしているかを確認し、rollouts.yaml で実際に有効になっているかどうかをチェックしましょう。

次のステップ

このアーキテクチャの地図を手に入れたところで、より深く掘り下げる準備が整いました。次の記事では、gRPC の最も根本的な2つの抽象概念であるチャネルコールの生涯を追います。Channel クラスの階層、C 型と C++ 型を橋渡しする CppImplOf パターン、そしてコールの作成からバッチ操作、コンプリーションキューへの配信までの流れを詳しく見ていきます。さらに、Promise ベースの新しい CallSpine バックボーンと、内部構造を大きく変えつつある V2 から V3 への移行についても取り上げます。