プロジェクト参画前のキャッチアップで前職からの悩みが解消された話を備忘を兼ねて記事にしました。
これからgRPCを学びたい人向け。
プロジェクト参画前のキャッチアップで前職からの悩みが解消された話を備忘を兼ねて記事にしました。
これからgRPCを学びたい人向け。
こんにちは。サーバーエンジニアのりつきです。
この度参画させてもらったプロジェクトのキャッチアップのためgRPCを学習したところ、使い所さえわかれば私のweb開発での悩みどころを解消してくれる良いフレームワークだと言うことが分かったので記事として残すことにしました。
gRPCそのものの解説は他にも参考になる記事があるので「実際の業務ではどのように使われているのか」や「どう便利なのか」についても解説できたらと思います。
全3回予定と少々長いですがお付き合いいただけたら幸いです。
まずは下記をご覧ください。
openapi.yml
requestBody:
description: 何がしかのAPI
content:
application/json:
schema:
type: object
properties:
message:
type: string
NanigashikaRequest.go
type NanigashikaRequest struct {
messaqe string
}
ごく単純なAPI仕様書とリクエスト用のGolangの構造体ですね。messageパラメータに何がしかの値を入れて処理をしたいと言うことが見て取れます。
しかし上記の仕様とコード通りではGolang側に[message]の[g]が[q]となっているスペルミスがあるため期待通りの通信ができません。
サーバーとクライアントが別々のチームで開発をしている状況でこのようなミスが起こった場合、クライアントのチームからサーバーチームに「通信がうまくいかないよー」と連絡が入り、サーバーチームが調査してスペルミスが見つかり、修正後PR作成してレビュー通して開発環境に反映して...などと言うフローを踏むのでこのスペルミス一つでそこそこの時間が取られてしまうわけです。
OpenAPI Generatorなどを利用しAPI仕様書からコードを自動生成してこれを防ぐこともできますが、ツールがAPI仕様書の規格とは別に開発されており個別に導入する必要があるなどの手間がかかることもあります。その点gRPCであれば統一された仕組みを利用できるので導入にかかるコストを削減できます。
gRPCとはGoogleによって開発・公開されているRPCフレームワークです。
後述する要素技術によってサーバーに実装されたメソッドをクライアントアプリケーションからローカルオブジェクトのように直接呼び出すことが可能です。
さらに、サーバーとクライアントが扱う言語が異なっていてもgRPCがサポートする言語間であれば通信が可能です。
公式サイトではC++で実装されたサーバーにRubyやJavaでアクセスするような図が掲載されています。
ただし、ブラウザからの通信リクエストではgRPCを直接利用するのは現状一般的ではなくgRPC-Webという別のプロトコルが利用され、プロキシサーバを経由させる必要があるなど少々考えることが多くなるのでその場合は注意が必要です。
gRPCについて理解するため下記の3つの技術について紹介します。
このあたりの紹介は他にも参考になる記事がたくさんあるので簡潔に。
RPCとは、遠隔手続き呼び出し(Remote Procedure Call)の略称で、簡単に言うとサーバーに実装された処理をクライアントから呼び出し、その結果を受け取ると言うクライアント・サーバー型の通信プロトコルのことです。
これを利用すると、クライアント側はサーバーで実行される処理の詳細を理解していなくても呼び出す方法さえ知っていれば結果を呼び出すことができます。
gRPCにおいて異なるプログラミング言語間でのデータのやり取りを実現するためのデータフォーマットのことです。
バイナリ形式であるためデータ量が小さく通信も早いという特徴があります。
gRPCでは.proto拡張子のついたファイルを利用してこのフォーマットを用いた通信機能のコードを自動生成することができ、これによりAPI仕様と実装のズレを無くすことができます。
様々なwebコンテンツの登場により一度のアクセスで大量のアクセスを送るようになったことで従来のHTTPバージョンでは通信速度の限界を迎えてしまう中、新たにストリーミング機能やバイナリ形式での通信などネットワークを効率よく使うための様々な機能を搭載したHTTPのバージョンがHTTP/2です。
gRPCはこのHTTP/2上で構築されているため効率よく通信ができます。
私としてはこれに尽きます。冒頭に記載した通りAPI仕様書と実装の管理が独立しているとちょっとしたミスで作業時間が取られてしまいます。業務に置いて時間のロスはそのままお金のロスになるので減らすに越したことはありません。
時間のロスを減らせばその分を新規の機能開発や別の不具合修正に回すことができ、結果としてサービスの価値向上につながります。
スペルミスが原因で残業なんてのも嫌ですしね。
.protoファイルを一元管理しておけば一つの開発における通信の仕様書を集約することができるので言語の異なるサーバー間やサーバーとクライアントアプリケーションの通信など複数の通信経路があっても仕様書を探す際に迷子にならずに済みます。
特にマイクロサービスの開発など複数のサービスに跨って開発を行う場合においては各サービス毎の通信の規格を揃えることができるためこの恩恵を受けることができます。
私がアサインされた案件でもマイクロサービス開発を行っており複数のサービス間での通信を行っていますが、通信の仕様は(一部のgRPCを利用していない箇所を除き).protoファイルを管理しているリポジトリに集約されており、各サービス間のAPI仕様を一箇所で確認することができます。
今回のサンプルはMac環境で実行しています。Windowsの環境では事前準備の手順が結構変わってしまいます。
Homebrewが使えなかったりコードの生成を行うライブラリのパスを通してやらないといけなかったり...
以前に私がWindowsでgRPCの導入をしようとした時はDockerでLinuxのコンテナを立ち上げてそこで実行するというかなり遠回りした方法で行いました。
今回は二つの.protoファイルを記述してみます。この後の作業のため以下のような構成でディレクトリおよびファイルを作っておいてください。
- examgrpc/
- pkg/
- grpc/
- proto/
- message.proto : 記述する.protoファイル
- service.proto : 記述する.protoファイル
.protoファイルは下記のように記述します。
message.proto
// バージョンの宣言
syntax = "proto3";
// パッケージの宣言
package proto;
// 自動生成させるGoのコードの置き先
option go_package = "pkg/grpc";
// 型の定義
message HelloRequest {
string message = 1;
}
message HelloResponse {
string resMessage = 1;
}
service.proto
// バージョンの宣言
syntax = "proto3";
// パッケージの宣言
package proto;
// 自動生成させるGoのコードの置き先
option go_package = "pkg/grpc";
import "proto/message.proto";
// サービスの定義
service GreetingService {
// メソッドの定義
rpc Hello (HelloRequest) returns (HelloResponse);
}
記述についてより詳しく知りたい場合は公式のドキュメントも参照してみてください。
また、少し調べればより導入に特化した記事も見つけることができます。
今回はMac環境でGo言語を利用したいのでHomebrewを使ってインストールを行います。
コマンド操作は全てexamgrpc/直下
$ brew install go
$ which go
/opt/homebrew/bin/go Go言語がインストールされたパスが表示されればOK
$ brew install protobuf
$ which protoc
/opt/homebrew/bin/protoc コマンドがインストールされたパスが出力されればOK
$ go mod init examgrpc
$ go install google.golang.org/protobuf/cmd/protoc-gen-go
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc
$ protoc-gen-go --version
protoc-gen-go v1.26.0 バージョンが表示されればOK されていなければ(大抵の場合)パスが通っていないので通す
$ echo 'export PATH=$PATH:~/go/bin' >> ~/.zshrc // 実行しているシェルがbashであれば~/.bashrc
$ source ~/.zshrc
いよいよコードの生成を行います。下記を実行してください。
$ protoc ./proto/*.proto go_out=. --go-grpc_out=. --proto_path=.
エラーが出ずにコマンドが完了していれば下記のファイルが生成されているはずです。
- examgrpc/
- pkg/
- grpc/
- message.pb.go : メッセージ型の定義から生成された構造体が含まれる
- service.grpc.pb.go : サービスの定義から生成されたインターフェイスが含まれる
- service.pb.go : protoc-gen-go によって生成されるメッセージのデフォルトの実装, 他の生成されたファイルにも同様の記述がある
- proto/
- message.proto
- service.proto
実行したprotocコマンドの各オプションに関しても詳しく解説すると長くなってしまうのでコマンド全体で「proto/ディレクトリ以下の.protoファイルからpkg/grpc/ディレクトリにGolangのファイルを自動生成している」と言うことだけ理解していただければ問題ありません。
message.pb.goにはメッセージ型の定義から生成された構造体が記述されています。
定義したパラメータの他にエンコード/デコードのためのパラメータが含まれています。また、定義したパラメータのゲッターも実装されています。
// 30行目付近
type HelloRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 65行目付近
func (x *HelloRequest) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
type HelloResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
ResMessage string `protobuf:"bytes,1,opt,name=resMessage,proto3" json:"resMessage,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache. protoimpl.SizeCache
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 110行目付近
func (x *HelloResponse) GetResMessage() string {
if x != nil {
return x.ResMessage
}
return ""
}
service_grpc.pb.goにはサービスの定義から生成されたサーバー/クライアントそれぞれのインターフェイス
が記述されています。
生成されたインターフェイス
はあくまでも定義の部分なので実際に通信を行うにはサーバーにrpcメソッドの中身を実装し、クライアントは自動生成されたコードからrpcメソッドを呼び出すコードを書いてやる必要があります。
実装するコードについて詳しくは次回の実装編で解説します。
// 30行目付近
// サービスの定義(クライアント)
type GreetingServiceClient interface {
// メソッドの定義
Hello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error)
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 60行目付近
// サービスの定義(サーバー)
type GreetingServiceServer interface {
// メソッドの定義
Hello(context.Context, *HelloRequest) (*HelloResponse, error)
mustEmbedUnimplementedGreetingServiceServer()
}
上記の構造体およびインターフェイス
を元に通信の実装を行うので構造体やパラメータの命名など細かいミスを防ぐことができ、それによって発生する無駄な作業を削ることができるのです。とっても便利。
もちろん使いこなすには相応の学習コストがかかりますし、長く運用している案件ではサーバーがHTTP/2に対応していないなど導入には総合的な判断が必要になるため「どんな時でもgRPC一択!」と言うわけには行きませんが、私個人としては自分にある程度技術選定の決定権がある新規の案件などでは積極的に導入したいなと思えるものです。
また詳しくは後続の記事に記載しますが「.protoファイルからのコード生成の実行」を自動で行うことで、実務での開発をさらに効率化することも可能です。
次回は実装編と言うことでここで生成したコードを利用して実際にサーバーとクライアントのプログラムを書いていこうと思います。
よければ読んでいただけると幸いです。
また、EMoshUでは一緒に開発できる仲間を募集しております。
ご興味を持たれましたら、ぜひ募集要項をご覧ください。カジュアル面談なども実施しております。