はじめに
この記事はGo 3 Advent Calendar 2020の21日目です。
gRPCのメトリクスを取得するためにPrometheusを使う際にGraceful Shutdown を行う際の例になります。
やりたいことは「どちらかのサーバでエラーや特定のシグナルが発生した場合すべてのサーバを安全に停止する」です。
注意
Goで作るgRPCのServerでのメトリクス取得が対象です。
要約
今回はrunを使ってGraceful Shutdownを作ります。
errgroupという便利なパッケージもあります。
ざっくりとした背景
gRPCのメトリクスを取得しようとした際、go-grpc-prometheusにならって
以下のようにgPRCのHTTP2なサーバとPrometheus用のHTTPなサーバの2つをたてると思います。こちらから抜粋
// メトリクス用のサーバを別のgoroutineで起動
go func() {
if err := httpServer.ListenAndServe(); err != nil {
log.Fatal("Unable to start a http server.")
}
}()
// gRPC用のサーバを起動
log.Fatal(grpcServer.Serve(lis))
ただ、上記のexampleには「Graceful Shutdownを実装していないから本番ではつかわないでね」のような注意書き
があります。
1つのサーバ起動でのGraceful Shutdownはいろんな参考になる例が存在しますが、2つのサーバをGraceful Shutdownする方法があまりわからなかったところ、社内で聞いた情報を忘れないように記事にしようと思いました。
Graceful Shutdownのおさらい
特定のsignalを受け取った場合などに以下のメソッドを呼び出してサーバが正常に終了します。
- net/httpの場合
Server.Shutdown - grpcの場合
Server.GracefulStop
runを使ってGraceful Shutdown
errgroupと似ていますが、Group.Add()で実行の関数と割り込み時の関数をGroupに追加して、Group.Run()でgoroutineによって実行の関数が実行され、
実行中の関数が終了した場合にすべての割り込み時の関数を呼び出す流れになります。
実行と割り込みを別で管理できるので個人的に便利だと思います。
詳しくはREADMEをご確認ください
今回の例では以下のようになると思います。
var g run.Group
{
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGTERM, os.Interrupt)
cancel := make(chan struct{})
g.Add(
func() error {
select {
case <-c:
log.Println("Received signal, exiting gracefully...")
case <-cancel:
}
return nil
},
func(err error) {
close(cancel)
},
)
}
{
g.Add(
func() error {
if err := httpServer.ListenAndServe(); err != http.ErrServerClosed {
log.Printf("Failed to serve http server: %v", err)
return err
}
return nil
},
func(err error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := httpServer.Shutdown(ctx); err != nil {
log.Printf("Failed to shutdown http server: %v", err)
}
},
)
}
{
g.Add(
func() error {
listener, err := net.Listen("tcp", ":9093")
if err != nil {
log.Printf("Failed to listen grpc server: %v", err)
return err
}
if err := server.Serve(listener); err != nil {
log.Printf("Failed to serve grpc server: %v", err)
return err
}
return nil
},
func(err error) {
server.GracefulStop()
},
)
}
if err := g.Run(); err != nil {
log.Fatalf("error: %v", err)
}
Signalを受け取る、HTTPサーバを起動する、gRPCサーバを起動する3つのブロックになります。
runはprometheus でも使われているので参考にしてみてください。
errgroupを使ってGraceful Shutdown
TBD...
まとめ
errgroupやrunを使うことで複数のサーバのGraceful Shutdownが簡単にできるので、参考になればと思います。