最近golangのコードを書いている(既存コードの修正をしている)ことが多いので覚えているうちに。
数年前に自分が書いたデーモン(API)が無停止アップデート出来なくて困っている。
今時だとこういうものはコンテナで動いてポコポコ上げたり落としたりするのだろう。
その辺りはまぁ理想はそうなのだがいろんな事情があって僕のデーモンは物理サーバで動いているしあまりプロダクション環境から切り離してメンテナンスしたくもない。
そんなこんなでついに必要に迫られて、server-starter の導入を決意した。
ひとまず自分のおもちゃで練習する。
ここではserver-starter対応前のコードとして net/http
パッケージの ListenAndServe
関数の例 を使う。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| package main
import ( "io" "log" "net/http" )
func main() { h1 := func(w http.ResponseWriter, _ *http.Request) { io.WriteString(w, "Hello from a HandleFunc #1!\n") } h2 := func(w http.ResponseWriter, _ *http.Request) { io.WriteString(w, "Hello from a HandleFunc #2!\n") }
http.HandleFunc("/", h1) http.HandleFunc("/endpoint", h2)
log.Fatal(http.ListenAndServe(":8080", nil)) }
|
ではこのコードをserver-starter対応に書き換えていく。
server-starterを導入するとserver-starter自身が直接接続を受け持ち、アプリケーションに対して net.Listener
介してクライアントとの接続が提供される。
もう少し詳細に言うと、listener
パッケージの ListenAll()
関数をアプリケーションから呼ぶと net.Listener
の配列が返ってくる。
http.ListenAndServe
関数では net.Listener
を直接扱えないのでまず net.Listener
を扱える形に変更する。
その前に main
の最後の1行を少し変形する。
1 2 3 4
| srv := &http.Server{Addr: ":8080"} if err := srv.ListenAndServe(); err != nil { log.Fatal(err) }
|
server-starter対応ではアップデート前の古いプロセスを安全に落とすことが必要である。
言い換えるとアプリケーションには特定のシグナルを受け取り綺麗にシャットダウンする仕組みが必要になる。
この辺りは net/http
の Server.Shutdown
に良い例が載っている。
server-starterで古いプロセスを落とすために飛んでくるシグナルはSIGTERMである。
まずSIGTERMを受け取ってshutdownするコードを追加する。
1 2 3 4 5 6 7 8
| sigterm := make(chan os.Signal, 1) signal.Notify(sigterm, syscall.SIGTERM) <-sigterm
if err := srv.Shutdown(context.Background()); err != nil { log.Printf("HTTP server Shutdown: %v", err) } log.Println("server has shutdown")
|
元の例では シグナルの待機を別のgoroutineで行っているが、いくつかの例を見ると ListenAndServe
(Serve
)を別のgoroutineにしているものが目立ったのでそれに倣って
1 2 3 4 5 6
| srv := &http.Server{Addr: ":8080"} go func() { if err := srv.ListenAndServe(); err != nil { log.Fatal(err) } }
|
こんなふうに書き換える。
ここまで来ればあと一歩。listener
から net.Listener
をもらって Serve
しよう。
1 2 3 4 5 6 7 8 9 10 11
| listeners, err := listener.ListenAll() if err != nil { log.Fatal(err) } srv := &http.Server{}
go func() { if err := srv.Serve(listeners[0]); err != nil { log.Fatal(err) } }()
|
こうして完成したコードが以下である。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| package main
import ( "context" "io" "log" "net/http" "os" "os/signal" "syscall"
"github.com/lestrrat-go/server-starter/listener" )
func main() { h1 := func(w http.ResponseWriter, _ *http.Request) { io.WriteString(w, "Hello from a HandleFunc #1!\n") } h2 := func(w http.ResponseWriter, _ *http.Request) { io.WriteString(w, "Hello from a HandleFunc #2!\n") }
http.HandleFunc("/", h1) http.HandleFunc("/endpoint", h2)
listeners, err := listener.ListenAll() if err != nil { log.Fatal(err) } srv := &http.Server{}
go func() { if err := srv.Serve(listeners[0]); err != nil { log.Fatal(err) } }()
sigterm := make(chan os.Signal, 1) signal.Notify(sigterm, syscall.SIGTERM) <-sigterm
if err := srv.Shutdown(context.Background()); err != nil { log.Print(err) }
log.Println("server has shutdown") }
|
なお、start-server経由で実行する場合、コードの中にlistenするポートの情報を持てない。
1
| start_server --port 8080 ./main
|
のように指定しよう。