Docker コンテナは正常にシャットダウンします - 夢を実現するための道

正常な終了とは何ですか?

正常なシャットダウン: シャットダウンの前に、通常のシャットダウン プロセスを実行し、シャットダウンを実行しているオペレーティング システムなどの接続とリソースを解放します。

現在、ビジネス システム コンポーネントは多数あり、コンポーネント間の呼び出し関係は比較的複雑であり、1 つのコンポーネントのオフラインまたはシャットダウンには複数のコンポーネントが関係します。

オンライン アプリケーションの場合、アプリケーションの停止から再起動、サービスの復元までのサービス アップデートの展開プロセス中に、通常のビジネス リクエストが影響を受けないようにする方法 2>、これはアプリケーション開発と運用保守チームが解決しなければならない問題です。

従来の解決策は、アプリケーションの更新プロセスを、手動トラフィック収集、アプリケーションの停止、更新と再起動の 3 つのステップに分割することでした。これにより、クライアントは認識できなくなります。手動操作によるアップデートの実行。この方法はシンプルで効果的ですが、多くの制限があります。トラフィックを抽出するためにゲートウェイのサポートを使用する必要があるだけでなく、アプリケーションを停止する前に転送中のリクエストが処理されたことを確認するために手動で判断する必要もあります。

同時に、アプリケーションの正常なシャットダウンを保証するためのメカニズムがアプリケーション層にもいくつかあります。現在、Tomcat、Spring Boot、Dubbo などのフレームワークが関連する組み込み実装を提供しています。たとえば、SpringBoot 2.3には、正常なシャットダウンを簡単かつ直接実装できる正常なシャットダウンが組み込まれています。同時に、通常の Java アプリケーションは Runtime.getRuntime().addShutdownHook() に基づいてその実装をカスタマイズすることもできます。 実装原理は基本的に同じで、オペレーティング システムが SIGTERM シグナルを送信するのを待ってから、シグナルを監視するためのいくつかの処理アクションを実行します。

正常なシャットダウンとは、アプリケーションを停止するときにアプリケーションを正常にシャットダウンするために実行される一連の操作を指します。多くの場合、これらの操作には、既存のリクエストが完了するのを待つこと、スレッドを閉じること、接続を閉じること、リソースを解放することなどが含まれます。正常なシャットダウンにより、データの異常や損失、アプリケーションの異常、その他の問題を引き起こす可能性のあるプログラムの異常なシャットダウンを回避できます。 正常なシャットダウンは基本的に、JVM がシャットダウンしようとする前に実行される追加の処理コードです

現状

この段階では、ビジネスのコンテナ化後、サービスはシェル スクリプトを通じて開始されます。コンテナ内の PID 1 を持つ対応するプロセスはシェル プロセスですが、シェル プログラムは信号を転送せず、終了信号にも応答しません。したがってコンテナ アプリケーションでは、シェルがアプリケーション コンテナ内で起動され、pid=1 の位置を占める場合、シェルは k8s によって送信された SIGTERM シグナルを受信できず、強制終了できるのは、次の場合のみです。タイムアウト。 

場合

package main

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main()  {
    c := make(chan os.Signal)
    signal.Notify(c, syscall.SIGTERM, syscall.SIGINT)
    go func() {
        for s := range c {
            switch s {
            case syscall.SIGINT, syscall.SIGTERM:
                fmt.Println("退出", s)
                ExitFunc()
            default:
                fmt.Println("other", s)
            }
        }
    }()

    fmt.Println("进程启动...")
    time.Sleep(time.Duration(200000)*time.Second)
}

func ExitFunc()  {
    fmt.Println("正在退出...")
    fmt.Println("执行清理...")
    fmt.Println("退出完成...")
    os.Exit(0)
}

1. Signal.Notify は括弧内に指定されたシグナルをリッスンします。指定されていない場合は、すべてのシグナルをリッスンします。

2. スイッチで監視信号を判定し、SININT、SIGTERMの場合はExitfunc関数で終了します。

SHELLモードとCMDモードによる違い

Dockerfile では、シェル モードと実行モードを含むアプリケーションの起動に CMD と ENTRYPOINT が使用されます。

シェル モードを使用する場合、PID 1 のプロセスはシェルであり、実行モードを使用する場合、PID 1 のプロセスはビジネス自体です。

SHELL模式

FROM golang as builder
WORKDIR /go/
COPY app.go    .
RUN go build app.go
FROM ubuntu
WORKDIR /root/
COPY --from=builder /go/app .
CMD ./app

# 构建镜像

docker build -t app:v1.0-shell .

# 进入容器查看进程

docker exec -it app-shell ps aux

USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.7  0.0   2608   548 pts/0    Ss+  03:22   0:00 /bin/sh -c ./
root           6  0.0  0.0 704368  1684 pts/0    Sl+  03:22   0:00 ./app
root          24  0.0  0.0   5896  2868 pts/1    Rs+  03:23   0:00 ps aux

可以看见PID为1的进程是sh进程

此时执行docker stop,业务进程是接收不到SIGTERM信号的,要等待一个超时时间后被KILL

日志没有输出SIGTERM关闭指令

docker stop app-shell
app-shell

docker logs app-shell
进程启动...

 

EXEC模式

FROM golang as builder
WORKDIR /go/
COPY app.go    .
RUN go build app.go
FROM ubuntu
WORKDIR /root/
COPY --from=builder /go/app .
CMD ["./app"]

# 构建镜像

docker build -t app:v1.0-exec .

docker exec -it app-exec ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  2.0  0.0 703472  1772 pts/0    Ssl+ 03:33   0:00 ./app
root          14  0.0  0.0   5896  2908 pts/1    Rs+  03:34   0:00 ps aux

可以看见PID为1的进程是应用进程

此时执行docker stop,业务进程是可以接收SIGTERM信号的,会优雅退出

docker stop app-exec
app-exec

docker logs app-exec
进程启动...
退出 terminated
正在退出...
执行清理...
退出完成...

 注: ubuntu がアプリケーションのベース イメージとして使用されている場合、上記のテストは成功します。アプリケーションのベース イメージとして alpine が使用されている場合、シェル モードと実行モードは同じで、両方とも PID 1 のプロセスです。

アプリケーションを直接起動する場合とスクリプトを使用して起動する場合の違い

 実際の運用環境では、アプリケーションの起動コマンドの後に多くの起動パラメータが続くため、通常は起動スクリプトを使用してアプリケーションを起動し、アプリケーションの起動を容易にします。

コンテナ内の PID 1 を持つ対応するプロセスはシェル プロセスですが、シェル プログラムはシグナルを転送せず、終了シグナルに応答しません。したがって、コンテナアプリケーションでは、アプリケーションコンテナ内でシェルが起動され、pid=1の位置を占有している場合、k8sが送信するSIGTERMシグナルを受信できず、タイムアウト後に強制終了するしかありません。

# 启动脚本

cat > start.sh<< EOF 
#!/bin/sh
sh -c /root/app

# dockerfile

FROM golang as builder
WORKDIR /go/
COPY app.go    .
RUN go build app.go
FROM alpine
WORKDIR /root/
COPY --from=builder /go/app .
ADD start.sh /root/
CMD ["/bin/sh","/root/start.sh"]

# 构建镜像

docker build -t app:v1.0-script .

docker exec -it app-script ps aux
PID   USER     TIME  COMMAND
1     root     0:00  /bin/sh /root/start.sh
6     root     0:00  /root/app
19    root     0:00  ps aux

docker stop app-script

# 登待超时后被强行KILL
docker logs app-script
进程启动...

解決

• アプリケーション: アプリケーション自体は、リクエストが確実に行われるように、正常なシャットダウン処理ロジックを実装する必要があります。処理は完了するまで続行でき、リソースは事実上閉じられて解放されますなど。アプリケーション層については、Javaアプリケーションであっても、他の言語で書かれたアプリケーションであっても、その実装原理は基本的に同じであり、同様の監視・処理インターフェースが提供されており、仕様に従って要求を実現するだけです。

• プラットフォーム: プラットフォーム層は、アプリケーションを負荷分散から削除して、アプリケーションは新しいリクエスト接続を再度受け入れ、正常なシャットダウン処理を実行するようアプリケーションに通知できます。従来のデプロイメント モードでは、この部分の作業には手動処理が必要になる場合がありますが、K8s コンテナ プラットフォームでは、K8s ポッドの削除により、デフォルトでコンテナ内のメイン プロセスに正常なシャットダウン コマンドが送信されます。 a>ので、柔軟性が高まります。上記の分析を通じて、理論的には、アプリケーションのコンテナ化はデプロイ後の正常なシャットダウンをサポートし、従来の方法よりも自動化された操作を実現することさえできます。このドキュメントでは、このソリューションの詳細な検証をこのドキュメントの後半で行います。 一部のアプリケーションがコンテナにデプロイされる場合、それらはコンテナのメイン プロセスを通じてデプロイされません。K8s は、Pod が停止する前に指定された処理を実行する PreStop コールバック関数も提供します。コマンドは HTTP リクエストにすることもできる で、デフォルトの待機時間は 30 秒です。正常なシャットダウン プロセスが 30 秒を超えると、強制的に終了します。同時に、

• コンテナ アプリケーションでのサードパーティの初期化: アプリケーションの構築で tini や Dam-init などのサードパーティの初期化を使用する

オプション 1: k8s Prestop パラメーターを介した呼び出し

 k8s の prestop パラメータを介してコンテナ内のプロセス シャットダウン スクリプトを呼び出し、正常なシャットダウンを実現します。

前のスクリプトによって開始された dockerfile に基づいて、正常なシャットダウン スクリプトを定義し、k8s-prestop を通じて POD を閉じる前に正常なシャットダウン スクリプトを呼び出して、ポッドの正常なシャットダウンを実現します。 

# 启动脚本

cat > start.sh<< EOF 
#!/bin/sh
./app
EOF

# 关闭脚本
cat > stop.sh << EOF
#!/bin/sh
ps -ef|grep app|grep -v grep|awk '{print $1}'|xargs kill -15
EOF

# dockerfile

FROM golang as builder
WORKDIR /go/
COPY app.go    .
RUN go build app.go
FROM alpine
WORKDIR /root/
COPY --from=builder /go/app .
ADD start.sh /root/
CMD ["/bin/sh","/root/start.sh"]

# 构建镜像

docker build -t app:v1.0-prestop .

# k8s部署

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-prestop
  labels:
    app: prestop
spec:
  replicas: 1
  selector:
    matchLabels:
      app: prestop
  template:
    metadata:
      labels:
        app: prestop
    spec:
      containers:
      - name: prestop
        image: harbor.codemiracle.com/library/app:v1.0-prestop
        lifecycle:
          preStop:
            exec:
              command:
              - sh
              - /root/stop.sh

# 查看POD日志,然后删除pod副本

kubectl get pod 

kubectl logs app-prestop-847f5c4db8-mrbqr -f
进程启动...

# 另外窗口删除POD

kubectl logs app-prestop-847f5c4db8-mrbqr -f
进程启动...


退出 terminated
正在退出...
执行清理...
退出完成...

可以看见执行了Prestop脚本进行优雅关闭。同样的可以将yaml文件中的Prestop脚本取消进行对比测试可以发现就会进行强制删除。

 オプション 2: シェル スクリプトを変更して実行する

# start.sh

#!/bin/sh
exec ./app

shell中添加一个 exec 即可让应用进程替代当前shell进程,可将SIGTERM信号传递到业务层,让业务实现优雅关闭

オプション 3: 3 番目の初期化ツールから開始する

サードパーティの init プロセスを通じて SIGTERM をプロセスに渡します

コンテナのメインプロセスとして dump-init または tini を使用し、終了シグナルを受信すると、プロセスグループ内のすべてのプロセスに終了シグナルが転送されます。 、主にアプリケーション自体が信号処理をオフにする必要がないシナリオに適しています。 docker-init 自体も統合された tini です。​ 

FROM golang as builder
WORKDIR /go/
COPY app.go    .
RUN go build app.go
FROM alpine
WORKDIR /root/
COPY --from=builder /go/app .
ADD start.sh tini /root/
RUN chmoad a+x start.sh && apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["/root/tini", "--", /root/start.sh"]

は、最初に初期化プロセスとして /root/tini を実行し、次に /root/start.sh スクリプトを開始しました。この設定は、コンテナー内のプロセスが正しい方法で開始および終了されることを確認し、コンテナーの停止時にクリーンアップおよびシャットダウンする信号を処理するのに役立ちます。

tini はプロセスを初期化し、シグナルを正しく処理するためのツールであり、通常はコンテナー内のプロセスが正しい方法で開始および終了することを保証するために使用されます。

# 构建镜像

docker build -t app:v1.0-tini .

# 运行

docker run -itd --name app-tini app:v1.0-tini

# 查看日志

docker logs app-tini

进程启动...

发现容器快速停止了,但没有输出应用关闭和清理的日志

アプリケーション起動のメインプロセスとして tini または dump-init を使用します。 tini と Dam-init はシャットダウン信号を子プロセスに渡しますが、子プロセスが完全に終了するまで待機せずに終了します。その代わり、彼は移籍直後に退団した。 

https://github.com/krallin/tini/issues/180 

別のサードパーティ コンポーネントのにおい男爵は、子プロセス自体がシャットダウンする前に、子プロセスが正常にシャットダウンするのを待つ機能を実現できます。しかし、プロジェクト自体はあまり人気がなく、長期間維持されていません。 

FROM golang as builder
WORKDIR /go/
COPY app.go    .
RUN go build app.go
FROM ubuntu
WORKDIR /root/
COPY --from=builder /go/app .
ADD start.sh /root/
ADD smell-baron /bin/smell-baron
RUN chmod a+x /bin/smell-baron  && chmod a+x start.sh
ENTRYPOINT ["/bin/smell-baron"]
CMD ["/root/start.sh"]

# 构建镜像

docker build -t app:v1.0-smell-baron .

docker run -itd --name app-smell-baron app:v1.0-smell-baron

docker stop  app-smell-baron

进程启动...
退出 terminated
正在退出...
执行清理...
退出完成...

1. コンテナ化されたアプリケーションの起動コマンドには EXEC モードを使用することをお勧めします。

2. アプリケーション自体のコード レベルで正常なシャットダウンを実装しているが、シェル起動スクリプトがある企業の場合は、コンテナ化して k8s にデプロイした後にオプション 1 とオプション 2 を使用することをお勧めします。

3. アプリケーション自体のコード レベルで正常なシャットダウンを実現できない企業の場合は、オプション 3 を使用することをお勧めします。

 GitHub - insidewhy/smell-baron: Docker コンテナー用に C で書かれた小さな init システム。

GitHub - Yelp/dumb-init: Linux コンテナ用の最小限の init システム

https://github.com/krallin/tini

学習ノートとしてインターネットから収集したもの

 

おすすめ

転載: blog.csdn.net/qq_34777982/article/details/134482872