最近流行りのgRPC.
スキーマ言語であるProtocol Buffersによってデータをシリアライズ化する事ができ、
これまで主流だったjson通信よりもより高速に通信を行えるとして、
マイクロサービス開発で採用されるケースが増えている。
この間gRPCで構築したクライアント・サーバー間の通信をやろうとした時にタイトルの通りのバグが発生し
だいぶ手こずったので、忘備録として原因と解決策を残しておこうと思う。
ソースコード
ソースはこちら。
https://github.com/yzmw1213/PostService
やろうとした事
goで書いたgRPCサーバーで、以下2つのサービスを実装する。
- 投稿サービス
- 投稿につけるタグの管理サービス(マスタデータとしての扱い)
そして、typescriptで実装している client側のコードから、上のサービスにリクエストを送り処理を行う。
import { Tag, CreateTagRequest } from "~/grpc/tag_pb"
import { TagServiceClient } from "~/grpc/TagServiceClientPb"
post() {
const client = new TagServiceClient(
"http://localhost:8080", {}, {}
)
const request = new CreateTagRequest()
var tag = new Tag()
tag.setTagId(postTag.tagID)
tag.setTagName(postTag.tagName)
tag.setStatus(postTag.status)
request.setTag(tag)
// TagServiceのcreateTagメソッドにリクエストを送る
client.createTag(request, {}, (err, res) => {
if (err != null) {
console.log(err)
}
console.log(res)
})
}
上記のようにして、クライアント側からタグサービスにタグ作成のリクエストを行った際、次のエラーが起こった。
{ code: 12, message: "unknown service post_grpc.TagService" }
解決策
code: 12 は何の事や...と思って公式のgitを見てみた。
すると、
// HTTP Mapping: 501 Not Implemented
UNIMPLEMENTED = 12;
要するに、
「そのサービス、実装されてないで」 っていう意味の内容だった。
そこで、呼び出しを行っているサービスがgRPCサーバーに登録されているかどうか確認する。
grpcサーバーの動作確認にはgrpcurlを使うといい。
この記事に色々詳しい事が書かれていた。
https://qiita.com/yukina-ge/items/a84693f01f3f0edba482
例えばポート50051番でgRPCサーバーを構築しているとすると、以下のように叩くと良い。
# ポートに登録されているサービスの一覧
$ grpcurl -plaintext localhost:50051 list
grpc.reflection.v1alpha.ServerReflection
post_grpc.PostService
ServerReflectionとPostServiceは登録されているようだ。
あれ、じゃあTagServiceは...と思ってサーバー側のコード読んだら明らかなミスに気がついた。
package grpc
import (
"fmt"
"log"
"net"
"os"
"os/signal"
"github.com/yzmw1213/PostService/grpc/post_grpc"
"github.com/yzmw1213/PostService/usecase/interactor"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
type server struct {
PostUsecase interactor.PostInteractor
TagUsecase interactor.TagInteractor
}
// NewPostGrpcServer gRPCサーバー起動
func NewPostGrpcServer() {
lis, err := net.Listen("tcp", "0.0.0.0:50051")
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
server := &server{}
s := makeServer()
// PostServiceを serverに登録
post_grpc.RegisterPostServiceServer(s, server)
// TagServiceの登録が抜けている!!!
// Register reflection service on gRPC server.
reflection.Register(s)
log.Println("main grpc server has started")
go func() {
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}()
ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt)
// Block until a sgnal is received
<-ch
fmt.Println("Stopping the server")
s.Stop()
fmt.Println("Closing the client")
lis.Close()
fmt.Println("End of Program")
}
func makeServer() *grpc.Server {
s := grpc.NewServer(
grpc.UnaryInterceptor(grpc.UnaryServerInterceptor(transmitStatusInterceptor)),
)
return s
}
post_grpc.RegisterPostServiceServer で、PostServiceを登録はしているが
TagServiceも同様にgRPCサーバーに登録しなければならない。
次のコードを追加し、解決した。
// PostServiceを serverに登録
post_grpc.RegisterPostServiceServer(s, server)
// 以下を追加
// タグサービス登録
post_grpc.RegisterTagServiceServer(s, server)
振り返り
今回、原因の特定にかなり時間を費やしてしまった。
clientとserverの間にenvoy Proxyを置いており、proxyの方に気を取られまくっていたので。。
悩んであれこれ試した割にはだいぶ初歩的なミスだった...。
grpcurl、これからは初手から使っていこう。
補足
マイクロサービス運用を想定しているgRPCサーバーの構築では、
1 service / 1 server の前提で書かれている記事が数多い印象があるが、必ずしも
全てのサービス毎に細くサーバーを分ける分ける必要もなく、上で実装しているように
それぞれRegisterServiceすれば運用に支障は無いと思っている。
関連性の高いサービス(たとえば、ユーザー登録サービスと認証サービスとか)は
この様に同一サーバーでの運用することが十分可能だと思う。
今後、認証サービスも書く予定なので試してみようと思う。