ふわっとテック日記

テック系のことをメインに書いていきます。go言語/typescript/javascriptが好きです。たまに趣味(筋トレ)の話や日常系の記事も書きたいな〜と思っています。

gRPC触ってみた(2)

前回記事: gRPC触ってみた


今回は、grpcのサーバ実装で様々なオプションを付与することができるServerOptionについて触れていきます。

ServerOption

grpcのサーバ側で使用するgrpc.Server構造体は、以下の関数を利用して初期化されます。

func NewServer(opt ...ServerOption) *Server {
    ....
}

引数に任意の数のServerOptionインターフェースを満たす構造体を渡すことで、credentialsやcodec、keepaliveを始めとして、様々な挙動をサーバー処理の前後に入れ込むことができます。

UnaryInterceptor / ChainUnaryInterceptor

例えば下記のUnaryInterceptor関数ではサーバ処理にフックを付与するUnaryServerInterceptorを引数に渡すことで、ServerOptionを返します。

これをNewServerに渡すことでUnaryServerInterceptorで定義した処理をリクエスト受け取り時に実行できるようになります。

grpc package - google.golang.org/grpc - Go Packages

こちらがUnaryServerInterceptorの定義です。

引数のhandler(UnaryHandler)はサービスメソッドのラッパーであり、UnaryServerInterceptor内で必ず実行する必要があります。

grpc package - google.golang.org/grpc - Go Packages

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を設定できます。

grpc package - google.golang.org/grpc - Go Packages

以下、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を返します。

grpc package - google.golang.org/grpc - Go Packages

サーバが返そうとするデータサイズが指定バイト数よりも大きい場合、以下のようなエラーレスポンスを返します。

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型で表現されます。

codes package - google.golang.org/grpc/codes - Go Packages

サーバ側で任意のステータスコードを返したい時はこの中の定数から指定してあげれば良いでしょう。

HTTPのステータスコードとは異なるので注意してください。




今回は2例のみ挙げましたが、ServerOptionを設定できる関数は他にも色々あります。

割と柔軟な設定ができるので今後もいろいろと触っていきたいなーと思ってます。

おまけ: go-grpc-middleware

GitHub - grpc-ecosystem/go-grpc-middleware: Golang gRPC Middlewares: interceptor chaining, auth, logging, retries and more.

こちらのパッケージを使用することで各種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
    }
}