こんにちは、技術戦略部 SREグループのカンタンです。
MoTが提供しているサービスを成長させるために様々なマイクロサービスを次から次と開発しています。マイクロサービスの増加に伴って全体のシステムが複雑になり、前回の記事で話た共通ログフォーマットも含めて様々な機能の共通化がとても大事になってきています。
API開発のハードルを下げるため、サービス間の通信をgRPCに統一しようとしていて、本記事ではMoTで採用しているProtocol Buffers (PB)の一元管理方法を紹介させていただきます。
マイクロサービス間のAPI通信をgRPCに統一しようとしている理由がいくつかあります:
gRPCのデメリットもいくつかあります:
全体的にgRPCのメリットがデメリットを大きく上回っているため、MoTではマイクロサービス間の通信をgRPCに統一しようとしています。現在はGolang、Python、Ruby、ScalaなどのgRPCサービスが十数個稼働しています。
gRPCサーバを作るのに必要なものを説明していきたいと思います。説明のためGolangを使いますがgRPCを対応している言語であればどの言語でも同じような流れになります。
(1) .proto ファイルを作成します。例えば下記のように SayHello というメソッドを持っている HelloWorldService サービスを定義します。PBの書き方はこちらにご参考ください。
syntax = "proto3";
// Package
package mypackage;
option go_package = "github.com/MyOrg/myrepo";
// サービス
service HelloWorldService {
// SayHello API: 引数としてもらったユーザ名に挨拶する
rpc SayHello(SayHelloRequest) returns (SayHelloResponse);
}
// SayHello APIのリクエスト
message SayHelloRequest {
// ユーザ名
string name = 1;
}
// SayHello APIのレスポンス
message SayHelloResponse {
// 挨拶メッセージ
string message = 1;
}
(2) protocというツールを使って、 .proto ファイルからコードを自動生成します。Macの場合はbrewでインストールできます。
# protoc
brew install protobuf
# protoc-gen-go: Go protocol buffers plugin
# protoc-gen-go-grpc: Go protocol buffers gRPC plugin
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
# protocがprotoc-gen-goを見つけるため、GOPATHの設定が必要
export GOPATH=... # 必要に応じて、設定する
export PATH=$GOPATH/bin:$PATH
# フォルダー準備
mkdir output
# .protoファイルからGoコードを生成する
protoc \
-I./ \
--go_out=./output \
--go-grpc_out=./output \
--go-grpc_opt=require_unimplemented_servers=false \
./hello_world_service.proto
# 成果物確認
$ tree
.
├── hello_world_service.proto
└── output
└── github.com
└── MyOrg
└── myrepo
├── hello_world_service.pb.go
└── hello_world_service_grpc.pb.go
(3) 自動生成されたコードを使ってgRPCサーバを実装します。
cd prototest
# フォルダー構造
$ tree
├── go.mod
├── go.sum
├── main.go
└── pb # 生成されたコードをこのフォルダーに置く
├── hello_world_service.pb.go
└── hello_world_service_grpc.pb.go
package main
import (
"context"
"fmt"
"net"
"github.com/labstack/gommon/log"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
pb "github.com/MobilityTechnologies/prototest/pb"
)
// --------------------
// HelloWorldServiceServerの実装
// --------------------
type HelloWorldServiceServer struct {}
func (s *HelloWorldServiceServer) SayHello(ctx context.Context, request *pb.SayHelloRequest) (*pb.SayHelloResponse, error) {
name := request.GetName()
message := fmt.Sprintf("Hello %s!", name)
return &pb.SayHelloResponse{
Message: message,
}, nil
}
func newServer() *HelloWorldServiceServer {
return &HelloWorldServiceServer{}
}
// --------------------
// gRPCサーバを起動する
// --------------------
func main() {
port := 50052
listen, err := net.Listen("tcp", fmt.Sprintf(":%v", port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// Server
server := grpc.NewServer()
// Register HelloWorldService
pb.RegisterHelloWorldServiceServer(server, newServer())
// Add server reflection
reflection.Register(server)
log.Infof("listening on port %v ...", port)
if err := server.Serve(listen); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
(4) サーバを起動して、grpcurlを使って動作確認します。
# サーバをビルドして起動する
$ go build && ./prototest
{"time":"2021-11-30T10:58:06.853145+09:00","level":"INFO","prefix":"-","file":"main.go","line":"57","message":"listening on port 50052 ..."}
$ grpcurl -plaintext localhost:50052 list 156ms Tue Nov 30 11:00:54 2021
grpc.reflection.v1alpha.ServerReflection
mypackage.HelloWorldService
$ grpcurl -plaintext -d '{"name": "John"}' localhost:50052 mypackage.HelloWorldService/SayHello Tue Nov 30 11:06:06 2021
{
"message": "Hello John!"
}
API仕様が定義されている.proto Protocol Buffersファイルを管理する時にいくつか気をつけないことがあります:
上記のことを頭の片隅に置いてPB管理のいくつかのアプローチを比較してみたいと思います。
このアプローチでは .proto ファイルをサーバ側で定義して管理します。サーバとクライアントはそれぞれ .proto ファイルから自分の言語のコードを生成します。
メリット
デメリット
このアプローチでは .proto ファイルの管理とコード生成を全てサーバリポジトリで行います。サーバは全てのクライアントの言語でコードを生成します。
メリット
デメリット
このアプローチでは、全てのサービスの .proto ファイルを同じリポジトリでmonorepo形式で一元管理します。
メリット
デメリット
MoTでは上記の(C)アプローチを採用してPBをmonorepoで一元管理しています。仕組みを作った時に一番参考になった「How We Build gRPC Services At Namely」という優秀な記事はおすすめです。
protorepo というGitHubリポジトリで全てのサービスの .proto ファイルを管理しています。リポジトリ構造は以下のようになっています。
.
├── .ci --> CI用の設定
│ ├── run.sh
├── CODEOWNERS
├── Makefile
├── README.md
├── docs --> 自動生成されたDocumentationの保存フォルダー
│ ├── pb_md
│ │ ├── business.md
│ │ ├── core.md
│ │ ├── delivery.md
│ │ └── payment.md
│ ├── pb_html
│ │ ├── business.html
│ │ ├── core.html
│ │ ├── delivery.html
│ │ └── payment.html
│ └── swagger
│ ├── business.html
│ ├── core.html
│ ├── delivery.html
│ └── payment.html
├── motpb --> .protoファイルの保存フォルダー
│ ├── business
│ │ ├── .protolangs
│ │ ├── package.json
│ │ ├── file1.proto
│ │ └── file2.proto
│ ├── core
│ │ ├── .protolangs
│ │ ├── package.json
│ │ ├── coordinates.proto
│ │ ├── money.proto
│ │ └── package.json
│ ├── delivery
│ │ ├── .protolangs
│ │ ├── package.json
│ │ ├── file1.proto
│ │ ├── file2.proto
│ │ └── file3.proto
│ └── payment
│ ├── .protolangs
│ ├── package.json
│ ├── file1.proto
│ ├── file2.proto
│ └── file3.proto
└── prototool.yaml --> lintingルール
motpb フォルダーの下にパッケージごとにフォルダーを分けて .protoファイルを保存しています。パッケージフォルダーの中に .protolangs と package.json という2つの特別ファイルを保存しています:
ruby
go
scala
{
"name": "delivery",
"version": "2.0.0",
"description": "Delivery Protocol Buffers",
"dependencies": {
"core": "^1.3.0"
}
}
docs フォルダーにPBから生成された様々な形式のDocumentationを保存しています (Markdown, HTML, Swagger/Open API)。
PBからのコード生成はCIで行うようにしています。そうすることで、開発者が何もインストールせずにGitHubにプルリクエストを出して、レビューしてもらって、マージすることだけで必要な成果物が生成されて利用可能になります。
CI処理の流れは以下のようになります。
PBから生成されたコードは言語ごとに別のリポジトリに保存されていてます。生成時にタグを付けることで生成されたコードのバージョン管理も行っています。生成されたコードをライブラリとして取得できるため、サーバとクライアント側での利用も楽です。
補足
├── README.md
├── business
│ ├── file1.pb.go
│ ├── file2.pb.go
│ ├── go.mod
│ └── go.sum
├── core
│ ├── coordinates.pb.go
│ ├── go.mod
│ ├── go.sum
│ └── money.pb.go
├── delivery
│ ├── file1.pb.go
│ ├── file2.pb.go
│ ├── file3.pb.go
│ ├── go.mod
│ └── go.sum
└── payment
├── file1.pb.go
├── file2.pb.go
├── file3.pb.go
├── go.mod
└── go.sum
module github.com/MobilityTechnologies/protorepo-go-artifacts/delivery
go 1.17
require (
github.com/MobilityTechnologies/protorepo-go-artifacts/core v1.3.0
google.golang.org/grpc v1.42.0
google.golang.org/protobuf v1.27.1
)
MoTがgRPCを採用し始めた時と合わせてこの仕組みを用意していて、2年以上の実績があります。いくつか調整が必要でしたが全体的に満足しています。
メリットの方が圧倒的に多いですが、デメリットもいくつかあります
MoTのProtocol Buffersの管理方法を紹介させていただきました。gRPCとProtocol Buffersを使うことで開発の効率が上がってビジネスロジックに集中できます。様々なサービスのAPI仕様を一元管理することで、悩む時間が減って設計ミスが防ぎやすくなっていてシステム全体の品質も上がっています。
gRPCの導入を検討されている方、またはPBの管理方法に悩んでいる方へ、この記事がご参考になれば幸いです!
興味のある方は 採用ページ も見ていただけると嬉しいです。
Twitter @mot_techtalk のフォローもよろしくお願いします!