gRPC入門|高速なAPI通信の仕組みとProtocol Buffersの基本

kento_morota 23分で読めます

マイクロサービス間の通信をもっと高速にしたい、型安全なAPI通信を実現したい——こうした要件に応えるのがgRPCです。Googleが開発したこのフレームワークは、HTTP/2とProtocol Buffersを基盤とし、REST APIと比較して大幅なパフォーマンス向上を実現します。

本記事では、gRPCの基本概念からProtocol Buffersの書き方、サーバー・クライアントの実装、4種類の通信パターンまでを体系的に解説します。

gRPCとは何か

gRPC(gRPC Remote Procedure Call)は、Googleが開発したオープンソースの高性能RPCフレームワークです。RPCとは「Remote Procedure Call(遠隔手続き呼び出し)」の略で、リモートサーバー上の関数をローカルの関数のように呼び出す仕組みです。

gRPCの特徴

Protocol Buffersによるシリアライゼーション
gRPCはデフォルトでProtocol Buffers(protobuf)を使ってデータをバイナリ形式にシリアライズします。JSON(テキスト形式)と比較して、データサイズが小さく、シリアライズ/デシリアライズの速度が高速です。

HTTP/2ベースの通信
HTTP/2のマルチプレキシング、ヘッダー圧縮、サーバープッシュなどの機能を活用し、高効率な通信を実現します。1つのTCP接続上で複数のリクエスト・レスポンスを同時に処理できます。

厳密なインターフェース定義
.protoファイルでサービスとメッセージの型を定義し、そこから各プログラミング言語のクライアント・サーバーコードを自動生成します。サーバーとクライアントが異なる言語で実装されていても、型の不整合が起きません。

多言語対応
Go、Java、Python、C++、Node.js、C#、Ruby、Kotlinなど、主要な言語すべてに対応しています。

REST APIとの比較

gRPCとREST APIの主な違いを整理します。

プロトコル
REST APIはHTTP/1.1が主流(HTTP/2も使用可能)。gRPCはHTTP/2を必須とします。

データ形式
REST APIはJSON(テキスト形式)が主流。gRPCはProtocol Buffers(バイナリ形式)を使用します。バイナリのためデータ量が少なく、パース処理も高速です。

コード生成
REST APIは手動でクライアントコードを作成するか、OpenAPIから生成します。gRPCは.protoファイルから自動的にクライアント・サーバーのスタブコードが生成されます。

ストリーミング
REST APIでは双方向ストリーミングの実現が困難です。gRPCは4種類の通信パターン(後述)をネイティブにサポートしています。

ブラウザ対応
REST APIはブラウザから直接呼び出せます。gRPCはブラウザから直接呼び出せないため、gRPC-WebやBFF(Backend for Frontend)経由で利用します。

Protocol Buffersの基本

Protocol Buffers(protobuf)は、gRPCのインターフェース定義言語(IDL)であり、シリアライゼーション形式でもあります。

.protoファイルの書き方

// user.proto
syntax = "proto3";  // proto3構文を使用

package user;       // パッケージ名

option go_package = "github.com/example/user-service/pb";

// メッセージ型の定義
message User {
  string id = 1;           // フィールド番号1
  string name = 2;         // フィールド番号2
  string email = 3;        // フィールド番号3
  int32 age = 4;           // フィールド番号4
  UserRole role = 5;       // enum型
  repeated string tags = 6; // 配列(繰り返しフィールド)
  optional string bio = 7;  // オプショナルフィールド
  google.protobuf.Timestamp created_at = 8;
}

// Enum型の定義
enum UserRole {
  USER_ROLE_UNSPECIFIED = 0; // デフォルト値(proto3の慣例)
  USER_ROLE_ADMIN = 1;
  USER_ROLE_MEMBER = 2;
  USER_ROLE_GUEST = 3;
}

// サービスの定義
service UserService {
  // ユーザーの取得(Unary RPC)
  rpc GetUser(GetUserRequest) returns (GetUserResponse);

  // ユーザーの作成
  rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);

  // ユーザー一覧の取得(Server Streaming RPC)
  rpc ListUsers(ListUsersRequest) returns (stream User);

  // ユーザーの一括作成(Client Streaming RPC)
  rpc BatchCreateUsers(stream CreateUserRequest) returns (BatchCreateUsersResponse);

  // チャット(Bidirectional Streaming RPC)
  rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}

// リクエスト・レスポンスのメッセージ定義
message GetUserRequest {
  string id = 1;
}

message GetUserResponse {
  User user = 1;
}

message CreateUserRequest {
  string name = 1;
  string email = 2;
  int32 age = 3;
}

message CreateUserResponse {
  User user = 1;
}

message ListUsersRequest {
  int32 page_size = 1;
  string page_token = 2;
}

message BatchCreateUsersResponse {
  int32 created_count = 1;
  repeated User users = 2;
}

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

フィールド番号の重要性

Protocol Buffersでは、各フィールドに一意の番号を割り当てます。この番号はバイナリエンコードのキーとして使われるため、以下のルールに注意が必要です。

番号は変更しない
一度割り当てたフィールド番号を変更すると、過去のデータとの互換性が壊れます。

削除したフィールドの番号は再利用しない
フィールドを削除する場合は、reservedキーワードでその番号を予約し、将来の再利用を防ぎます。

message User {
  reserved 4, 8;           // 削除したフィールドの番号を予約
  reserved "old_field";     // 削除したフィールド名を予約

  string id = 1;
  string name = 2;
  string email = 3;
  // age(4) は削除済み
  UserRole role = 5;
}

1〜15は1バイト、16〜2047は2バイト
よく使うフィールドには1〜15の番号を割り当てると、エンコードサイズを節約できます。

gRPCの4つの通信パターン

gRPCは4種類の通信パターンをサポートしています。それぞれの特徴と使いどころを解説します。

Unary RPC(単項RPC)

最も基本的なパターンで、REST APIと同じリクエスト・レスポンスの1対1通信です。

// サービス定義
rpc GetUser(GetUserRequest) returns (GetUserResponse);

クライアントが1つのリクエストを送り、サーバーが1つのレスポンスを返します。通常のCRUD操作はこのパターンで実装します。

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

クライアントが1つのリクエストを送り、サーバーが複数のレスポンスをストリームで返すパターンです。

// サービス定義
rpc ListUsers(ListUsersRequest) returns (stream User);

大量のデータをページング的に返す場合や、リアルタイムのイベント通知に適しています。ログのテーリングや株価のリアルタイム配信などが代表的なユースケースです。

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

クライアントが複数のリクエストをストリームで送り、サーバーが1つのレスポンスを返すパターンです。

// サービス定義
rpc BatchCreateUsers(stream CreateUserRequest) returns (BatchCreateUsersResponse);

大量のデータを一括アップロードする場合や、センサーデータの継続的な送信に適しています。

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

クライアントとサーバーが独立してメッセージを送受信するパターンです。

// サービス定義
rpc Chat(stream ChatMessage) returns (stream ChatMessage);

チャットアプリケーション、リアルタイムの共同編集、ゲームのマルチプレイヤー通信などに適しています。

Go言語でgRPCサーバーを実装する

実際にGo言語でgRPCサーバーを実装する手順を紹介します。

プロジェクトのセットアップ

# protoc(Protocol Buffersコンパイラ)のインストール
# macOS
brew install protobuf

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

# .protoファイルからGoコードを生成
protoc --go_out=. --go_opt=paths=source_relative \
  --go-grpc_out=. --go-grpc_opt=paths=source_relative \
  proto/user.proto

サーバーの実装

// server/main.go
package main

import (
    "context"
    "fmt"
    "log"
    "net"
    "sync"

    pb "github.com/example/user-service/pb"
    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

// サーバーの構造体
type userServer struct {
    pb.UnimplementedUserServiceServer
    mu    sync.Mutex
    users map[string]*pb.User
}

// GetUser の実装(Unary RPC)
func (s *userServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) {
    s.mu.Lock()
    defer s.mu.Unlock()

    user, ok := s.users[req.Id]
    if !ok {
        return nil, status.Errorf(codes.NotFound, "ユーザーが見つかりません: %s", req.Id)
    }

    return &pb.GetUserResponse{User: user}, nil
}

// CreateUser の実装(Unary RPC)
func (s *userServer) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) {
    s.mu.Lock()
    defer s.mu.Unlock()

    id := fmt.Sprintf("user_%d", len(s.users)+1)
    user := &pb.User{
        Id:    id,
        Name:  req.Name,
        Email: req.Email,
        Age:   req.Age,
    }
    s.users[id] = user

    return &pb.CreateUserResponse{User: user}, nil
}

// ListUsers の実装(Server Streaming RPC)
func (s *userServer) ListUsers(req *pb.ListUsersRequest, stream pb.UserService_ListUsersServer) error {
    s.mu.Lock()
    defer s.mu.Unlock()

    for _, user := range s.users {
        if err := stream.Send(user); err != nil {
            return err
        }
    }
    return nil
}

func main() {
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("リッスンに失敗: %v", err)
    }

    // インターセプター(ミドルウェア)の追加
    grpcServer := grpc.NewServer(
        grpc.UnaryInterceptor(loggingInterceptor),
    )

    pb.RegisterUserServiceServer(grpcServer, &userServer{
        users: make(map[string]*pb.User),
    })

    log.Printf("gRPCサーバー起動: :50051")
    if err := grpcServer.Serve(lis); err != nil {
        log.Fatalf("サーバー起動に失敗: %v", err)
    }
}

// ログ出力用のインターセプター(ミドルウェア)
func loggingInterceptor(
    ctx context.Context,
    req interface{},
    info *grpc.UnaryServerInfo,
    handler grpc.UnaryHandler,
) (interface{}, error) {
    log.Printf("gRPCメソッド呼び出し: %s", info.FullMethod)
    resp, err := handler(ctx, req)
    if err != nil {
        log.Printf("エラー: %v", err)
    }
    return resp, err
}

クライアントの実装

// client/main.go
package main

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

    pb "github.com/example/user-service/pb"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
)

func main() {
    // サーバーに接続
    conn, err := grpc.NewClient("localhost:50051",
        grpc.WithTransportCredentials(insecure.NewCredentials()),
    )
    if err != nil {
        log.Fatalf("接続に失敗: %v", err)
    }
    defer conn.Close()

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

    // ユーザー作成(Unary RPC)
    createResp, err := client.CreateUser(ctx, &pb.CreateUserRequest{
        Name:  "田中太郎",
        Email: "tanaka@example.com",
        Age:   30,
    })
    if err != nil {
        log.Fatalf("ユーザー作成に失敗: %v", err)
    }
    log.Printf("作成されたユーザー: %v", createResp.User)

    // ユーザー取得(Unary RPC)
    getResp, err := client.GetUser(ctx, &pb.GetUserRequest{
        Id: createResp.User.Id,
    })
    if err != nil {
        log.Fatalf("ユーザー取得に失敗: %v", err)
    }
    log.Printf("取得したユーザー: %v", getResp.User)

    // ユーザー一覧取得(Server Streaming RPC)
    stream, err := client.ListUsers(ctx, &pb.ListUsersRequest{})
    if err != nil {
        log.Fatalf("ストリーム開始に失敗: %v", err)
    }

    for {
        user, err := stream.Recv()
        if err == io.EOF {
            break
        }
        if err != nil {
            log.Fatalf("ストリーム受信に失敗: %v", err)
        }
        log.Printf("ユーザー: %v", user)
    }
}

gRPCのエラーハンドリングとメタデータ

本番運用で欠かせないエラーハンドリングとメタデータの仕組みを解説します。

ステータスコードとエラーの詳細

gRPCはHTTPのステータスコードとは異なる独自のステータスコードを定義しています。

import (
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
    "google.golang.org/genproto/googleapis/rpc/errdetails"
)

func (s *userServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) {
    // バリデーション
    if req.Id == "" {
        // BadRequest詳細を含むエラー
        st := status.New(codes.InvalidArgument, "IDは必須です")
        detail := &errdetails.BadRequest{
            FieldViolations: []*errdetails.BadRequest_FieldViolation{
                {
                    Field:       "id",
                    Description: "IDは空にできません",
                },
            },
        }
        st, _ = st.WithDetails(detail)
        return nil, st.Err()
    }

    user, ok := s.users[req.Id]
    if !ok {
        // NotFoundエラー
        return nil, status.Errorf(codes.NotFound, "ユーザーID %s が見つかりません", req.Id)
    }

    return &pb.GetUserResponse{User: user}, nil
}

主なステータスコードには以下があります。

  • codes.OK:成功
  • codes.InvalidArgument:無効な引数(HTTP 400相当)
  • codes.NotFound:リソースが見つからない(HTTP 404相当)
  • codes.AlreadyExists:リソースが既に存在する(HTTP 409相当)
  • codes.PermissionDenied:権限不足(HTTP 403相当)
  • codes.Unauthenticated:未認証(HTTP 401相当)
  • codes.Internal:内部エラー(HTTP 500相当)

メタデータの送受信

gRPCのメタデータは、HTTPヘッダーに相当する仕組みです。認証トークンやリクエストIDの伝播に使います。

import "google.golang.org/grpc/metadata"

// クライアント側:メタデータの送信
md := metadata.New(map[string]string{
    "authorization": "Bearer eyJhbGci...",
    "x-request-id":  "req-12345",
})
ctx = metadata.NewOutgoingContext(ctx, md)
resp, err := client.GetUser(ctx, req)

// サーバー側:メタデータの受信
func (s *userServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) {
    md, ok := metadata.FromIncomingContext(ctx)
    if ok {
        if tokens := md.Get("authorization"); len(tokens) > 0 {
            // トークンの検証
            token := tokens[0]
            // ...
        }
    }
    // ...
}

gRPCの本番運用で考慮すべきポイント

gRPCを本番環境で運用する際に押さえておくべきポイントを紹介します。

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

import (
    "google.golang.org/grpc/health"
    healthpb "google.golang.org/grpc/health/grpc_health_v1"
    "google.golang.org/grpc/reflection"
)

grpcServer := grpc.NewServer()

// ヘルスチェックの登録
healthServer := health.NewServer()
healthpb.RegisterHealthServer(grpcServer, healthServer)
healthServer.SetServingStatus("user.UserService", healthpb.HealthCheckResponse_SERVING)

// リフレクション(開発環境のみ推奨)
reflection.Register(grpcServer)

TLS / mTLSの設定

本番環境ではTLS暗号化が必須です。サービス間通信ではmTLS(双方向TLS)を採用することで、クライアントとサーバーの双方を認証できます。

import "google.golang.org/grpc/credentials"

// TLS付きサーバー
creds, err := credentials.NewServerTLSFromFile("server.crt", "server.key")
grpcServer := grpc.NewServer(grpc.Creds(creds))

// TLS付きクライアント
creds, err := credentials.NewClientTLSFromFile("ca.crt", "")
conn, err := grpc.NewClient("localhost:50051",
    grpc.WithTransportCredentials(creds),
)

ロードバランシングの注意点

gRPCはHTTP/2の長時間接続を使うため、従来のL4(TCP)ロードバランサーでは接続単位の分散になり、リクエストが偏る問題が生じます。L7(HTTP/2対応)のロードバランサーを使用するか、クライアントサイドロードバランシングを実装する必要があります。

Kubernetesの場合、Envoyプロキシ、Istioサービスメッシュ、またはgRPC用のネイティブロードバランシング機能を利用する方法が一般的です。

まとめ

gRPCは、高性能なサービス間通信を実現する強力なフレームワークです。本記事のポイントを振り返りましょう。

gRPCの核心技術
HTTP/2による高効率な通信と、Protocol Buffersによる型安全かつコンパクトなデータシリアライゼーションが、gRPCの高性能の源泉です。

Protocol Buffersの設計
.protoファイルでサービスとメッセージを定義し、自動コード生成によりサーバー・クライアントの型安全性を保証します。フィールド番号の管理がスキーマの後方互換性を維持する鍵です。

4つの通信パターン
Unary、Server Streaming、Client Streaming、Bidirectional Streamingの4パターンを使い分けることで、さまざまなユースケースに対応できます。

本番運用の考慮点
エラーハンドリング、TLS暗号化、ヘルスチェック、ロードバランシングの適切な設定が、安定運用の鍵となります。

使い分けの指針
サービス間通信やパフォーマンスが重要な内部APIにはgRPC、ブラウザからのアクセスや外部公開APIにはREST APIやGraphQLという使い分けが一般的です。

まずはシンプルな.protoファイルを定義し、コード生成からサーバー・クライアントの実装まで一通り体験してみてください。RESTとは異なる開発体験に触れることで、gRPCの価値をより深く理解できるはずです。

#gRPC#Protocol Buffers#API
共有:
無料メルマガ

週1回、最新の技術記事をお届け

AI・クラウド・開発の最新記事を毎週月曜にメールでお届けします。登録は無料、いつでも解除できます。

プライバシーポリシーに基づき管理します

起業準備に役立つ情報、もっとありますよ。

まずは話だけ聞いてもらう