gRPC

gRPC 完全ガイド: 高性能RPCフレームワークの全容

1. はじめに

1.1 gRPCとは

gRPC(gRPC Remote Procedure Call)は、Googleが開発したオープンソースの高性能RPCフレームワークである。2015年にGoogleが社内で使用していたStubbyと呼ばれるRPC基盤の後継として公開され、現在はCloud Native Computing Foundation(CNCF)のもとで管理されている。

gRPCの「g」はリリースバージョンごとに異なる意味が割り当てられており(例: gRPC、good、green、glamorousなど)、一貫した頭字語ではない。しかし、その技術的な意味は明確である。gRPCは、クライアントアプリケーションがローカルオブジェクトのメソッドを呼び出すかのように、別のマシン上のサーバーアプリケーションのメソッドを直接呼び出すことを可能にするフレームワークである。

1.2 従来のAPI通信方式との比較

現代のマイクロサービスアーキテクチャにおいて、サービス間通信は以下のような方式が広く利用されている。

特性REST (HTTP/1.1 JSON)GraphQLgRPC
プロトコルHTTP/1.1HTTP/1.1HTTP/2
データ形式JSON/XMLJSONProtocol Buffers (バイナリ)
スキーマ定義OpenAPI(任意)SDL(必須).proto(必須)
コード生成ツール依存ツール依存ネイティブサポート
ストリーミング限定的Subscription双方向ストリーミング
ブラウザサポートネイティブネイティブgRPC-Web経由
パフォーマンス低〜中
型安全性

1.3 gRPCが解決する課題

gRPCは以下の課題を解決するために設計されている。

  1. パフォーマンス: HTTP/2とProtocol Buffersの組み合わせにより、JSONベースのREST APIと比較して最大10倍のスループット向上を実現する
  2. 型安全性: .protoファイルによる厳密なインターフェース定義により、コンパイル時に型の不整合を検出できる
  3. 多言語対応: 単一の.proto定義から10以上の言語のクライアント・サーバーコードを自動生成できる
  4. 双方向ストリーミング: HTTP/2のストリーム多重化により、単一のTCPコネクション上で複数のリクエスト・レスポンスを同時に処理できる
  5. 契約駆動開発: サービスのインターフェースが明示的に定義され、クライアントとサーバーの開発を独立して進められる

1.4 gRPCの採用事例

gRPCは以下のような大規模システムで採用されている。

  • Google: 内部のマイクロサービス間通信(毎秒数十億のRPCコール)
  • Netflix: マイクロサービスアーキテクチャの通信基盤
  • Slack: リアルタイムメッセージング基盤
  • Uber: サービスメッシュにおけるサービス間通信
  • Square: 決済処理システム
  • CoreOS (etcd): 分散キーバリューストアのクライアント通信
  • Kubernetes: コンポーネント間通信(kubeletとコンテナランタイム間のCRI)
  • Envoy Proxy: xDS APIによる動的設定配信

2. gRPCの基盤技術

2.1 HTTP/2

gRPCはトランスポート層としてHTTP/2を採用している。HTTP/2は以下の特徴を持ち、gRPCの高性能を支えている。

2.1.1 バイナリフレーミング

HTTP/1.1がテキストベースのプロトコルであるのに対し、HTTP/2はバイナリフレームを使用する。すべての通信はフレームと呼ばれる小さなメッセージに分割され、バイナリ形式でエンコードされる。

HTTP/2 フレーム構造:
+-----------------------------------------------+
|                 Length (24bit)                 |
+---------------+-------------------------------+
|   Type (8bit) |   Flags (8bit)               |
+-+-------------+-------------------------------+
|R|         Stream Identifier (31bit)           |
+=+=============+===============================+
|                Frame Payload                  |
+-----------------------------------------------+

主要なフレームタイプ:

  • DATA: リクエスト/レスポンスのボディデータ
  • HEADERS: HTTPヘッダー情報
  • SETTINGS: 接続設定パラメータ
  • WINDOW_UPDATE: フロー制御のウィンドウサイズ更新
  • RST_STREAM: ストリームの異常終了
  • GOAWAY: コネクションの正常終了通知

2.1.2 ストリーム多重化

HTTP/1.1では1つのTCPコネクションで同時に1つのリクエスト/レスポンスしか処理できず(Head-of-Line Blocking問題)、並列処理には複数のTCPコネクションが必要だった。HTTP/2では単一のTCPコネクション上で複数のストリームを多重化でき、各ストリームが独立したリクエスト/レスポンスのペアを処理する。

HTTP/1.1:
Client ──[Request 1]──────────────────> Server
Client <──[Response 1]─────────────────Server
Client ──[Request 2]──────────────────> Server
Client <──[Response 2]─────────────────Server

HTTP/2:
Client ──[Stream 1: Req]──┐
Client ──[Stream 3: Req]──┼──> Server
Client <──[Stream 1: Res]─┤
Client ──[Stream 5: Req]──┤
Client <──[Stream 3: Res]─┤
Client <──[Stream 5: Res]─┘

2.1.3 ヘッダー圧縮(HPACK)

HTTP/2はHPACKと呼ばれるヘッダー圧縮アルゴリズムを使用する。HPACKはハフマン符号化と動的テーブルを組み合わせ、繰り返し送信されるヘッダーを大幅に圧縮する。gRPCではメタデータの送受信にこの圧縮が自動的に適用される。

2.1.4 フロー制御

HTTP/2はコネクションレベルとストリームレベルの2段階のフロー制御を提供する。受信側はWINDOW_UPDATEフレームを送信することで、送信側が送れるデータ量を制御できる。これにより、処理能力の異なるクライアント・サーバー間でも安定した通信が実現される。

2.1.5 サーバープッシュ

HTTP/2ではサーバーがクライアントのリクエストなしにレスポンスを送信できるサーバープッシュ機能がある。ただし、gRPCではこの機能は直接使用されず、代わりにサーバーストリーミングRPCがこの役割を担う。

2.2 Protocol Buffers(protobuf)

Protocol Buffers(protobuf)は、Googleが開発した言語中立・プラットフォーム中立のデータシリアライゼーションフォーマットである。gRPCのデフォルトのシリアライゼーション機構として使用される。

2.2.1 Protocol Buffersの特徴

  1. 効率的なバイナリエンコーディング: JSONと比較して3〜10倍小さいメッセージサイズ
  2. 高速なシリアライゼーション/デシリアライゼーション: JSONの20〜100倍の速度
  3. 前方互換性・後方互換性: フィールドの追加・削除が既存クライアントに影響しない
  4. 厳密な型定義: スキーマからコードを自動生成し、型安全性を保証
  5. 自己記述的: .protoファイルがドキュメントとしても機能

2.2.2 Protocol Buffersのバイナリエンコーディング

Protocol Buffersはタグ-値ペアのバイナリ形式でデータをエンコードする。各フィールドはフィールド番号とワイヤタイプで識別される。

ワイヤタイプ:
0 - Varint (int32, int64, uint32, uint64, sint32, sint64, bool, enum)
1 - 64-bit (fixed64, sfixed64, double)
2 - Length-delimited (string, bytes, embedded messages, repeated fields)
5 - 32-bit (fixed32, sfixed32, float)

例えば、以下のメッセージ定義において:

message Person {
  string name = 1;
  int32 age = 2;
}

name = "John", age = 30 の場合:

JSON:  {"name":"John","age":30}  → 27バイト
Protobuf: 0a 04 4a 6f 68 6e 10 1e  → 8バイト (約70%削減)

内訳:
0a = フィールド1, ワイヤタイプ2 (length-delimited)
04 = 長さ4バイト
4a 6f 68 6e = "John" (UTF-8)
10 = フィールド2, ワイヤタイプ0 (varint)
1e = 30 (varint エンコーディング)

2.2.3 Varintエンコーディング

Protocol Buffersの整数型はVarintエンコーディングを使用する。これは可変長エンコーディングで、小さな値ほど少ないバイト数で表現できる。

値: 300
バイナリ: 100101100 (9ビット)

Varintエンコーディング:
1. 7ビットグループに分割: 0000010 0101100
2. リトルエンディアン順: 0101100 0000010
3. 継続ビット付加: 10101100 00000010
結果: AC 02 (2バイト)

通常のint32では4バイト必要だが、Varintでは2バイトで済む

2.3 Protocol Buffersの言語仕様(proto3)

2.3.1 基本的なメッセージ定義

// proto3構文を指定(デフォルトはproto2)
syntax = "proto3";

// パッケージ名(名前空間の衝突を防ぐ)
package example.api.v1;

// Go用のパッケージオプション
option go_package = "github.com/example/api/v1;apiv1";

// Java用のオプション
option java_package = "com.example.api.v1";
option java_multiple_files = true;
option java_outer_classname = "UserProto";

// 基本的なメッセージ定義
message User {
  // スカラー型フィールド
  string user_id = 1;
  string name = 2;
  string email = 3;
  int32 age = 4;
  bool is_active = 5;
  double score = 6;

  // 列挙型
  UserRole role = 7;

  // ネストされたメッセージ
  Address address = 8;

  // 繰り返しフィールド(リスト)
  repeated string tags = 9;

  // マップ型
  map<string, string> metadata = 10;

  // oneof(排他的フィールド)
  oneof contact {
    string phone = 11;
    string slack_handle = 12;
  }

  // タイムスタンプ(Well-Known Types)
  google.protobuf.Timestamp created_at = 13;
  google.protobuf.Timestamp updated_at = 14;

  // オプショナルフィールド(proto3ではoptionalキーワードで明示)
  optional string nickname = 15;
}

// 列挙型定義
enum UserRole {
  USER_ROLE_UNSPECIFIED = 0;  // proto3では0がデフォルト値
  USER_ROLE_ADMIN = 1;
  USER_ROLE_EDITOR = 2;
  USER_ROLE_VIEWER = 3;
}

// ネストされたメッセージ
message Address {
  string street = 1;
  string city = 2;
  string state = 3;
  string zip_code = 4;
  string country = 5;
}

2.3.2 スカラー型の一覧

// Protocol Buffers スカラー型一覧
//
// 整数型:
//   int32    - 可変長エンコーディング。負の数には非効率
//   int64    - 可変長エンコーディング。負の数には非効率
//   uint32   - 可変長エンコーディング。符号なし
//   uint64   - 可変長エンコーディング。符号なし
//   sint32   - 可変長エンコーディング。負の数に効率的(ZigZagエンコーディング)
//   sint64   - 可変長エンコーディング。負の数に効率的(ZigZagエンコーディング)
//   fixed32  - 固定4バイト。値が2^28より大きい場合に効率的
//   fixed64  - 固定8バイト。値が2^56より大きい場合に効率的
//   sfixed32 - 固定4バイト。符号付き
//   sfixed64 - 固定8バイト。符号付き
//
// 浮動小数点型:
//   float    - 32ビット IEEE 754
//   double   - 64ビット IEEE 754
//
// ブール型:
//   bool     - true/false
//
// 文字列型:
//   string   - UTF-8エンコーディングまたは7ビットASCII
//   bytes    - 任意のバイト列

2.3.3 Well-Known Types

import "google/protobuf/timestamp.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/wrappers.proto";
import "google/protobuf/any.proto";
import "google/protobuf/struct.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/field_mask.proto";

message WellKnownTypesExample {
  // Timestamp: 日時を表現
  google.protobuf.Timestamp created_at = 1;

  // Duration: 時間の長さを表現
  google.protobuf.Duration timeout = 2;

  // Wrappers: null許容のスカラー型
  google.protobuf.StringValue optional_name = 3;
  google.protobuf.Int32Value optional_count = 4;
  google.protobuf.BoolValue optional_flag = 5;

  // Any: 任意の型のメッセージを格納
  google.protobuf.Any details = 6;

  // Struct: 動的なJSON的構造
  google.protobuf.Struct config = 7;

  // Empty: 空のメッセージ(引数なし・戻り値なしのRPCに使用)
  // rpc Ping(google.protobuf.Empty) returns (google.protobuf.Empty);

  // FieldMask: 部分更新のためのフィールド指定
  google.protobuf.FieldMask update_mask = 8;
}

2.3.4 フィールド番号のルールとベストプラクティス

message FieldNumberRules {
  // フィールド番号は1〜536,870,911(2^29 - 1)の範囲
  // 19000〜19999はprotobufの内部使用のため予約
  //
  // ベストプラクティス:
  // - 1〜15: 1バイトエンコーディング。頻繁に使うフィールドに割り当て
  // - 16〜2047: 2バイトエンコーディング
  // - 2048〜: 3バイト以上

  string frequently_used = 1;    // 1バイト (フィールド番号1〜15)
  string less_frequent = 16;     // 2バイト (フィールド番号16〜2047)

  // 削除したフィールドの番号は再利用しない(reserved で予約)
  reserved 4, 5, 100 to 200;
  reserved "old_field_name", "deprecated_field";
}

3. gRPCサービス定義

3.1 サービスの基本構造

gRPCサービスは.protoファイルで定義される。サービスはメソッド(RPC)の集合であり、各メソッドはリクエストメッセージとレスポンスメッセージを持つ。

syntax = "proto3";

package bookstore.v1;

option go_package = "github.com/example/bookstore/v1;bookstorev1";

import "google/protobuf/empty.proto";
import "google/protobuf/field_mask.proto";
import "google/protobuf/timestamp.proto";

// 書籍管理サービスの定義
service BookstoreService {
  // Unary RPC: 単一リクエスト→単一レスポンス
  rpc GetBook(GetBookRequest) returns (GetBookResponse);
  rpc CreateBook(CreateBookRequest) returns (CreateBookResponse);
  rpc UpdateBook(UpdateBookRequest) returns (UpdateBookResponse);
  rpc DeleteBook(DeleteBookRequest) returns (google.protobuf.Empty);
  rpc ListBooks(ListBooksRequest) returns (ListBooksResponse);

  // Server Streaming RPC: 単一リクエスト→ストリームレスポンス
  rpc WatchBooks(WatchBooksRequest) returns (stream BookEvent);

  // Client Streaming RPC: ストリームリクエスト→単一レスポンス
  rpc UploadBookCover(stream UploadBookCoverRequest) returns (UploadBookCoverResponse);

  // Bidirectional Streaming RPC: ストリームリクエスト→ストリームレスポンス
  rpc ChatAboutBooks(stream ChatMessage) returns (stream ChatMessage);
}

// === リクエスト/レスポンスメッセージ ===

message GetBookRequest {
  string book_id = 1;
}

message GetBookResponse {
  Book book = 1;
}

message CreateBookRequest {
  Book book = 1;
  string idempotency_key = 2;  // 冪等性キー
}

message CreateBookResponse {
  Book book = 1;
}

message UpdateBookRequest {
  Book book = 1;
  google.protobuf.FieldMask update_mask = 2;  // 部分更新マスク
}

message UpdateBookResponse {
  Book book = 1;
}

message DeleteBookRequest {
  string book_id = 1;
}

message ListBooksRequest {
  int32 page_size = 1;     // ページサイズ(最大100)
  string page_token = 2;   // ページトークン
  string filter = 3;       // フィルタ式(例: "author = 'Doe'")
  string order_by = 4;     // ソート順(例: "title asc")
}

message ListBooksResponse {
  repeated Book books = 1;
  string next_page_token = 2;
  int32 total_count = 3;
}

message WatchBooksRequest {
  string filter = 1;       // 監視対象のフィルタ
}

message BookEvent {
  enum EventType {
    EVENT_TYPE_UNSPECIFIED = 0;
    EVENT_TYPE_CREATED = 1;
    EVENT_TYPE_UPDATED = 2;
    EVENT_TYPE_DELETED = 3;
  }
  EventType event_type = 1;
  Book book = 2;
  google.protobuf.Timestamp event_time = 3;
}

message UploadBookCoverRequest {
  oneof data {
    BookCoverMetadata metadata = 1;  // 最初のメッセージ
    bytes chunk = 2;                  // 後続のデータチャンク
  }
}

message BookCoverMetadata {
  string book_id = 1;
  string content_type = 2;   // "image/jpeg", "image/png"
  int64 total_size = 3;
}

message UploadBookCoverResponse {
  string cover_url = 1;
  int64 bytes_received = 2;
}

message ChatMessage {
  string user_id = 1;
  string content = 2;
  google.protobuf.Timestamp timestamp = 3;
}

// === ドメインモデル ===

message Book {
  string book_id = 1;
  string title = 2;
  string author = 3;
  string isbn = 4;
  string description = 5;
  int32 page_count = 6;
  repeated string categories = 7;
  BookStatus status = 8;
  double price = 9;
  string currency = 10;
  string cover_url = 11;
  google.protobuf.Timestamp published_at = 12;
  google.protobuf.Timestamp created_at = 13;
  google.protobuf.Timestamp updated_at = 14;
}

enum BookStatus {
  BOOK_STATUS_UNSPECIFIED = 0;
  BOOK_STATUS_DRAFT = 1;
  BOOK_STATUS_PUBLISHED = 2;
  BOOK_STATUS_OUT_OF_PRINT = 3;
}

3.2 4つの通信パターン

gRPCは4つの通信パターンを提供する。

3.2.1 Unary RPC(単項RPC)

最も基本的なパターン。クライアントが1つのリクエストを送信し、サーバーが1つのレスポンスを返す。従来のHTTP REST APIに最も近い通信パターンである。

Client                          Server
  |                               |
  |------- Request (1回) -------->|
  |                               |
  |<------ Response (1回) --------|
  |                               |

ユースケース: CRUD操作、認証、単純なクエリ

3.2.2 Server Streaming RPC(サーバーストリーミングRPC)

クライアントが1つのリクエストを送信し、サーバーがストリームでレスポンスを返す。サーバーはすべてのメッセージを送信後、ストリームを終了する。

Client                          Server
  |                               |
  |------- Request (1回) -------->|
  |                               |
  |<------ Response 1 ------------|
  |<------ Response 2 ------------|
  |<------ Response 3 ------------|
  |<------ Response N ------------|
  |<------ Stream End ------------|
  |                               |

ユースケース: リアルタイムフィード、大量データの段階的配信、イベント通知

3.2.3 Client Streaming RPC(クライアントストリーミングRPC)

クライアントがストリームでリクエストを送信し、サーバーが1つのレスポンスを返す。サーバーはクライアントがすべてのメッセージを送信し終えてから(または途中で)レスポンスを返す。

Client                          Server
  |                               |
  |------- Request 1 ------------>|
  |------- Request 2 ------------>|
  |------- Request 3 ------------>|
  |------- Request N ------------>|
  |------- Stream End ----------->|
  |                               |
  |<------ Response (1回) --------|
  |                               |

ユースケース: ファイルアップロード、センサーデータの集約、バッチ処理

3.2.4 Bidirectional Streaming RPC(双方向ストリーミングRPC)

クライアントとサーバーが独立してストリームでメッセージを送受信する。2つのストリームは独立して動作し、任意の順序でメッセージを送受信できる。

Client                          Server
  |                               |
  |------- Request 1 ------------>|
  |<------ Response 1 ------------|
  |------- Request 2 ------------>|
  |------- Request 3 ------------>|
  |<------ Response 2 ------------|
  |<------ Response 3 ------------|
  |------- Stream End ----------->|
  |<------ Response 4 ------------|
  |<------ Stream End ------------|
  |                               |

ユースケース: チャット、リアルタイムゲーム、協調編集、双方向データ同期


4. gRPCの実装

4.1 開発環境のセットアップ

4.1.1 Protocol Buffersコンパイラのインストール

# macOS
brew install protobuf

# Ubuntu/Debian
sudo apt-get install -y protobuf-compiler

# バージョン確認
protoc --version
# libprotoc 25.1

4.1.2 Go言語での環境構築

# gRPCとProtocol Buffersプラグインのインストール
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

# PATHの設定
export PATH="$PATH:$(go env GOPATH)/bin"

# プロジェクト構造
mkdir -p myservice/{cmd/{server,client},proto,internal/{server,client}}
cd myservice
go mod init github.com/example/myservice
go get google.golang.org/grpc
go get google.golang.org/protobuf

4.1.3 Python環境での環境構築

# gRPCツールのインストール
pip install grpcio grpcio-tools grpcio-reflection

# コード生成
python -m grpc_tools.protoc \
  -I./proto \
  --python_out=./generated \
  --grpc_python_out=./generated \
  --pyi_out=./generated \
  proto/bookstore/v1/bookstore.proto

4.1.4 Java(Gradle)環境での環境構築

// build.gradle
plugins {
    id 'java'
    id 'com.google.protobuf' version '0.9.4'
}

dependencies {
    implementation 'io.grpc:grpc-netty-shaded:1.62.2'
    implementation 'io.grpc:grpc-protobuf:1.62.2'
    implementation 'io.grpc:grpc-stub:1.62.2'
    compileOnly 'org.apache.tomcat:annotations-api:6.0.53'
}

protobuf {
    protoc {
        artifact = 'com.google.protobuf:protoc:3.25.3'
    }
    plugins {
        grpc {
            artifact = 'io.grpc:protoc-gen-grpc-java:1.62.2'
        }
    }
    generateProtoTasks {
        all()*.plugins {
            grpc {}
        }
    }
}

4.1.5 コード生成コマンド

# Goのコード生成
protoc \
  --proto_path=proto \
  --go_out=. \
  --go_opt=paths=source_relative \
  --go-grpc_out=. \
  --go-grpc_opt=paths=source_relative \
  proto/bookstore/v1/bookstore.proto

# Buf(推奨ツール)を使用したコード生成
buf generate

4.1.6 Buf設定ファイル

# buf.yaml - モジュール定義
version: v2
modules:
  - path: proto
    name: buf.build/example/myservice
deps:
  - buf.build/googleapis/googleapis
  - buf.build/grpc-ecosystem/grpc-gateway
lint:
  use:
    - STANDARD
  except:
    - PACKAGE_VERSION_SUFFIX
breaking:
  use:
    - FILE

---
# buf.gen.yaml - コード生成設定
version: v2
plugins:
  # Go
  - remote: buf.build/protocolbuffers/go
    out: gen/go
    opt: paths=source_relative
  - remote: buf.build/grpc/go
    out: gen/go
    opt: paths=source_relative

  # Python
  - remote: buf.build/protocolbuffers/python
    out: gen/python
  - remote: buf.build/grpc/python
    out: gen/python

  # TypeScript (connect-es)
  - remote: buf.build/connectrpc/es
    out: gen/ts
    opt: target=ts

4.2 Go言語での実装例

4.2.1 サーバー実装

// cmd/server/main.go
package main

import (
    "context"
    "fmt"
    "log"
    "net"
    "os"
    "os/signal"
    "sync"
    "syscall"
    "time"

    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/health"
    healthpb "google.golang.org/grpc/health/grpc_health_v1"
    "google.golang.org/grpc/keepalive"
    "google.golang.org/grpc/reflection"
    "google.golang.org/grpc/status"
    "google.golang.org/protobuf/types/known/emptypb"
    "google.golang.org/protobuf/types/known/timestamppb"

    pb "github.com/example/myservice/proto/bookstore/v1"
)

// BookstoreServer はサービスの実装
type BookstoreServer struct {
    pb.UnimplementedBookstoreServiceServer  // 前方互換性のための埋め込み
    mu    sync.RWMutex
    books map[string]*pb.Book
    watchers []chan *pb.BookEvent
}

func NewBookstoreServer() *BookstoreServer {
    return &BookstoreServer{
        books:    make(map[string]*pb.Book),
        watchers: make([]chan *pb.BookEvent, 0),
    }
}

// Unary RPC: 書籍の取得
func (s *BookstoreServer) GetBook(ctx context.Context, req *pb.GetBookRequest) (*pb.GetBookResponse, error) {
    if req.GetBookId() == "" {
        return nil, status.Errorf(codes.InvalidArgument, "book_id is required")
    }

    s.mu.RLock()
    defer s.mu.RUnlock()

    book, ok := s.books[req.GetBookId()]
    if !ok {
        return nil, status.Errorf(codes.NotFound, "book not found: %s", req.GetBookId())
    }

    return &pb.GetBookResponse{Book: book}, nil
}

// Unary RPC: 書籍の作成
func (s *BookstoreServer) CreateBook(ctx context.Context, req *pb.CreateBookRequest) (*pb.CreateBookResponse, error) {
    if req.GetBook() == nil {
        return nil, status.Errorf(codes.InvalidArgument, "book is required")
    }
    if req.GetBook().GetTitle() == "" {
        return nil, status.Errorf(codes.InvalidArgument, "book.title is required")
    }

    s.mu.Lock()
    defer s.mu.Unlock()

    book := req.GetBook()
    now := timestamppb.Now()
    book.CreatedAt = now
    book.UpdatedAt = now

    if _, exists := s.books[book.GetBookId()]; exists {
        return nil, status.Errorf(codes.AlreadyExists, "book already exists: %s", book.GetBookId())
    }

    s.books[book.GetBookId()] = book

    s.notifyWatchers(&pb.BookEvent{
        EventType: pb.BookEvent_EVENT_TYPE_CREATED,
        Book:      book,
        EventTime: now,
    })

    return &pb.CreateBookResponse{Book: book}, nil
}

// Server Streaming RPC: 書籍イベントの監視
func (s *BookstoreServer) WatchBooks(req *pb.WatchBooksRequest, stream pb.BookstoreService_WatchBooksServer) error {
    ch := make(chan *pb.BookEvent, 100)

    s.mu.Lock()
    s.watchers = append(s.watchers, ch)
    s.mu.Unlock()

    defer func() {
        s.mu.Lock()
        for i, w := range s.watchers {
            if w == ch {
                s.watchers = append(s.watchers[:i], s.watchers[i+1:]...)
                break
            }
        }
        s.mu.Unlock()
        close(ch)
    }()

    for {
        select {
        case event := <-ch:
            if err := stream.Send(event); err != nil {
                return status.Errorf(codes.Internal, "failed to send event: %v", err)
            }
        case <-stream.Context().Done():
            return stream.Context().Err()
        }
    }
}

// Client Streaming RPC: 書籍カバー画像のアップロード
func (s *BookstoreServer) UploadBookCover(stream pb.BookstoreService_UploadBookCoverServer) error {
    var metadata *pb.BookCoverMetadata
    var totalBytes int64

    for {
        req, err := stream.Recv()
        if err != nil {
            if err.Error() == "EOF" {
                break
            }
            return status.Errorf(codes.Internal, "failed to receive: %v", err)
        }

        switch data := req.GetData().(type) {
        case *pb.UploadBookCoverRequest_Metadata:
            metadata = data.Metadata
        case *pb.UploadBookCoverRequest_Chunk:
            totalBytes += int64(len(data.Chunk))
        }
    }

    if metadata == nil {
        return status.Errorf(codes.InvalidArgument, "metadata not received")
    }

    coverURL := fmt.Sprintf("https://storage.example.com/covers/%s.jpg", metadata.GetBookId())
    return stream.SendAndClose(&pb.UploadBookCoverResponse{
        CoverUrl:      coverURL,
        BytesReceived: totalBytes,
    })
}

// Bidirectional Streaming RPC: 書籍についてのチャット
func (s *BookstoreServer) ChatAboutBooks(stream pb.BookstoreService_ChatAboutBooksServer) error {
    for {
        msg, err := stream.Recv()
        if err != nil {
            return nil
        }

        response := &pb.ChatMessage{
            UserId:    "system",
            Content:   fmt.Sprintf("You said: %s", msg.GetContent()),
            Timestamp: timestamppb.Now(),
        }
        if err := stream.Send(response); err != nil {
            return status.Errorf(codes.Internal, "failed to send: %v", err)
        }
    }
}

func (s *BookstoreServer) notifyWatchers(event *pb.BookEvent) {
    for _, ch := range s.watchers {
        select {
        case ch <- event:
        default:
        }
    }
}

func main() {
    port := os.Getenv("GRPC_PORT")
    if port == "" {
        port = "50051"
    }

    lis, err := net.Listen("tcp", fmt.Sprintf(":%s", port))
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }

    opts := []grpc.ServerOption{
        grpc.KeepaliveParams(keepalive.ServerParameters{
            MaxConnectionIdle:     15 * time.Minute,
            MaxConnectionAge:      30 * time.Minute,
            MaxConnectionAgeGrace: 5 * time.Minute,
            Time:                  5 * time.Minute,
            Timeout:               1 * time.Minute,
        }),
        grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
            MinTime:             5 * time.Second,
            PermitWithoutStream: true,
        }),
        grpc.MaxRecvMsgSize(10 * 1024 * 1024),
        grpc.MaxSendMsgSize(10 * 1024 * 1024),
        grpc.MaxConcurrentStreams(1000),
    }

    grpcServer := grpc.NewServer(opts...)

    bookstoreServer := NewBookstoreServer()
    pb.RegisterBookstoreServiceServer(grpcServer, bookstoreServer)

    healthServer := health.NewServer()
    healthpb.RegisterHealthServer(grpcServer, healthServer)
    healthServer.SetServingStatus("bookstore.v1.BookstoreService", healthpb.HealthCheckResponse_SERVING)

    reflection.Register(grpcServer)

    go func() {
        sigCh := make(chan os.Signal, 1)
        signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
        <-sigCh
        log.Println("Received shutdown signal, gracefully stopping...")
        healthServer.SetServingStatus("bookstore.v1.BookstoreService", healthpb.HealthCheckResponse_NOT_SERVING)
        grpcServer.GracefulStop()
    }()

    log.Printf("gRPC server listening on :%s", port)
    if err := grpcServer.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

4.2.2 クライアント実装

// cmd/client/main.go
package main

import (
    "context"
    "fmt"
    "io"
    "log"
    "time"

    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
    "google.golang.org/grpc/keepalive"
    "google.golang.org/grpc/metadata"
    "google.golang.org/grpc/status"

    pb "github.com/example/myservice/proto/bookstore/v1"
)

func main() {
    opts := []grpc.DialOption{
        grpc.WithTransportCredentials(insecure.NewCredentials()),
        grpc.WithKeepaliveParams(keepalive.ClientParameters{
            Time:                10 * time.Second,
            Timeout:             3 * time.Second,
            PermitWithoutStream: true,
        }),
        grpc.WithDefaultServiceConfig(`{
            "loadBalancingConfig": [{"round_robin": {}}],
            "methodConfig": [{
                "name": [{"service": "bookstore.v1.BookstoreService"}],
                "waitForReady": true,
                "timeout": "30s",
                "retryPolicy": {
                    "maxAttempts": 3,
                    "initialBackoff": "0.1s",
                    "maxBackoff": "1s",
                    "backoffMultiplier": 2.0,
                    "retryableStatusCodes": ["UNAVAILABLE", "DEADLINE_EXCEEDED"]
                }
            }]
        }`),
    }

    conn, err := grpc.NewClient("localhost:50051", opts...)
    if err != nil {
        log.Fatalf("failed to connect: %v", err)
    }
    defer conn.Close()

    client := pb.NewBookstoreServiceClient(conn)

    md := metadata.New(map[string]string{
        "authorization": "Bearer my-token",
        "x-request-id":  "req-12345",
    })
    ctx := metadata.NewOutgoingContext(context.Background(), md)

    // Unary RPC
    createResp, err := client.CreateBook(ctx, &pb.CreateBookRequest{
        Book: &pb.Book{
            BookId: "book-001",
            Title:  "gRPC入門",
            Author: "山田太郎",
        },
        IdempotencyKey: "create-book-001",
    })
    if err != nil {
        st, ok := status.FromError(err)
        if ok {
            log.Printf("gRPC error: code=%s, message=%s", st.Code(), st.Message())
        }
        return
    }
    fmt.Printf("Created book: %s\n", createResp.GetBook().GetTitle())

    // Server Streaming RPC
    go func() {
        stream, err := client.WatchBooks(ctx, &pb.WatchBooksRequest{})
        if err != nil {
            return
        }
        for {
            event, err := stream.Recv()
            if err == io.EOF {
                break
            }
            if err != nil {
                return
            }
            fmt.Printf("Book event: type=%s, book=%s\n",
                event.GetEventType(), event.GetBook().GetTitle())
        }
    }()
}

4.3 Python実装例

# server.py
import asyncio
import logging
from concurrent import futures

import grpc
from grpc_health.v1 import health, health_pb2, health_pb2_grpc
from grpc_reflection.v1alpha import reflection

from generated import bookstore_pb2
from generated import bookstore_pb2_grpc

logger = logging.getLogger(__name__)


class BookstoreServicer(bookstore_pb2_grpc.BookstoreServiceServicer):

    def __init__(self):
        self.books: dict[str, bookstore_pb2.Book] = {}
        self.watchers: list[asyncio.Queue] = []

    async def GetBook(self, request, context):
        if not request.book_id:
            await context.abort(grpc.StatusCode.INVALID_ARGUMENT, "book_id is required")

        book = self.books.get(request.book_id)
        if book is None:
            await context.abort(grpc.StatusCode.NOT_FOUND, f"book not found: {request.book_id}")

        return bookstore_pb2.GetBookResponse(book=book)

    async def WatchBooks(self, request, context):
        queue = asyncio.Queue(maxsize=100)
        self.watchers.append(queue)
        try:
            while context.is_active():
                try:
                    event = await asyncio.wait_for(queue.get(), timeout=30.0)
                    yield event
                except asyncio.TimeoutError:
                    continue
        finally:
            self.watchers.remove(queue)


async def serve():
    server = grpc.aio.server(
        futures.ThreadPoolExecutor(max_workers=10),
        options=[
            ('grpc.max_send_message_length', 10 * 1024 * 1024),
            ('grpc.max_receive_message_length', 10 * 1024 * 1024),
            ('grpc.keepalive_time_ms', 300000),
            ('grpc.keepalive_timeout_ms', 60000),
        ],
    )

    bookstore_pb2_grpc.add_BookstoreServiceServicer_to_server(BookstoreServicer(), server)

    health_servicer = health.HealthServicer()
    health_pb2_grpc.add_HealthServicer_to_server(health_servicer, server)

    service_names = (
        bookstore_pb2.DESCRIPTOR.services_by_name["BookstoreService"].full_name,
        reflection.SERVICE_NAME,
        health.SERVICE_NAME,
    )
    reflection.enable_server_reflection(service_names, server)

    server.add_insecure_port("[::]:50051")
    await server.start()
    await server.wait_for_termination()


if __name__ == "__main__":
    asyncio.run(serve())

5. gRPCインターセプター

5.1 インターセプターの概要

インターセプター(ミドルウェア)は、RPCの処理前後にロジックを挿入する仕組みである。ログ記録、認証、メトリクス収集、エラーハンドリングなどの横断的関心事を実装するために使用される。

5.2 Unaryインターセプター

func UnaryServerLoggingInterceptor() grpc.UnaryServerInterceptor {
    return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
        start := time.Now()
        resp, err := handler(ctx, req)
        duration := time.Since(start)
        st, _ := status.FromError(err)
        log.Printf("[gRPC] method=%s duration=%s code=%s", info.FullMethod, duration, st.Code())
        return resp, err
    }
}

func UnaryServerAuthInterceptor(tokenValidator func(string) (context.Context, error)) grpc.UnaryServerInterceptor {
    return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
        if info.FullMethod == "/grpc.health.v1.Health/Check" {
            return handler(ctx, req)
        }
        md, ok := metadata.FromIncomingContext(ctx)
        if !ok {
            return nil, status.Errorf(codes.Unauthenticated, "missing metadata")
        }
        authHeaders := md.Get("authorization")
        if len(authHeaders) == 0 {
            return nil, status.Errorf(codes.Unauthenticated, "missing authorization header")
        }
        token := strings.TrimPrefix(authHeaders[0], "Bearer ")
        newCtx, err := tokenValidator(token)
        if err != nil {
            return nil, status.Errorf(codes.Unauthenticated, "invalid token: %v", err)
        }
        return handler(newCtx, req)
    }
}

5.3 インターセプターチェーンの構築

func newGRPCServer() *grpc.Server {
    chainedUnary := grpc.ChainUnaryInterceptor(
        UnaryServerRecoveryInterceptor(),
        UnaryServerLoggingInterceptor(),
        UnaryServerAuthInterceptor(validateToken),
    )
    chainedStream := grpc.ChainStreamInterceptor(
        StreamServerLoggingInterceptor(),
    )
    return grpc.NewServer(chainedUnary, chainedStream)
}

6. エラーハンドリング

6.1 gRPCステータスコード

gRPCコードHTTPコード説明
OK0200成功
CANCELLED1499クライアントがキャンセル
UNKNOWN2500不明なエラー
INVALID_ARGUMENT3400無効な引数
DEADLINE_EXCEEDED4504デッドライン超過
NOT_FOUND5404リソースが見つからない
ALREADY_EXISTS6409リソースが既に存在
PERMISSION_DENIED7403権限不足
RESOURCE_EXHAUSTED8429リソース枯渇(レート制限)
FAILED_PRECONDITION9400前提条件の失敗
ABORTED10409操作の中断
OUT_OF_RANGE11400範囲外
UNIMPLEMENTED12501未実装
INTERNAL13500内部エラー
UNAVAILABLE14503サービス利用不可
DATA_LOSS15500データ損失
UNAUTHENTICATED16401認証エラー

6.2 リッチエラーモデル(Google Error Model)

import "google.golang.org/genproto/googleapis/rpc/errdetails"

func (s *BookstoreServer) CreateBook(ctx context.Context, req *pb.CreateBookRequest) (*pb.CreateBookResponse, error) {
    var violations []*errdetails.BadRequest_FieldViolation
    if req.GetBook().GetTitle() == "" {
        violations = append(violations, &errdetails.BadRequest_FieldViolation{
            Field:       "book.title",
            Description: "Title is required and cannot be empty",
        })
    }
    if len(violations) > 0 {
        st := status.New(codes.InvalidArgument, "invalid book data")
        detailedSt, _ := st.WithDetails(&errdetails.BadRequest{FieldViolations: violations})
        return nil, detailedSt.Err()
    }
    // ...
}

// クライアント側でのエラー詳細取得
func handleError(err error) {
    st := status.Convert(err)
    for _, detail := range st.Details() {
        switch d := detail.(type) {
        case *errdetails.BadRequest:
            for _, v := range d.GetFieldViolations() {
                log.Printf("Field: %s, Error: %s", v.GetField(), v.GetDescription())
            }
        case *errdetails.RetryInfo:
            log.Printf("Retry after: %s", d.GetRetryDelay().AsDuration())
        }
    }
}

7. 認証とセキュリティ

7.1 TLS/SSL認証

# CA証明書の生成
openssl req -x509 -newkey rsa:4096 -days 365 -nodes \
  -keyout ca-key.pem -out ca-cert.pem \
  -subj "/C=JP/ST=Tokyo/L=Shibuya/O=Example/CN=Example CA"

# サーバー証明書
openssl req -newkey rsa:4096 -nodes \
  -keyout server-key.pem -out server-req.pem \
  -subj "/C=JP/ST=Tokyo/L=Shibuya/O=Example/CN=localhost"

openssl x509 -req -in server-req.pem -days 365 \
  -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial \
  -out server-cert.pem -extfile server-ext.cnf
// TLSサーバー設定
func newTLSServer() (*grpc.Server, error) {
    serverCert, _ := tls.LoadX509KeyPair("server-cert.pem", "server-key.pem")
    caCert, _ := os.ReadFile("ca-cert.pem")
    certPool := x509.NewCertPool()
    certPool.AppendCertsFromPEM(caCert)

    tlsConfig := &tls.Config{
        Certificates: []tls.Certificate{serverCert},
        ClientAuth:   tls.RequireAndVerifyClientCert,  // mTLS
        ClientCAs:    certPool,
        MinVersion:   tls.VersionTLS13,
    }
    return grpc.NewServer(grpc.Creds(credentials.NewTLS(tlsConfig))), nil
}

7.2 トークンベース認証

type tokenAuth struct {
    token string
}

func (t *tokenAuth) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
    return map[string]string{"authorization": "Bearer " + t.token}, nil
}

func (t *tokenAuth) RequireTransportSecurity() bool { return true }

8. ロードバランシングとサービスディスカバリ

8.1 クライアントサイドロードバランシング

conn, _ := grpc.NewClient(
    "dns:///myservice.example.com:50051",
    grpc.WithDefaultServiceConfig(`{"loadBalancingConfig": [{"round_robin": {}}]}`),
    grpc.WithTransportCredentials(insecure.NewCredentials()),
)

8.2 Envoyプロキシベースロードバランシング

static_resources:
  clusters:
    - name: bookstore_cluster
      type: STRICT_DNS
      lb_policy: ROUND_ROBIN
      typed_extension_protocol_options:
        envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
          "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
          explicit_http_config:
            http2_protocol_options: {}
      health_checks:
        - grpc_health_check:
            service_name: "bookstore.v1.BookstoreService"

8.3 リトライとヘッジング

リトライ:
  Request 1 ──> [FAIL]
  Request 2 ──────────> [FAIL]
  Request 3 ─────────────────> [SUCCESS]

ヘッジング:
  Request 1 ──────────────────>  (pending)
  Request 2 ──────> [SUCCESS]   ← 0.5s後に送信
  Request 1 をキャンセル

9. デッドラインとタイムアウト

gRPCは「デッドライン」の概念を使用し、サービス間でデッドラインを伝播させる。

Client (deadline: 10:00:05.000)
  → Service A (remaining: 4.5s)
    → Service B (remaining: 3.2s)
      → Service C (remaining: 1.8s)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := client.GetBook(ctx, &pb.GetBookRequest{BookId: "book-001"})

10. メタデータとコンテキスト伝播

// クライアント側
md := metadata.New(map[string]string{
    "authorization": "Bearer my-token",
    "x-request-id":  "req-12345",
    "x-trace-id":    "trace-67890",
})
ctx := metadata.NewOutgoingContext(context.Background(), md)

var headerMD, trailerMD metadata.MD
resp, err := client.GetBook(ctx, req,
    grpc.Header(&headerMD),
    grpc.Trailer(&trailerMD),
)

// サーバー側
md, _ := metadata.FromIncomingContext(ctx)
grpc.SendHeader(ctx, metadata.New(map[string]string{"x-server-version": "2.0.0"}))
grpc.SetTrailer(ctx, metadata.New(map[string]string{"x-request-cost": "0.05"}))

10.1 OpenTelemetry統合

func newTracedServer() *grpc.Server {
    return grpc.NewServer(grpc.StatsHandler(otelgrpc.NewServerHandler()))
}

func newTracedClient() (*grpc.ClientConn, error) {
    return grpc.NewClient("localhost:50051",
        grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
        grpc.WithTransportCredentials(insecure.NewCredentials()),
    )
}

11. パフォーマンスチューニング

11.1 ベンチマーク比較

指標REST/JSONgRPC/Protobuf改善率
シリアライズ速度1,200 ns150 ns8x
デシリアライズ速度1,800 ns200 ns9x
メッセージサイズ1,024 B320 B3.2x
Unary RPCレイテンシ2.5 ms0.8 ms3.1x
スループット (req/s)15,00085,0005.7x

11.2 メッセージ圧縮

import "google.golang.org/grpc/encoding/gzip"

resp, err := client.ListBooks(ctx, req, grpc.UseCompressor(gzip.Name))

12. gRPC-Gateway(RESTプロキシ)

service BookstoreService {
  rpc GetBook(GetBookRequest) returns (GetBookResponse) {
    option (google.api.http) = { get: "/v1/books/{book_id}" };
  }
  rpc CreateBook(CreateBookRequest) returns (CreateBookResponse) {
    option (google.api.http) = { post: "/v1/books" body: "book" };
  }
}
// gRPC-Gatewayサーバー
mux := runtime.NewServeMux()
pb.RegisterBookstoreServiceHandlerFromEndpoint(ctx, mux, "localhost:50051", opts)
http.ListenAndServe(":8080", mux)

13. gRPC-Webとブラウザ対応

13.1 Connect RPC

import { createClient } from "@connectrpc/connect";
import { createConnectTransport } from "@connectrpc/connect-web";
import { BookstoreService } from "./gen/bookstore/v1/bookstore_pb";

const transport = createConnectTransport({ baseUrl: "https://api.example.com" });
const client = createClient(BookstoreService, transport);

// Unary RPC
const response = await client.getBook({ bookId: "book-001" });

// Server Streaming
for await (const event of client.watchBooks({})) {
  console.log(`Event: ${event.eventType}`);
}

14. ヘルスチェックとリフレクション

# Kubernetes gRPC ヘルスチェック
livenessProbe:
  grpc:
    port: 50051
    service: ""
readinessProbe:
  grpc:
    port: 50051
    service: "bookstore.v1.BookstoreService"
# grpcurlによるデバッグ
grpcurl -plaintext localhost:50051 list
grpcurl -plaintext -d '{"book_id": "book-001"}' localhost:50051 bookstore.v1.BookstoreService/GetBook

15. KubernetesでのgRPCデプロイ

apiVersion: v1
kind: Service
metadata:
  name: bookstore-service
spec:
  type: ClusterIP
  ports:
    - port: 50051
      targetPort: grpc
      appProtocol: grpc
  selector:
    app: bookstore

15.1 Istioサービスメッシュ統合

apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: bookstore-dr
spec:
  host: bookstore-service
  trafficPolicy:
    connectionPool:
      http:
        h2UpgradePolicy: UPGRADE
    loadBalancer:
      simple: ROUND_ROBIN
    outlierDetection:
      consecutive5xxErrors: 5
      interval: 10s
      baseEjectionTime: 30s

16. テスト戦略

16.1 ユニットテスト

func TestGetBook_NotFound(t *testing.T) {
    s := NewBookstoreServer()
    _, err := s.GetBook(context.Background(), &pb.GetBookRequest{BookId: "nonexistent"})
    require.Error(t, err)
    st, _ := status.FromError(err)
    assert.Equal(t, codes.NotFound, st.Code())
}

16.2 インテグレーションテスト(bufconn)

func setupTestServer(t *testing.T) (pb.BookstoreServiceClient, func()) {
    lis := bufconn.Listen(1024 * 1024)
    grpcServer := grpc.NewServer()
    pb.RegisterBookstoreServiceServer(grpcServer, NewBookstoreServer())
    go grpcServer.Serve(lis)

    conn, _ := grpc.NewClient("passthrough:///bufconn",
        grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) {
            return lis.DialContext(ctx)
        }),
        grpc.WithTransportCredentials(insecure.NewCredentials()),
    )
    return pb.NewBookstoreServiceClient(conn), func() { conn.Close(); grpcServer.Stop() }
}

16.3 負荷テスト(ghz)

ghz --insecure \
  --proto proto/bookstore/v1/bookstore.proto \
  --call bookstore.v1.BookstoreService/GetBook \
  --data '{"book_id": "book-001"}' \
  --concurrency 100 --duration 30s \
  localhost:50051

17. 可観測性(Observability)

17.1 Prometheusメトリクス

srvMetrics := grpcprom.NewServerMetrics(
    grpcprom.WithServerHandlingTimeHistogram(),
)
grpcServer := grpc.NewServer(
    grpc.ChainUnaryInterceptor(srvMetrics.UnaryServerInterceptor()),
    grpc.ChainStreamInterceptor(srvMetrics.StreamServerInterceptor()),
)

公開されるメトリクス:

  • grpc_server_handling_seconds_bucket - RPC処理時間
  • grpc_server_handled_total - RPCリクエスト数
  • grpc_server_msg_sent_total / grpc_server_msg_received_total - ストリームメッセージ数

18. API設計のベストプラクティス

18.1 命名規則(Google API Design Guide準拠)

service BookstoreService { ... }      // サービス名: 名詞 + Service
rpc GetBook(GetBookRequest) ...       // メソッド名: 動詞 + 名詞
message GetBookRequest { ... }        // リクエスト: メソッド名 + Request
message GetBookResponse { ... }       // レスポンス: メソッド名 + Response

enum BookStatus {
    BOOK_STATUS_UNSPECIFIED = 0;       // 0は常にUNSPECIFIED
    BOOK_STATUS_DRAFT = 1;
}

18.2 ページネーション

message ListBooksRequest {
    int32 page_size = 1;
    string page_token = 2;
}
message ListBooksResponse {
    repeated Book books = 1;
    string next_page_token = 2;
    int32 total_size = 3;
}

18.3 フィールドマスクによる部分更新

message UpdateBookRequest {
    Book book = 1;
    google.protobuf.FieldMask update_mask = 2;
}

19. Protocol Buffersの互換性管理

19.1 後方互換な変更

  • 新しいフィールドの追加(新番号)
  • 新しいメソッド/サービスの追加
  • 新しい列挙値の追加

19.2 破壊的変更(避けるべき)

  • フィールド番号の変更・再利用
  • フィールドの型変更
  • サービスメソッドの削除
message Book {
    reserved 3;                // 古いフィールド番号を予約
    reserved "author";         // 古いフィールド名を予約
    AuthorInfo author_info = 4; // 新しいフィールドとして追加
}

19.3 Bufによる互換性チェック

buf breaking --against '.git#branch=main'
buf lint

20. gRPCアーキテクチャパターン

20.1 マイクロサービスアーキテクチャでの位置づけ

                     ┌──────────────────┐
                     │   API Gateway    │
                     │  (gRPC-Gateway)  │
                     └────────┬─────────┘
                              │
                ┌─────────────┼─────────────┐
                │             │             │
          ┌─────▼─────┐ ┌────▼─────┐ ┌────▼─────┐
          │  User      │ │ Catalog  │ │  Order   │
          │  Service   │ │ Service  │ │  Service │
          │  (gRPC)    │ │ (gRPC)   │ │  (gRPC)  │
          └────────────┘ └──────────┘ └──────────┘

20.2 Backend for Frontend(BFF)パターン

  Mobile App          Web App           Admin Panel
      │                  │                   │
      ▼                  ▼                   ▼
 ┌─────────┐      ┌───────────┐      ┌────────────┐
 │Mobile BFF│      │  Web BFF  │      │ Admin BFF  │
 │(gRPC-GW) │      │(gRPC-Web) │      │ (gRPC)     │
 └────┬─────┘      └─────┬─────┘      └──────┬─────┘
      └───────────────────┼────────────────────┘
                    ┌─────▼─────┐
                    │   gRPC    │
                    │ Services  │
                    └───────────┘

21. まとめと今後の展望

21.1 gRPCを選択すべきケース

ケース推奨度理由
マイクロサービス間通信非常に高い高性能、型安全、多言語対応
リアルタイムストリーミング非常に高い双方向ストリーミングのネイティブサポート
モバイルバックエンド高い低帯域幅、バッテリー効率
IoTデバイス通信高い軽量なバイナリプロトコル
公開API(外部向け)中程度gRPC-Gateway/Connectでの二重公開が必要
ブラウザ直接通信中程度gRPC-Web/Connectが必要
シンプルなCRUD API低いRESTの方がエコシステムが成熟

21.2 エコシステム

カテゴリツール説明
コード管理BufProto管理、リント、互換性チェック
API GatewaygRPC-GatewayREST<->gRPCブリッジ
ブラウザConnect-RPCモダンなgRPC-Web代替
デバッグgrpcurl / grpcuiCLI/GUIデバッグツール
負荷テストghzgRPC専用ベンチマークツール
サービスメッシュIstio / LinkerdgRPCネイティブサポート
プロキシEnvoyL7 gRPCロードバランサー
可観測性OpenTelemetry分散トレーシング

21.3 今後の展望

  1. HTTP/3対応: QUICプロトコルベースのHTTP/3対応が進行中。Head-of-Line Blocking問題の解消
  2. Connect RPCの普及: ブラウザネイティブ対応を含む3プロトコルの単一定義からの提供
  3. AIワークロード対応: LLM推論サービスなどでの大規模ストリーミングレスポンス処理
  4. WebTransport: ブラウザからの直接的なgRPC通信の将来的な可能性
  5. Buf Schema Registry: 組織間でのスキーマ共有と管理の標準化

参考資料