今更だけどserver-starterを使おう

最近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/httpServer.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

のように指定しよう。