こんにちは、おさわです😆
今回は、フロントエンジニアがインフラを理解する【その2】と題しまして、RPCについて学びました。
4月から新しい環境となり、その研修の一環としてチーム開発を同期と進めています。
その中で、「gRPCを使う」ということを言われ、ホゥ...🤨となったので、学習がてらまとめました!
RPC (Remote Procedure Call) とは?
日本語では「遠隔手続き呼出し」と呼びます。
-
RESTの場合:
GET /api/users/1という「場所」にアクセスしてリソースを取得する -
RPCの場合:
getUser(id: 1)という「関数」を、ネットワーク越しに実行する
エンジニアにとっては、URLやHTTPメソッドを意識するよりも「型定義された関数を叩く」感覚に近いため、開発体験が良いのが特徴だそうです🤩
gRPC (Google Remote Procedure Call) とは?
Googleが開発した、モダンで高速なRPCフレームワークです。主に以下の3つの技術要素で成り立っています。(参考:https://grpc.io/)
① Protocol Buffers (Protobuf)
データのシリアライズ(構造化データをバイナリに変換すること)に使われます。
JSONは人間が読める「テキスト」ですが、Protobufは機械が効率よく読める「バイナリ」です。
- メリット: データサイズが圧倒的に小さく、通信が高速
- 型安全性:
.protoファイルという設計図を作成し、そこからTypeScriptやGo、Javaなどのコードを自動生成します
1. JSONの場合:冗長なテキスト情報
例えば、ユーザー情報を送る以下のJSONを見てください。
{
"id": 101,
"name": "Yamada"
}
このデータは、コンピュータから見ると非常にムダが多いです。
-
"id" や "name" という**フィールド名(キー)**が毎回送られる
-
{ } , : " といった記号もすべて1バイト以上のデータとしてカウントされる
-
数値の 101 も、テキストとして 1 0 1 の3文字分(3バイト)消費される
このJSONを転送するには、合計で 約30バイト ほど必要になります。
2. Protobufの場合:極限まで削ったバイナリ
Protobufでは、まず .proto という設計図(スキーマ)をクライアントとサーバーの両方が持っていることが前提になります。
message User {
int32 id = 1; // idは「1番」のフィールド
string name = 2; // nameは「2番」のフィールド
}
この設計図があるおかげで、実際の通信では "id" という文字列を送る必要がなくなります。代わりに「1番のデータは 101 です」「2番のデータは Yamada です」という情報だけを送ります。
上記のデータをProtobufでバイナリ化すると、わずか 11バイト 程度になります。
↓ 具体的にどう変換されるか
-
Tag(1バイト): 「フィールド番号1」であることを示す
-
Value(1バイト): 数値 101
-
Tag(1バイト): 「フィールド番号2」であることを示す
-
Length(1バイト): 文字列の長さ(8文字)
-
Value(8バイト): Yoshioka という生データ
「キーの名前」や「区切り記号」が一切消え、データそのものだけが並ぶのがバイナリの強みです!
なぜ「バイナリ」はフロントエンドに嬉しいのか?
「たかだか数十バイトの差でしょ?」と思うかもしれませんが、大規模なWebアプリではこの差が決定的なパフォーマンスの差になります。
-
パース(解析)が速い: JSONは文字列をスキャンして構造を理解する必要がありますが、バイナリは構造が決まっているため、CPUがそのままメモリに展開でき、解析コストが極めて低いです
-
モバイル回線に強い: 通信量が減るため、電波が不安定な環境や従量課金の環境でユーザー体験が向上します
-
型安全: 設計図(.proto)からTypeScriptの型を生成できるため、フロントエンドで「APIからどんなデータが来るか」を迷うことがなくなります
② HTTP/2
gRPCは基盤となる通信プロトコルに HTTP/2 を採用しています。
- 双方向ストリーミング: クライアントとサーバーで同時にデータを送り合える(チャットやリアルタイム通知に強い)
- 多重化: 一つのコネクションで複数のリクエストを並列処理できる
HTTP/1.1 vs HTTP/2 とバイナリ転送
-
HTTP/1.1 (テキスト): GET /index.html HTTP/1.1 のように、人間が読める文字列でやり取りします。解析に時間がかかり、データの重複も多いです。また、1つのリクエストが終わるまで次のリクエストを送れない(順次処理)。これを避けるためにブラウザは複数のコネクション(ホース)を張りますが、効率が悪いです(Head-of-Line Blocking)
-
HTTP/2 (バイナリ): データを「フレーム」という小さなバイナリ(0と1の塊)の単位に分割します。1つのコネクション(ホース)の中に、複数の「ストリーム」を流せます。HTML、CSS、JS、gRPCのデータを同時に混ぜて送れるため、待ち時間が発生しません
③ インターフェース定義言語 (IDL)
.proto ファイルに「どんな関数があり、どんな引数を受け取るか」を定義します。これにより、フロントエンドとバックエンドの「型」が強制的に一致するため、「APIの仕様が変わってフロントが壊れた」という事故が防げます。
フロントエンドエンジニアが知っておくべき「gRPC-Web」
実は、ブラウザ(JavaScript)はHTTP/2の低レイヤーな操作を完全にはサポートしていないため、gRPCを直接叩くことができません。
そこで使われるのが gRPC-Web です!
- ブラウザとgRPCサーバーの間に「プロキシ(Envoyなど)」を挟み、ブラウザからのリクエストをgRPCに変換して伝えます
なぜフロント(ブラウザ)からgRPCを呼ぶ際にプロキシが必要なのか
その理由は「ブラウザの制約」にあります。
ブラウザができないこと
gRPCはHTTP/2の非常に細かい制御(特定のバイナリフレームの操作など)を必要としますが、現在のブラウザの fetch や XHR APIでは、そこまで低レイヤーな制御が許されていません。
Envoyによる「翻訳」
そこで、ブラウザとサーバーの間に Envoy を配置します。
-
フロントエンド: gRPC-Web ライブラリを使用して、HTTP/1.1(またはブラウザ互換のHTTP/2)でリクエストを送ります。データはバイナリですが、HTTPヘッダーなどでラップされています
-
Envoy (プロキシ): この「ブラウザ用gRPCリクエスト」を受け取り、純粋な「gRPC(HTTP/2バイナリ)」に変換して、バックエンドのgRPCサーバーへ転送します
-
バックエンド: Envoyからの純粋なgRPCを受け取って処理します