最新のマイクロサービスが REST よりも gRPC を好む理由

最新のマイクロサービスが REST よりも gRPC を好む理由

モノリシック アーキテクチャでは、コンポーネントはメモリ内のメソッド呼び出しを介して通信します。これは瞬時に行われ、信頼性が高くなります。ただし、マイクロサービス アーキテクチャに移行すると、これらのコンポーネントはネットワーク境界によって分離されます。通信はアウトプロセス ネットワーク呼び出し (プロセス間通信、つまり IPC) になります。

長年にわたり、JSON ペイロードを使用した HTTP/1.1 経由の REST (Representational State Transfer) が、Web API を構築するためのデフォルトの標準となってきました。 REST は、公開 Web サービスやクライアントとサーバー間の対話には優れていますが、高頻度で低遅延の内部サービス間通信に使用すると、重大なボトルネックが発生します。

このため、最新のマイクロサービスは gRPC (Google Remote Procedure Call) に急速に移行しています。この記事では、REST の制限を分析し、gRPC のアーキテクチャの柱を詳しく分析し、Java での gRPC サービスの完全な実装について説明します。


1. マイクロサービスにおける REST のボトルネック

REST は 20 年以上にわたって Web を支えてきました。ただし、その基盤となるテクノロジーは、内部分散アーキテクチャ用に最適化されていません。

A. プレーンテキスト JSON のオーバーヘッド

JSON は人間が判読できるため、デバッグが容易ですが、マシン間の通信には非常に非効率です。

  • シリアル化/逆シリアル化のコスト: テキスト文字列トークンを解析してオブジェクトに変換すると、かなりの CPU サイクルが消費されます。
  • 大きなペイロード サイズ: JSON キーは単一のリクエストごとに繰り返されます (例: {"transactionId": "123", "amount": 99.99})。 1 秒あたり何百万ものリクエストを処理するシステムの場合、この冗長なメタデータは膨大なネットワーク帯域幅を浪費します。

B. HTTP/1.1 接続の制限

REST は通常、HTTP/1.1 上で実行されますが、これにはいくつかの構造的非効率性があります。

  • Head-of-Line (HoL) ブロッキング: 単一の TCP 接続では、クライアントは次の要求を送信する前に現在の要求の応答を待つ必要があります。
  • 接続プーリングの枯渇: 同時リクエストを処理するには、クライアントは複数の TCP 接続を開く必要があります。その結果、接続が継続的にオープンおよびクローズされるため、OS リソースのオーバーヘッドが大幅に増加し、ソケットが枯渇し、スロースタートのパフォーマンスが低下します。

C. 弱い API コントラクト

REST API には、コンパイル時のコントラクトが組み込まれていません。 OpenAPI/Swagger は API の文書化に役立ちますが、コードベースとは別のものです。バックエンド開発者が JSON フィールド名を変更し、コンパイル時の警告なしにダウンストリーム サービスを誤って中断することは簡単です。


2. gRPC のアーキテクチャの柱

2015 年に Google によって導入された gRPC は、クラウド ネイティブ アプリケーション専用に設計されたオープンソースの高性能 RPC フレームワークです。次の 3 つの主要なテクノロジーを使用して REST のボトルネックを解決します。

gRPC の 3 つのアーキテクチャの柱: プロトコル バッファー、HTTP/2 トランスポート、コード生成

A. プロトコル バッファー (Protobuf)

gRPC は、JSON の代わりに、インターフェイス定義言語 (IDL) およびメッセージ シリアル化形式として プロトコル バッファ を使用します。

  • Protobuf は、高度に最適化されたバイナリ シリアル化メカニズムです。
  • フィールドは、テキスト名ではなく小さな数値タグとしてシリアル化されます。
  • ペイロードは JSON よりも最大 70 ~ 80% 小さく、シリアル化は最大 10 倍高速で、CPU と帯域幅の使用量が大幅に削減されます。

B. HTTP/2 多重化

gRPC はトランスポート プロトコルとして HTTP/2 を利用し、いくつかのパフォーマンスの最適化を実現します。

  • 真の多重化: 複数の要求と応答を 単一 の長時間存続する TCP 接続上で同時にインターリーブすることができ、Head-of-Line ブロッキングを完全に排除します。
  • ヘッダー圧縮 (HPACK): HTTP ヘッダーが圧縮され、リクエスト サイズがさらに削減されます。
  • ストリーミング サポート: HTTP/2 は、単方向クライアント ストリーミング、サーバー ストリーミング、および完全な双方向ストリーミングをネイティブにサポートします。

C. コード生成と厳密な契約

.proto ファイルで API 構造を定義することにより、gRPC は数十の言語 (Java、Go、Python、C++ など) でクライアント スタブとサーバーの基本クラスを生成します。コントラクトはコンパイル時に安全です。サーバーがインターフェースを更新した場合、新しいコントラクトに一致しないクライアントのビルドは直ちに失敗します。


3. Java 実装: gRPC サービスの構築

実用的な Java gRPC アプリケーションを構築してみましょう。当社は、クライアントが支払いリクエストを送信して取引確認を受け取る、高スループットの 支払い処理サービス を実装します。

ステップ 1: API コントラクトを定義する (payment.proto)

まず、プロトコル バッファ ファイルでサービス インターフェイスとメッセージ構造を定義します。

syntax = "proto3";

option java_multiple_files = true;
option java_package = "com.ghaznix.grpc.payment";
option java_outer_classname = "PaymentProto";

package payment;

// The Payment Service definition
service PaymentService {
  // Unary RPC: Sends a single payment request and receives a confirmation
  rpc ProcessPayment (PaymentRequest) returns (PaymentResponse);
}

// The Payment Request message
message PaymentRequest {
  string transaction_id = 1;
  string customer_id = 2;
  double amount = 3;
  string currency = 4;
}

// The Payment Response message
message PaymentResponse {
  string transaction_id = 1;
  string status = 2;
  string message = 3;
  string timestamp = 4;
}

ステップ 2: サーバー側ロジックの実装

.proto ファイル (通常は Maven または Gradle gRPC プラグインによって自動的に処理されます) をコンパイルした後、ツールは PaymentServiceImplBase クラスを生成します。このクラスを拡張してビジネス ロジックを実装します。

package com.ghaznix.grpc.payment;

import io.grpc.stub.StreamObserver;
import java.time.Instant;

public class PaymentServiceImpl extends PaymentServiceGrpc.PaymentServiceImplBase {

    @Override
    public void processPayment(PaymentRequest request, StreamObserver<PaymentResponse> responseObserver) {
        System.out.println("Received payment request for customer: " + request.getCustomerId() 
                + " with amount: " + request.getAmount() + " " + request.getCurrency());

        // Process payment logic (simulation)
        String status = request.getAmount() > 0 ? "SUCCESS" : "DECLINED";
        String message = status.equals("SUCCESS") ? "Payment processed successfully." : "Invalid payment amount.";

        PaymentResponse response = PaymentResponse.newBuilder()
                .setTransactionId(request.getTransactionId())
                .setStatus(status)
                .setMessage(message)
                .setTimestamp(Instant.now().toString())
                .build();

        // Send the response to the client
        responseObserver.onNext(response);
        
        // Signal that the RPC execution is complete
        responseObserver.onCompleted();
    }
}

ステップ 3: gRPC サーバーをブートストラップする

次に、特定のポートで gRPC サーバーを起動し、サービス実装を登録するためのサーバー クラスを作成します。

package com.ghaznix.grpc.payment;

import io.grpc.Server;
import io.grpc.ServerBuilder;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

public class PaymentServer {

    private Server server;

    public void start() throws IOException {
        int port = 50051;
        server = ServerBuilder.forPort(port)
                .addService(new PaymentServiceImpl())
                .build()
                .start();
        
        System.out.println("gRPC Server started, listening on port " + port);

        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.err.println("*** Shutting down gRPC server since JVM is shutting down");
            try {
                PaymentServer.this.stop();
            } catch (InterruptedException e) {
                e.printStackTrace(System.err);
            }
            System.err.println("*** Server shut down");
        }));
    }

    public void stop() throws InterruptedException {
        if (server != null) {
            server.shutdown().awaitTermination(30, TimeUnit.SECONDS);
        }
    }

    public void blockUntilShutdown() throws InterruptedException {
        if (server != null) {
            server.awaitTermination();
        }
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        final PaymentServer server = new PaymentServer();
        server.start();
        server.blockUntilShutdown();
    }
}

ステップ 4: クライアントを実装する (ブロッキング スタブと非同期スタブ)

gRPC の主な利点は、クライアント用に 2 種類のスタブを生成することです。

  1. BlockingStub: 同期、ブロック実行 (標準の REST 呼び出しと同様)。
  2. スタブ: コールバック オブザーバーを使用した非同期のノンブロッキング実行。
package com.ghaznix.grpc.payment;

import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import java.util.concurrent.TimeUnit;

public class PaymentClient {

    private final ManagedChannel channel;
    private final PaymentServiceGrpc.PaymentServiceBlockingStub blockingStub;

    public PaymentClient(String host, int port) {
        // Create a communication channel to the server
        this.channel = ManagedChannelBuilder.forAddress(host, port)
                .usePlaintext() // Disables TLS for local development
                .build();
        
        // Create the synchronous blocking stub
        this.blockingStub = PaymentServiceGrpc.newBlockingStub(channel);
    }

    public void shutdown() throws InterruptedException {
        channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
    }

    public void executePayment(String txId, String customerId, double amount) {
        PaymentRequest request = PaymentRequest.newBuilder()
                .setTransactionId(txId)
                .setCustomerId(customerId)
                .setAmount(amount)
                .setCurrency("USD")
                .build();

        System.out.println("Sending payment request for: " + txId);
        
        // Execute unary synchronous call
        PaymentResponse response = blockingStub.processPayment(request);
        
        System.out.println("Server Response: " + response.getStatus() + " | " + response.getMessage());
    }

    public static void main(String[] args) throws InterruptedException {
        PaymentClient client = new PaymentClient("localhost", 50051);
        try {
            client.executePayment("TX-99081", "customer-abc-123", 250.75);
        } finally {
            client.shutdown();
        }
    }
}

4. アーキテクチャの比較: gRPC と REST

以下の表は、両方のパラダイムの中核となる特徴をまとめたものです。

建築上の特徴 REST (表現型状態転送) gRPC (Google リモート プロシージャ コール)
プロトコル HTTP/1.1 (標準)、HTTP/2 (オプション) HTTP/2 (厳格な要件)
ペイロード形式 プレーンテキスト JSON、XML、HTML バイナリプロトコルバッファ (Protobuf)
API パラダイム リソース (URI パス + GET/POST/PUT/DELETE) リモート プロシージャ (メソッド/関数呼び出し)
契約の品質 緩い (OpenAPI/Swagger はオプション/外部) 厳密 (コンパイル時に .proto ファイルで定義)
多重化 いいえ (HTTP/1.1 での行頭ブロック) はい (単一の TCP 接続上の複数のストリーム)
ストリーミング タイプ 単方向サーバー送信イベント (SSE) のみ クライアント、サーバー、双方向ストリーミング
コード生成 必要な外部ツール (Swagger Codegen など) Protobuf コンパイラによる組み込み (protoc)
ブラウザのサポート ユニバーサル (Web クライアントへの直接アクセス) 制限付き (grpc-web プロキシ変換が必要)

5. REST ではなく gRPC を選択するのはどのような場合ですか?

gRPC にはパフォーマンス上の大きな利点がありますが、特効薬ではありません。プロトコルの選択は、システム アーキテクチャ内のコンテキストによって異なります。

  • 内部マイクロサービス メッシュに gRPC を使用する: 大容量のサービス間通信には、gRPC の低遅延、バイナリのコンパクトさ、ストリーミング機能が優れた選択肢となります。これにより、内部サーバーの CPU オーバーヘッドが軽減され、全体的な応答時間が短縮されます。
  • エッジ/パブリック API には REST を使用する: Web ブラウザーは変換レイヤーなしではネイティブに gRPC 呼び出しを行うことができないため、パブリック API、サードパーティ Webhook との統合、およびクライアント ブラウザーからアーキテクチャのエッジへの直接通信には、依然として REST が最適な選択肢です。

API ゲートウェイ レベルでの REST の公開と、内部マイクロサービス通信に gRPC を利用するという 2 つを組み合わせることで、アーキテクトは高度な相互運用性と超高速性を備えたシステムを構築できます。


Ghaznix ブログでソフトウェア開発とバックエンド エンジニアリングの洞察をさらに詳しく見る→