molpako.py

もるぱこのブログです

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

以上です。