マイクロサービス間通信におけるgRPCとRESTの比較

# 概要 
マイクロサービス間の通信方法にはREST APIやGraphQL、gRPCなど様々な選択肢がありますが、近年注目されているのがgRPCです。本記事では、特にgRPCとRESTを比較し、マイクロサービス間通信という文脈でのgRPCの優位性について見ていきます。

# 想定読者
- REST APIの使用経験がある人
- マイクロサービスに興味がある人

# gRPCとは
gRPCとは、Googleによって作成されたOSSの**RPC用フレームワーク**です。gRPCの大きな特徴は、JSONのようなテキストデータではなく**バイナリデータ**によるデータの送受信を**HTTP/2上で**行うという点です。これにより高速なデータの送受信が可能となります。

ちなみに、gRPCは元々Google社内で使用されていたStubbyという技術が基盤となっており、現在はOSS化された上でCNCFに寄贈されています。この流れは、BorgがKubernetesとしてOSS化される流れに似ていますね。

ただし、上記の説明だけではよく分からないと思うので、まずはRPCとHTTP2の概要を説明します。gRPCの細かい仕様に関する説明は、その後の章でRESTとの比較を行う中で触れていきます。

本編に入る前にgRPCの公式マスコットキャラのパンケーキくんが可愛かったので添えておきます。癒されてください。

https://x.com/grpcio/status/1104161262005559301
# RPCとは
RPC（Remote Procedure Call）[^1]とは、異なるコンピュータ上の関数（Procedure）を呼び出すための仕組みであり、ローカル内での関数呼び出しと同様に、ネットワーク経由で他のコンピュータ上の関数を呼び出すことを可能とする技術です。RPCの理論自体は1970年代からあり、80年代に実用化されています。[^2]gRPC以外で過去に実装されたRPCフレームワークとして、XML-RPCやJSON-RPC、CORBA、Java RMIなどが存在します。

RPCを使用すると、例えばローカルのプログラム内でmain関数から他の関数を呼び出すのと同様に、対象の関数が別のサーバ上に存在する場合でも、ローカル上で呼び出しているかのように機能します。
実際の通信時にはクライアント/サーバに該当するプログラムである**スタブ**というものが存在し、スタブを仲介して対象の関数の実行に必要な引数や戻り値がペイロードとして送受信されます。これによりネットワーク上での通信に関する情報は隠蔽され、開発者目線では自分が呼び出したい関数の情報のみに注力することが可能となります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3081962/2c491769-d843-bcfc-e05a-f20e9022e69c.png)
*出典　gRPC. "Introduction to gRPC". 2024. https://grpc.io/docs/what-is-grpc/introduction/. (参照 2024-09-01).*

# HTTP/2とは
HTTP/2は、現在インターネット上で広く使用されているHTTP/1.1の後継で、より高速なWeb通信のプロトコルとして注目されています。
HTTP/1.1では一回のクライアントのリクエストに対し、サーバは単一のレスポンスしか返せないため、ファイルの送受信などは逐次的にしか行われません。対照的に、HTTP/2ではストリームという概念の導入により、単一のTCP接続上で複数のリクエストとレスポンスを並列に扱うことができます。これにより複数のデータを並列に送受信することが出来、Webページを表示するために必要なコンポーネントを効率よく伝送することが可能です。その他にも、バイナリ形式でのデータ送信機能や、データのヘッダー部分の圧縮機能、クライアントが必要とする前にサーバーがリソースを先行して送信する機能、重要なデータに優先順位をつけて処理できる機能なども存在します。これらの技術の組み合わせにより、HTTP/2はHTTP/1.1より高速な通信を実現しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3081962/af6c3c7b-576d-37de-1af9-4c0d97870321.png)

*出典　サイバートラストBLOG. "HTTP/2 とは？". 2016. https://www.cybertrust.co.jp/blog/ssl/knowledge/http2.html. (参照 2024-09-01).*

HTTP/2は以下のさくらインターネットさんの記事で分かりやすく解説されているので、こちらを一読することをおすすめします。

https://knowledge.sakura.ad.jp/7734/

# REST APIとの比較
gRPCは特にマイクロサービス間の通信技術として注目されています。しかし、gRPC以外の選択肢を考える場合、多くの開発者に馴染みのあるREST APIで実装することになるでしょう。では、REST APIと比較した場合のgRPCの具体的な利点とは何なのでしょうか？gRPCがマイクロサービス間通信でどう役に立つのでしょうか？
上記の疑問を解決するために、以降の内容ではRESTとgRPCを以下の観点で比較していきます。
1. 伝送効率
2. スキーマ定義
3. 通信の方向性

## 観点1. 伝送効率
マイクロサービスではプロセス間通信が分散環境で行われるため、ネットワークの遅延やオーバーヘッドがパフォーマンスに大きな影響を与えます。モノリシックなアプリケーション（単一プロセスモノリス）内でオブジェクトやメソッドを呼び出す場合、ローカルマシン上の同一プロセス内でメモリやランタイムを共有しているので、呼び出し時のオーバーヘッドを考慮する必要はほぼありません。しかし、マイクロサービスでは呼び出す対象のプロセスがどこに存在しているか分かりません。例えば対象のプロセスがコンテナとしてパブリッククラウド上で稼働している場合、そのコンテナは別のサブネット、データセンター、AZ、VPC、はたまた別のリージョンにある可能性すらあります。
このことを考えると、サービス間の通信速度の重要性がモノリスよりも高いことが分かると思います。

### HTTP/2の使用
gRPCはHTTP/2上で稼働するので、先述したHTTP/2の通信のコネクションやヘッダーの圧縮機能、バイナリでのデータの送受信などの利点を享受することができます。これらを総合するとgRPCはRESTと比較してデータの送受信が**7~10倍**速いという例もあります。[^3][^4]

### Protocol Buffersによるシリアライズ
REST APIではデータの送受信は基本的にJSONによるテキストベースで行われますが、gRPCは通信にバイナリデータを使用します。より正確に言うと、HTTP/2自体がデータの送受信のフォーマットにバイナリ形式を採用しているため、gRPCでは**Protocol Buffers**[^5]というものによってデータのシリアライズ（バイナリ化）が行われます。
バイナリ形式は数値や文字列などのデータ型をビット列で表現するため、一般的にJSONよりもデータサイズが小さくなります。例えば、整数や浮動小数点数はテキスト形式では複数の文字を使用して表現されますが、バイナリでは通常固定長のビット列で表現されます。また、JSONはキーと値のペアを表現するために括弧や引用符を使用する必要がありますが、バイナリではそのような表現を行うためのデータ分のオーバーヘッドが生じません。

例えば、以下のようなJSONのデータがあったとします。

```json
{
    "user_id": 12345,
    "name": "John Smith",
    "age": 58,
    "gender": "female",
    "country": "US"
  }
```

これをProtocol Buffersを使用してシリアライズすると、

```
\x08\xb9`\x12\nJohn Smith\x18:"\x06female*\x02US
```

のようになります。ちなみにこちら例におけるデータサイズは、JSONが112バイト、シリアライズ後のデータが48バイトとなっています。

このように、gRPCではHTTP/2の機能を活用した高速な通信が可能となっています。ただし、裏を返せばgRPCの高速性は大きくHTTP/2の機能に依存しており、gRPCが高速というよりはHTTP/2が高速とも言えます。いずれにせよ、サービス間の通信でAPIコールが多発する場合、gRPCはアプリケーション全体のパフォーマンスの向上に大きく寄与するでしょう。

## 観点2. スキーマ定義
マイクロサービスに限定した話ではありませんが、APIの利用においてスキーマの定義は有用です。スキーマはAPI設計における一種の契約として機能し、クライアント/サーバー間でのデータの送受信の方法について明確かつ一貫した規約を設けます。スキーマが定義されている場合、クライアントはどのようなリクエストを送信すべきか、またどのような形式でレスポンスが返却されるかを正確に予測することが可能です。
しかし、スキーマが存在しない場合、クライアントはAPIからどのようなレスポンスが返却されるかを把握することが出来ません。その場合、クライアントはAPIコールの度に返却されたJSONを動的に解析する必要がありますが、仮に以下のような複雑な構造をもったJSONが返ってきた場合、クライアントにとっては重労働になります。

```json
{"user":{"id":"12345","name":"John Smith","email":"john.smith@example.com","profile":{"age":58,"gender":"female","address":{"street":"123 Elm Street","city":"Shanghai","state":"IL","postal_code":"62701"},"preferences":{"newsletter":true,"notifications":{"email":true,"sms":false}}},"orders":[{"order_id":"A123","date":"1985-08-01","items":[{"product_id":"P001","name":"Widget","quantity":2,"price":19.99},{"product_id":"P002","name":"Gadget","quantity":1,"price":29.99}],"total":69.97},{"order_id":"B456","date":"2024-07-15","items":[{"product_id":"P003","name":"Doodad","quantity":3,"price":9.99}],"total":29.97}]}}
```

また、スキーマを定義することで、APIの後方互換性を確保し、破壊的な変更を未然に防ぐことが可能です。例えば、新しいフィールドが追加された場合、スキーマに基づいてクライアント側が適切に対応できるかどうかを事前に検証できます。これにより、クライアントが古いバージョンのAPIを利用している場合でも、予期せぬエラーを防止し、安定した運用を継続することが可能です。

### Protocol Buffersによるスキーマ定義
REST APIには、デフォルトでスキーマを明示的に定義する機構は備わっていません。一方で、gRPCではProtocol Buffersというインターフェース記述言語を使用してメッセージのスキーマが厳密に定義されます。gRPCを使用する場合、Protocol Buffersの記述が必須であり、スキーマは必ず存在するため、gRPCはデフォルトでスキーマ駆動開発となります。

### Open APIを使用する場合との比較
REST APIにはスキーマを定義する機構は備わっていないものの、SwaggerやOpenAPI[^6]のような外部ツールを利用すれば明示的なスキーマを定義することとは可能です。しかし、明示的なスキーマが存在することがgRPCの利点だとした場合、Open APIを使用してスキーマを用意すれば、gRPCの優位性は無くなるのでしょうか？

例として、Open APIとgRPCのProtocal Buffersを使用して同じスキーマを定義したものを見てみましょう。

#### Open APIの例
```yaml
openapi: 3.0.0
info:
  title: User Service API
  version: v1
paths:
  /user:
    get:
      summary: Get User
      description: Retrieves a user by ID.
      operationId: getUser
      parameters:
        - in: query
          name: user_id
          schema:
            type: string
          required: true
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserResponse'
        '404':
          description: User not found
components:
  schemas:
    UserRequest:
      type: object
      properties:
        user_id:
          type: string
    UserResponse:
      type: object
      properties:
        user_id:
          type: string
        name:
          type: string
        age:
          type: string
        gender:
          type: string
        country:
          type: string
```

#### Protocol Buffersの例
```protobuf
syntax = "proto3";

package user;

service UserService {
  rpc GetUser (UserRequest) returns (UserResponse) {}
}

message UserRequest {
  string user_id = 1;
}

message UserResponse {
  string user_id = 1;
  string name = 2;
  string age = 3;
  string gender = 4; 
  string country = 5;
}
```

上記の二つを見比べると、Protocol BuffersではURI、HTTPリクエスト、ステータスコードなどのHTTPに関連する実装が隠蔽されており、Open APIの場合よりも簡潔な記述になっていることが分かります。

元々、RESTはリソース指向アーキテクチャ（ROA）[^7]というものに基づいており、特定のリソース（URI）に対してGET・POST・DELETE等のHTTPメソッドを通じてデータの追加・編集・削除等のCRUD操作を行うことが目的です。そのため、Open APIでのスキーマ定義はこのREST APIの特性を体現しているにすぎません。

対して、gRPCはRPCの章で説明したように関数を呼び出すことが目的であり、処理の単位は関数です。従って、対象の関数の定義やその引数、戻り値が適切に設定されていれば、開発者はHTTPの通信プロトコルの詳細を意識する必要がないという思想です。gRPCではインターネット上の通信よりも本質的なプログラミングの処理にフォーカスできるという点で、開発者の認知負荷は減るでしょう。

## 観点3. 通信形態
マイクロサービスは分散システムであり、複数の独立したサービスが互いに通信を行います。従って、クライアント/サーバのような1対1での通信ではなく、多対多のメッシュ型トポロジーを取る場合もあります。しかし、REST APIは通常リクエスト/レスポンス型の通信モデルで単一のクライアントが単一のサーバーと通信を行うので、多対多の通信には向いていません。

その点、gRPCでは様々な通信形態が存在します。具体的には以下の四つが存在します。

1. Unary RPC 
クライアントから**単一の**リクエストを送信し、サーバーから**単一の**レスポンスを受け取る通信形態

2. Server Streaming RPC 
クライアントから**単一の**リクエストを送信し、サーバーから**複数の**レスポンスをストリーミングで受け取る通信形態

3. Client Streaming RPC 
クライアントから**複数の**リクエストをストリーミングで送信し、サーバーから**単一の**レスポンスを受け取る通信形態

4. Bidirectional Streaming RPC 
クライアントとサーバーが**双方向にストリーミングで**メッセージをやり取りする通信形態

以上を見ると、通信の方向性やデータの処理方式において、gRPCは非常に柔軟な通信形態を取れることが分かる思います。RESTを使用している場合、4の双方向ストリーミングを実現する際は別途Web Socket APIで実装する必要がありますが、gRPCでは単一のプロトコルで対応可能です。

# 最後に
gRPCには様々なメリットがありますが、様々なサービス間通信の一技術に過ぎません。チームメンバーのスキルセット的にRESTの方が開発を行いやすいケースもあるでしょうし、複数のデータソースからデータを集めたい場合はGraphQLの方が適しているかもしれません。あるいは、同期的なAPIを使用するよりも、メッセージキューやイベント駆動で非同期に処理させて、サービス間を疎結合にした方が良いケースもあるかもしれません。
各サービスの特性、パフォーマンス、スケーラビリティ、チームのスキルセット等を考慮し、様々な選択肢の中からシステム全体の要件に最適な通信方法を選択することがマイクロサービスの醍醐味ともいえるでしょうしょう。

以下は、マイクロサービスアーキテクチャの書籍[^8]からの引用です。

> まず、解決したい問題に基づいて、技術を選ぶ。状況と好みの通信スタイルに基づいて、最も適切な技術を選択する。先に技術を選ぶという罠に陥らない。

[^1]:RFC 1050. "RPC: Remote Procedure Call Protocol specification". 1988. https://datatracker.ietf.org/doc/html/rfc1050. (参照 2024-09-01).

[^2]: Wikipedia. "遠隔手続き呼出し". 2023. https://ja.wikipedia.org/wiki/%E9%81%A0%E9%9A%94%E6%89%8B%E7%B6%9A%E3%81%8D%E5%91%BC%E5%87%BA%E3%81%97. (参照 2024-09-01).

[^3]: Medium. "Evaluating Performance of REST vs. gRPC". 2019. https://medium.com/@EmperorRXF/evaluating-performance-of-rest-vs-grpc-1b8bdf0b22da. (参照 2024-09-01).

[^4]:  Wantedly Engineer Blog. "Real World Performance of gRPC - gRPC 利用による劇的なパフォーマンス改善". 2020. https://developers.mews.com/synchronous-communication-with-grpc/. (参照 2024-09-01).

[^5]:  Protocol Buffers Documentation. 2024. https://protobuf.dev/. (参照 2024-09-01).

[^6]:  OpenAPI Initiative. 2024. https://www.openapis.org/. (参照 2024-09-01).

[^7]:  Roy Thomas Fielding. "Architectural Styles and
the Design of Network-based Software Architectures". 2000. https://ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm. (参照 2024-09-01).

[^8]: Sam Newman. マイクロサービスアーキテクチャ 第2版. 2022年. O'Reilly Japan.