Skip to content
Published on

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

Authors

目次

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.v1ecommerce.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マッピング
0OK成功200
1CANCELLEDリクエスト取消499
2UNKNOWN不明なエラー500
3INVALID_ARGUMENT不正な引数400
4DEADLINE_EXCEEDEDタイムアウト504
5NOT_FOUNDリソースなし404
6ALREADY_EXISTS既に存在409
7PERMISSION_DENIED権限不足403
8RESOURCE_EXHAUSTEDリソース枯渇429
13INTERNAL内部サーバーエラー500
14UNAVAILABLEサービス利用不可503
16UNAUTHENTICATED認証失敗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 パフォーマンス比較

一般的なベンチマーク結果:

メトリックProtobufJSON比率
シリアライゼーション速度150ns1500ns10倍速い
デシリアライゼーション速度200ns2000ns10倍速い
データサイズ68 bytes220 bytes3.2倍小さい
メモリアロケーション1 alloc8 allocs8倍少ない

13.2 gRPC vs RESTスループット

シナリオgRPC (req/s)REST (req/s)gRPC優位性
小さいペイロード (100B)45,00015,0003倍
中程度ペイロード (1KB)35,00010,0003.5倍
大きいペイロード (100KB)8,0002,5003.2倍
ストリーミング (10万メッセージ)150,000/sN/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. 参考資料

  1. gRPC公式ドキュメント - gRPC公式ガイドとチュートリアル
  2. Protocol Buffers Language Guide - Protobuf proto3文法ガイド
  3. gRPC Go Tutorial - Go言語gRPCチュートリアル
  4. Buf - Protobufツール - モダンなProtobuf管理ツール
  5. gRPC-Web GitHub - gRPC-Web公式リポジトリ
  6. Connect-RPC - ブラウザ互換gRPCフレームワーク
  7. Envoy gRPC設定 - EnvoyのgRPCサポート
  8. grpcurl GitHub - gRPCコマンドラインツール
  9. evans GitHub - gRPCインタラクティブクライアント
  10. Istio gRPC Traffic Management - IstioとgRPC統合
  11. gRPC Health Checking Protocol - ヘルスチェックプロトコル仕様
  12. Google API Design Guide - GoogleのAPI設計ガイド
  13. gRPC Performance Best Practices - gRPCパフォーマンス最適化ガイド