来月からgRPCでのAPI開発をすることになっているので、事前に学習した成果をまとめた。
この記事は以下の構成になっている(あとから見返して思い出しやすいように)。
- gRPCのイメージを座学的にざっくり → 詳細につかむ
- gRPC APIのサンプルを実際に動かしてみることで、イメージより強固にする
1. ざっくり理解
1.1. gRPC
REST APIの対抗馬となるAPIのフレームワーク的なもの。
対比させると下記のような形。
項目 | REST API | gRPC |
---|---|---|
HTTPプロトコル | HTTP/1.1 | HTTP/2 |
メッセージのフォーマット | JSON や XML(テキスト) | Protocol Buffers(バイナリ) |
コードの自動生成 | サードパーティ製ツール(Swaggerなど) | Protocol Buffers(※protoc + gRPC拡張プラグイン) |
リクエスト実行 | <エンドポイントURL> (例. GET /user/1) | gRPCスタブ(内部的にはProtocol Buffers形式へのシリアライズ + HTTP/2でリクエスト) |
1.2. HTTP/2
REST APIで使われる、従来型のHTTP/1.1と互換性を保ちながら内部的には効率化が図られているプロトコル。
- リクエストとレスポンス:
- ヘッダ: キーバリューのテキスト (※HTTP1.1と異なり、送信時に圧縮される)
- ボディ: キーバリューのバイナリ (※HTTP/1.1ではテキスト)
- ポート番号: 80(http)/443(https) (※HTTP/1.1と変わらない)
- 効率化のポイント:
- コネクションの使い回しが可能
- サーバー側の並列処理可能
- ヘッダの圧縮がなされる
1.3. Protocol Buffers
Googleが開発した「データフォーマット および シリアライズ手法」。
.proto
ファイルという「メッセージ(構造体)の定義を行う」ファイルを記述し、protoc
というビルドツール(+各言語の拡張プラグイン)に.proto
ファイルを流すと、
各言語にあわせた「メッセージのシリアライズ・デシリアライズ処理」のコード を生成することができる。
さらに、protocに grpcの拡張プラグインを入れてやる と、.protoc
ファイルを、以下の定義をすることができるファイルに 意味を拡張して扱うことができる ようになる。
- メッセージ(リクエストパラメータ、レスポンスパラメータの型)の定義
- gRPCサービス(後述)の定義
上記の定義を行った上でprotocに.proto
ファイルを流すと、各言語に合わせた以下のソースファイルを生成することができる。
- 「リクエストパラメータ・レスポンスパラメータのシリアライズ・デシリアライズ処理」のコード(サーバー・クライアント双方で使う)
- 「gRPCサービスのインターフェース」のコード(サーバーで使う)
- 「gRPCサービスのスタブ」のコード(クライアントで使う)
※gRPCサービスとは?
-
「gRPCサービス名 + 複数のgRPCメソッドの定義」 のことである。
- gRPCサービス名: REST APIのリソース名に相当
- gRPCメソッド: REST APIのHTTPメソッド(GETやPOSTなど)に相当
2. ちょっと詳細に理解
全体像がイメージできたところで、それぞれちょっと詳細に説明。
2.1. gRPC
- Googleが自社サービス向けに開発していた「 RPC (後述)」の技術(Stubby/スタビー)をベースに、「 HTTP/2 (後述)」などの技術が取り入れられてオープンソース化されたもの。
- マイクロサービス (後述)構成のアプリケーション(=分散アプリケーション)で、gRPCが採用される事例が増えているらしい。
- さまざまな言語でgRPCを扱うためのライブラリが公開されている。
- gRPCを使う際の「通信プロトコル」、「データの表現とシリアライズ方法」はデフォルトでは以下の設定となっており、双方のメリットを教授できる。(それぞれ置き換えは可能であるが)
- 通信プロトコル: HTTP/2
- データフォーマットと シリアライズ(後述): ProtocolBuffers (後述)
- 先述の通り、「Protocol Buffers」のビルドツール「protoc」に「各言語の拡張プラグイン(必要あれば) + grpc拡張プラグイン」を組合わせることで、以下を生成することができる
- 「リクエストパラメータ・レスポンスパラメータのシリアライズ・デシリアライズ処理」のコード(サーバー・クライアント双方で使う: ※通常意識せずとも使われる)
- 「gRPCサービスのインターフェース」のコード(サーバーで使う)
- 「gRPCサービスの スタブ (後述)」のコード(クライアントで使う)
- サーバー側では「gRPCサービスのインターフェース」のコードに対して、その実装を記述してやりつつ、main関数に以下の記述をしてプログラムを実行してやればgRPCサーバーとして動くようになる。
- gRPCサーバーがListenするプロトコル(TCP, UDPとか)・ポート番号
- gRPCサーバー処理対象のサービスの登録
- gRPCサーバーの起動
- なお、gRPCではリクエスト〜レスポンスの流れは以下のような感じになる。
# 処理の流れ
①クライアント側でgRPCスタブのメソッドを実行
→スタブが「Protocol Buffers」形式でリクエストデータをシリアライズし、「HTTP/2」でリクエストを送信。
②サーバー側でリクエストを受信
→gRPCサービスが「Protocol Buffers」形式でリクエストデータをデシリアライズし、対象のRPCメソッドを実行する。
レスポンスデータを「Protocol Buffers」形式でシリアライズし、「HTTP/2」でレスポンスを送信。
③クライアント側でレスポンスを受信
→スタブが「Protocol Buffers」形式でレスポンスデータをデシリアライズする。
2.2. RPC
Remote Procedure Callの略称であり、「リモート端末上でクライアントが指定した手続きを実行してもらう」仕組み のこと。
以下の機能で構成される。
- クライアントアプリケーションが、リクエスト(実行対象の処理および、与えるパラメータ)を送信(Call)する
- サーバーアプリケーション(Remote)は、リクエストの内容に応じて、具体的な処理(Procedure)を実行して結果をレスポンスとして返す
「JSON/XML形式でリクエスト・レスポンスをHTTP/1.1でやり取りするREST API」 や、
「Protocol Buffers形式でリクエスト・レスポンスをシリアライズし、HTTP/2でやり取りするgRPC」 はRPCの一種だといえる
2.3. HTTP/2
- 1コネクション内で、複数の「ストリーム(仮想的なコネクション)」を確立して、同時並行で複数のリクエスト、レスポンスを処理できる。
- 「ストリーム」同士、影響を及ぼすことがないので、1コネクションにおいて1リクエスト・レスポンスに時間がかかっても、別のリクエスト・レスポンスを処理することが可能 。
- ストリームには処理の「優先度」を設定可能。
- HTTPではテキストベースでのやり取りだったが、HTTP/2ではバイナリベース。
- HTTPヘッダはキー・バリュー形式。
- リクエストされたコンテンツに対し、続いてリクエストされる可能性が高いコンテンツを自動的にクライアント側に送信する「サーバープッシュ機能」があり、クライアント側から再リクエストを行うオーバーヘッド解消に寄与する。
- HTTP/2の ポートはHTTPと同様、httpが80番、httpsが443番 。
2.4. シリアライズ
「構造化されたデータをバイト列に変換する処理」のこと
2.5. Protocol Buffers
Googleが開発したIDL(Interface Definition Language)の一種。
他のコンポーネントとのやり取りのための 「データフォーマットとシリアライズ手法」 であり、バイナリデータも効率的に扱える。
さまざまな言語(C, Ruby, Go etc.)で、Protocol Buffersを扱うためのライブラリが公開されている。
JSONと対比される 。
JSONではデータの型を定義する必要はないが、Protocol Buffersでは、.proto
ファイルでデータの型を定義する 必要がある。
Protocol Buffersでは、データをフィールドとして束ねた単位を「メッセージ」と呼ぶ 。
例えば、JSONでいうところの
{
aaa_str: "<文字列>"
aaa_int: "<整数値>"
}
に相当するメッセージ「Aaa」を定義することを例とすると、
Protocol Buffersでは.proto
ファイルへ以下のように記述することになる。
// Protocol Buffersのバージョン指定
syntax = "proto3";
// パッケージ名
package xxxx;
// メッセージの型定義(以下のように記述する)
// message <メッセージの型名(パスカルケース)> {
// <型> <フィールド名(スネークケース)> = <フィールド番号(1からの連番。あけてもOK)>; // フィールド名1〜15は1バイトに、それ以降は2バイトにエンコードされるため、頻繁に利用するフィールドは1〜15を割り当てるべきらしい。
// <型> <フィールド名> = <フィールド番号>;
// ...
// {
message Aaa {
string aaa_str = 1;
int32 aaa_int = 2;
}
2.6. スタブ
引用: https://e-words.jp/w/%E3%82%B9%E3%82%BF%E3%83%96.html
また、分散システムなどで、ローカル側でメソッド呼び出しなどを受け付けるオブジェクトなどのことをスタブということがある。
自身は処理そのものは行わずリモート側への仲介を行う機能のみを持つ。スタブ自体はローカルであるため、呼び出し元は通信の詳細を意識せずローカル呼び出しのみを用いてコードを記述することができる。
gRPCでの「スタブ」の意味はざっくり説明すると以下のような形。
- クライアント側にある「サーバー側のメソッドの実行を取り次ぐ機能を持つオブジェクト」 である
- クライアント側でスタブのメソッドを実行する ことで、「通信の詳細(= Protocol Buffers形式でのリクエストデータのシリアライズ、HTTP/2での通信とか) を意識せず、対応するサーバー側のメソッドを実行する」 ことができる
参考: https://www.grpc.io/docs/what-is-grpc/introduction/
2.7. マイクロサービスアーキテクチャ
各機能を実現するシステムを物理的に分けたシステム構成のこと。
どこまでわけるとマイクロサービスと呼べるのか、というのは明確ではない。
モノリシックなアーキテクチャでは障害が発生したら全機能が停止となってしまうが、マイクロサービスなアーキテクチャでは、一部機能は継続できるというメリットがある。
替わりにアーキテクチャが複雑になるというデメリットがある。
参考: https://zenn.dev/takuyanagai0213/articles/a011b372eac99c
3. GoでのgRPCによるAPI実装の流れ
gRPCの概念がつかめたところで、具体的にどんな感じでgRPCでAPIを実装していくのかサンプルを動かす形でつかむ。
流れは以下の通りである。
1. Protocol Buffersビルドツールをインストール
2. Protocol BuffersビルドツールのGo言語の拡張プラグイン + gRPC拡張プラグインをインストール
3. .protoファイルを記述(以下の内容)
- APIのエンドポイントのインターフェース定義(= gRPCサービスのインターフェース定義)
- リクエストパラメータ、レスポンスパラメータのフォーマット定義(= メッセージ定義)
4. .protoファイルを言語指定してビルドし、以下のファイルを生成
- xxx_grpc.pb.go: gRPCサービスのインターフェースのコード
- xxx.pb.go: メッセージのシリアライズ・デシリアライズ処理のコード
5. gRPCサービスのインターフェースに対する実装を記述
6. main関数へ以下を記述
- gRPCで使うプロトコル(TCPなど)・ポート番号の記述
- gRPCサーバーへ処理対象とするgRPCサービスを登録
- gRPCサーバーの起動
なお、以下前提となっていること、ご承知おきを。
- Macで。
- Go言語で。Goインストール済みであること(Goの導入方法)。
- 2022/10 時点
3.1. Protocol Buffersビルドツールをインストール
参考: https://grpc.io/docs/protoc-installation/
# インストール
brew install protobuf
=>
明示的にインストールした憶えはないのだが、なんかすでにインストールされていた・・・
Warning: protobuf 3.19.3 is already installed and up-to-date.
# バージョン確認
protoc --version
=>
libprotoc 3.19.3 的な感じで表示される。
3.2. Protocol BuffersビルドツールのGo言語の拡張プラグイン + gRPC拡張プラグインをインストール
# Go言語の拡張プラグイン(protoc-gen-go)のインストール
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
=>
go: downloading google.golang.org/protobuf v1.28.1
# gRPC拡張プラグイン(protoc-gen-go-grpc)のインストール
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
=>
go: downloading google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.2.0
go: downloading google.golang.org/grpc v1.2.1
go: downloading google.golang.org/protobuf v1.27.1
# Protocol Buffer コンパイラがGoプラグインを見つけることができるように.zshrcへ以下を追加
export PATH="$PATH:$(go env GOPATH)/bin"
3.3. .protoファイル
を記述
- APIのエンドポイントのインターフェース定義(= gRPCサービスのインターフェース定義)
- リクエストパラメータ、レスポンスパラメータのフォーマット定義(= メッセージ定義)
を実施する。
・・・はじめから記述したいところだが、ここでは以下のサンプルコードを使い、.protoファイルを更新する形を取る。
- リポジトリ
- サンプルコードの場所
examples/helloworld/
greeter_server/ # gRPCサーバー(GreeterServerサービスが処理対象)を起動するコード
main.go
greeter_client/ # gRPCメソッド(GreeterServerサービスのSayHelloメソッド)を実行するコード
main.go
3.3.1. サンプルコードを実行してみる
# リポジトリのclone
git clone -b v1.48.0 --depth 1 https://github.com/grpc/grpc-go
# サンプルコードの場所へ移動
cd grpc-go/examples/helloworld
# gRPCサーバーを起動
go run greeter_server/main.go
# 別のターミナルから、gRPCメソッドSayHelloを実行
go run greeter_client/main.go
=>
# サーバー側の出力
2022/09/13 09:56:43 server listening at [::]:50051 ← リクエストの待受開始
2022/09/13 09:57:18 Received: world ← クライアントからのリクエストを受け取った際
# クライアント側の出力
2022/09/13 09:57:18 Greeting: Hello world ← サーバー側からのレスポンスを受け取った際
3.3.2. .proto
ファイルを更新する
前提: 先述の通り、.proto
ファイルには以下の内容が記述されている
①サービス定義
- サービス名
- サービスにぶらさがるRPCメソッドと、その引数(リクエストパラメータ)の型、返り値(レスポンスパラメータ)の型
②メッセージ定義
- リクエストパラメータのフォーマット
- レスポンスパラメータのフォーマット
サンプルの「サービス定義ファイル(helloworld.proto)」。
// 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;
}
内容としては以下の通り。
- サービス名: Greeter
- RPCメソッド:
- RPCメソッド名: SayHello
- リクエストパラメータの型: HelloRequest
- レスポンスパラメータの型: HelloReply
- メッセージタイプ
- 型名: HelloRequest
- フィールド: name (string型, ID: 1)
- 型名: HelloReply
- フィールド: message (string型, ID: 1)
上記のサービス定義ファイル(helloworld.proto)に、以下のRPCメソッドを追加する
rpc SayHelloAgain (HelloRequest) returns (HelloReply) {}
3.4. .proto
ファイルを言語指定してビルド
.proto
ファイルをprotocでビルドすると、以下のファイルが生成される
- xxx_grpc.pb.go: gRPCサービスのインターフェースのコード
- xxx.pb.go: メッセージのシリアライズ・デシリアライズ処理のコード
ここでは、3.3. から引き続いて、更新したサンプルコードの.proto
ファイルから.go
ファイルをビルドする。
# examples/helloworldディレクトリへ移動
cd examples/helloworld
# 再ビルド
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
helloworld/helloworld.proto
=>
``helloworld/helloworld.pb.go`` と ``helloworld/helloworld_grpc.pb.go``ファイルが再生成される。
それぞれのファイルの内容は以下の通り。
- ``.pb.go``: ``HelloRequest``メッセージ と ``HelloReply`` メッセージのシリアライズ・デシリアライズ処理
- ``_grpc.pb.go``: 自動生成されたクライアント向けのコード、サーバー向けのコード
.protoファイルと.goファイルの記述の対比(サーバー側関連コード)は以下のようになる。
サービス
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
rpc SayHelloAgain (HelloRequest) returns (HelloReply) {}
}
// GreeterServer is the server API for Greeter service.
// All implementations must embed UnimplementedGreeterServer
// for forward compatibility
type GreeterServer interface {
// Sends a greeting
SayHello(context.Context, *HelloRequest) (*HelloReply, error)
SayHelloAgain(context.Context, *HelloRequest) (*HelloReply, error)
mustEmbedUnimplementedGreeterServer()
}
メッセージ
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
// The request message containing the user's name.
type HelloRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
}
...
// The response message containing the greetings
type HelloReply struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
}
3.5. gRPCサービスのインターフェースに対する実装を記述
(3.4.から引き続いてサンプルコードを使用する)
3.3.〜 3.4.ではSayHelloAgain
というgRPCメソッドを、gRPCサービスGreeterServer
に追加してビルドし、
xxx_grpc.pb.goファイルのgRPCサービスのインターフェースGreeterServer
にSayHelloAgain
メソッドが追加された。
GreeterServerの実装は、main.go
にあるが、ここには構造体GreeterServer
に対してSayHello
メソッドしか実装されていないのが現状である。
構造体GreeterServer
にSayHalloAgain
メソッドを実装する。
example/helloworld/greeter_server/main.go
へ以下の関数を追加する
func (s *server) SayHelloAgain(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
return &pb.HelloReply{Message: "Hello again " + in.GetName()}, nil
}
3.6. main関数の記述
gRPCサーバープログラムとしては、以下をmain関数に実装する必要がある。
- gRPCで使うプロトコル(TCPなど)・ポート番号の記述
- gRPCサーバーへ処理対象とするgRPCサービスを登録
- gRPCサーバーの起動
が、3.5.まで使ってきたサンプルコードは残念ながらすでに実装されており、とくにやることはない。
(そのかわり・・・というのはおかしいが、)ここでは3.5. までで追加したSayHello
gRPCメソッドについて、これをクライアント側のプログラムで動かせるようにする。
example/helloworld/greeter_client/main.go
へ以下を追記
r, err = c.SayHelloAgain(ctx, &pb.HelloRequest{Name: *name})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.GetMessage())
3.7. 実行
追加したSayHelloAgainメソッドが動くことを確認する。
# 1. ディレクトリ移動
cd grpc-go/examples/helloworld
# 2. gRPCサーバーを起動
go run greeter_server/main.go
=>
2022/09/15 10:18:12 server listening at [::]:50051
2022/09/15 10:18:24 Received: world (※こちらは3.の実行時に出力される)
# 3. 別のターミナルから、クライアント側の処理を実行
go run greeter_client/main.go
=>
2022/09/15 10:18:24 Greeting: Hello world
2022/09/15 10:18:24 Greeting: Hello again world
感想
サンプルコードをつかってよりイメージがつかめたと思うが、
.proto
ファイルをイチから書いたわけでもないし、main関数の方もイチから書いてないし・・・
ということで、実際にAPIを想定して実装してみた方がよさそう。
参考
gRPC
↓かなりわかりやすい
マイクロサービスでのgrpc採用について
スタブ
grpcのスタブはシリアライズ・デシリアライズ機能を持つ
ProtocolBuffers
分散アプリケーション
REST API と gRPCの比較
gRPCのメリット
.protoの記述内容