※前回記事:
今回は、grpcのサーバ実装で様々なオプションを付与することができるServerOptionについて触れていきます。
ServerOption
grpcのサーバ側で使用するgrpc.Server構造体は、以下の関数を利用して初期化されます。
func NewServer(opt ...ServerOption) *Server {
....
}
引数に任意の数のServerOptionインターフェースを満たす構造体を渡すことで、credentialsやcodec、keepaliveを始めとして、様々な挙動をサーバー処理の前後に入れ込むことができます。
UnaryInterceptor / ChainUnaryInterceptor
例えば下記のUnaryInterceptor
関数ではサーバ処理にフックを付与するUnaryServerInterceptor
を引数に渡すことで、ServerOptionを返します。
これをNewServerに渡すことでUnaryServerInterceptorで定義した処理をリクエスト受け取り時に実行できるようになります。
こちらがUnaryServerInterceptorの定義です。
引数のhandler(UnaryHandler
)はサービスメソッドのラッパーであり、UnaryServerInterceptor内で必ず実行する必要があります。
UnaryInterceptorの実装例です。
func main() {
....
grpcServer := grpc.NewServer(
grpc.UnaryInterceptor(
logging(),
),
)
....
}
func logging() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
log.Printf("サーバ処理前のログ")
resp, err = handler(ctx, req) // handlerの実行は必須です。ここが本来のサーバ処理を実行しています。
if err != nil {
return nil, err
}
log.Printf("サーバ処理後のログ")
return resp, nil
}
}
logging()関数で返しているUnaryServerInterceptor内で、本来のサーバ処理に加えてlog.Printfでログ出し処理を追加しています。
UnaryInterceptor関数ではUnaryServerInterceptorを1つしか指定できませんが、ChainUnaryInterceptor
を使用すれば複数のUnaryServerInterceptorを設定できます。
以下、ChainUnaryInterceptorの実装例です。
func main() {
....
grpcServer := grpc.NewServer(
grpc.ChainUnaryInterceptor(
logging1(),
logging2(),
),
)
....
}
func logging1() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
log.Printf("サーバ処理前のログ1")
resp, err = handler(ctx, req)
if err != nil {
return nil, err
}
log.Printf("サーバ処理後のログ1")
return resp, nil
}
}
func logging2() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
log.Printf("サーバ処理前のログ2")
resp, err = handler(ctx, req)
if err != nil {
return nil, err
}
log.Printf("サーバ処理後のログ2")
return resp, nil
}
}
ChainUnaryInterceptorで指定する複数のUnaryServerInterceptorは、最初に指定したものがラッパーの一番外側、最後が一番内側になります。
なので上記の例だと、処理の順番は
ログ出力「サーバ処理前のログ1」
↓
ログ出力「サーバ処理前のログ2」
↓
サーバ処理
↓
ログ出力「サーバ処理後のログ2」
↓
ログ出力「サーバ処理後のログ1」
となります。
MaxSendMsgSize
MaxSendMsgSize
関数は、サーバが返すデータの最大バイト数を指定するServerOptionを返します。
サーバが返そうとするデータサイズが指定バイト数よりも大きい場合、以下のようなエラーレスポンスを返します。
2022/11/03 14:48:13 rpc error: code = ResourceExhausted desc = grpc: trying to send message larger than max (109 vs. 1)
上記ではエラーコードがResourceExhausted
となっていますが、grpcのステータスコードは以下のように、uint32のエイリアスであるCode型で表現されます。
サーバ側で任意のステータスコードを返したい時はこの中の定数から指定してあげれば良いでしょう。
HTTPのステータスコードとは異なるので注意してください。
今回は2例のみ挙げましたが、ServerOptionを設定できる関数は他にも色々あります。
割と柔軟な設定ができるので今後もいろいろと触っていきたいなーと思ってます。
おまけ: go-grpc-middleware
こちらのパッケージを使用することで各種Interceptorの実装が容易になり、お手軽なマイクロサービス構築が可能となります。
以下の例では、複数のUnaryServerInterceptorを1つにまとめることができるChainUnaryServer
を実装しています。
import (
....
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
)
func main() {
....
grpcServer := grpc.NewServer(
grpc.UnaryInterceptor(
grpc_middleware.ChainUnaryServer(
logging1(),
logging2(),
),
),
)
....
}
func logging1() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
log.Printf("サーバ処理前のログ1")
resp, err = handler(ctx, req)
if err != nil {
return nil, err
}
log.Printf("サーバ処理後のログ1")
return resp, nil
}
}
func logging2() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
log.Printf("サーバ処理前のログ2")
resp, err = handler(ctx, req)
if err != nil {
return nil, err
}
log.Printf("サーバ処理後のログ2")
return resp, nil
}
}
コメント