Goでgraceful restartをミニマムに書く

前回の記事でgraceful shutdownをミニマムに実装した。その続きでgraceful restartをミニマムに標準パッケージのみで実装してみる。

takayukinakata.hatenablog.com

参考にしたのは以下のパッケージ。

github.com

実装

前回と同様、理解のために最低限の実装を行なっている。エラー処理もところどころ真面目にやっていない。

package main

import (
    "fmt"
    "log"
    "net"
    "net/http"
    "os"
    "os/exec"
    "os/signal"
    "strconv"
    "sync"
    "syscall"
    "time"
)

// indexHandler ...
func indexHandler(w http.ResponseWriter, r *http.Request) {
    time.Sleep(10 * time.Second)
    w.Write([]byte("Hello, middleware!\n"))
}

func main() {
    http.HandleFunc("/", indexHandler)
    srv := &http.Server{Addr: ":8888", Handler: http.DefaultServeMux}

    var wg sync.WaitGroup
    srv.ConnState = func(conn net.Conn, state http.ConnState) {
        switch state {
        case http.StateActive:
            log.Println("StateActive!!!")
            wg.Add(1)
        case http.StateIdle:
            log.Println("StateIdle!!!")
            wg.Done()
        }
    }

    if isMaster() {
        log.Printf("master pid: %d\n", os.Getpid())
        laddr, _ := net.ResolveTCPAddr("tcp", "localhost:8888")
        l, _ := net.ListenTCP("tcp", laddr)
        log.Println(l)
        supervise(l)
    }

    log.Printf("worker pid: %d\n", os.Getpid())
    fdStr := os.Getenv("__MASTER__")
    fd, _ := strconv.Atoi(fdStr)
    file := os.NewFile(uintptr(fd), "listen socket")
    defer file.Close()
    l, _ := net.FileListener(file)
    waitSignal(l)
    err := srv.Serve(l)
    wg.Wait()
    if err != nil {
        log.Fatal(err)
    }
}

func waitSignal(l net.Listener) {
    c := make(chan os.Signal, 1)
    signal.Notify(c, syscall.SIGINT)
    go func() {
        sig := <-c
        switch sig {
        case syscall.SIGINT:
            signal.Stop(c)
            l.Close()
        }
    }()
}

func isMaster() bool {
    return os.Getenv("__MASTER__") == ""
}

func supervise(l *net.TCPListener) {
    p, _ := forkExec(l)
    c := make(chan os.Signal, 1)
    signal.Notify(c, syscall.SIGUSR2)
    for {
        switch sig := <-c; sig {
        case syscall.SIGUSR2:
            new, _ := forkExec(l)
            p.Signal(syscall.SIGINT)
            p.Wait()
            p = new
        }
    }
}

func forkExec(l *net.TCPListener) (*os.Process, error) {
    progName, err := exec.LookPath(os.Args[0])
    if err != nil {
        return nil, err
    }
    pwd, err := os.Getwd()
    if err != nil {
        return nil, err
    }
    f, err := l.File()
    if err != nil {
        return nil, err
    }
    defer f.Close()
    files := []*os.File{os.Stdin, os.Stdout, os.Stderr, f}
    fdEnv := fmt.Sprintf("%s=%d", "__MASTER__", len(files)-1)
    return os.StartProcess(progName, os.Args, &os.ProcAttr{
        Dir:   pwd,
        Env:   append(os.Environ(), fdEnv),
        Files: files,
    })
}

動作確認すると、

// server
$ go run test.go // server起動

// handlerを修正: "Hello, middleware!\n" => "Hello, middleware!!!!!!\n"

// client1
$ curl localhost:8888

// server master process にUSR2シグナルを送る
$ kill -USR2 xxxxx

// client2
$ curl localhost:8888

// client1
$ curl localhost:8888
Hello, middleware! // 修正前のレスポンスが返る

// client2
$ curl localhost:8888
Hello, middleware!!!!!! // 修正後のレスポンスが返る

// server
// 停止することなく動いたまま

感想

ポイントは

  • 再起動のシグナルを受け取るmaster processと実際のサーバ処理を行うworker processを用意すること
  • master processは最初に作成したリスナーをworker process作成時に渡し、masterとworkerでリスナーを共有すること
  • master processは再起動のシグナルを受け取ったら新しいworker processを作成してから古いworker processにシグナルを送りgraceful shutdownをさせること

だろうか。ソケットまわりを勉強して「ソケットもファイルも同じインターフェースで処理できる」ということを知っていたが理解はしていなかった。しかし、実際に標準netパッケージのfunc (*TCPListener) Filefunc FileListenerを使用してリスナーとファイルの相互変換を実装したことで少しは理解できたかなと感じた。あ、リスナーとソケットは同じ意味くらいの認識でいます。 また、masterとworkerでソケットを共有するために、func StartProcessをうまく利用する実装方法を知ることができて勉強になった。

なお、masterとworkerでソケットを共有するというのは以下のブログにある図が非常にわかりやすかった。

blog.shibayu36.org