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"}
問題:
- JSON 非効率: テキスト、キー繰り返し
- HTTP/1.1: head-of-line blocking
- 契約が緩い: スキーマ強制なし
- 単方向: 双方向 streaming が難しい
- クライアントコード記述の負担
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 比較
| モデル | 要求 | 応答 | ユースケース |
|---|---|---|---|
| Unary | 1 | 1 | CRUD |
| Server Streaming | 1 | N | 大結果、ライブ更新 |
| Client Streaming | N | 1 | アップロード、バッチ |
| Bidirectional | N | N | チャット、ゲーム |
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) がシンプルな後継。
参考資料
현재 단락 (1/262)
- **gRPC = HTTP/2 + Protobuf**: 5-10倍速く、小さい RPC