Shogo's Blog

Sep 21, 2023 - 1 minute read - go golang

ぼくのかんがえたさいきょうのGo HTTPサーバー起動方法

これまで何度か HTTP Server の Graceful Shutdown について記事を書きました。

最終的に Go 1.8 で Server.Shutdown が導入され、この件は解決を見ました。 しかし、最近「あれ?本当に正しく Server.Shutdown 使えている?」と疑問に思い、少し考えてみました。 というか ↑ の記事もまだ考慮が足りない気がする。

ぼくのかんがえたさいきょうの Go HTTP サーバー起動方法

とりあえず完成形のコード。

package main

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

func main() {
	// シグナルを待ち受ける
	chSignal := make(chan os.Signal, 1)
	signal.Notify(chSignal, syscall.SIGINT, syscall.SIGTERM)
	defer signal.Stop(chSignal)

	// 起動したいサーバーを準備
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "from pid %d.\n", os.Getpid())
	})
	s := &http.Server{
		Addr:    ":8080",
		Handler: mux,
	}
	http.Handle("/", h)

	// 別 goroutine で起動する
	chServe := make(chan error, 1)
	go func() {
		defer close(chServe)
		chServe <- s.ListenAndServe()
	}()

	select {
	case err := <-chServe:
		// ListenAndServe はたまに失敗するので、失敗したら死んでほしい
		// 他のプロセスが使用しているポートを開いたときとか
		log.Fatal(err)
	case <-chSignal:
	}

	signal.Stop(chSignal)

	// Graceful Shutdown 開始
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()
	if err := s.Shutdown(ctx); err != nil {
		log.Fatal(err)
	}

	// HTTPサーバーが完全に終了するのを待つ
	// (OSがリソース閉じてくれるはずだけど、お行儀よく)
	s.Close()
	<-chServe // http.ErrServerClosed が返ってくるのがわかっているので、特に何もしない。
}

大事なのは、ListenAndServe がサーバーの起動に失敗したときはプロセスを終了させること。 HTTP サーバーとしての役割を果たさないのに、プロセスだけ生き残っても仕方がないのでね。

ただし ListenAndServe がエラーを返したらプロセス即終了、という訳にはいかない。 たとえば、Shutdown を呼び出すと ListenAndServehttp.ErrServerClosed エラーで終了してしまう。 この場合はプロセスを終了させてはいけなくって、すべてのリクエストが正常に処理されるのを待つ必要がある。

ListenAndServe の失敗監視」と「シグナルの受信監視」を並行してやらないといけないので、意外と難しい。


まあ、最近はロードバランサーがよしなに取り計らってくれるので、このへん多少雑でも構わない。