Skip to content
Published on

gRPC 徹底ガイド 2025: Protocol Buffers、4つの通信モデル、Interceptor、ロードバランシング

Authors

TL;DR

  • gRPC = HTTP/2 + Protobuf: 5-10倍速く、小さい RPC
  • 4つのモデル: Unary, Server Streaming, Client Streaming, Bidirectional
  • Protobuf 最適化: フィールド番号、wire type、varint エンコーディング
  • Interceptor: 横断的関心事(認証、ログ、メトリクス)を処理
  • Deadline 伝播: 分散環境での timeout 累積を防ぐ
  • gRPC-Web: ブラウザで gRPC を使う

1. gRPC 登場の背景

1.1 RPC の歴史

RPC (Remote Procedure Call) = 他マシンの関数をローカル関数のように呼ぶ。

  • 1980年代: Sun RPC (NFS の基盤)
  • 1990年代: CORBA (複雑、失敗)
  • 2000年代: SOAP/WSDL (XML、遅い)
  • 2010年代: REST (JSON over HTTP)
  • 2015年以降: gRPC (Google 内部の Stubby を公開)

1.2 REST の限界

GET /api/users/123 HTTP/1.1
Host: api.example.com

HTTP/1.1 200 OK
Content-Type: application/json
{"id": 123, "name": "Alice", "email": "alice@example.com"}

問題:

  1. JSON 非効率: テキスト、キー繰り返し
  2. HTTP/1.1: head-of-line blocking
  3. 契約が緩い: スキーマ強制なし
  4. 単方向: 双方向 streaming が難しい
  5. クライアントコード記述の負担

1.3 gRPC の約束

service UserService {
  rpc GetUser(GetUserRequest) returns (User);
  rpc StreamUsers(Empty) returns (stream User);
}

message User {
  int64 id = 1;
  string name = 2;
  string email = 3;
}

自動コード生成(全言語)、スキーマ強制、5-10倍小さいペイロード、HTTP/2 multiplexing、双方向 streaming。


2. Protocol Buffers 深掘り

2.1 基本構造

syntax = "proto3";

package myapp;

message User {
  int64 id = 1;
  string name = 2;
  string email = 3;
  repeated string tags = 4;
  Address address = 5;
}

message Address {
  string street = 1;
  string city = 2;
}

2.2 フィールド番号の意味

フィールド番号は バイナリ形式の識別子。名前変更しても番号が同じなら互換性あり。削除済み番号は再利用不可。

// v1
message User {
  string user_name = 1;
}

// v2 (互換)
message User {
  string display_name = 1;
}

2.3 Wire Format

各フィールド: (field_number << 3) | wire_type

Wire Types:

  • 0: VARINT (int32, int64, bool)
  • 1: FIXED64 (double, fixed64)
  • 2: LENGTH_DELIMITED (string, bytes, message)
  • 5: FIXED32 (float, fixed32)

2.4 Varint エンコーディング

小さい数値は少ないバイトで表現。

0      -> 1 byte
127    -> 1 byte
128    -> 2 bytes
16383  -> 2 bytes
16384  -> 3 bytes

小さい ID やカウンタに非常に効率的。

2.5 サイズ比較

// JSON: 78 bytes
{"id":123,"name":"Alice","email":"alice@example.com"}

Protobuf なら同データを約35 bytes (45% 小)。メッセージが大きいほど差が広がる(キーが繰り返されないため)。

2.6 Schema Evolution

ルール:

  • OK: 新フィールド追加(別番号で)
  • OK: フィールド名変更
  • NG: フィールド番号変更
  • NG: 型変更
  • NG: 削除後の番号再利用(reserved を使う)
message User {
  reserved 4, 5;
  reserved "old_field";

  int64 id = 1;
  string name = 2;
  string email = 3;
}

3. 4つの通信モデル

3.1 Unary RPC

rpc GetUser(GetUserRequest) returns (User);

1 リクエスト -> 1 レスポンス。REST に近い。CRUD や一般的な API。

3.2 Server Streaming

rpc StreamUsers(Empty) returns (stream User);

1 リクエスト -> N レスポンス。

for user in stub.StreamUsers(Empty()):
    print(user.name)

大きな結果セット、リアルタイム更新、ログ streaming に使用。

3.3 Client Streaming

rpc UploadEvents(stream Event) returns (UploadResponse);

N リクエスト -> 1 レスポンス。大容量アップロード、バッチ処理に。

3.4 Bidirectional Streaming

rpc Chat(stream ChatMessage) returns (stream ChatMessage);

N <-> N。両側が独立にメッセージ送信。

def Chat(self, request_iterator, context):
    for msg in request_iterator:
        yield ChatMessage(text=f"Echo: {msg.text}")

チャット、リアルタイムゲーム、IoT 双方向通信。

3.5 比較

モデル要求応答ユースケース
Unary11CRUD
Server Streaming1N大結果、ライブ更新
Client StreamingN1アップロード、バッチ
BidirectionalNNチャット、ゲーム

4. HTTP/2 の役割

4.1 HTTP/1.1 の限界

  • リクエストごとに新規接続(または keep-alive で直列)
  • Head-of-line blocking
  • ヘッダー繰り返し

4.2 HTTP/2 Multiplexing

単一 TCP 接続で多数の stream を並列処理。HPACK ヘッダー圧縮、バイナリフレーミング、サーバプッシュ。

4.3 gRPC の HTTP/2 活用

  • 各 RPC = 1 stream
  • 同一接続で数千 RPC 同時
  • TCP 接続オーバーヘッド最小化
  • 双方向 streaming が自然に可能

4.4 ヘッダー圧縮

HPACK は静的テーブル、動的テーブル、Huffman 符号化で ヘッダーサイズを 80% 以上削減


5. Interceptor

5.1 何か?

Interceptor は RPC 呼び出し前後で動くミドルウェア。認証、ログ、メトリクス、分散トレーシング、エラー処理、リトライなどの横断的関心事を扱う。

5.2 Server Interceptor (Python)

import grpc

class AuthInterceptor(grpc.ServerInterceptor):
    def intercept_service(self, continuation, handler_call_details):
        metadata = dict(handler_call_details.invocation_metadata)
        token = metadata.get('authorization')

        if not verify_token(token):
            return grpc.unary_unary_rpc_method_handler(
                lambda req, ctx: ctx.abort(grpc.StatusCode.UNAUTHENTICATED, 'Invalid token')
            )

        return continuation(handler_call_details)

server = grpc.server(
    futures.ThreadPoolExecutor(max_workers=10),
    interceptors=[AuthInterceptor()]
)

5.3 Client Interceptor

class RetryInterceptor(grpc.UnaryUnaryClientInterceptor):
    def intercept_unary_unary(self, continuation, client_call_details, request):
        for attempt in range(3):
            try:
                return continuation(client_call_details, request)
            except grpc.RpcError as e:
                if e.code() != grpc.StatusCode.UNAVAILABLE:
                    raise
                time.sleep(2 ** attempt)
        raise

5.4 Go Interceptor

func loggingInterceptor(
    ctx context.Context,
    req interface{},
    info *grpc.UnaryServerInfo,
    handler grpc.UnaryHandler,
) (interface{}, error) {
    start := time.Now()
    resp, err := handler(ctx, req)
    log.Printf("%s took %v", info.FullMethod, time.Since(start))
    return resp, err
}

5.5 Chained Interceptors

順に実行。Tracing -> Auth -> Logging -> Metrics のような連結。


6. Deadline と Cancellation

6.1 Deadline の重要性

# 間違い - 無限待機
response = stub.GetUser(GetUserRequest(user_id=123))

# 正しい - 5秒 deadline
response = stub.GetUser(GetUserRequest(user_id=123), timeout=5.0)

deadline がないと: ネットワーク障害で無限待機、スレッド枯渇、UX 悪化。

6.2 Deadline Propagation

Client (5s) -> Service A (4s) -> Service B (残り 1s)。gRPC が呼び出しチェーン全体に deadline を伝播 -> timeout 累積を防止

def call_chain(context):
    response = downstream_stub.SomeRPC(request, timeout=context.time_remaining())

6.3 Cancellation

future = stub.GetUser.future(request)
time.sleep(2)
future.cancel()

サーバ側は context.is_active()context.cancelled() をチェック。

6.4 Go の Context

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

response, err := client.GetUser(ctx, &GetUserRequest{UserId: 123})

Context は deadline、キャンセル信号、Metadata(認証 token など)を運ぶ。


7. ロードバランシング

7.1 Client-Side LB

channel = grpc.insecure_channel(
    'dns:///my-service:50051',
    options=[('grpc.lb_policy_name', 'round_robin')]
)

ポリシー: pick_first (デフォルト)、round_robin、カスタム。

7.2 Look-aside LB

クライアントが LB サービスに利用可能サーバを問い合わせ。例: gRPC + xDS (Envoy)。

7.3 Proxy LB

Client -> Envoy Proxy -> Servers。Envoy、Linkerd、Istio (Service Mesh) など。クライアントは単一エンドポイントのみ、LB ロジックは proxy 側。

7.4 Health Check

syntax = "proto3";

package grpc.health.v1;

service Health {
  rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
  rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
}

message HealthCheckResponse {
  enum ServingStatus {
    UNKNOWN = 0;
    SERVING = 1;
    NOT_SERVING = 2;
  }
  ServingStatus status = 1;
}

標準 gRPC Health Check Protocol。全 gRPC サーバが実装可能。


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

8.1 メッセージサイズ制限

デフォルト 4MB。

options = [
    ('grpc.max_send_message_length', 100 * 1024 * 1024),
    ('grpc.max_receive_message_length', 100 * 1024 * 1024),
]

大容量は streaming 推奨。単一メッセージにしない。

8.2 Connection Pooling

チャネルを使い回す。リクエストごとに作らない。HTTP/2 multiplexing を活用。

8.3 KeepAlive

options = [
    ('grpc.keepalive_time_ms', 10000),
    ('grpc.keepalive_timeout_ms', 5000),
    ('grpc.keepalive_permit_without_calls', True),
    ('grpc.http2.max_pings_without_data', 0),
]

死んだ接続の検出、NAT timeout 防止。

8.4 圧縮

server = grpc.server(..., compression=grpc.Compression.Gzip)

channel = grpc.insecure_channel('localhost:50051', compression=grpc.Compression.Gzip)

アルゴリズム: gzip、deflate、snappy。大メッセージに有効、小さいと逆にオーバーヘッド。


9. gRPC-Web — ブラウザ対応

9.1 問題

ブラウザは HTTP/2 trailers や生 HTTP/2 stream を扱えないため、gRPC を直接使えない。

9.2 gRPC-Web

ブラウザ向け派生: HTTP/1.1 または HTTP/2、trailers を body にエンコード、CORS 対応。

[Browser] -- gRPC-Web --> [Envoy Proxy] -- gRPC --> [Server]

9.3 使用例

import { UserServiceClient } from './generated/user_pb_service'

const client = new UserServiceClient('https://api.example.com')

client.getUser(new GetUserRequest().setUserId(123), (err, response) => {
    if (err) console.error(err)
    else console.log(response.getName())
})

9.4 制限

  • Client Streaming 非対応(多くの実装)
  • Bidirectional Streaming 非対応
  • ペイロードが若干大きい(base64)

9.5 Connect

Buf 社の Connect は gRPC-Web の後継。HTTP/1.1、HTTP/2、gRPC、gRPC-Web すべて対応。API がシンプルで TypeScript first。

import { createPromiseClient } from "@bufbuild/connect"
import { UserService } from "./gen/user_connect"

const client = createPromiseClient(UserService, transport)
const response = await client.getUser({ userId: 123 })

10. デバッグとツール

10.1 grpcurl

REST の curl のように gRPC 呼び出し:

grpcurl -plaintext localhost:50051 list

grpcurl -plaintext -d '{"user_id": 123}' \
  localhost:50051 \
  UserService/GetUser

Reflection が必要:

from grpc_reflection.v1alpha import reflection
SERVICE_NAMES = (UserService_pb2.DESCRIPTOR.services_by_name['UserService'].full_name, reflection.SERVICE_NAME)
reflection.enable_server_reflection(SERVICE_NAMES, server)

10.2 BloomRPC / Postman

GUI クライアント。

10.3 ログ

import logging
logging.basicConfig(level=logging.DEBUG)
os.environ['GRPC_VERBOSITY'] = 'DEBUG'
os.environ['GRPC_TRACE'] = 'all'

10.4 分散トレーシング

from opentelemetry.instrumentation.grpc import GrpcInstrumentorServer
GrpcInstrumentorServer().instrument()

全 gRPC 呼び出しが自動的に trace に含まれる。


クイズ

1. Protobuf のフィールド番号がなぜ重要?

: バイナリ形式の 識別子 だから。JSON のようにキー名を毎回送らず、小さい整数だけ -> ペイロード縮小。schema evolution の核: 名前は変えられるが番号は変えられない。同じ番号なら旧バージョンと互換。削除済み番号は絶対再利用禁止(reserved で印を付ける)。1-15 は 1 byte、16-2047 は 2 byte -> 頻出フィールドは 1-15 に割り当てる。

2. gRPC が REST より速い理由は?

: 4つの要因: (1) Protobuf — JSON 比 5-10倍小、パース高速、(2) HTTP/2 — multiplexing、単一接続で多数リクエスト、(3) HPACK — 繰り返しヘッダー 80%+ 削減、(4) バイナリプロトコル — テキストパース不要。結果: スループット 5-10倍、latency 半減。欠点: デバッグ困難(バイナリ)、ブラウザ直接非対応(gRPC-Web 必要)。

3. Bidirectional streaming はいつ使う?

: 両側が独立にメッセージを送る必要があるとき。ユースケース: (1) チャット — ユーザ送信とサーバからの他ユーザメッセージ push、(2) リアルタイムゲーム — クライアント入力とサーバのゲーム状態双方向、(3) IoT — デバイスのセンサデータとサーバコマンド、(4) 音声認識 — 音声 stream と transcript の双方向。WebSocket に似るが、gRPC の強い型付け + Protobuf 効率。

4. Deadline propagation の重要性は?

: 分散システムで呼び出しチェーンの 累積 timeout を防ぐ。例: Client (5s) -> A (4s 消費) -> B を 5s timeout で呼ぶと -> 合計 9s 可能。正解: A が B を残り時間 (1s) で呼ぶ。gRPC は Context 経由で deadline を自動伝播。全ダウンストリーム呼び出しが親の deadline を継承。Go の context.Context、Python の context.time_remaining()。分散システム安定性の鍵。

5. gRPC-Web と通常の gRPC の違いは?

: ブラウザは HTTP/2 trailers や生 stream など gRPC 機能を直接扱えない。gRPC-Web はブラウザ向け派生で (1) HTTP/1.1 か HTTP/2、(2) trailers を body にエンコード、(3) CORS 対応。制限: Client Streaming、Bidirectional Streaming 非対応(Server Streaming は OK)。通常は Envoy proxy 経由で gRPC-Web <-> gRPC を変換。Connect (Buf) がシンプルな後継。


参考資料