c4se記:さっちゃんですよ☆

.。oO(さっちゃんですよヾ(〃l _ l)ノ゙☆)

.。oO(此のblogは、主に音樂考察Programming に分類されますよ。ヾ(〃l _ l)ノ゙♬♪♡)

音樂は SoundCloud に公開中です。

考察は現在は主に Cosense で公表中です。

Programming は GitHub で開發中です。

作ってよかった graceful shutdown ライブラリ

qiita.comの 12/12 (火) です。

去る 12/2 (土) に Go Conference mini 2023 Winter IN KYOTO で「作ってよかったgraceful shutdownライブラリ」と云ふ事を喋りました。

event report は以下。 scrapbox.io

ここでは喋った事を紹介します。

speakerdeck.com

はてなでは 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 を作るかしてゐさうだと感じました。

見附からなければ作れば宜しいのですから、作りました。

github.com

先程の例は、かうなります。複數の 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 の更新は大抵は既に運用してますからね

よかったですね。

Erlang/OTP みたいな application 管理を實裝したくなりますが、抑へてをります (ぉ。

勿論 feedback を受け附けてをります。