﻿# 執行 Docker 容器可使用 dumb-init 或 tini 改善程序優雅結束的問題

現在越來越多環境都跑在 Docker 環境下，但不知道你是否有遇過想要停止容器，但執行 `docker stop` 之後卻無法立即停止的狀況？這個問題會牽扯到我在 [當 .NET Core 執行在 Linux 或 Docker 容器中如何優雅的結束](/post/2020/07/22/How-to-handle-graceful-shutdown-in-NET-Core) 文章中提到的 **訊號**(Signal) 是如何傳送到容器的程序。這篇文章我將更深入的探討這個問題，以及提供解決方法。

### 關於容器與訊號的關係

當你在執行 Docker 容器時，主要執行**程序**(Process)的 PID 將會是 `1`，只要這個程序停止，容器就會跟著停止。

由於容器中一直沒有像 [systemd](https://wiki.freedesktop.org/www/Software/systemd/) 或 [sysvinit](https://wiki.archlinux.org/title/Init) 這類的**初始化系統**(init system)，少了**初始化系統**來管理程序，會導致當程序不穩定的時候，無法進一步有效的處理程序的狀態，或是無法有效的控制 Signal 處理機制。

我們以 `docker stop` 為例，這個命令實質上是對容器中的 PID `1` 送出一個 `SIGTERM` 訊號，如果程序本身並沒有處理 Signal 的機制，就會直接忽略這類訊號，這就會導致 `docker stop` 等了 10 秒之後還不結束，然後 Docker Engine 又會對 PID `1` 送出另一個 `SIGKILL` 訊號，試圖強迫砍掉這個程序，這才會讓容器徹底停下來。

### 示範容器中的主程式沒有正確處理 Signal 的狀況

我們用一個最簡單的例子來說明：

1.  執行簡單的 `sleep` 命令
    
    ```bash
    docker run -it --rm --name=test ubuntu /bin/sh -c "sleep 100000"
    ```
    
2.  然後我們試著停止這個容器
    
    ```bash
    docker stop test
    ```
    
    此時你會發現要等 10 秒，容器才會結束！
    

會發生無法立刻停止的狀況，其實是 `/bin/sh` 預設並不會處理(handle)訊號，所以他會把所有不認得的訊號忽略，直到作業系統把他砍掉為止。

### 示範容器中的主程式有正確處理 Signal 的狀況

1.  建立一個空資料夾，並且建立一個 `test.sh` 檔案
    
    我在這份 Shell 腳本中使用 `trap 'exit 0' SIGTERM` 來處理 `SIGTERM` 訊號，接收到訊號就直接以狀態碼 `0` 正常的退出：
    
    ```bash
    #!/usr/bin/env sh
    trap 'exit 0' SIGTERM
    while true; do :; done
    ```
    
2.  撰寫一個 `Dockerfile` 來建置一個名為 `test:latest` 的 Image
    
    ```dockerfile
    FROM alpine
    COPY test.sh /
    ENTRYPOINT [ "/test/sh" ]
    ```
    
3.  建置如容器映像
    
    ```bash
    docker build -t test:latest .
    ```
    
4.  執行容器
    
    ```bash
    docker run -it --rm --name=test test:latest
    ```
    
5.  然後我們試著停止這個容器
    
    ```bash
    docker stop test
    ```
    
    此時你會發現，容器收到訊號之後就會立刻結束！
    

### 使用 [dumb-init](https://github.com/Yelp/dumb-init) 初始化系統

其實大部分的程式都沒有**處理訊號**(signal handling)，甚至有很多不熟悉 Linux 的開發者根本不知道有**訊號**的存在。

以下我用一個簡單的例子來示範 [dumb-init](https://github.com/Yelp/dumb-init) 的使用方式：

1.  建立一個 `Dockerfile` 並安裝 `dumb-init` 套件
    
    這邊我用 `alpine` 超經量容器映像，搭配 `curl` 下載一份 9GB 的超大檔案 (代表會下載很久)。
    
    你只要加入一個 `ENTRYPOINT` 指令，讓我原本要在容器中執行的程式，直接連著 `/usr/bin/dumb-init --` 後面執行，使用上就是這麼簡單！👍
    
    ```dockerfile
    FROM alpine
    RUN apk update && apk add --no-cache dumb-init curl ca-certificates && rm -rf /var/cache/apk/*
    
    COPY test.sh /
    
    ENTRYPOINT [ "/usr/bin/dumb-init", "--" ]
    CMD [ "curl", "http://http.speed.hinet.net/test_9216m.zip", "-o", "/dev/null" ]
    ```
    
    > 請記得要在 `/usr/bin/dumb-init` 後面加入一個 `--` 參數，分隔 `dumb-init` 與 **主程式** 之間，因為 `dumb-init` 也有自己的參數選項可以設定。
    
2.  建置容器映像
    
    ```bash
    docker build -t test:latest .
    ```
    
3.  執行容器
    
    ```bash
    docker run -it --rm --name=test test:latest
    ```
    
4.  然後我們試著停止這個容器
    
    ```bash
    docker stop test
    ```
    
    此時你會發現，無論 curl 有沒有執行完，當 `dumb-init` 收到 `SIGTERM` 訊號時，就會轉發給透故 `dumb-init` 啟動的 `curl` 程序！
    

### 使用 [dumb-init](https://github.com/Yelp/dumb-init) 控制所有啟動的程序

有時候我們會透過 Shell Script 啟動一些其他的程式，有些甚至是背景服務。但是，當 Shell 接收到 `SIGTERM` 訊號的時候，並不會轉傳收到的訊號給**子程序**(Sub-process)，所以就算你的 Shell Script 收到訊號，其他子程序是不會收到訊號的，所以程序並不會停止。

這個狀況有個非常簡單的解決方式，就是把 `#!/usr/bin/env sh` 修改成 `#!/usr/bin/dumb-init /bin/sh` 即可！

```bash
#!/usr/bin/dumb-init /bin/sh
my-web-server &  # launch a process in the background
my-other-server  # launch another process in the foreground
```

### 使用 [dumb-init](https://github.com/Yelp/dumb-init) 控制訊號覆寫 (Signal rewriting)

我之前在 [當 .NET Core 執行在 Linux 或 Docker 容器中如何優雅的結束](/post/2020/07/22/How-to-handle-graceful-shutdown-in-NET-Core) 文章中也有提到，有些特定的服務並不會接收 `SIGTERM` 訊號。例如 [nginx](https://nginx.org/) 預設若要執行優雅的結束，必須對他送出 `SIGQUIT` 訊號。而 Apache HTTP Server 則要送出 `SIGWINCH` 訊號，才會優雅的結束。

由於 `docker stop` 預設會送出 `SIGTERM` 服務為主，所以如果你打算自己封裝 `nginx` 容器的話，送出正確的訊號就十分重要。

如果你直接使用 [nginx](https://hub.docker.com/_/nginx) 容器映像，其實不用特別處理，因為你可以看 nginx 的 [Dockerfile](https://hub.docker.com/layers/nginx/library/nginx/latest/images/sha256-85be2e86df0a8e6420da7968921a18b66ae8b84c47777b932e86c700da7add4a?context=explore) 已經設定了 `STOPSIGNAL SIGQUIT` 指令，所以當有人對這個容器送出 `docker stop` 命令時，本來就會轉送 `SIGQUIT` 訊號過去，不需要靠 `dumb-init` 的幫助。

當然，如果你有特別的需求，才需要把 `SIGTERM` (`15`) 轉送成 `SIGQUIT` (`3`) 這樣寫：

```bash
ENTRYPOINT [ "/usr/bin/dumb-init", "--rewrite", "15:3", "--" ]
CMD [ "curl", "http://http.speed.hinet.net/test_9216m.zip", "-o", "/dev/null" ]
```

> 完整的訊號名稱與編號可以透過 Linux 下的 `kill -l` 命令查詢。

以下是 `dumb-init` 的參數選項說明：

```
dumb-init v1.2.2
Usage: dumb-init [option] command [[arg] ...]

dumb-init is a simple process supervisor that forwards signals to children.
It is designed to run as PID1 in minimal container environments.

Optional arguments:
   -c, --single-child   Run in single-child mode.
                        In this mode, signals are only proxied to the
                        direct child and not any of its descendants.
   -r, --rewrite s:r    Rewrite received signal s to new signal r before proxying.
                        To ignore (not proxy) a signal, rewrite it to 0.
                        This option can be specified multiple times.
   -v, --verbose        Print debugging information to stderr.
   -h, --help           Print this help message and exit.
   -V, --version        Print the current version and exit.

Full help is available online at https://github.com/Yelp/dumb-init
```

### 使用 [tini](https://github.com/krallin/tini) 初始化系統

[tini](https://github.com/krallin/tini) 是一套更簡單的 init 系統，專門用來執行一個子程序(spawn a single child)，並等待子程序結束，即便子程序已經變成**僵屍程序**(zombie process)也能捕捉到，同時也能轉送 Signal 給子程序。

如果你使用 Docker 來跑容器，可以非常簡便的在 `docker run` 的時候用 `--init` 參數，就會自動注入 [tini](https://github.com/krallin/tini) 程式 (`/sbin/docker-init`) 到容器中，並且自動取代 `ENTRYPOINT` 設定，讓原本的程式直接跑在 [tini](https://github.com/krallin/tini) 程序底下！

> 注意：Docker 1.13 以後的版本才開始支援 `--init` 參數，並內建 [tini](https://github.com/krallin/tini) 在內。

1.  不用 `--init` 的情況
    
    直接啟動 `sleep` 程式跑 100 秒
    
    ```bash
    docker run -it --rm --name=test alpine sleep 100
    ```
    
    使用 `ps -ef` 可以得知 `sleep 100` 程式會直接跑在 PID `1` 底下
    
    ```bash
    docker exec -it test ps -ef
    ```
    
    ```
    PID   USER     TIME  COMMAND
        1 root      0:00 sleep 100
        8 root      0:00 ps -ef
    ```
    
    停止容器需要 10 秒才能完成
    
    ```bash
    docker stop test
    ```
    
2.  使用 `--init` 的情況
    
    使用 `--init` 啟動 `sleep` 程式跑 100 秒
    
    ```bash
    docker run -it --rm --name=test --init alpine sleep 100
    ```
    
    使用 `ps -ef` 可以得知 `sleep 100` 程式會跑在 `/sbin/docker-init --` 命令下
    
    ```bash
    docker exec -it test ps -ef
    ```
    
    ```
    PID   USER     TIME  COMMAND
        1 root      0:00 /sbin/docker-init -- sleep 100
        8 root      0:00 sleep 100
        9 root      0:00 ps -ef
    ```
    
    停止容器只需要 1 秒內就可以完成
    
    ```bash
    docker stop test
    ```
    

### [dumb-init](https://github.com/Yelp/dumb-init) vs. [tini](https://github.com/krallin/tini)

這兩套其實都是可以正確擔任 PID `1` 的重責大任的 init systems，但是要怎樣選擇呢？

-   如果你想自己建置 Docker Image，但是主程式**沒有辦法正確處理訊號**或是**無法有效管理子程序**時，可以使用 [dumb-init](https://github.com/Yelp/dumb-init) 來啟動程式。
    
-   如果你只想直接使用其他人寫好的 Docker Image，但 Image 內的主程式沒有正確的處理訊號，或是執行過程會發生資源洩漏或變成僵屍等狀況，可以在 `docker run` 的時候直接使用 `--init` 啟動容器，該容器的主程式就會自動跑在 [tini](https://github.com/krallin/tini) 底下！
    
    > 已經有許多 Docker Images 內建 tini 在內，詳見 [tini-images](https://github.com/krallin/tini-images) Repo 說明。
    

### 相關連結

-   [Yelp/dumb-init: A minimal init system for Linux containers](https://github.com/Yelp/dumb-init)
-   [krallin/tini: A tiny but valid `init` for containers](https://github.com/krallin/tini)
-   [Choosing an init process for multi-process containers](https://ahmet.im/blog/minimal-init-process-for-containers/)
-   [How to use --init parameter in docker run - Stack Overflow](https://stackoverflow.com/questions/43122080/how-to-use-init-parameter-in-docker-run)
-   [Docker run reference | Docker Documentation](https://docs.docker.com/engine/reference/run/#specify-an-init-process)
-   [Docker Best Practices](https://gist.github.com/StevenACoffman/41fee08e8782b411a4a26b9700ad7af5)
-   [HiNet連線速率測試 - HiNet測速網站](http://speed.hinet.net/httptest.html)
-   [Install Certificates in Alpine Image to establish Secured Communication (SSL/TLS) | Hacker Noon](https://hackernoon.com/alpine-docker-image-with-secured-communication-ssl-tls-go-restful-api-128eb6b54f1f)
-   [當 .NET Core 執行在 Linux 或 Docker 容器中如何優雅的結束](/post/2020/07/22/How-to-handle-graceful-shutdown-in-NET-Core)
-   [Gracefully Stopping Docker Containers - CenturyLink Cloud Developer Center](https://web.archive.org/web/20221116155640/https://www.ctl.io/developers/blog/post/gracefully-stopping-docker-containers/)
