🥞

gRPCをちゃんと理解したい!

2024/11/15に公開

今趣味で作っている Next.js の Web アプリなのですが、サーバーとの通信部分についてひょんなところから gRPC を使ってみることになりました。

gRPC については噂程度しか聞いたことがない……というレベルだったので、これを機に gRPC やその周辺技術を概念からがっつり理解しようと思い調べたので記事に起こします。

  • gRPC って何?美味しそうだねという方
  • REST はよく使うけれど gRPC は得体が知れずちょっと怖いという方
  • gRPC を調べて見たけど、RPC や Protobuf、Connect などそれっぽい用語がいっぱいあって概要を掴むのが難しかった方

におすすめかなと思っております!

本記事では、gRPC の概念をより理解するために必要な前提知識や、REST との違い、gRPC を起点に生まれた周辺技術を交えながら、 gRPC を使い始める準備をしようと思います。 gRPC を概要だけでなく周辺までサッと理解することにより、より知識に深めのグラデーションがかかれば嬉しいなと思っています。そのため、多少遠回りしながらの説明になってしまうと思いますが、最後までお付き合いいただければ幸いです。

HTTP プロトコルのおさらい

ここから!?と思ったらすみません。🙇
後の gRPC とその周辺技術の理解につながってくるかも?と思い、最初におさらいをしました。

HTTP は、アプリケーション間で情報や命令をやり取りするためのプロトコルで、主にクライアントとサーバー間の通信で使われるものです。

このプロトコルにはいくつかバージョンがあるのですが、今回紹介するうちで最も古くからある HTTP/1.1 は、1 リクエストで 1 回の TCP 接続が行われる形です。どういうことかというと、たとえばブラウザで Web サイトを開いた際に、3 個の分割された CSS ファイルと、5 個の画像があった場合、そのすべてを 1 つずつリクエストするということです。コネクションの再利用ができないという問題があったんですね。また、リクエスト時に送るメタ情報である HTTP ヘッダーもまた、内容がほぼ同じだとしても 1 回 1 回きちんと情報を詰めて送らなければならずオーバーヘッドが大きいです。このような効率の悪いリクエストになってしまうため、サーバーの処理速度は遅くなるしネットも混雑してしまうよね……というデメリットがありました。

この解消に当たったのが HTTP/2 です。1 つの TCP 接続内で複数のリクエスト・レスポンスを並行して処理するようにしたり、冗長な HTTP ヘッダーを圧縮するようにしたりして、これまで課題になっていたパフォーマンスをいくつか改善しました。さらにパフォーマンスを改善するべく HTTP/3 も登場しています。

ちなみに、私は普段フロントエンドの方に軸足をおいているのですが、開発している Web サイトが、サーバーとの通信において どの HTTP プロトコルのバージョンの形式で通信しているのか意識することはありませんでした。

なんでだろうと思ってよくよく調べてみると、HTTP/2 はブラウザのサポートが必要であり(既に多くのブラウザがサポート済み)、サーバー側もまた HTTP/2 の通信を行うかどうかの設定が必要になります。HTTP/2 は、HTTP/1.1 の上位互換であるため、ブラウザが HTTP/2 で通信できるかサーバーに問い合わせた上で、可能だったら HTTP/2 で、対応していなければ HTTP/1.1 で通信する、という仕組みがそもそもあるみたいです。そのため、もしかしたらサーバーサイド側の開発をしている方であればそういった設定をしたことがあったかもしれませんが、自分の場合はあまり普段 HTTP プロトコルのバージョンを意識することがなかった、という感じでした(知っておくべきですね。汗)。

ネットワークタブからプロトコルを確認して感動しました😊

ネットワークタブの様子

API 設計におけるアプローチ

ここで本題に戻りまして gRPC とは何かについてですが、Google が開発した RPC の機構を備えた HTTP/2 プロトコルベースのフレームワークであり、クライアントとサーバーが通信する際のアーキテクチャの 1 つです。

順を追って紐解いていけたらと思いますが、通信アーキテクチャの一種であるというのが最初のポイントになります。通信アーキテクチャはここでは、アプリケーション上で異なる場所のサーバーに何かをしたいクライアントと、クライアントからリクエストを受けてレスポンスを返すサーバーが、お互いに会話する方法という枠組みを指します。

厳密に抽象度を整えると微妙に合わないこともありますが、このようなクライアントとサーバーとのやり取りという面で見ると、HTTP プロトコルベースの通信アーキテクチャである REST は抽象度の近しい仲間になりそうです。

何が言いたいかと言いますと、(後述しますが)データ形式や通信フロー等には特徴があるものの、基本的に「クライアントがリクエストを送り、サーバーは命令にしたってなんらかの処理をする」というベースの通信方法は全く同じということなんですね(調べた当初私はこのレベルから知らなかったので、gRPC が何かすごい機構を持っている気でいました)。

次に、先ほど gRPC について RPC の機構を備えたフレームワークと説明したので、このあたりのお話です。RPC は Remote Procedure Call の略であり、簡単にいうと「遠くにある別サーバーの関数(メソッドやサービス)をローカル呼び出しのようにコールできる」ような機構です。Go の実際のクライアント側の呼び出し部分のコードは、とてもイメージが湧きやすいです。

// 別サーバーのサービスを呼び出す
c := pb.NewGreeterService(conn)

~~~

// まるで自分のサーバーから呼び出すような形でHelloRequestを呼ぶ(実際には異なるサーバー間で通信される)
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: *name})
if err != nil {
    log.Fatal(err)
}

RPC は古くからある通信の考え方のため多くの記事が存在します。そのため、ここではこれ以上の詳しい説明は省かせてもらいます。

REST はメソッドを呼び出すのではなく、リソースに対して何らかのリクエストをります。
API 設計におけるこのアプローチの違いが gRPC を理解する助けになります。

前述の通り、クライアントとサーバーが HTTP プロトコルをベースにしてリクエストのやりとりをする、という最終的なやり取りについては gRPC と REST において違いはありませんが、設計におけるアプローチ方法は大きく異なります。

しかしこの設計アプローチについてはあくまで gRPC ではなく RPC に重きを置いた特徴になっています。ここから gRPC の特徴についてより掘り下げてみました。

Protocol Buffers (Protobuf)

gRPC の大きな特徴の 1 つに、Protocol Buffers (Protobuf)という仕組みの存在が挙げられます。

https://protobuf.dev/

色々と調べたものの、改めて自分の言葉で Protobuf について説明するのはとても困難なのですが、これが何かを一言で言うとデータ変換のためのフォーマットになるかなと思います。ただ、説明自体がとても難しいので、この技術がどのように利用され開発されていくのかという具体例とともにお伝えしていきます。

まず Protobuf は.protoというファイル形式で、以下のようにサービスやメソッド、リクエストとレスポンスのスキーマ定義を行います。インターフェースのような役割です。

// Copyright 2015 gRPC authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

syntax = "proto3";

option go_package = "google.golang.org/grpc/examples/helloworld/helloworld";
option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";

package helloworld;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

このスキーマ定義をもとに、開発者はprotocという Protobuf 公式のコンパイラを利用して、好きな言語でコンパイルしてコード生成をすることができます。

func (s *server) SayHello(_ context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
	// implements!!
	return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}

gRPC はバイナリデータを利用して通信するのですが、その際のシリアライズやデシリアライズ処理なども、このコンパイラが自動生成してくれます。こうして、私たちは好きな言語でメソッドの中身を実装していくわけですね。

gRPC はこの Protobuf を利用したフレームワークとなります。この枠組みを起点にして REST と比べた際の gRPC の特徴は以下になります。

  • スキーマ定義が必須
    • REST においてはスキーマ定義は必須ではありません(しかし OpenAPI などスキーマ駆動な開発にするための任意の仕組みは存在します)。
  • 通信データが軽量
    • 通信に利用されるのはバイナリデータのため、REST でよく利用される JSON 等と比べるとデータが小さく収まります。

スキーマ定義が必須というのは良いですよね。しかも、スキーマから自動生成されたコードをクライアント側でもサーバー側でも利用することができます。サーバー A は Go で生成しておいて、クライアント B は Ruby で生成して通信する、という手段だったとして、同じ.protoファイルを共通して利用できるのも嬉しいところです(同じファイルでクライアントもサーバーも利用できる、というのは感動しました。RPC の特徴から何となく想像がつく方もいらっしゃると思いますが、gRPC は Web ブラウザとサーバーというよりは、マイクロサービスにおけるサーバー間通信においてよく利用される技術という背景もあったからなのでしょうか……?)。

やや余談にはなりますが、gRPC には通信にタイムアウトを設定します。REST にはタイムアウト設定はない(API 通信する際に axios などのクライアントが設定することはありますが)ので、この設定は gRPC 独特と言えそうです。通信フォーマットをバイナリにして速度パフォーマンスを向上させている技術なので、こういった設定項目からも通信が遅延しないようにする工夫を感じました。

https://grpc.io/docs/what-is-grpc/core-concepts/#deadlines

gRPC から生まれた周辺技術

最初の説明時に gRPC は HTTP/2 ベースと記載しましたが、この HTTP/2 ベースのプロトコルが中心になることや、前述のデータフォーマット形式になることは、利用において制約が伴うことになりました。具体的に言うと、Web ブラウザとサーバー間の通信には対応できなかったんですよね。

そこで Google が開発したのが gRPC-Web であり、この技術はブラウザと gRPC の通信を可能にしました。様々なテコ入れを行い、最終的にはブラウザと gRPC サーバーの間に通信を行えるプロキシを立てる手法によって、めでたく Web ブラウザからでも gRPC に接続することができました 🎉

https://grpc.io/docs/platforms/web/
https://github.com/grpc/grpc-web

また、サーバー同士や Web ブラウザとサーバー間で gRPC をより便利に接続するため、Connect というライブラリも生まれました。

https://connectrpc.com/

Connect を利用する嬉しさは多くありそうなのですが、正直まだ理解が浅い状態です。
現段階で調べられているところのみをピックアップして 3 点お伝えさせてください。

1 点目は ConnectRPC という、gRPC の補完用に開発された新しいフレームワークの存在です。HTTP/2 と HTP/1.1 両方のプロトコルで動作するようになっていたり、バイナリ以外のデータ形式をサポートしていたりします。Web ブラウザのクライアントとサーバー間において ConnectRPC で通信する方法に切り替えることにより、サーバー側は gRPC 互換を保ちながらも クライアント側は gRPC-Web のようにプロキシサーバーを立てる必要なく通信できるようになりました。

とはいえ ConnectRPC は gRPC と似つつも独自のプロトコルで実装されています。クライアントが ConnectRPC 方式で接続したいのであれば、当然サーバー側も Connect プロトコルを理解して処理する必要があります。そうなると gRPC というよりは ConnectRPC を使う感じになってしまいますよね。元々マイクロサービスにおけるサーバー間通信に秀でた技術として名高いため、「gRPC が使いたいんだ!」みたいなこともあるかもしれません。また、Web ブラウザからもサーバーからもリクエストがあるサーバーの場合は「Web ブラウザからのリクエストの場合は Connect を使っても良いけど、サーバー間だったら gRPC がいいよ」という場合もあると思います。

ここで Connect の特徴 2 点目になりますが、Connect ライブラリを利用すると、1 つのサーバーに対して gRPC、gRPC-Web、Connect プロトコルの 3 つに対応した API エンドポイントを立てることができます。

具体的にどんなイメージでコード生成するのかについては、ふわっとしたサンプルコードがある方が分かりやすいと思いますので、以下に載せます。

buf.gen.yml
version: v2
plugins:
  - local: protoc-gen-go
    out: gen
    opt: paths=source_relative
  - local: protoc-gen-connect-go
    out: gen
    opt: paths=source_relative

上記は.protoで定義したスキーマ定義をコード生成する際の設定ファイルです。protoc-gen-goは gRPC 用、protoc-gen-connect-goは Connect RPC 用のコードを生成してくれます。生成されたファイルのツリー構成は以下です。

gen
└── greet
    └── v1
        ├── greet.pb.go
        └── greetv1connect
            └── greet.connect.go

このコードは以下の公式から転用させてもらいましたので、詳しく知りたい方は覗いてみてください。

https://connectrpc.com/docs/go/getting-started#generate-code

ちなみに先ほどスキーマ定義ファイルの公式コンパイルツールとしてprotocを紹介しましたが、上記例のコンパイルはBufという、protocをさらに色々と強化した、 Protobuf 周辺技術をいい感じに利用するための仕組みを利用しています。(最近はこちらの方がよく利用されているみたいです)。

https://buf.build/docs/

3 つ目の特徴として、クライアント側から通信する際にもシームレスに通信プロトコルを変換できるような機構が備えられています。

こちらも具体的なコードを見ていただくと早いのでサンプルをご紹介します。
以下のコードは、Node.js クライアントにおいて、サーバーへの say リクエストが Connect プロトコルで通信されるように変換しています。

import { createClient } from "@connectrpc/connect";
import { createConnectTransport } from "@connectrpc/connect-node";

const transport = createConnectTransport({
  httpVersion: "1.1",
  baseUrl: "http://demo.connectrpc.com",
});

async function main() {
  const client = createClient(ElizaService, transport);
  const res = await client.say({
    sentence: "I feel happy.",
  });
  console.log(res.sentence);
}
void main();

createConnectTransportの他にもcreateGrpcTransportcreateGrpcWebTransportが用意されているため、これらのメソッドを利用してサクッと変換することができるというわけです。また、Connect プロトコルは HTTP/1.1 および HTTP/2 の両方で通信可能になっているため、REST API リクエストの際にはブラウザに任せきりだった HTTP プロトコルバージョンの選択も、httpVersionオプションでできるようになっています。

https://connectrpc.com/docs/node/using-clients

以上が Connect ライブラリの主な機能です。

ベースには gRPC を便利にする ConnectRPC を開発したというのがあり、それらを皮切りに各サーバーやクライアントがシームレスに連携できるよう、処理をうまいこと共通化してくれたといったところでしょうか。通信の難しいところを隠蔽してくれるので、中身の実装の方にグッと集中できるようになりそうで、とても良い枠組みだなと思いました。

ところで Connect は Google ではないということだったので、「あら、ひょっとしてバチバチなのかな」と思ったのですがそんなことはなく、Connect は gRPC を補完して広めるために生まれたとのことでした。。

https://buf.build/blog/connect-a-better-grpc

終わりに

ここまでお読みいただきありがとうございます。

本記事は、gRPC を使い始める準備ができる状態でをゴールとして、gRPC の概念やその周辺技術をご紹介してきました。REST 開発経験のある自分の調査備忘録としてもおいているため、REST との違いを中心とした話が多かったかなと思います。汗

gRPC には REST のようなリクエスト・レスポンスのやり取りをするシンプルな通信モデルに加えて、クライアントもしくはサーバーが複数のリクエストやレスポンスを送ったり、双方向がリアルタイムでやり取りすることができたりするみたいです。このあたりの調査や検証までは全く行き届きませんでしたが、気になる方はぜひそのあたりも深掘りしてみてください。

改めてきちんと紐解いてみると、アプリケーション間でクライアントとサーバーが通信するという形は REST と変わらないので得体が知れなかったものではなくなり、安心しました。一方、周辺の思想や取り巻く技術は知らなかったものが多くあり、とっても勉強になりました!

Discussion