いまさらgRPC入門:gRPCのドキュメント実況中継
TL;DR: gRPCの公式ドキュメントを読んで動かしてみた様子をGif動画付きでお届け。
こんにちは、kadoyauです。ドワンゴではニコニコ動画の開発をしています。
今日は業務は関係なく、自分が新しい技術を身につけるときの流れに沿ってgRPCを触るところを実況中継風にお届けします。この記事は入門そのものなので、経験がある方には退屈です。その場合、次の記事はいかがでしょう。
CPUレジスタ上の整数除算のおはなし
レンダリング結果を目標画像に近づけるために、オブジェクトや光源などの環境を求めるという逆問題
あと、ニコニコ動画は日々進化しているので「1年ぐらい見てないや〜」という人は結構違うので新機能を使っていただければと思います(ダイマ)。新機能とかどういう開発してるの?というのに興味がある方は週刊ニコニコインフォをご覧いただければと思います。 このままだとこの記事が「2020年おすすめ動画まとめ」になってしまうので、(そっちも魅力的ですが)本題に入りたいと思います。
この記事の目次
まえがき
gRPCイントロダクション
gRPCコアコンセプト
gRPCを実際に触ってみる
まえがき
OpenAPIとかgRPCとかGraphQLとかいっぱい出てきたな!
みなさんは、サービス間の連携には、どのような手法をつかっていますか?
一昔まではRESTのAPIがスタンダードだったと思いますが、次のような課題や革新がありました
外的環境の変化:PCだけでなく様々なモバイル機器からの利用の増加(多デバイスからAPIが叩かれることが前提になった)
課題:増えるAPI、信用ならざるドキュメント。当然増えるドキュメントと実装の食い違いによるコミュニケーションコスト。
革新:HTTP/2などの基礎技術の発展
この結果、OpenAPIやgRPC、GraphQLなどの技術が生まれてきたと認識しています。
これらはそれぞれ生まれてきた背景が異なりますが、スキーマを定義するとドキュメントやAPI実装のモックやクライアント実装が(理想的には)自動で生成されるというのは共通していると思います。一部ではSDDと呼ばれていますね。 未知との遭遇
私の所属する伝統的なサービスの開発チームではおなじみのREST APIを利用しています。
OpenAPIによるドキュメントおよびAPIクライアントの自動生成は行っていますが、トラブルが0になるわけではありません。サーバーサイドの実装は漏れることがありますし(dreddなどをつかって結合テストを書くなどの手段はあります)、YAMLも結構複雑になってきて追うのが大変に感じます。 そんな折、サービス間APIについての横断的な議論を社内Slackで見かけました。詳しい議論はここでは重要でないのでおいておきますが、横目で見ていて私が感じたことは、「使ったことがないから、細部はよくわからない」ということでした。
gRPCは弊社の別チームで採用されておりますが、私自身はWebで記事を読んだぐらいで、触ったことはありません。
パラダイムが変わる前に
このとき、以前Shiro Kawaiさんがおっしゃっていたことを思い出しました。
与えられた問題に100%の力で取り組む、というのは美徳とされがちだけど、同種の問題が途切れなく与えつづけられた場合、他の問題解決への適応を試しておく余力が無いので、パラダイムが変わった時に詰むよ、というのはどのくらい(意識され|教えられ)ているんだろうか。
わたしにとってgRPCは、
「現状使わなくても不自由がなく」
「取り入れるのがしんどそうな気がする」
技術です。「RESTがもうダメだ〜」となるまで触るモチベーションは起きづらい、というのが正直なところです。これは「同種の問題が途切れなく与え続けられた場合」に該当します。ユーザーがUI変更を嫌がるのと心理は似ていますね。このような構造は生きているといたるところにあり、昔から様々な啓蒙書(いいプログラマーになるにはというような本)で対策が提唱されているかと思います。
いまこそ学ぶべきときだと思いました。
注:完全に翻訳しているわけではありません。詳細は原文を見てください。
gRPCのイントロダクション
低レイテンシ、高スケーラブル、分散システム
モバイルのクライアントからクラウドのサーバと通信するような構成での開発をするとき
正確で効率的で言語非依存の新しいプロトコルを設計したいとき
拡張できるレイヤードな設計にしたいとき
gRPCの特徴
gRPCでは、クライアントは異なるホストのサーバーのメソッドを直接呼び出すことができる
kadoyau.icon ローカルのメソッドを呼ぶように、他のサーバのメソッドを呼べるんだ
gRPC client(メソッドを呼び出す側)はgRPC server(メソッドを呼び出される側)の環境を気にしなくていい
clientとserverの言語が違っていてもOK
例:gRPC serverがJavaで、gRPC clientがRuby
ただし、gRPCがサポートしている言語に限られる
kadoyau.icon【PHPerに悲しいお知らせ】PHPでgRPCってどこまでいけるの?の通り、PHPでのgRPCは茨の道です。観測範囲でこのスタックで運用している人を知らないので、やっていたらぜひ記事を書いてください!みたい!! gRPCを使うためにすること
servicesと呼び出されるメソッド(もちろんパラメータと戻り値の型も)を定義する
gRPC以外のRPCでもこの手順は同様
RPCは古くからある考え方であり、インターネットが普及する以前より存在していた。一方で、どのようにクライアントからリクエストを送信するかや、どのようなフォーマットでデータをやり取りするかについては時代に応じて変化しており、さまざまな実装が存在する。近年のWebベースのシステムにおいてはHTTP/HTTPSベースでサーバー・クライアント間のやり取りを行い、またその際のデータフォーマートにはXMLを利用する「XML-RPC」や、同じくHTTP/HTTPSベースでデータフォーマットにJSONを利用する「JSON-RPC」といったものが多く使われている。 serviceの定義にはProtocol Buffersというフォーマットを使う(後述)
サービスの定義以外に次のことをする必要がある
サーバーサイドがやること
定義したインタフェースを実装する
クライアントからの呼び出しをhandleするgRPC serverを起動する
以上。あとはgRPC infrastructureが、リクエストをデコードしてservice methodを実行して、レスポンスをエンコードしてくれるらしい クライアントサイドがやること
メソッドをサービスとして実装したstub(他の言語ではクライアントといわれるもの)というobjectが提供されるので、たんにそれを呼べばよい
以上。あとはgRPCが、リクエストをサーバに送った後、サーバのprotocol bufferのレスポンスを返してくれるらしい
Protocol Buffer
Protocol BuffersはIDLとメッセージ交換フォーマットをかねたもの
kadoyau.icon Protocol BuffersもGoogleで長いこと利用されていたものらしい。Rebuild.fmでも森田さんがProtocol Buffersのほうが先にオープンソースになっていたと言っていたっけ(参考:gRPC#5a8544543f4425000016f160) Protocol Buffersをつかってみる
まず、シリアライズしたいデータのための構造をprotoファイルに定義する
protoファイルは拡張子が.protoの、ただのテキストファイル
Protocol bufferのデータは、messagesとして構造化される
code:simple example.proto
message Person {
string name = 1; // このペアを field と呼ぶ
int32 id = 2;
bool has_ponycopter = 3;
}
これで、データ構造が定義できた
Protocol buffersのコンパイラー(protoc)をつかって、データにアクセスするクラスを自動生成する(好きな言語のクラスを作ることができる)
メッセージに定義した各fieldに対して、単純なアクセサが自動生成される。
例:Personではnameというfieldを定義した。このデータを取得するのはname()で、セッターはset_name()となる。
もちろん、シリアライズ/デシリアライズのためのメソッドも自動生成される
gRPC servicesもprotoファイルに定義していく
code:gRPC services example.proto
// Greeterというサービスを定義する
service Greeter {
// このメソッドは、HelloRequest型を受け取り、HelloReply型を返す
// このメソッドの定義方法は4つある(後述)。これは最も単純なUnary RPCという形式
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// HelloRequestの定義
message HelloRequest {
string name = 1; // リクエストメッセージはユーザ名を含む
}
// Hello Replyの定義
message HelloReply {
string message = 1; // レスポンスメッセージは挨拶(message)を含む
}
protoc(とgRPCのプラグイン)をつかえば、protoファイルからgRPC clientとgRPC serverのコードを自動生成できる。さっきやったのと同じ感じで。
kadoyau.icon「どうやってprotocとプラグインをPHPでインストールするの?
Protocol bufferの最新バージョンは3
Protocol buffersにはバージョンが有る。いままでよくつかわれていたのはversion 2なので殆どの例がv2のものだ
kadoyau.icon ドキュメントも2のものと3のものにわかれている
このドキュメントではversion 3(proto3)を使っている。2と比べて
構文がちょっと簡単
いくつか機能追加がある
gRPCのコアコンセプト
gRPC serviceで定義できるメソッドは次の4種類がある
同期 v.s. 非同期
同期RPC呼び出しは、サーバからのレスポンスがかえるまでブロックする
でもネットワークは本質的に非同期で、いろんな場合がある
現在のスレッドでブロッキングせずにRPCを開始できると便利
殆どの言語のgRPC programming APIは同期/非同期両方のflavorがある。チュートリアル読んでね。
kadoyau.icon flavor...
実際の詳細は言語ごとに異なる
タイムアウト
クライアント
DEADLINE_EXCEEDEDでRPCが終わるまでの時間を指定できる
サーバー
特定のRPCに対して、次のことがわかる
RPCがタイムアウトしたか
タイムアウトまであと何秒?
デフォルト値があるかは実装依存
kadoyau.iconこれは重要そう
RPC termination
クライアントとサーバは独立して、callの成功を決定できる。一致する保証はない
つまり、サーバーサイドではレスポンスを返したと判定したが、クライアントサイドでは失敗したと判定されたというRPCが存在しうる
キャンセル
クライアントでもサーバーでもRPCはいつでもキャンセルできる
キャンセルすると直ちにRPCが終了する。それ以上何も起きない
キャンセル前の状態にロールバックはしない
kadoyau.icon そりゃそうだよね
メタデータ
特定のRPC callに対する情報(認証情報とか)
key-valueペアのリスト
Metadata is opaque to gRPC itself
kadoyau.iconどういう意味?
クライアントやサーバー間でgRPC callに関係する情報を提供できる
言語によってアクセス方法が違う
チャンネル
gRPC channelは指定されたのhost/portでgRPC serverへのコネクションを提供する。これはクライアントのstubを作成するときに利用される
gRPCのデフォルトの挙動を変更するために、クライアントはチャンネルの引数を指定することができる
メッセージ圧縮のON/OFFの切り替えとかが切り替えられる
channelはconnectedやidleなどを含むstateを持っている
どのようにgRPCがチャンネルを閉じるかは実装依存
言語によってはquerying channel stateが許されている
手を動かしてみる
ここまでで概要は理解できたので、いよいよgRPCに入門していきます
確認環境:macOS 11.0.1
PHPは諦めてGoでやります
準備
code:zsh
# goの入門のプロなので入ってる
$ go version
go version go1.15.6 darwin/amd64
# protocのインストール
$ brew install protobuf
$ protoc --version
libprotoc 3.14.0 # 3以上なのを確認
# 手順に従ってプラグインを入れる
$ export GO111MODULE=on # Enable module mode
$ go get google.golang.org/protobuf/cmd/protoc-gen-go \
google.golang.org/grpc/cmd/protoc-gen-go-grpc
サンプルリポジトリをひっぱってくる
ghqを使ってリポジトリをひっぱってきてディレクトリ移動します。ghqはこういうときものすごく便利なのでおすすめです。引っ張ってくるディレクトリを考えたりしたくないです。 code:zsh
実行する
https://gyazo.com/0707355944bc4e595cfe9f5bf85016a7
左がサーバーで、右がクライアントです
kadoyau.icon クライアントを動かすとサーバがworldを受け取って、クライアントにHello worldを返しているような動作をしている
gRPC serviceにRPCを追加してみる
サンプルコードhelloworld/helloworld.protoをエディタで開く
yak shavingのコーナー:IntelliJで開くとprotobufのpluginのインストールを促されるので入れた
kadoyau.icon helloworld/helloworld.pb.goなどserviceをコンパイルして生成されたコードではサーバー、クライアントの両方がrpc SayHello(HelloRequest) returns (HelloReply)を持っていることがわかる
SayHelloAgain()を追加したいので、serviceに追加する
code:hellowold.proto
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
// rpcを追加
rpc SayHelloAgain (HelloRequest) returns (HelloReply) {}
}
コンパイルする
code:compile
$ protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
helloworld/helloworld.proto
するとgoのファイルが2種類生成されます
helloworld/helloworld.pb.go
helloworld/helloworld_grpc.pb.g
kadoyau.iconたしかにClientに新たに追加した部分が生成されているみたい。これがクライアントなのだろう。
https://gyazo.com/b4dbc29881f2d2795f175feadf731ac7
yak shavingのコーナー:コードを見てみようとしてIntelliJ IDEAを最新にすると、IntelliJ IDEAのGo pluginが動かない
https://gyazo.com/6cd2c77150c54ab7934197e98901a0fc
車両進入禁止を主張するGopher
サーバーサイドの実装を追加
code:greeter_server/main.go
// もとからある実装
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
log.Printf("Received: %v", in.GetName())
return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}
// SayHelloAgainの実装を追加
func (s *server) SayHelloAgain(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
return &pb.HelloReply{Message: "Hello again " + in.GetName()}, nil
}
kadoyau.icon gRPC serviceに追加したrpc SayHelloAgain (HelloRequest) returns (HelloReply) {}のサーバサイド実装をここに追加した
クライアントサイドでstubの呼び出しを追加する
code:greeter_client/main.go
// もともとある実装
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.GetMessage())
// 末尾にSayHelloAgainの呼び出しを追加
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())
動かす
kadoyau.icon 単に文字列出力を呼び出しを追加したので、Hello worldにつづいてHello again woldが表示されるはず
https://gyazo.com/8acf399f1fc2afbb9c701b68cdbc8e99
というわけでざっくりとgRPCを触ってきました。
このチュートリアルはUnary RPCなのでかなり基本的でした
ここまででまだわかっていないが、本格導入にあたって知りたいこと
Streamingを前提にしたアプリケーションのアーキテクチャのパターンが知りたい
APIドキュメントを自動生成したい
開発中にAPIを叩きたくなったらどうするの?
自動生成したクライアントでAPIを叩いて回る結合テストがしたい(サーバサイドの実装漏れをなるべく自動検知したい)
evansのドキュメントを読んだらまさにそれっぽいことが書いてあったのでつかえるらしい
If you want to keep your product quality, you must use CI with gRPC testing, should not do use manual testing.
蛇足:遊んでみる
サーバーを多重起動させてみる→同じportではたてることができない
https://gyazo.com/ee03a91be37433606f2a32a196c22629
あとがき
一緒にやっていくぞ的なノリをだしたくて実況中継方式(最近だとぴよぴーよ速報のひよこさんの怒涛のツッコミとかが好きです)をやってみましたが、ノイズとのトレードオフなのでこの形式はアドカレ以外ではやりづらいですね(なぜアドカレなら許されると思った?)