読者です 読者をやめる 読者になる 読者になる

オープンソースこねこね

Webプログラミングなどについてあれこれ。

Goでgraceful restartに対応したデーモンプロセスをつくる

Goで書かれたWebアプリのプロセスをデーモンにしたり、ダウンタイムなしでデプロイできるようにするための情報をいろいろ調べていたのですが、どうもSupervisorなど外部のツールを使ったりするのが定番か、herokuなどのPaaSにまかせてしまうという手法が多いようです。 でも個人的にGoの一番気に入っているところは依存のない独立した単一バイナリを生成できる点なので、Webアプリもなるべく外部に依存せずにどうにかできないものかと調査していました。

さてGoのデーモン化とgracefulのライブラリはそれぞれいくつかあるのですが、両方に対応したライブラリは見つからず、組み合わせたサンプルなども見当たらなかったので、各種ライブラリのソースを読んだりしながら、最終的に以下のものを取り上げてみました

この2つを組み合わせてgraceful restartできるデーモンを作ろうとしたわけですが、上記のライブラリではdaemonziseもgracefulも内部的にforkの代わりにos.StartProcessで外部コマンドとして自分自身を再起動し、環境変数を使って状態の制御をおこなうということをやっています。一緒に使うと、その辺が干渉してうまく動かない。。。

で、いろいろワークアラウンドをいれて、ひと通り動くようになったのが以下のコードです。やっていることはコード内のコメントを参照してください。

package main

import (
    "flag"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "os"
    "path/filepath"
    "strconv"
    "time"

    "github.com/VividCortex/godaemon"
    "github.com/facebookgo/grace/gracehttp"
    "github.com/kardianos/osext"
)

var (
    logfile = flag.String("l", "goserver.log", "log file")
    pidfile = flag.String("p", "goserver.pid", "pid file")
)

func main() {
    flag.Parse()

    // os.Args[0]を絶対パスに書き換える。
    // デーモナイズ後、カレントディレクトリが変わるので、
    // 相対パスのままだとgracehttpがos.StartProcessするとき自分自身を指し示すファイルパスを解決できない。
    bin, err := osext.Executable()
    if err != nil {
        log.Fatal(err)
    }
    os.Args[0] = bin

    // pidとlogのファイルのパスも絶対パスにする。
    logfilepath, err := filepath.Abs(*logfile)
    if err != nil {
        log.Fatal(err)
    }
    pidfilepath, err := filepath.Abs(*pidfile)
    if err != nil {
        log.Fatal(err)
    }

    // デーモナイズする
    // "LISTEN_FDS"があるときはgraceful restart時なので、スキップさせる。
    if os.Getenv("LISTEN_FDS") == "" {
        godaemon.MakeDaemon(&godaemon.DaemonAttr{})
    }

    // 以下のロジックはデーモナイズ後の状態で実行される。

    // ログの出力先をファイルに。
    f, err := os.OpenFile(logfilepath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close()
    log.SetOutput(f)

    // pidファイルのプロセスIDを書き込む
    pid := os.Getpid()
    if err = ioutil.WriteFile(pidfilepath, []byte(strconv.Itoa(pid)), 0666); err != nil {
        log.Fatal(err)
    }
    log.Printf("Generated pidfile %s (%d)\n", pidfilepath, pid)

    // WebAppサーバ
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        time.Sleep(5 * time.Second)  // 動作検証用に5秒リクエストをつかみっぱなしにする(restartしても切れないことを見る)
        fmt.Fprintf(w, "hello world!")
    })
    server := &http.Server{Addr: ":1234", Handler: mux}

    // graceful restartをサポートして起動
    gracehttp.Serve(server)
}

github.com/kardianos/osextは実行ファイルの絶対パスを取得するための便利ライブラリです。

これをビルドし起動した後、graceful restartするには-USR2シグナルを送る。

kill -USR2 5678

シャットダウンもリクエストが途中で切れないgraceful shutdownになる。

kill 5678

動作させてみたところうまく動いているようではあるが、os.Args[0]を書き換えたりしてキモいコードになっている。 daemonizeとgracefulをまとめて、ルーティングとmiddlewareまわりの構造を提供してくれる軽量フレームワークがあったらいいなあ、と妄想しています。