⚙️

[Rust]Tokio Hyper Axum Ctrl+C 止まらない 止まらない(SIGINT)

2024/01/06に公開

https://github.com/tokio-rs/axum

皆さんはBashやDockerからAxumのWebサーバーを立ち上げて止まらなくなり、仕方なく別プロセスからkillした経験はありませんか?
他のWebアプリケーションフレームワークではあり得ないAxumのCtrl+Cで止まらない挙動を解決した、"Hello, World!"を返すWebサーバーの作り方を紹介します。

https://github.com/clux/muslrust

Fly.ioで動かすことを目標としているため、必要最低限のファイルとソースコードでFROM clux/muslrust:stable AS builderからFROM gcr.io/distroless/static-debian12のイメージを作っていきます。
Dockerだけあれば動きますので、ローカル環境にGitやRustをインストールする必要はありません。

まずは各ディレクトリ、ファイルを作成します。

$ mkdir -p hello-world hello-world/src
$ cd hello-world
$ touch Dockerfile Cargo.toml src/main.rs

マルチステージビルドでイメージを作成します。
muslrustでcargo buildする際は不要なコードを削ります。

Dockerfile
FROM clux/muslrust:stable AS builder

WORKDIR /volume
COPY . .

RUN RUSTFLAGS="-C strip=symbols" cargo build --release

FROM gcr.io/distroless/static-debian12

WORKDIR /
COPY --from=builder /volume/target/x86_64-unknown-linux-musl/release/main .

ENTRYPOINT ["/main"]

Cargo.tomlで最新のAxumとTokioを取ってこれるよう設定します。
Axumと一緒に新しめのHyperもついてきます。

Cargo.toml
[package]
name = "main"
version = "0.1.0"
edition = "2021"

[dependencies]
axum = "*"
tokio = { version = "*", features = ["full"] }

AxumとTokioの機能を使い"Hello, World!"を返すWebサーバーを作ります。

main.rs
use axum::{routing::get, Router};
use tokio::{net::TcpListener, signal};

#[tokio::main]
async fn main() {
    let app = Router::new().route("/", get(|| async { "Hello, World!" }));
    let listener = TcpListener::bind("0.0.0.0:8080").await.unwrap();
    axum::serve(listener, app)
        .with_graceful_shutdown(async { signal::ctrl_c().await.unwrap() })
        .await
        .unwrap();
}

Axum 0.7.3から使えるようになったaxum::serve().with_graceful_shutdown()とTokioのtokio::signal::ctrl_c()を組み合わせることでAxumのCtrl+Cで止まらない挙動を解決できます。

https://github.com/tokio-rs/axum/pull/2398
https://docs.rs/axum/latest/axum/serve/struct.Serve.html#method.with_graceful_shutdown
https://docs.rs/tokio/latest/tokio/signal/fn.ctrl_c.html

完成したので実際に動かしてみましょう。

$ docker build -t hello-world .
$ docker run -d --rm --stop-signal=SIGINT -p 8080:8080 hello-world

ドキュメントにも書いてあるとおり、SIGINTを受け取ることで止まるため、Dockerで動かす時は--stop-signal=SIGINTオプションを付ける必要があります。
これがないとdocker container stopコマンドでSIGTERMを送ってしまいコンテナが正常に停止できなくなり、10秒程経過した後killされて落ちてしまいます。
もし--stop-signal=SIGINTオプションをつけ忘れた場合は以下のコマンドを試してみましょう。

$ docker container stop -s SIGINT ...

Dockerで動いているStaticバイナリはローカルにコピーできます。

$ docker build -t hello-world .
$ docker run -d --rm --stop-signal=SIGINT -p 8080:8080 hello-world
$ docker container cp "$(docker ps -aq | head -1):/main" main

StaticバイナリからWebサーバーを立ち上げて、停止する時はもちろんCtrl+Cです。

$ ./main
^C$ 

StaticバイナリもDockerイメージも非常に軽いため簡単に持ち運びができます。

$ docker image save hello-world > main.tar
$ docker image load < main.tar

バイナリサイズは2.4MB、イメージサイズは4.45MB。

$ ls -lahR
.:
total 7.7M
drwxr-xr-x 3 gitpod gitpod   81 Jan  5 20:00 .
drwxr-xr-x 7 gitpod gitpod  104 Jan  5 20:00 ..
-rw-r--r-- 1 gitpod gitpod  133 Jan  5 20:00 Cargo.toml
-rw-r--r-- 1 gitpod gitpod  268 Jan  5 20:00 Dockerfile
-rwxr-xr-x 1 gitpod gitpod 2.4M Jan  5 20:00 main
-rw-r--r-- 1 gitpod gitpod 5.4M Jan  5 20:00 main.tar
drwxr-xr-x 2 gitpod gitpod   21 Jan  5 20:00 src

./src:
total 4.0K
drwxr-xr-x 2 gitpod gitpod  21 Jan  5 20:00 .
drwxr-xr-x 3 gitpod gitpod  81 Jan  5 20:00 ..
-rw-r--r-- 1 gitpod gitpod 394 Jan  5 20:00 main.rs

$ docker images
REPOSITORY    TAG       IMAGE ID       CREATED              SIZE
hello-world   latest    ...            About a minute ago   4.45MB

Axumでいつでもどこでも使える"Hello, World!"を返すWebサーバーが完成しました。
もしSIGINTだけではなくSIGTERMも受け取りたい場合は以下の公式Exampleを参考にしてみてください。

https://github.com/tokio-rs/axum/blob/main/examples/graceful-shutdown/src/main.rs

今回は以前作って書いたスクラップ記事の内容をより良くすることができたので記事にしてみました。

https://zenn.dev/tkithrta/scraps/93dfbce98e5f38

ありがとうございました。

Discussion