Edited at

http.Serverをgracefulに停止させる

More than 3 years have passed since last update.


概要

net.httpサーバをCtrl+Cで終了できるようにしたい。現在処理しているリクエストに関しては処理が終わるのを待ってgracefulに終了したい。

以下のようなコードだとCtrl+Cで停止させると、リクエストが処理中でもプロセスが終了してまう。

func main(){

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(10 * time.Second)
fmt.Fprintf(w, "Hello World")
})
http.ListenAndServe(":80", nil)
}

Ctr+Cでgracefulに終了させるためには以下の処理を追加で実装する必要がある


  • Ctrl+Cのイベントを受け取る(SIGTERMのシグナルを受け取る)

  • signalを受け取ったらListenしているソケットを閉じる(新しいコネクションを受け付けないため)

  • 現在処理しているリクエストの終了を待ち、プロセスを終了させる


修正点


Ctrl+Cのイベントを受け取る

    c := make(chan os.Signal, 1)

signal.Notify(c, os.Interrupt)

シグナルをchannelで受け取る


signalを受け取ったらListenしているソケットを閉じる

    laddr, _ := net.ResolveTCPAddr("tcp", ":80")

listener, _ := net.ListenTCP("tcp", laddr)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(10 * time.Second)
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})

go func() {
sig := <-c
listener.Close()
}()

ListenAndServeではhttpモジュール内で生成されたlistenerへの参照が得られないので、listenerを生成して後でhttp.Serveで渡すように修正


現在処理しているリクエストの終了を待ち、プロセスを終了させる

http.ServerのConnState変数に関数を設定する事でコネクションの状態が変化した時にコールバックを呼んでもらえる

var activeConWg sync.WaitGroup

var numberOfActive = 0

func connectionStateChange(c net.Conn, st http.ConnState) {
if st == http.StateActive {
activeConWg.Add(1)
numberOfActive += 1
} else if st == http.StateIdle || st == http.StateHijacked {
activeConWg.Done()
numberOfActive -= 1
}
log.Printf("Number of active connection: %d\n", numberOfActive)
}

sync.WaitGroupを使ってアクティブなコネクションが増えた時はAddを、IdleかHijackedのステータスになったらDoneを呼ぶ事でmain側でアクティブなコネクションが0になったらWaitから抜ける事が出来る.

    srv := &http.Server{Handler: nil, ConnState: connectionStateChange}

srv.Serve(listener)
activeConWg.Wait()

コールバック関数をしかけたhttp.Serverのインスタンスを作り、Serveを呼び出す.

Serveから戻ったら(net.ListenerのCloseの呼び出しでServeから戻る)アクティブコネクションが0になるのを待って終了させる。


コード

最終的なコードは以下

package main

import (
"fmt"
"html"
"log"
"net"
"net/http"
"os"
"os/signal"
"sync"
"time"
)

var activeConWg sync.WaitGroup
var numberOfActive = 0

func connectionStateChange(c net.Conn, st http.ConnState) {
if st == http.StateActive {
activeConWg.Add(1)
numberOfActive += 1
} else if st == http.StateIdle || st == http.StateHijacked {
activeConWg.Done()
numberOfActive -= 1
}
log.Printf("Number of active connection: %d\n", numberOfActive)
}

func main() {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
laddr, _ := net.ResolveTCPAddr("tcp", ":80")
listener, _ := net.ListenTCP("tcp", laddr)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(10 * time.Second)
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})

go func() {
sig := <-c
fmt.Print(sig)
listener.Close()
}()
srv := &http.Server{Handler: nil, ConnState: connectionStateChange}
srv.Serve(listener)
activeConWg.Wait()
}


参考資料

http.ServerのServeの処理

https://github.com/golang/go/blob/master/src/net/http/server.go#L2273-L2289


  • AcceptでTemporaryと判断されないエラーを起こせばServeのイベントループから抜ける

  • net.ListenerのCloseメソッドを呼び出す事でイベントループから抜けれる

ConnState(関数じゃなくて定数の方)が取る値

https://github.com/golang/go/blob/master/src/net/http/server.go#L2140-L2186

http.Server内でsetState関数が呼ばれた際にConnState関数が呼ばれる

https://github.com/golang/go/blob/master/src/net/http/server.go#L1471-L1475

net.Errorについて書かれている

https://blog.golang.org/error-handling-and-go


  • 当初のコードでnet.Errorをハンドルしてたので調べてたが、不要だったのでコードからは消した

Stopping a listening HTTP Server in Go

http://www.hydrogen18.com/blog/stop-listening-http-server-go.html


  • 当初参考にしたブログ記事

  • Timeout使ってAcceptから定期的に戻ってフラグをチェックするような感じで書かれていたが、listner直接Closeすればもっと簡単に書けそうだったので結局真似しなかった