Shogo's Blog

May 3, 2015 - 3 minute read - go golang

Go言語でGraceful Restartをする

とあるHTTPサーバをGolangで立てようって話になったんだけど、 止まると困るので無停止でサーバ再起動をしたい。 PerlにはServer::Starterという有名モジュールがあるんだけど、 Golangはどうなってるの?ってことで調べてみました。

2017-01-22追記: Go1.8以降でGraceful Shutdownがbuild-inになるので、この記事で紹介したライブラリは不要となりました。 詳しくはGo1.8のGraceful Shutdownとgo-gracedownの対応を参照。

gracefulじゃないバージョン

Golangの標準ライブラリを使ってHTTPサーバを立ててみる例。 レスポンスが一瞬で終わってしまうとよくわからないので、sleepするhandlerを追加しておきます。

package main

import (
	"fmt"
	"log"
	"net/http"
	"os"
	"time"
)

var now = time.Now()

func main() {
	log.Printf("start pid %d\n", os.Getpid())
	s := &http.Server{Addr: ":8080", Handler: newHandler()}
	s.ListenAndServe()
}

// https://github.com/facebookgo/grace/blob/master/gracedemo/demo.go から一部拝借
func newHandler() http.Handler {
	mux := http.NewServeMux()
	mux.HandleFunc("/sleep/", func(w http.ResponseWriter, r *http.Request) {
		duration, err := time.ParseDuration(r.FormValue("duration"))
		if err != nil {
			http.Error(w, err.Error(), 400)
			return
		}
		time.Sleep(duration)
		fmt.Fprintf(
			w,
			"started at %s slept for %d nanoseconds from pid %d.\n",
			now,
			duration.Nanoseconds(),
			os.Getpid(),
		)
	})
	return mux
}

以下のような感じで実行してみる。 (それぞれのコマンドは処理が終わるまでブロックするので、コンソールを3つ程開いて実行してね!)

$ go run main.go
2015/05/03 12:04:08 start pid 69046
$ curl 'http://localhost:8080/sleep/?duration=20s'
$ kill -TERM 69046

curlからのリクエストをさばく前に終了してしまい curl: (52) Empty reply from server といわれてしまいます。

facebookgo/grace

facebook製のgraceは gracefulな終了と再起動をしてくれるライブラリ。

package main

import (
	"fmt"
	"log"
	"net/http"
	"os"
	"time"

	"github.com/facebookgo/grace/gracehttp"
)

var now = time.Now()

func main() {
	log.Printf("start pid %d\n", os.Getpid())
	s := &http.Server{Addr: ":8080", Handler: newHandler()}
	// s.ListenAndServe()
	gracehttp.Serve(s)
}

// newHanderは一緒なので、以下省略。適当に補完して実行して

さっきと同じように実行してみるとリクエストを捌き切るまで終了しなくなります。

$ go run main.go
2015/05/03 12:04:08 start pid 69046
2015/05/03 12:04:08 Serving [::]:8080 with pid 69046
$ curl 'http://localhost:8080/sleep/?duration=20s'
started at 2015-05-04 12:04:08.562569712 +0900 JST slept for 20000000000 nanoseconds from pid
$ kill -TERM 69046

TERMの代わりにUSR2シグナルを送るとgracefulに再起動できる。 ただ、再起動すると最初のプロセスは死んでしまうので、daemontoolsみたいなデーモン管理ツールと一緒には使えない。 そのためデーモン化に必要なもろもろ(PID・標準出力・標準エラー等をファイルに書き出す等)は全部自前でやる必要があります。 cmdctrlを使うとそこら辺の処理をやってくれる。

package main

import (
	"fmt"
	"log"
	"net/http"
	"os"
	"time"

	"github.com/facebookgo/cmdctrl"
	"github.com/facebookgo/grace/gracehttp"
)

var now = time.Now()

func main() {
	cmdctrl.SimpleStart()

	log.Printf("start pid %d\n", os.Getpid())
	s := &http.Server{Addr: ":8080", Handler: newHandler()}
	gracehttp.Serve(s)
}

// newHanderは一緒なので、以下省略。適当に補完して実行して
$ go run main.go -c hoge.conf -pidfile hoge.pid start
2015/05/03 12:04:08 start pid 69046
2015/05/03 12:04:08 Serving [::]:8080 with pid 69046
$ curl 'http://localhost:8080/sleep/?duration=20s'
started at 2015-05-04 12:04:08.562569712 +0900 JST slept for 20000000000 nanoseconds from pid
$ go run main.go stop

ただ、デーモン化はされないみたいなので、実際に使うには他にもいろいろ工夫しないといけないっぽい。

go-server-starter-listener

牧さん作のgo-server-starter-listener。 PerlのServer::Starterと一緒に使える。

package main

import (
	"fmt"
	"log"
	"net"
	"net/http"
	"os"
	"time"

	"github.com/lestrrat/go-server-starter-listener"
)

var now = time.Now()

func main() {
	log.Printf("start pid %d\n", os.Getpid())

	l, err := ss.NewListener()
	if l == nil || err != nil {
		// Fallback if not running under Server::Starter
		l, err = net.Listen("tcp", ":8080")
		if err != nil {
			panic("Failed to listen to port 8080")
		}
	}

	s := &http.Server{Handler: newHandler()}
	s.Serve(l)
}

// newHanderは一緒なので、以下省略。適当に補完して実行して

以下のようにstart_serverコマンドと組み合わせて起動することで、 Server::Starterの管理下で実行されるようになります。

$ start_server --port=8080 ./main
start_server (pid:6941) starting now...
starting new worker 6942
2015/05/03 08:27:54 start pid 6942
$ kill -HUP 6941

ただ、go-server-starter-listener自体はgracefulなシャットダウンに対応していないので、 再起動の途中のコネクションは破棄されてしまいます。 これを避けるにはmannersを使うといいようです。

package main

import (
	"fmt"
	"log"
	"net"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/braintree/manners"
	"github.com/lestrrat/go-server-starter-listener"
)

var now = time.Now()

func main() {
	log.Printf("start pid %d\n", os.Getpid())

	signal_chan := make(chan os.Signal)
	signal.Notify(signal_chan, syscall.SIGTERM)
	go func() {
		for {
			s := <-signal_chan
			if s == syscall.SIGTERM {
				manners.Close()
			}
		}
	}()

	l, err := ss.NewListener()
	if l == nil || err != nil {
		// Fallback if not running under Server::Starter
		l, err = net.Listen("tcp", ":8080")
		if err != nil {
			panic("Failed to listen to port 8080")
		}
	}

	manners.Serve(l, newHandler())
}

// newHanderは一緒なので、以下省略。適当に補完して実行して

manners自体はシグナルの扱いをやってくれないみたいなので、 そこだけ自分で書く必要がありますが、 今回調べた中ではこれがベストっぽい。 自前でデーモン化はできませんが、daemontoolsが使えるのでそれで十分でしょう。

ちなみに、Server::StarterのGo版go-server-starterもあるので、 デーモン化以外はGo化できそう。

2015-05-07 追記

こっち見んな! 作者の方によると、go-server-starter-listenerは非推奨らしいです。 go-server-starter にlistenerも一緒に入っているのでこちらを使います。

package main

import (
	"fmt"
	"log"
	"net"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/braintree/manners"
	"github.com/lestrrat/go-server-starter/listener"
)

var now = time.Now()

func main() {
	log.Printf("start pid %d\n", os.Getpid())

	signal_chan := make(chan os.Signal)
	signal.Notify(signal_chan, syscall.SIGTERM)
	go func() {
		for {
			s := <-signal_chan
			if s == syscall.SIGTERM {
				log.Printf("SIGTERM!!!!\n")
				manners.Close()
			}
		}
	}()

	listeners, err := listener.ListenAll()
	if err != nil {
		panic(err)
	}
	var l net.Listener
	if len(listeners) == 0 {
		// Fallback if not running under Server::Starter
		l, err = net.Listen("tcp", ":8080")
		if err != nil {
			panic("Failed to listen to port 8080")
		}
	} else {
		l = listeners[0]
	}

	manners.Serve(l, newHandler())
}

// newHanderは一緒なので、以下省略。適当に補完して実行して

こっちのほうが複数ポートの読み込みにも対応していて高機能みたいなので、 go-server-starter を使いましょう!

2015-05-09 追記

検証が不十分で、go-server-starterを使った上記のコード Server::Starterから起動されなかった場合のフォールバックが正しく機能しません。 現状では自前でSERVER_STARTER_PORT環境変数が定義されているのを確認するしかなさそうです。 handlename先輩がPRを出しているので、これがマージされたら状況が変わるかも。