systemd の socket activation を試す
systemd では .socket
ファイルを作成することによって socket activation という機能を使えることができます。socket activation はサービスの代わりに socket がリッスンし、リクエストをサービスに渡します。socketがリッスンしているため、サービスのリスタートなどでサービスが落ちている時間もリクエストを受け、サービスが上がったら接続を渡すことができます。
試したコードは molpako/go-server-systemd に置いていて、docker-compose も用意しているのですぐ試せる状態になっていると思います。
サーバーを作る
golang で簡単なサーバーを作ります。なんとなく時刻を返すサーバーにします。
func HelloServer(w http.ResponseWriter, req *http.Request) { io.WriteString(w, time.Now().Format(time.Stamp)) io.WriteString(w, "\n") }
coreos/go-systemd の activation.Listeners() は systemd から渡されたファイルディスクリプタを元に net.Listener
を作り []net.Listener
を返します。
func main() { listeners, err := activation.Listeners() if err != nil { log.Fatal(err) } if len(listeners) == 0 { log.Fatal("Unexpected number of socket activation fds") } mux := http.NewServeMux() mux.HandleFunc("/", HelloServer) srv := &http.Server{ Handler: mux, } if err := srv.Serve(listeners[0]); err != http.ErrServerClosed { log.Fatalf("HTTP server ListenAndServe: %v", err) } }
systemd
.socket ファイルにはリッスンするアドレスを記載します
# /etc/systemd/system/hello.socket [Socket] ListenStream=0.0.0.0:8076 [Install] WantedBy=sockets.target
.serviceファイルには先ほどgoで作ったサーバーのバイナリを指定します
# /etc/systemd/system/hello.service [Unit] Description=Hello World HTTP Requires=network.target After=multi-user.target [Service] Type=simple ExecStart=/app/go-server-systemd [Install] WantedBy=multi-user.target
それぞれのファイルを配置したあと socket を起動して、リクエストを飛ばしてみます。
root@5f8340a6141a:/app# systemctl start hello.socket root@5f8340a6141a:/app# curl 127.0.0.1:8076 Dec 10 15:56:55
返ってきた^^
socket activation を試してみる
ここから本題です。実際にサービス側を落としてリクエストがどうなるか見てみます。
root@5f8340a6141a:/app# systemctl stop hello.service Warning: Stopping hello.service, but it can still be activated by: hello.socket
socket はまだ生きているよというメッセージが出ました。
socketにリクエストを飛ばすと、レスポンスが返ってきました。socket がリクエストを受けたときに service を立ち上げてくれたようです。
root@5f8340a6141a:/app# curl 127.0.0.1:8076 Dec 10 16:15:49 root@5f8340a6141a:/app# systemctl is-active hello.service active
go の graceful shutdown を組み合わせてみる。
上記だけでは、接続が残っている時に service を restart した場合に、その接続が切れてしまいます。無理矢理ですが接続を残すためにsleep処理を入れて試してみます。
func HelloServer(w http.ResponseWriter, req *http.Request) { time.Sleep(5 * time.Second) io.WriteString(w, time.Now().Format(time.Stamp)) io.WriteString(w, "\n") }
root@5f8340a6141a:/app# curl 127.0.0.1:8076 curl: (52) Empty reply from server # systemctl restart hello.service
なので本題とはそれますが、ついでに go で作ったhttpサーバーに graceful shutdown を 導入してみます。
main.goでは HUP を受け取ると server.Shutdown() を実行してプログラムを終了するようにします。
// main.go ... idleConnsClosed := make(chan struct{}) go func() { sigint := make(chan os.Signal, 1) signal.Notify(sigint, syscall.SIGHUP) s := <-sigint log.Printf("HTTP server Reload: %v", s) if err := srv.Shutdown(context.Background()); err != nil { log.Printf("HTTP server Shutdown: %v", err) } close(idleConnsClosed) }() if err := srv.Serve(listeners[0]); err != http.ErrServerClosed { log.Fatalf("HTTP server ListenAndServe: %v", err) } <-idleConnsClosed }
.service ファイルに reload の処理を追加します
# /etc/systemd/system/hello.service [Service] ... ExecReload=kill -HUP $MAINPID
go の再ビルドと service を再起動して、準備完了です。
graceful shutdown 確認
root@5f8340a6141a:/app# cat test.sh #!/bin/bash for i in {1..100} do curl -s http://127.0.0.1:8076 done
root@5f8340a6141a:/app# nohup bash test.sh & [1] 902 root@5f8340a6141a:/app# tail -f nohup.out Dec 10 16:44:23 Dec 10 16:44:28 Dec 10 16:44:33
service を reload することによって graceful shutdown され、接続が切れることは無くなりました。
root@5f8340a6141a:/app# tail -f nohup.out Dec 10 16:51:41 Dec 10 16:51:46 ^C root@5f8340a6141a:/app# systemctl reload hello.service root@5f8340a6141a:/app# systemctl reload hello.service root@5f8340a6141a:/app# systemctl reload hello.service root@5f8340a6141a:/app# tail -f nohup.out Dec 10 16:51:41 Dec 10 16:51:46 Dec 10 16:51:51 Dec 10 16:51:56 Dec 10 16:52:01 Dec 10 16:52:06 # restart では graceful shutdown にならないので接続が切れる root@5f8340a6141a:/app# systemctl restart hello.service root@5f8340a6141a:/app# tail -f nohup.out Dec 10 16:54:07 Dec 10 16:54:12 curl: (52) Empty reply from server
以上です。