コンテンツへスキップ
戻る

gRPC とは何か?いつ使うべきなのか?

技術
公開日:  at  08:00 午前
··· 閲覧

gRPC とは何か?

gRPC は Google がオープンソースで公開している RPC(Remote Procedure Call)フレームワークです。核心となるアイデアはシンプルで、別のサービスのメソッドを呼び出すとき、ローカルのメソッドを呼ぶのと同じ感覚で書けるようにする、というものです。ネットワーク通信やシリアライズ・デシリアライズといった処理は、すべてフレームワークが引き受けてくれます。

従来、サービス間でデータをやり取りする場合、だいたい次のような形になります。

// curlバージョン
curl -X POST localhost:8080/api/orders \
  -H 'Content-Type: application/json' \
  -d '{"userId":"u123","amount":3000}'
// javaコード
    HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://httpbin.org/get"))
                .GET()
                .build();

    HttpResponse<String> response = client.send(
            request,
            HttpResponse.BodyHandlers.ofString()
    );

これは私たちにとって馴染み深いやり方で、リクエストを送る前に相手のドメイン、パス、HTTPメソッド、さらに仕様として必要なHTTPヘッダーやリクエストボディなどを、あらかじめ把握しておく必要があります。

しかし gRPC はそういうやり方をしません。

gRPC を使う前に、まずインターフェースを定義する .proto ファイルを書きます。これはリクエスト側とレスポンス側の間の仕様書のようなもので、Interface Definition Language(IDL)とも呼ばれます。内容は次のような感じです。

syntax = "proto3";

service WalletService {
  rpc GetBalance (BalanceRequest) returns (BalanceResponse);
}

message BalanceRequest {
  string user_id = 1;
}

message BalanceResponse {
  int64 balance = 1;
  string currency = 2;
}

Java バックエンドの感覚で理解するなら、こんな感じです。

ここまで書けたら、protobuf-maven-plugin のようなツールを使ってコードを生成します(ここでは Java を例にしています)。pom.xml に追加して compile を実行すると、次のようなファイルが自動生成されます。 protobuf をコンパイルして生成された gRPC のファイル構成 このうち WalletServiceGrpc.java は、サーバー側で使う ImplBase と、クライアント側で使う Stub を提供します。BalanceRequest と BalanceResponse は、リクエストとレスポンスの DTO です。

次に、サーバー側では WalletServiceGrpc.WalletServiceImplBase を継承した WalletServiceImpl を書きます。ビジネスロジックはこのサブクラスの中に書けばよく、通信の細かい部分はすべてフレームワークに任せて構いません。サンプルコードは以下の通りです。

public class WalletServiceImpl extends WalletServiceGrpc.WalletServiceImplBase {

    @Override
    public void getBalance(BalanceRequest request, StreamObserver<BalanceResponse> responseObserver) {
        BalanceResponse response = BalanceResponse.newBuilder()
                .setBalance(3000L)
                .setCurrency("TWD")
                .build();

        responseObserver.onNext(response);
        responseObserver.onCompleted();
    }
}

サーバー側の実装ができたら、gRPC のリクエストを受け付ける専用のポートを立てる必要があります。サンプルコードは以下の通りです。

        int port = 9090;
        Server server = ServerBuilder.forPort(port)
                .addService(new WalletServiceImpl())
                .build();

        server.start();
        System.out.println("gRPC server started, listening on port " + port);

        Runtime.getRuntime().addShutdownHook(new Thread(server::shutdown));
        server.awaitTermination();

これでサーバー側の宣言は完成です。次はクライアント側を見ていきましょう。

クライアント側でやることはごくわずかです。コネクションを張って Stub を取得すれば、あとはローカルのメソッドを呼ぶのと同じように使えます。

ManagedChannel channel = ManagedChannelBuilder
        .forAddress("localhost", 9090)
        .usePlaintext() // ローカルテストなので暗号化しない
        .build();

WalletServiceGrpc.WalletServiceBlockingStub stub = WalletServiceGrpc.newBlockingStub(channel);

BalanceResponse response = stub.getBalance(
        BalanceRequest.newBuilder().setUserId("u123").build());

最初の HTTP 版と比べると、こちらにはパスも、HTTPメソッドも、ヘッダーもなく、自分で DTO を書く必要すらありません。私たちはただ getBalance() を呼び出しただけで、残りは全部フレームワークが処理してくれています。

まとめ

現状、gRPC が主に使われているのはバックエンドのマイクロサービス間の通信です。レイテンシに敏感でスループットが大きいシステムに向いていますし、言語をまたいで使えるのも利点です。たとえば Python チームと同じ .proto ファイルを共有すれば、両者の間での認識のズレを減らすことができます。

一方で向いていない点もあります。外部公開向きではなく、対外サービスの主流は依然として REST API です。また、ブラウザから直接呼び出すのにも向いていません。たとえば HTTP キャッシュが必要な場面では、gRPC は常に POST メソッドを使うため、ブラウザのキャッシュが効かず、扱いづらくなります。

もう一つ注意すべきなのがロードバランシングです。gRPC が使う HTTP/2 は長期間維持されるコネクションなので、従来の L4 ロードバランサーだと、そのコネクション上のリクエストは全部同じサーバーに送られ続けてしまいます。つまり、gRPC の通信では L4 LB が事実上機能しなくなるケースがあるということで、実務では特に注意が必要です。L7 LB(Envoy、Istio)やクライアントサイドロードバランシングを使う必要があります。

さらに、gRPC はデバッグの難易度も上げます。リクエストをバイナリ形式で送るため、そのバイナリ通信を確認するためのツール自体にも学習コストがかかります。

さて、これで gRPC について一通りの基礎知識は身についたと思います。次はいよいよ実際に検証していきます——可読性を犠牲にして性能を得るこの仕組みが、実際どれだけの効果をもたらすのかをテストしていきます。

今回の記事はここまでです。それでは、また巡り会える日まで。


修正を提案する
この記事をシェアする:

次の記事
バックエンド屋の性能最適化 実戦(4):Slow Query でボトルネックを見つける