⚙️

[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 . .

ENV RUSTFLAGS="-C strip=symbols"
RUN 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 -p 8080:8080 --stop-signal=SIGINT 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 -p 8080:8080 --stop-signal=SIGINT 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 | gzip > main.tar.gz
$ docker image load < main.tar.gz

バイナリサイズは1.4MB、イメージサイズは3.44MB。圧縮すると1.3MB(2024年5月6日更新)

$ ls -lahR
.:
total 2.7M
drwxr-xr-x 3 gitpod gitpod   84 May  5 20:00 .
drwxr-xr-x 9 gitpod gitpod  126 May  5 20:00 ..
-rw-r--r-- 1 gitpod gitpod  133 May  5 20:00 Cargo.toml
-rw-r--r-- 1 gitpod gitpod  286 May  5 20:00 Dockerfile
-rwxr-xr-x 1 gitpod gitpod 1.4M May  5 20:00 main
-rw-r--r-- 1 gitpod gitpod 1.3M May  5 20:00 main.tar.gz
drwxr-xr-x 2 gitpod gitpod   21 May  5 20:00 src

./src:
total 4.0K
drwxr-xr-x 2 gitpod gitpod  21 May  5 20:00 .
drwxr-xr-x 3 gitpod gitpod  84 May  5 20:00 ..
-rw-r--r-- 1 gitpod gitpod 393 May  5 20:00 main.rs

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

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