🌾

gRPCの概要とREST、GraphQLとの簡単な比較

2023/12/05に公開

はじめに

今回の記事は、gRPCについて概要を掴むために特徴をまとめてみたものです。
gRPCの基本を抑えていきながらREST、GraphQLとの比較についても簡単に書いてみました。

gRPC、REST、GraphQLはそれぞれAPIの通信規格で、主な特徴を簡単にまとめると
・gRPCは高性能な通信に適している
・RESTは最も一般的でシンプル
・GraphQLはデータ取得の柔軟性が高い
と言ったところです。

この辺も意識しながらgRPCとは何なのかを掴んでいきたいと思います。

gRPCの概要

gRPCとはGoogleが開発したRPC(Remote Procedure Call)を実現するためのフレームワークです。
Remote Procedure Callとは遠隔の手続き呼び出しのように訳すことができます。
簡単に言うとプログラムがネットワーク上の別の場所にあるプログラムを実行することです。
RPCを使用することで、別アプリケーション内のロジックを
呼び出し元のアプリケーション内で実装されているかのように扱うことができます。
そのRPCを実現するための方式のひとつがgRPCということです。

ここでgRPCにおけるサーバーとクライアントの動きについて見ていきます。
gRPCのサーバーはリクエストを受けレスポンスを返す、
いわゆる一般的なサーバーと同様の動きをします。

クライアントの動きが少し特殊で、スタブが処理の中核を担っており、
スタブはリモートメソッドをローカルで処理しているように表現します。
要はRPCの特徴であるネットワーク上の別の場所にある
プログラム(リモートメソッド)の実行処理をスタブが担っている
ということです。
以下の画像のように、処理の流れ的には
クライアントとサーバー間で通信を行なっていることに間違いはないのですが、
その通信をスタブが内部的に行うことで、クライアントは別の場所にあるサーバー上のメソッドを
あたかもクライアント側で実装しているかのように呼び出し、操作できるようになります。

これにより、サーバー間の通信のことを意識せずに
別の場所にあるサーバー上のメソッドを簡単に扱うことができる
のです。

この処理の流れは料理のデリバリーサービスに例えるとわかりやすいかもしれません。
自分のスマホ(クライアントのスタブ)を操作するだけで、お店(サーバー)で
調理、盛り付け、配送(リモートメソッド群の操作)を行なってくれるので、
直接お店に行かずとも簡単に利用できますよね?
厳密に言うとちょっと違う部分もありますが、こんな感じでイメージしてもらえればと思います。

gRPCが登場した背景

続いてgRPCが誕生した背景についてです。
Googleでは10年以上Stubbyという自前のRPCフレームワークを使用して
マイクロサービス接続を行なっていました。Stubbyは独自のプロトコルを使用していましたが、
HTTP2などの標準規格の登場を機に、それらを活用してモバイル、IoT、クラウドなどへの
適応性を拡張していくべきという考えが生まれ、Stubbyの後継として開発されたのがgRPCです。

gRPCの大きな特徴

gRPCにはHTTP/2とProtocolBuffersの2つの技術をベースにしているという
大きな特徴があります。
この2つの技術をベースとすることでgRPCは高性能な通信を実現しています。
それぞれについて簡単に解説していきます。

HTTP/2ベース

gRPCはHTTP/2プロトコル上で通信が行われます。そのため高速な通信や
双方向ストリーミング通信を行うことができます。

HTTP/2の特徴

HTTP/2はHTTP/1.1のパフォーマンスを改善するために開発されました。
以下にHTTP/2の特徴をいくつかあげてみます。

・HTTP/2はバイナリプロトコルであり
通信時にデータがバイナリにシリアライズされて送られるため
転送量を圧縮し、効果的な通信が可能になります。
gRPCの場合シリアライザにはデフォルトでProtocolBuffersが利用されます。

・一つのコネクションで複数のリクエスト、レスポンスをやり取りできます。
コネクションは常に張られっぱなしになるためリクエストのたびに接続や切断を行う必要がなく、
都度接続を開くHTTP/1に比べてパフォーマンスの向上に繋がります。

・サーバーはクライアントの要求を待たずにリソースを送信できます。
これにより、クライアントが必要とする可能性が高いリソースを先読みで送ることができ、
クライアントの待ち時間を短縮することができます。

このように利点の大きいHTTP/2ですが、利用するためにはクライアント、サーバー両方が
HTTP/2に対応している必要があるためいつでも利用できる訳ではありません。

以下の記事はHTTP/2について詳しく解説しているので
よければ目を通してみてください。
https://www.nic.ad.jp/ja/newsletter/No68/0800.html

ProtocolBuffersを使用している

ProtocolBuffersとはプログラミング言語に依存しない、
スキーマ定義や構造化したデータのシリアライズなどを行う仕様・ツール群
です。
gRPCのシリアライズフォーマットはデフォルトはProtocolBuffersになっていますが、
JSONなどの形式に対応することも可能です。
基本的にProtocolBuffersが使用されることが多いので、今回はその辺の詳しい話は割愛します。
ProtocolBuffersは2016年のバージョン3へのアップデート時に破壊的変更が行われているので、
現在はproto3の使用が公式的に推奨されています。

ProtocolBuffersの特徴

ProtocolBuffersの特徴には以下のようなものがあげられます。

・ProtocolBuffersはバイナリ形式にデータをシリアライズします。
これによりJSONなどのテキストベースのフォーマットより効率的な通信を可能にします。

・ProtocolBuffersの行うシリアライズは非常に高速で大量のデータを処理するのに適しています。

・protoファイルを書いて型安全なスキーマ定義を行い、
それをコンパイルすることで任意の言語のコードを生成してくれます。

・多くのプログラミング言語でサポートされておりprotoファイルを使用して、
異なる言語向けのソースコードを自動生成できます。
これにより、異なる言語を利用するシステム間でのデータ交換が容易になります。

以下はスキーマ定義ファイルであるprotoファイルの書き方の例です。

// protoのバージョンの宣言。ここでバージョン3を指定しないとバージョン2になってしまう。
syntax = "proto3";

// protoファイルから自動生成させるコードの保存先
option go_package = "pkg/grpc";

// packageの宣言
package myapp;

// サービスの定義
service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply);
  rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse);
  rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);
  rpc BidiHello(stream HelloRequest) returns (stream HelloResponse);
}

// 型の定義
message HelloRequest {
  string name = 1;
  int32 id = 2;
  string detail = 3;
}

message HelloReply {
  string message = 1;
}

サービス定義

上のprotoファイルの書き方の例であげたサービスの定義についてですが、
一般的にgRPCで呼び出そうとするProcedure(関数)をメソッド、
そしてそのメソッドをいくつかまとめて一括りにしたものをサービス
と呼びます。
gRPCにおけるサービス定義は、リモートで実行するためのメソッドと
その入出力の型を指定するプロセスです。
他の多くのRPCシステムも同様の方式を取っているようです。
上の例でいうと、サービスの定義内で利用している型(HelloRequestなど)を
型の定義以下の部分でmessageを利用して定義しているということです。

messageはProtocolBuffersを用いて通信を行う際に利用する
複数のフィールドを持つことができる型定義
です。
このメッセージが各言語にコンパイルされることで、言語ごとの構造体やクラスに変換されます。

定義してある型の横にある番号(string name = 1;で言う1)は
タグ番号というフィールドの識別子です。
ProtocolBuffersではフィールドはフィールド名ではなくタグ番号で識別されます。
そのためmessage内でタグ番号は重複することはできず、一意でなければなりません。

gRPCで可能な4種類の通信方式

続いて、先ほどのprotoファイルの例の中からgRPCで利用可能な4つの通信方式について
解説します。

Unary RPC(1リクエスト、1レスポンス)

いわゆる普通の通信です。通常の関数呼び出しと同様に、
クライアントが単一のリクエストをサーバーに送信し、サーバーが単一のレスポンスを返します。

rpc SayHello(HelloRequest) returns (HelloResponse);

Server streaming RPC(1リクエスト、複数レスポンス)

クライアントからの1つのリクエストに対して、サーバーから複数のレスポンスが返る方式です。
サーバー側からプッシュ通知を行う場合などに利用されます。

rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse);

Client streaming RPC(複数リクエスト、1レスポンス)

クライアントから複数のリクエストを送信し、
それに対してサーバーが1つのレスポンスを返す方式
です。
複数回に分けてアップロードしたデータをすべて受け取った後に
サーバーが一度だけOKと応答を返す場合などに利用されます。

rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);

Bidirectional streaming RPC(複数リクエスト、複数レスポンス)

クライアントとサーバーが互いに複数のリクエストやレスポンスのやり取りを行う方式です。
WebSocketのような双方向通信などに利用されます。

rpc BidiHello(stream HelloRequest) returns (stream HelloResponse);

例を見ていただくとわかるように、
streamという単語をリクエスト・レスポンスの前に書くことで
複数のリクエスト・レスポンスの通信を定義することができます。
gRPCではこれら4つの通信方法を柔軟に使い分けることができます。

ステータスコードについて

gRPCではメソッドの呼び出しに成功した場合に、
HTTPレスポンスステータスコードは200を返すように固定されています。
その代わりにgRPCは独自のステータスコードが定義し、
それをクライアントへ返すことでエラー内容を伝える
形をとっています。
独自のステータスコードは全部で17種類あります。

番号 コード 内容
0 OK 正常な動作
1 Canceled 操作がキャンセルされた
2 Unknown 不明なエラー
3 InvalidArgument メソッド呼び出し時の引数が不正
4 DeadlineExceeded 操作の完了に対する期限が超過(タイムアウト)
5 NotFound 指定したリソースが見つからなかった
6 AlreadyExists 作成しようとしたリソースが既に存在する
7 PermissionDenied メソッド実行の権限不足
8 ResourceExhausted リソースの枯渇
9 FailedPrecondition システムの状態が操作の実行に適していない
10 Aborted 操作が異常終了させられた
11 OutOfRange 指定した範囲が無効
12 Unimplemented サーバーによって実装されていない処理を呼び出そうとした
13 Internal サーバーの内部エラー
14 Unavailable サービスが現時点では利用できない
15 DataLoss 不可逆的なデータ損失や破損が発生した
16 Unauthenticated 認証失敗

いつ、どのようなステータスコードが返ってくるかの一般的な定義は
公式に詳しく記載されています。

https://grpc.io/docs/guides/error/

gRPCが独自のステータスコードを利用する理由は、HTTP/2を使用していますが、
プロトコルに依存しない設計を目指しているためです。
また、RPC通信の特有のエラー状況を詳細に伝え、
開発者がエラーの原因をより正確に把握できることを目指しています。

インターセプタ

gRPCではメソッドの前後に処理を行う機能をインターセプタと呼んでいます。
主にロギングやバリデーション、認証処理など複数の箇所で共通して行いたい処理がある場合
使用されます。

インターセプタはサーバー側でもクライアント側でも実装可能ですが、
Unary(単一通信)用とストリーミング通信用で形が異なります。

かなり雑に書きますが、
Goでサーバー側のインターセプタを実装する際は以下のような書き方になります。

s := grpc.NewServer(
    grpc.UnaryInterceptor(hogeInterceptor())
)

このコード内のhogeInterceptor()部分に実際に行いたい処理を実装することで
インターセプタを利用することができます。

実装の詳細は省きますが、gRPCでメソッド間で共通の処理を定義したい場合は
インターセプタが利用できるということは覚えておくと役に立つと思います。

デッドライン

デッドラインはgRPCを利用する際にクライアントがサーバーからのレスポンスを
いつまで待てるかを定義するもの
です。
デッドラインに設定した時間を過ぎるとタイムアウトと見なされエラーが発生します。

例えば何かしらのサービスに会員登録する際に、サーバーが過負荷状態のためにリクエストが
長時間処理されなくなったとします。この場合、デッドラインを設けて一定時間経過すると
「サーバーが混み合っているので時間を置いてから再度操作してください。」
といったメッセージを返すようにすると、
ユーザーは時間を置いてから再度操作した方がいいことを知ることができます。
しかしデッドラインを設けていない場合、
ユーザーは状態を把握できないまま長時間待つことになりストレスを抱える原因になります。

また、ネットワークやサーバーの障害などが原因で処理の実行待ちが発生した場合も
デッドラインを設定し、タイムアウトを発生させることで
障害を早期に検知することができ、迅速な対応が可能になります。

デッドラインを設定することでサーバーが無限にリクエストを処理し続けることを防ぎ、
リソースの無駄な消費やユーザー体験の低下を防ぐことができます。

gRPCのメリット、デメリット

一般的に言われるgRPCのメリット、デメリットについてまとめてみました。

メリット

・マイクロサービス間での高速な通信
gRPCのメリットであり一番の特徴でもあるのがこれです。HTTP/2プロトコルやProtocolBuffersを利用することにより高速な通信を実現しています。また、特定の言語に依存しないため多言語化しやすいマイクロサービス間での通信にも利用しやすいです。

・大規模データや高頻度の通信時のパフォーマンスの確保
上のマイクロサービス間通信と通ずる話ですが、高性能な通信に強みを持っているので大規模なデータや高頻度の通信時に一定のパフォーマンスを確保することができます。特に大規模なデータや複雑なデータ構造を扱う場合、ProtocolBuffersによる効率的なシリアライゼーションがパフォーマンスを大きく向上させます。

・型安全でスキーマファーストな開発
protoファイルを元に型安全な開発が行えます。また、複数サービス間のコミュニケーションもprotoファイルを通して行うことができ、特に大規模な開発を行う場合は同時並行的に開発を進められるのがメリットになります。

・複数のストリーミング方式を選択できる
柔軟な通信方式によって自由なアーキテクチャを組むことができます。これにより、リアルタイム通信や大量のデータを高速に処理することも可能になります。

デメリット

・HTTP/1上で動かない
gRPCはHTTPの下位互換性はないのでHTTP2上でしか動きません。そのため実装するシステム内にHTTP/2に未対応のものがあると利用することができません。

・一部の言語では未対応
主要な言語には対応していますが、一部未対応の言語が存在します。JSONのように幅広い言語で使用できる訳ではないので注意が必要です。

・動作確認が簡単にできない
gRPCは人間には読めないバイナリ形式でデータ通信を行うため、JSONのように簡単に動作確認やデバッグを行うことができません。

・フロントエンド、バックエンド間の通信には向かない
gRPCではブラウザとサーバー間で直接通信を行うことができません。gRPC-Webという機能を使い、クライアントとサーバー間にProxyサーバーを立てれば通信は可能ですが、RESTやGraphQLのように簡単に実装できる通信方法を利用せずにあえてgRPCを使う理由はあまり多くないでしょう。

RESTとの違い

ここからはRESTとgRPCの違いについて書いていきたいと思います。

まず第一にRESTとgRPCは処理が注目する焦点が異なります。
RESTはリソースの取得、操作に焦点を当てている(リソース指向)のに対し、
gRPCはリモートメソッドのコールに焦点を当てて(サービス指向)います。
そのため結果的にリソースが取得できたとしても
それはあくまで副次的なものであり、それ自体が目的ではありません。

また、RESTはあくまで設計思想です。
gRPCはフレームワークであり、規格に沿った実装として提供されているため、
RESTと比較すると一貫性の高い実装になりやすいです。

続いてgRPCのメリット、デメリットは先に書いたので
RESTとgRPCを比較したRESTのメリット、デメリットを考えてみます。

RESTのメリット

  • RESTのHTTPメソッドを使用してURLを通じてリソースを操作する処理は
    シンプルで学習コストも低く、誰にでも扱いやすいのはメリットと言えるでしょう。
  • RESTはHTTP/1.1に基づいておりほとんどのWebサービスに利用することができます。
  • RESTクライアント(ブラウザなど)を利用したリソースの参照が簡単
    (ブラウザでレスポンス確認できる)で、動作確認もgRPCよりやりやすいです。
  • ステートレスな通信を行うのでサーバーのスケーラビリティも向上します。

RESTのデメリット

  • RESTはテキストベース(JSONなど)の通信を使用するため、
    gRPCのようなバイナリベースの通信に比べるとパフォーマンスが劣る場合があります。
  • リアルタイム通信を行う場合はWebSocketなどの技術を併用する必要があります。

GraphQLとの違い

続いてGraphQLとgRPCを比較してみます。
RESTの時と同様、GraphQLとgRPCを比較したGraphQLのメリット、デメリットを考えてみます。

GraphQLがどんなものかイメージがつかない方は
以前自分が概要をまとめた記事があるのでよかったらご覧ください。
https://zenn.dev/y_yuita/articles/d08b8691b72e87

GraphQLのメリット

  • GraphQLは柔軟なクエリを利用してクライアントが必要なデータを正確に指定できます。
    これにより情報の過剰、過小な取得を防ぐことができます。
  • ブラウザでplaygroundを利用することでREST同様簡単に動作確認を行うことができます。
  • gRPCと被るような部分ですが、スキーマファーストの開発が行えることや
    サブスクリプションを利用したリアルタイム通信を行うことができます。

GraphQLのデメリット

  • GraphQLはクエリの柔軟度が高い分、複雑なクエリを実行するとパフォーマンスの悪化を
    引き起こす可能性があります。特に大量のデータを取得するような場合は注意が必要です。
  • ファイルのアップロードを標準でサポートしていないため
    ファイルを扱う際には追加の設定などが必要になります。

ただ、無理やり比べてはみましたがそもそもGraphQLとgRPCでは
強みを活かせる使用用途が異なるので
今回の比較も当てになるかと言われると微妙かもしれません。。。
GraphQLはフロントエンド、バックエンド間の通信に強みを持っており、
gRPCはバックエンドのマイクロサービス間での通信に強みを持っています。

RESTも含めてですが、それぞれの強みを活かせるアーキテクチャ構成を考えるのが
一番重要であるというのが、面白味はないですが最適な結論かなと思います。

最後に

今回はRESTやGraphQLの話も交えながらgRPCについて書いてみました。
私自身実務などでgRPCを利用している訳ではないのですが、
API通信について学ぶ上で概念だけでも知っておきたいと思い、
今回記事にまとめてみたという次第です。

実際gRPCで動いているサービスに携わったことがある訳でもないので
もし指摘事項等あれば教えていただけると幸いです🙇‍♂️

最後までご覧いただき、ありがとうございました。

参考にした記事など

↓↓↓特に分かりやすい記事でした↓↓↓
https://qiita.com/S4nTo/items/0ff0445542538ef49a05

https://grpc.io/
https://protobuf.dev/
https://www.nic.ad.jp/ja/newsletter/No68/0800.html
https://www.integrate.io/jp/blog/grpc-vs-rest-how-does-grpc-compare-with-traditional-rest-apis-ja/
https://www.amazon.co.jp/スターティングgRPC-技術の泉シリーズ(NextPublishing)-武上-将樹-ebook/dp/B087R87L6Z
https://zenn.dev/hsaki/books/golang-grpc-starting

Discussion