Go 1.8 の HTTP Server Graceful Shutdown を試す

Go 1.8 で `net/http` パッケージに `Server.Shutdown` メソッドが追加されます。

シグネチャはこちら:

```go
func (srv *Server) Shutdown(ctx context.Context) error
```

## 使い方

ざっくりと使い方はこんな感じ。

```go
srv := &http.Server{Addr: ":8080"}

// サーバはブロックするので別の goroutine で実行する
go func() {
	if err := srv.ListenAndServe(); err != nil {
		log.Print(err)
	}
}()

// シグナルを待つ
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGTERM)
<-sigCh

// シグナルを受け取ったらShutdown
ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
if err := srv.Shutdown(ctx); err != nil {
	log.Print(err)
}
```

## go-server-starter で試してみる

[go-server-starter](https://github.com/lestrrat/go-server-starter) でプロセスを再起動するようにし、ベンチマークをかけてみます。

### go get

```
go get github.com/lestrrat/go-server-starter/cmd/start_server
```

### コード

末尾に掲載しています。

### run

サーバを起動します。

```
> $GOPATH/bin/start_server --port 8080 --pid-file app.pid -- ./go18http &

starting new worker 4043
```

HTTPリクエストを送ってみます。

```
> curl http://localhost:8080/

Hello, Go 1.8!
```

### restart

サーバの再起動は `HUP` です。

```
> kill -HUP `cat app.pid`

received HUP (num_old_workers=TODO)
spawning a new worker (num_old_workers=TODO)
starting new worker 4051
new worker is now running, sending TERM to old workers:4043
sleep 0 secs
killing old workers
2016/12/13 06:23:01 http: Server closed
old worker 4043 died, status:0
```

### stop

サーバの停止は `TERM` です。

```
> kill -TERM `cat app.pid`
```

## ab で負荷をかけてみる

サーバが起動させた状態で、以下のようにシグナル `HUP` を1秒毎に送り続けて再起動させ続けます。

```
> while true; do kill -HUP `cat app.pid`; sleep 1; done
```

別のセッションで `ab` で負荷をかけてみます。

```
ab -r -n 100000 -c 1000 http://localhost:8080/
```

### Results

```
Server Software:
Server Hostname:        localhost
Server Port:            8080

Document Path:          /
Document Length:        15 bytes

Concurrency Level:      1000
Time taken for tests:   10.674 seconds
Complete requests:      100000
Failed requests:        0
Total transferred:      11200000 bytes
HTML transferred:       1500000 bytes
Requests per second:    9368.48 [#/sec] (mean)
Time per request:       106.741 [ms] (mean)
Time per request:       0.107 [ms] (mean, across all concurrent requests)
Transfer rate:          1024.68 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    1   2.5      1      27
Processing:   100  105   5.4    103     178
Waiting:        0    2   3.4      1      48
Total:        100  106   6.9    105     194

Percentage of the requests served within a certain time (ms)
  50%    105
  66%    106
  75%    107
  80%    108
  90%    111
  95%    114
  98%    122
  99%    134
 100%    194 (longest request)
```

再起動を繰り返しながらでも、問題なく処理できたようです。

### Environment

- go version go1.8beta2 linux/amd64
- Amazon EC2 c4.large
- Linux version 4.4.23-31.54.amzn1.x86_64


## コード全体

```go
package main

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

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

func handler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "Hello, ")
	w.(http.Flusher).Flush()
	time.Sleep(time.Millisecond * 100)
	fmt.Fprint(w, "Go 1.8!\n")
}

func main() {
	listeners, err := listener.ListenAll()
	if err != nil && err != listener.ErrNoListeningTarget {
		log.Fatal(err)
	}

	server := &http.Server{Handler: http.HandlerFunc(handler)}

	go func(){
		if err := server.Serve(listeners[0]); err != nil {
			log.Print(err)
		}
	}()

	sigCh := make(chan os.Signal, 1)
	signal.Notify(sigCh, syscall.SIGTERM)

	<-sigCh
	ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
	if err := server.Shutdown(ctx); err != nil {
		log.Print(err)
	}
}
```

License: MIT

----

当初の記事の公開時点では、コードの書き方に問題があり、Shutdownを待たずにプロセス終了してしまっており、Gracefulになっていませんでした。

shogo82148 さんのコメントのおかげで修正できました。ありがとうございます。