- Published on
gRPC & Protocol Buffers 完全ガイド 2025:マイクロサービス通信の新しい標準
- Authors

- Name
- Youngju Kim
- @fjvbn20031
目次
1. REST vs gRPC:なぜgRPCなのか?
マイクロサービスアーキテクチャにおいて、サービス間(かん)通信(つうしん)はシステムのパフォーマンスと安定性(あんていせい)を左右(さゆう)する重要(じゅうよう)な要素(ようそ)です。REST APIはそのシンプルさと汎用性(はんようせい)で長(なが)い間(あいだ)標準(ひょうじゅん)でしたが、サービス数(すう)が増加(ぞうか)するにつれてその限界(げんかい)が明(あき)らかになり始(はじ)めました。
1.1 RESTの限界
RESTベースのJSON通信の問題点は大きく3つあります。
第一に、シリアライゼーション/デシリアライゼーションのオーバーヘッドです。JSONはテキストベースのためパース費用(ひよう)が高(たか)く、データサイズがバイナリフォーマットと比(くら)べて3〜10倍(ばい)大(おお)きいです。毎秒(まいびょう)数万件(すうまんけん)のリクエストを処理(しょり)する内部(ないぶ)サービス通信でこの差(さ)は無視(むし)できません。
第二に、**スキーマの欠如(けつじょ)**です。OpenAPI/Swaggerで文書化(ぶんしょか)できますが強制力(きょうせいりょく)がなく、クライアントとサーバー間の契約(けいやく)が緩(ゆる)いです。APIが変更(へんこう)されるとランタイムで初(はじ)めてエラーを発見(はっけん)します。
第三に、**HTTP/1.1の制約(せいやく)**です。Head-of-Line Blocking、接続(せつぞく)あたり1つのリクエスト、ヘッダー圧縮(あっしゅく)の欠如(けつじょ)など、高性能(こうせいのう)通信には不適切(ふてきせつ)です。
1.2 比較表
| 項目 | REST (JSON) | gRPC (Protobuf) |
|---|---|---|
| プロトコル | HTTP/1.1(主に) | HTTP/2 |
| データフォーマット | JSON(テキスト) | Protocol Buffers(バイナリ) |
| スキーマ | 任意(OpenAPI) | 必須(.proto) |
| コード生成 | 任意 | 自動(多言語) |
| ストリーミング | 制限的(SSE、WebSocket) | ネイティブサポート(4パターン) |
| ブラウザサポート | ネイティブ | gRPC-Web必要 |
| シリアライゼーション速度 | 遅い | 速い(10倍) |
| ペイロードサイズ | 大きい | 小さい(3〜10倍) |
| 学習曲線 | 低い | 中程度 |
| デバッグ | 容易(人間が読める) | 困難(バイナリ) |
1.3 いつ何を使うべきか
RESTを選択(せんたく)する場合:
- ブラウザクライアントが主要(しゅよう)な消費者(しょうひしゃ)
- 外部(がいぶ)開発者(かいはつしゃ)向けの公開API
- シンプルなCRUD操作(そうさ)が中心
- チームのgRPC経験(けいけん)が不足(ふそく)
gRPCを選択する場合:
- マイクロサービス間の内部通信
- リアルタイムストリーミングが必要
- 高性能・低遅延(ていちえん)が核心(かくしん)要件
- 多言語(たげんご)クライアントのサポート
- 強い型安全性(かたあんぜんせい)が必要
2. Protocol Buffers 基礎
Protocol Buffers(Protobuf)はGoogleが開発(かいはつ)した言語中立(げんごちゅうりつ)、プラットフォーム中立のバイナリシリアライゼーションフォーマットです。gRPCのデフォルトIDL(Interface Definition Language)でありシリアライゼーションメカニズムです。
2.1 .protoファイルの基本構造
syntax = "proto3";
package ecommerce.v1;
option go_package = "github.com/mycompany/ecommerce/v1;ecommercev1";
option java_package = "com.mycompany.ecommerce.v1";
import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";
// 商品サービス定義
service ProductService {
// 単一商品取得
rpc GetProduct(GetProductRequest) returns (Product);
// 商品リスト取得
rpc ListProducts(ListProductsRequest) returns (ListProductsResponse);
// 商品作成
rpc CreateProduct(CreateProductRequest) returns (Product);
// 商品更新
rpc UpdateProduct(UpdateProductRequest) returns (Product);
// 商品削除
rpc DeleteProduct(DeleteProductRequest) returns (google.protobuf.Empty);
}
2.2 スカラー型
message ScalarTypes {
// 数値型
double price = 1; // 64ビット浮動小数点
float rating = 2; // 32ビット浮動小数点
int32 quantity = 3; // 可変長エンコーディング(負数に非効率)
int64 total_sales = 4; // 可変長エンコーディング
uint32 age = 5; // 符号なし32ビット整数
uint64 view_count = 6; // 符号なし64ビット整数
sint32 temperature = 7; // 負数に効率的なエンコーディング
sint64 altitude = 8; // 負数に効率的なエンコーディング
fixed32 hash = 9; // 常に4バイト
fixed64 large_hash = 10; // 常に8バイト
// 文字列/バイト
string name = 11; // UTF-8エンコーディング文字列
bytes thumbnail = 12; // 任意バイトシーケンス
// ブーリアン
bool is_active = 13;
}
2.3 メッセージ型とネスト
message Product {
string id = 1;
string name = 2;
string description = 3;
Money price = 4;
Category category = 5;
repeated string tags = 6; // 繰り返しフィールド(配列)
map<string, string> metadata = 7; // マップ型
google.protobuf.Timestamp created_at = 8;
ProductStatus status = 9;
// ネストメッセージ
message Dimension {
double width = 1;
double height = 2;
double depth = 3;
string unit = 4;
}
Dimension dimension = 10;
}
message Money {
string currency_code = 1; // ISO 4217
int64 units = 2; // 整数部分
int32 nanos = 3; // ナノ単位の小数部分
}
2.4 Enum型
enum ProductStatus {
PRODUCT_STATUS_UNSPECIFIED = 0; // 常に0番がデフォルト
PRODUCT_STATUS_DRAFT = 1;
PRODUCT_STATUS_ACTIVE = 2;
PRODUCT_STATUS_ARCHIVED = 3;
PRODUCT_STATUS_DELETED = 4;
}
enum Category {
CATEGORY_UNSPECIFIED = 0;
CATEGORY_ELECTRONICS = 1;
CATEGORY_CLOTHING = 2;
CATEGORY_BOOKS = 3;
CATEGORY_FOOD = 4;
}
2.5 Oneof型
message PaymentMethod {
string id = 1;
oneof method {
CreditCard credit_card = 2;
BankTransfer bank_transfer = 3;
DigitalWallet digital_wallet = 4;
}
}
message CreditCard {
string card_number_masked = 1;
string expiry_month = 2;
string expiry_year = 3;
string brand = 4;
}
message BankTransfer {
string bank_name = 1;
string account_number_masked = 2;
string routing_number = 3;
}
message DigitalWallet {
string provider = 1; // "apple_pay", "google_pay"
string email = 2;
}
2.6 フィールド番号とバージョニング
Protocol Buffersにおいて、フィールド番号(ばんごう)はワイヤフォーマットでフィールドを識別(しきべつ)する核心(かくしん)要素(ようそ)です。
フィールド番号のルール:
- 1〜15:1バイトでエンコード(よく使うフィールドに割り当て)
- 16〜2047:2バイトでエンコード
- 19000〜19999:予約(よやく)範囲(はんい)(使用不可)
下位互換性(かいごかんせい)維持(いじ)ルール:
message Product {
string id = 1;
string name = 2;
// 削除されたフィールド:番号と名前を予約
reserved 3, 6 to 8;
reserved "old_price", "legacy_tag";
// 新しいフィールドの追加は常に安全
string description = 4;
Money price = 5;
// フィールド9から追加
string sku = 9;
}
バージョニング戦略:
- フィールド追加(ついか):常(つね)に安全(あんぜん)(新しいフィールドを知らないクライアントは無視する)
- フィールド削除(さくじょ):reservedで番号を予約してから削除
- フィールド型変更:絶対にしない(新しいフィールドを追加する)
- パッケージバージョン分離:
ecommerce.v1、ecommerce.v2
3. 4つの通信パターン
gRPCはHTTP/2ベースで4つの通信パターンをサポートします。
3.1 Unary RPC(単項)
最(もっと)も基本的(きほんてき)なパターンで、クライアントが1つのリクエストを送信(そうしん)し、サーバーが1つのレスポンスを返(かえ)します。
service UserService {
rpc GetUser(GetUserRequest) returns (User);
}
message GetUserRequest {
string user_id = 1;
}
message User {
string id = 1;
string name = 2;
string email = 3;
}
Goサーバー実装:
func (s *userServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
user, err := s.repo.FindByID(ctx, req.UserId)
if err != nil {
if errors.Is(err, ErrNotFound) {
return nil, status.Errorf(codes.NotFound, "user %s not found", req.UserId)
}
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
}
return toProtoUser(user), nil
}
Node.jsクライアント:
import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
const packageDefinition = protoLoader.loadSync('user.proto', {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const proto = grpc.loadPackageDefinition(packageDefinition);
const client = new proto.UserService(
'localhost:50051',
grpc.credentials.createInsecure()
);
// Unary呼び出し
client.getUser({ user_id: 'user-123' }, (err, response) => {
if (err) {
console.error('Error:', err.message);
return;
}
console.log('User:', response);
});
3.2 Server Streaming RPC(サーバーストリーミング)
クライアントが1つのリクエストを送信し、サーバーがストリームで複数(ふくすう)のメッセージを返します。
service OrderService {
// リアルタイム注文状況追跡
rpc TrackOrder(TrackOrderRequest) returns (stream OrderStatus);
}
message TrackOrderRequest {
string order_id = 1;
}
message OrderStatus {
string order_id = 1;
string status = 2;
string location = 3;
google.protobuf.Timestamp updated_at = 4;
}
Goサーバー実装:
func (s *orderServer) TrackOrder(req *pb.TrackOrderRequest, stream pb.OrderService_TrackOrderServer) error {
orderID := req.OrderId
// イベントチャネル購読
events := s.eventBus.Subscribe(orderID)
defer s.eventBus.Unsubscribe(orderID, events)
for {
select {
case event := <-events:
status := &pb.OrderStatus{
OrderId: orderID,
Status: event.Status,
Location: event.Location,
UpdatedAt: timestamppb.Now(),
}
if err := stream.Send(status); err != nil {
return err
}
if event.Status == "delivered" {
return nil
}
case <-stream.Context().Done():
return stream.Context().Err()
}
}
}
3.3 Client Streaming RPC(クライアントストリーミング)
クライアントが複数のメッセージをストリームで送信し、サーバーが1つのレスポンスを返します。
service UploadService {
// ファイルアップロード
rpc UploadFile(stream FileChunk) returns (UploadResponse);
}
message FileChunk {
string filename = 1;
bytes content = 2;
int64 offset = 3;
}
message UploadResponse {
string file_id = 1;
int64 total_size = 2;
string checksum = 3;
}
Goサーバー実装:
func (s *uploadServer) UploadFile(stream pb.UploadService_UploadFileServer) error {
var totalSize int64
var filename string
buffer := bytes.Buffer{}
for {
chunk, err := stream.Recv()
if err == io.EOF {
// すべてのチャンク受信完了
fileID, checksum := s.storage.Save(filename, buffer.Bytes())
return stream.SendAndClose(&pb.UploadResponse{
FileId: fileID,
TotalSize: totalSize,
Checksum: checksum,
})
}
if err != nil {
return status.Errorf(codes.Internal, "failed to receive chunk: %v", err)
}
filename = chunk.Filename
totalSize += int64(len(chunk.Content))
buffer.Write(chunk.Content)
}
}
3.4 Bidirectional Streaming RPC(双方向ストリーミング)
クライアントとサーバーが同時(どうじ)にメッセージをやり取(と)りします。リアルタイムチャット、ゲーム、株価(かぶか)配信(はいしん)などに適(てき)しています。
service ChatService {
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}
message ChatMessage {
string user_id = 1;
string room_id = 2;
string content = 3;
google.protobuf.Timestamp sent_at = 4;
MessageType type = 5;
}
enum MessageType {
MESSAGE_TYPE_UNSPECIFIED = 0;
MESSAGE_TYPE_TEXT = 1;
MESSAGE_TYPE_IMAGE = 2;
MESSAGE_TYPE_SYSTEM = 3;
}
Goサーバー実装:
func (s *chatServer) Chat(stream pb.ChatService_ChatServer) error {
for {
msg, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
// 同じルームのすべてのクライアントにブロードキャスト
s.mu.RLock()
clients := s.rooms[msg.RoomId]
s.mu.RUnlock()
for _, client := range clients {
if err := client.Send(msg); err != nil {
log.Printf("failed to send to client: %v", err)
}
}
}
}
4. HTTP/2マルチプレクシング
gRPCはHTTP/2の上(うえ)に構築(こうちく)されており、以下(いか)の機能(きのう)を活用(かつよう)します。
4.1 HTTP/2の核心的特徴
マルチプレクシング: 1つのTCP接続で複数のリクエスト/レスポンスを同時に処理します。HTTP/1.1のHead-of-Line Blocking問題を解決します。
ヘッダー圧縮(HPACK): 重複(じゅうふく)ヘッダーを除去(じょきょ)しハフマンエンコーディングで圧縮します。繰(く)り返(かえ)しリクエストでヘッダーオーバーヘッドを大幅(おおはば)に削減(さくげん)します。
サーバープッシュ: サーバーがクライアントのリクエストなしにデータを送信できます。gRPCストリーミングの基盤です。
バイナリフレーミング: HTTP/1.1のテキストプロトコルとは異なり、HTTP/2はバイナリフレームで通信します。
HTTP/2コネクション
├── Stream 1: GetUser RPC
├── Stream 3: ListProducts RPC (同時実行)
├── Stream 5: TrackOrder RPC (サーバーストリーミング)
└── Stream 7: Chat RPC (双方向ストリーミング)
4.2 コネクション管理
// サーバー設定
server := grpc.NewServer(
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(4 * 1024 * 1024), // 4MB
grpc.MaxSendMsgSize(4 * 1024 * 1024), // 4MB
)
5. インターセプター(ミドルウェア)
インターセプターはgRPCのミドルウェアで、リクエスト前後(ぜんご)に共通(きょうつう)ロジックを実行(じっこう)できます。
5.1 Unaryインターセプター
// ロギングインターセプター
func loggingUnaryInterceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
start := time.Now()
// リクエストロギング
log.Printf("gRPC call: %s", info.FullMethod)
// ハンドラー実行
resp, err := handler(ctx, req)
// レスポンスロギング
duration := time.Since(start)
statusCode := status.Code(err)
log.Printf("gRPC response: method=%s duration=%v status=%s",
info.FullMethod, duration, statusCode)
return resp, err
}
// 認証インターセプター
func authUnaryInterceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
// 公開メソッドは認証スキップ
if isPublicMethod(info.FullMethod) {
return handler(ctx, req)
}
// メタデータからトークン抽出
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Errorf(codes.Unauthenticated, "missing metadata")
}
tokens := md.Get("authorization")
if len(tokens) == 0 {
return nil, status.Errorf(codes.Unauthenticated, "missing token")
}
// トークン検証
claims, err := validateToken(tokens[0])
if err != nil {
return nil, status.Errorf(codes.Unauthenticated, "invalid token: %v", err)
}
// コンテキストにユーザー情報追加
ctx = context.WithValue(ctx, userClaimsKey, claims)
return handler(ctx, req)
}
5.2 ストリームインターセプター
func loggingStreamInterceptor(
srv interface{},
ss grpc.ServerStream,
info *grpc.StreamServerInfo,
handler grpc.StreamHandler,
) error {
start := time.Now()
log.Printf("gRPC stream started: %s", info.FullMethod)
// ラップされたストリームでメッセージカウント
wrapped := &wrappedStream{
ServerStream: ss,
recvCount: 0,
sendCount: 0,
}
err := handler(srv, wrapped)
log.Printf("gRPC stream ended: method=%s duration=%v recv=%d send=%d",
info.FullMethod, time.Since(start), wrapped.recvCount, wrapped.sendCount)
return err
}
5.3 インターセプターチェーン
server := grpc.NewServer(
grpc.ChainUnaryInterceptor(
recoveryUnaryInterceptor, // パニックリカバリ(最外層)
loggingUnaryInterceptor, // ロギング
metricsUnaryInterceptor, // メトリクス収集
authUnaryInterceptor, // 認証
rateLimitUnaryInterceptor, // レート制限
validationUnaryInterceptor, // 入力バリデーション(最内層)
),
grpc.ChainStreamInterceptor(
recoveryStreamInterceptor,
loggingStreamInterceptor,
authStreamInterceptor,
),
)
6. エラーハンドリングとステータスコード
6.1 gRPCステータスコード
| コード | 名前 | 説明 | HTTPマッピング |
|---|---|---|---|
| 0 | OK | 成功 | 200 |
| 1 | CANCELLED | リクエスト取消 | 499 |
| 2 | UNKNOWN | 不明なエラー | 500 |
| 3 | INVALID_ARGUMENT | 不正な引数 | 400 |
| 4 | DEADLINE_EXCEEDED | タイムアウト | 504 |
| 5 | NOT_FOUND | リソースなし | 404 |
| 6 | ALREADY_EXISTS | 既に存在 | 409 |
| 7 | PERMISSION_DENIED | 権限不足 | 403 |
| 8 | RESOURCE_EXHAUSTED | リソース枯渇 | 429 |
| 13 | INTERNAL | 内部サーバーエラー | 500 |
| 14 | UNAVAILABLE | サービス利用不可 | 503 |
| 16 | UNAUTHENTICATED | 認証失敗 | 401 |
6.2 リッチエラーレスポンス
import (
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc/status"
)
func (s *productServer) CreateProduct(ctx context.Context, req *pb.CreateProductRequest) (*pb.Product, error) {
// 入力バリデーション
violations := validateCreateProduct(req)
if len(violations) > 0 {
st := status.New(codes.InvalidArgument, "invalid product data")
br := &errdetails.BadRequest{}
for _, v := range violations {
br.FieldViolations = append(br.FieldViolations, &errdetails.BadRequest_FieldViolation{
Field: v.Field,
Description: v.Description,
})
}
st, err := st.WithDetails(br)
if err != nil {
return nil, status.Errorf(codes.Internal, "unexpected error: %v", err)
}
return nil, st.Err()
}
// ビジネスロジック
product, err := s.repo.Create(ctx, req)
if err != nil {
if isDuplicate(err) {
st := status.New(codes.AlreadyExists, "product already exists")
ri := &errdetails.ResourceInfo{
ResourceType: "Product",
ResourceName: req.Sku,
Description: "A product with this SKU already exists",
}
st, _ = st.WithDetails(ri)
return nil, st.Err()
}
return nil, status.Errorf(codes.Internal, "failed to create product: %v", err)
}
return product, nil
}
7. DeadlineとTimeout
7.1 Deadline伝播
// クライアント:deadline設定
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := client.GetProduct(ctx, &pb.GetProductRequest{ProductId: "prod-123"})
if err != nil {
st := status.Convert(err)
if st.Code() == codes.DeadlineExceeded {
log.Println("Request timed out")
}
}
// サーバー:deadline確認
func (s *server) GetProduct(ctx context.Context, req *pb.GetProductRequest) (*pb.Product, error) {
deadline, ok := ctx.Deadline()
if ok {
remaining := time.Until(deadline)
if remaining < 100*time.Millisecond {
return nil, status.Errorf(codes.DeadlineExceeded, "insufficient time remaining")
}
}
product, err := s.repo.FindByID(ctx, req.ProductId)
if err != nil {
return nil, err
}
// 価格サービスに残り時間の80%を割り当て
remaining := time.Until(deadline)
priceCtx, cancel := context.WithTimeout(ctx, remaining*80/100)
defer cancel()
price, err := s.priceClient.GetPrice(priceCtx, &pb.GetPriceRequest{
ProductId: req.ProductId,
})
if err != nil {
price = s.priceCache.Get(req.ProductId)
}
product.Price = price
return product, nil
}
8. ロードバランシング
8.1 クライアントサイドロードバランシング
import (
"google.golang.org/grpc"
"google.golang.org/grpc/resolver"
_ "google.golang.org/grpc/balancer/roundrobin"
)
// カスタムリゾルバー登録
resolver.Register(&myResolver{})
conn, err := grpc.Dial(
"my-service:///product-service",
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
8.2 プロキシベースロードバランシング(Envoy)
# envoy.yaml
static_resources:
listeners:
- name: grpc_listener
address:
socket_address:
address: 0.0.0.0
port_value: 9090
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: grpc
codec_type: AUTO
route_config:
name: local_route
virtual_hosts:
- name: grpc_service
domains: ["*"]
routes:
- match:
prefix: "/"
grpc: {}
route:
cluster: grpc_backend
timeout: 30s
retry_policy:
retry_on: "unavailable,resource-exhausted"
num_retries: 3
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: grpc_backend
connect_timeout: 5s
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: {}
load_assignment:
cluster_name: grpc_backend
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: product-service
port_value: 50051
9. gRPC-Web:ブラウザでgRPCを使用
9.1 gRPC-Webが必要な理由
ブラウザはHTTP/2フレーミングを直接制御できないため、gRPCをネイティブに使用できません。gRPC-Webはこのギャップを埋(う)めるプロキシ層(そう)です。
9.2 TypeScriptクライアント(Connect-Web)
import { createGrpcWebTransport } from "@connectrpc/connect-web";
import { createClient } from "@connectrpc/connect";
import { ProductService } from "./gen/product_connect";
const transport = createGrpcWebTransport({
baseUrl: "https://api.example.com",
});
const client = createClient(ProductService, transport);
// Unary呼び出し
async function getProduct(id: string) {
try {
const product = await client.getProduct({ productId: id });
console.log("Product:", product.name, product.price);
} catch (err) {
if (err instanceof ConnectError) {
console.error("gRPC Error:", err.code, err.message);
}
}
}
// Server Streaming
async function trackOrder(orderId: string) {
for await (const status of client.trackOrder({ orderId })) {
console.log("Order status:", status.status, status.location);
updateUI(status);
}
}
10. リフレクションとヘルスチェック
10.1 サーバーリフレクション
import "google.golang.org/grpc/reflection"
func main() {
server := grpc.NewServer()
pb.RegisterProductServiceServer(server, &productServer{})
// リフレクション有効化(開発環境用)
reflection.Register(server)
lis, _ := net.Listen("tcp", ":50051")
server.Serve(lis)
}
10.2 ヘルスチェック
import "google.golang.org/grpc/health"
import healthpb "google.golang.org/grpc/health/grpc_health_v1"
func main() {
server := grpc.NewServer()
// ヘルスチェックサービス登録
healthServer := health.NewServer()
healthpb.RegisterHealthServer(server, healthServer)
// サービス状態設定
healthServer.SetServingStatus("product.v1.ProductService", healthpb.HealthCheckResponse_SERVING)
// 定期的ヘルスチェック
go func() {
for {
if dbHealthy() {
healthServer.SetServingStatus("", healthpb.HealthCheckResponse_SERVING)
} else {
healthServer.SetServingStatus("", healthpb.HealthCheckResponse_NOT_SERVING)
}
time.Sleep(10 * time.Second)
}
}()
}
10.3 Kubernetesヘルスプローブ
# kubernetes deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: product-service
spec:
template:
spec:
containers:
- name: product-service
image: product-service:latest
ports:
- containerPort: 50051
name: grpc
livenessProbe:
grpc:
port: 50051
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
grpc:
port: 50051
initialDelaySeconds: 5
periodSeconds: 5
11. サービスメッシュ統合
11.1 IstioとgRPC
# istio virtual service
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: product-service
spec:
hosts:
- product-service
http:
- match:
- headers:
content-type:
exact: application/grpc
route:
- destination:
host: product-service
subset: v2
weight: 90
- destination:
host: product-service
subset: v1
weight: 10
retries:
attempts: 3
perTryTimeout: 2s
retryOn: "unavailable,resource-exhausted"
timeout: 10s
11.2 LinkerdとgRPC
LinkerdはHTTP/2をネイティブにサポートするため、gRPCトラフィックを自動的に認識します。
# linkerd service profile
apiVersion: linkerd.io/v1alpha2
kind: ServiceProfile
metadata:
name: product-service.default.svc.cluster.local
spec:
routes:
- name: GetProduct
condition:
method: POST
pathRegex: /ecommerce.v1.ProductService/GetProduct
responseClasses:
- condition:
status:
min: 200
max: 299
- name: ListProducts
condition:
method: POST
pathRegex: /ecommerce.v1.ProductService/ListProducts
isRetryable: true
12. テスティング
12.1 grpcurlでテスト
# サービス一覧取得(リフレクション必要)
grpcurl -plaintext localhost:50051 list
# サービスメソッド一覧
grpcurl -plaintext localhost:50051 list ecommerce.v1.ProductService
# メソッド説明
grpcurl -plaintext localhost:50051 describe ecommerce.v1.ProductService.GetProduct
# Unary呼び出し
grpcurl -plaintext \
-d '{"product_id": "prod-123"}' \
localhost:50051 ecommerce.v1.ProductService/GetProduct
# ヘッダー付き呼び出し
grpcurl -plaintext \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
-d '{"name": "New Product", "price": {"currency_code": "USD", "units": 29, "nanos": 990000000}}' \
localhost:50051 ecommerce.v1.ProductService/CreateProduct
12.2 Goでの統合テスト
func TestGetProduct(t *testing.T) {
// インメモリgRPCサーバー
lis := bufconn.Listen(1024 * 1024)
server := grpc.NewServer()
pb.RegisterProductServiceServer(server, newTestProductServer())
go func() {
if err := server.Serve(lis); err != nil {
t.Fatal(err)
}
}()
defer server.Stop()
// クライアント接続
conn, err := grpc.DialContext(context.Background(), "bufnet",
grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) {
return lis.Dial()
}),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
require.NoError(t, err)
defer conn.Close()
client := pb.NewProductServiceClient(conn)
// テスト実行
resp, err := client.GetProduct(context.Background(), &pb.GetProductRequest{
ProductId: "test-product-1",
})
require.NoError(t, err)
assert.Equal(t, "test-product-1", resp.Id)
assert.Equal(t, "Test Product", resp.Name)
}
13. パフォーマンスベンチマーク
13.1 Protobuf vs JSON パフォーマンス比較
一般的なベンチマーク結果:
| メトリック | Protobuf | JSON | 比率 |
|---|---|---|---|
| シリアライゼーション速度 | 150ns | 1500ns | 10倍速い |
| デシリアライゼーション速度 | 200ns | 2000ns | 10倍速い |
| データサイズ | 68 bytes | 220 bytes | 3.2倍小さい |
| メモリアロケーション | 1 alloc | 8 allocs | 8倍少ない |
13.2 gRPC vs RESTスループット
| シナリオ | gRPC (req/s) | REST (req/s) | gRPC優位性 |
|---|---|---|---|
| 小さいペイロード (100B) | 45,000 | 15,000 | 3倍 |
| 中程度ペイロード (1KB) | 35,000 | 10,000 | 3.5倍 |
| 大きいペイロード (100KB) | 8,000 | 2,500 | 3.2倍 |
| ストリーミング (10万メッセージ) | 150,000/s | N/A | - |
14. 実践マイクロサービス実装例
14.1 プロジェクト構造
ecommerce-grpc/
├── proto/
│ ├── buf.yaml
│ ├── buf.gen.yaml
│ └── ecommerce/
│ └── v1/
│ ├── product.proto
│ ├── order.proto
│ ├── payment.proto
│ └── common.proto
├── services/
│ ├── product/
│ │ ├── main.go
│ │ ├── server.go
│ │ ├── repository.go
│ │ └── server_test.go
│ ├── order/
│ │ ├── main.go
│ │ ├── server.go
│ │ └── saga.go
│ └── payment/
│ ├── main.go
│ └── server.go
├── gen/
│ └── ecommerce/
│ └── v1/
├── docker-compose.yaml
└── Makefile
14.2 Bufによるコード生成
# buf.gen.yaml
version: v2
plugins:
- remote: buf.build/protocolbuffers/go
out: gen
opt:
- paths=source_relative
- remote: buf.build/grpc/go
out: gen
opt:
- paths=source_relative
- require_unimplemented_servers=false
- remote: buf.build/connectrpc/go
out: gen
opt:
- paths=source_relative
# コード生成
buf generate proto
# リント
buf lint proto
# 下位互換性チェック
buf breaking proto --against '.git#branch=main'
15. クイズ
以下のクイズでgRPCとProtocol Buffersの理解度(りかいど)をチェックしましょう。
Q1: gRPCがHTTP/2を使用することで得られる最大の利点は何ですか?
正解:マルチプレクシング(Multiplexing)
HTTP/2のマルチプレクシングにより、1つのTCP接続で複数のgRPC呼び出しを同時に処理できます。HTTP/1.1では接続あたり1つのリクエストしか処理できずHead-of-Line Blockingが発生しますが、HTTP/2では複数のストリームが独立して動作します。その他、ヘッダー圧縮(HPACK)、バイナリフレーミング、サーバープッシュなどの利点があります。
Q2: Protocol Buffersでフィールド番号1〜15を頻繁に使うフィールドに割り当てる理由は?
正解:エンコーディングサイズが小さいため
フィールド番号1〜15は1バイトでエンコードされ、16〜2047は2バイトでエンコードされます。頻繁に使用するフィールドに低い番号を割り当てると、メッセージ全体のサイズを削減できます。特にrepeatedフィールドのように複数回出現するフィールドでは効果が大きいです。
Q3: Bidirectional StreamingとServer Streamingの違いは何ですか?
正解:クライアントもストリームでメッセージを送信できるかどうか
Server Streamingではクライアントが1つのリクエストを送信し、サーバーがストリームで応答します。Bidirectional Streamingではクライアントとサーバーが独立したストリームで同時にメッセージをやり取りできます。2つのストリームは独立しているため、サーバーはクライアントのすべてのメッセージを受け取るまで待つ必要がありません。
Q4: gRPCでdeadlineが重要な理由は何ですか?
正解:分散システムでリクエストタイムアウトを伝播してリソースの無駄遣いを防止
deadlineはクライアントが設定した最大待機時間で、gRPCはこれを自動的に下流のサービス呼び出しに伝播します。サービスAがサービスBを呼び出し、BがCを呼び出すとき、Aのdeadlineが既に過ぎていればBとCの作業は意味がありません。deadline伝播によりこのような不要な作業を防止します。
Q5: gRPC-Webが必要な理由は何ですか?
正解:ブラウザがHTTP/2フレーミングを直接制御できないため
ブラウザのFetch APIとXMLHttpRequestはHTTP/2フレームレベルの制御をサポートしていません。gRPCはHTTP/2のトレーラー(trailers)を使用してステータスコードを伝達しますが、ブラウザではこれを読み取ることができません。gRPC-Webはプロキシ(Envoyなど)を通じてgRPCプロトコルをブラウザが理解できる形式に変換します。
16. 参考資料
- gRPC公式ドキュメント - gRPC公式ガイドとチュートリアル
- Protocol Buffers Language Guide - Protobuf proto3文法ガイド
- gRPC Go Tutorial - Go言語gRPCチュートリアル
- Buf - Protobufツール - モダンなProtobuf管理ツール
- gRPC-Web GitHub - gRPC-Web公式リポジトリ
- Connect-RPC - ブラウザ互換gRPCフレームワーク
- Envoy gRPC設定 - EnvoyのgRPCサポート
- grpcurl GitHub - gRPCコマンドラインツール
- evans GitHub - gRPCインタラクティブクライアント
- Istio gRPC Traffic Management - IstioとgRPC統合
- gRPC Health Checking Protocol - ヘルスチェックプロトコル仕様
- Google API Design Guide - GoogleのAPI設計ガイド
- gRPC Performance Best Practices - gRPCパフォーマンス最適化ガイド