qiita.comの 12/12 (火) です。
去る 12/2 (土) に Go Conference mini 2023 Winter IN KYOTO で「作ってよかったgraceful shutdownライブラリ」と云ふ事を喋りました。
event report は以下。 scrapbox.io
ここでは喋った事を紹介します。
はてなでは Go を (も) 使って application を作ってゐます。或る時「Go の code は記述量が多い」事が話題になりました。その中の一つに、graceful shutdown を實現するのに、似た code を每囘長々と書いてゐる事が擧がりました。そこで graceful shutdown の記述を改善できないか探索する事にしました。
graceful に起動・終了するとはどう云ふ事か? と云ふ一般論は省略しませう。slide には少し書きました。
滿たしたい條件は以下のものです。
- signal を trap し、graceful に終了する
- context.Context で cancel できる
- 終了處理に timeout を設定できる
- 任意の server を扱へる
- batch server 等
- 複數の server を扱へる
- 一部の server が起動に失敗したら、process は graceful に exit 1 する
- parallel に終了處理を行ふ
- net/http server を簡單に扱へる
- 記述を省略できる
- 記述の形が明確である
http.ErrServerClosed
を扱ふのを忘れない
- gRPC server を簡單に扱へる
- 記述を省略できる
- 記述の形が明確である
以下の code で實現します。
package main import ( "context" "errors" "log" "net" "net/http" "os" "os/signal" "sync" "time" "google.golang.org/grpc" ) var shutdownTimeout time.Duration = 10 * time.Second func main() { ctx := context.Background() ctx, stop := signal.NotifyContext(ctx, os.Interrupt) // signal を trap する defer stop() ctx, cancel := context.WithCancelCause(ctx) // net/http の例 srv1 := &http.Server{Addr: ":80"} go func(ctx context.Context) { // parallel に起動する if err := srv1.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { cancel(err) // 起動に失敗したら、異常終了する } }(ctx) // google.golang.org/grpc の例 srv2 := grpc.NewServer() go func(ctx context.Context) { // parallel に起動する listener, err := net.Listen("tcp", ":8000") if err != nil { cancel(err) // 起動に失敗したら、異常終了する return } if err := srv2.Serve(listener); err != nil { cancel(err) // 起動に失敗したら、異常終了する } }(ctx) // 終了処理 <-ctx.Done() if err := context.Cause(ctx); err != nil && !errors.Is(err, context.Canceled) { log.Fatalln(err.Error()) // 起動に失敗したら、異常終了する } ctx, cancelT := context.WithTimeout(context.Background(), shutdownTimeout) // context.Context を新たに生成する。終了處理に timeout を設定する defer cancelT() var wg sync.WaitGroup wg.Add(1) // parallel に終了する go func(ctx context.Context) { defer wg.Done() if err := srv1.Shutdown(ctx); err != nil { log.Println(err.Error()) } }(ctx) wg.Add(1) // parallel に終了する go func(ctx context.Context) { defer wg.Done() stopped := make(chan struct{}) go func() { srv2.GracefulStop() close(stopped) }() select { case <-stopped: case <-ctx.Done(): } }(ctx) wg.Wait() if err := context.Cause(ctx); err != nil && !errors.Is(err, context.Canceled) { log.Fatalln(err.Error()) // timeout したら異常終了する } }
…今見たら、timeout せずにしかし終了處理が error を返した場合に exit 0 してゐますね。まぁ結果的に作った library では、正しく exit 1 しますから氣にしない。
まづ既存の library を探しました。しかし設計が古かったり、net/http server 專用であったり、單一の server した扱へなかったりと、條件を充分に滿たすものは見附かりませんでした。先日話して、皆さん 1. graceful な shutdown を氣にしないか、2. 手書きするか、3. closed source な framework を作るかしてゐさうだと感じました。
見附からなければ作れば宜しいのですから、作りました。
先程の例は、かうなります。複數の server を扱ふので少し冗長ですが。
package main import ( "context" "errors" "fmt" "log" "net/http" "time" "github.com/ne-sachirou/go-graceful" "github.com/ne-sachirou/go-graceful/gracefulhttp" "github.com/ne-sachirou/go-graceful/gracefulgrpc" "google.golang.org/grpc" ) func main() { ctx := context.Background() srv := graceful.Servers{ Servers: []graceful.Server{ &gracefulhttp.Server{Server: &http.Server{Addr: ":80"}}, &gracefulgrpc.Server{Addr: ":8000", Server: grpc.NewServer()}, }, } if err := srv.Graceful(ctx, graceful.GracefulShutdownTimeout(10 * time.Second)); err != nil { log.Fatalln(err.Error()) } }
大分短く成りました。
Mackerel の一部 system に導入した結果も slide に書きました。
同じ library を使ふ利點は、改善が容易くなる事です。
- graceful shutdown の仕組みを作る者は、
- この library を議論の場にできる
- この library を改善すれば、改善した結果を波及させ易い
- この library に依存する者は、
- コピペする boilerplate が減る
- コピペする code がよいものであるか否か調査する手閒が減る
- コピペを續けても差異が產まれづらい
- 依存 library の更新と、graceful shutdown の仕組みの改善を同一視できる。依存 library の更新は大抵は既に運用してますからね
- コピペする boilerplate が減る
よかったですね。
Erlang/OTP みたいな application 管理を實裝したくなりますが、抑へてをります (ぉ。
勿論 feedback を受け附けてをります。