なになれ

IT系のことを記録していきます。

Kubernetesでエラーを返さずにPodを更新するにはwaitするのが解決策だった

Kubernetesでエラーを返さずにPodを更新するにはどうすれば良いか色々検証しました。
結論はPodが終了する直前にwaitするです。

以下、試したことを書きます。
結論だけ知りたい方はこちらからどうぞ。

今回はNode.jsで検証しましたが 、そのほか全般的に当てはまる内容も含まれると思います。

DockerでNode.jsを動かすルールを理解する

Node.jsのDocker imageのドキュメントにベストプラクティスが紹介されています。

github.com

これによると、 DockerでNode.jsを起動するとPID1で起動することになり、SIGTERMなどのシグナルを受け取れないとあります。

下記のようにnodeを起動して停止させてみると数秒経過しないと停止しません。

$ docker run --rm -it --name node-example node:latest 
$ docker stop node-example

これはシグナルを受け取れずに強制終了させているからです。

Dockerの場合、--init オプションをつけることでこの問題を回避できます。

$ docker run --rm -it --name node-example --init node:latest 
$ docker stop node-example

こうするとすぐにnodeが停止します。

また、npm startで起動した場合でもシグナルを受け取れないとあります。

KubernetesのPodが終了するときにはSIGTERMシグナルがコンテナに送られます。
Kubernetesで動かす場合でもシグナルを受け取れるようにする必要があります。

Kubernetesには dockerの --init オプションのようなものはないのですが、PID1として起動しない方法はあります。
それが shareProcessNamespaceです。

kubernetes.io

これらを踏まえて、DockerとKubernetesをセットアップします。

Dockerfile例

FROM node:12-alpine
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD [ "node", "./bin/www" ]

nodeコマンドでDockerイメージを作ります。

Kubernetesyaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  selector:
    matchLabels:
      ingress-app: myapp
  template:
    metadata:
      labels:
        ingress-app: myapp
    spec:
      shareProcessNamespace: true
      containers:
        - name: myapp
          image: hi1280/myapp:0.1
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 3000

shareProcessNamespaceをtrueにします。

SIGTERMシグナルを受け取り終了を試みる

今回はNode.jsでExpressを使用してアプリケーションを作成しています。

単純にSIGTERMシグナルを受け取る(失敗パターンその1)

ソース github.com

単純にshareProcessNamespaceをtrueにしただけです。
これでNode.jsがSIGTERMシグナルを受け取って終了するはずです。

fortioという負荷テストツールを使って、複数回アクセスしながらDeploymentリソースでローリングアップデートをしてみます。

github.com

下記がfortioの実行コマンドです。

$ fortio load -t 0 -qps 32 -c 8 http://localhost:31213

しばらく経った後でコマンド実行を止めると結果が表示されます。
ローリングアップデート後にいくつかのアクセスが失敗していることが分かります。

Code  -1 : 10 (0.9 %)
Code 200 : 1125 (99.1 %)

これはSIGTERMシグナルに反応してすぐにPodが終了してしまうからだと思われます。

アプリ内でSIGTERMシグナルを制御する(失敗パターンその2)

Node.jsでSIGTERMを受け取り、正常に終了するためのライブラリがあったので使用しました。
github.com

ソース github.com

fortioの結果は下記のようになり、見た感じ良さそうです。

Code 200 : 768 (100.0 %)

ただPodのログをみてみると、responseのstatusが不明なログがいくつかあります。
これも失敗していそうです。

myapp-657c67f595-sgj87 myapp GET / 200 503.329 ms - 23
myapp-657c67f595-sgj87 myapp GET / - - ms - -
myapp-657c67f595-sgj87 myapp GET / - - ms - -
myapp-657c67f595-sgj87 myapp GET / - - ms - -
myapp-657c67f595-sgj87 myapp GET / 200 501.409 ms - 23
myapp-657c67f595-sgj87 myapp GET / - - ms - -
myapp-657c67f595-sgj87 myapp GET / 200 500.461 ms - 23
myapp-657c67f595-sgj87 myapp GET / - - ms - -

Podが終了する直前にこのようなログが発生していることが分かりました。

ちなみにPodのログを見るのに stern というツールが重宝しました。

github.com

Podの終了直前でwaitする

最終的にうまく行ったのがwaitするという方法です。

ソース github.com

preStopというPodが終了する直前に呼ばれる処理でsleepコマンドを実行しています。

コンテナライフサイクルフック - Kubernetes

これを実行したところ、statusが不明なログがなくなり、全てのアクセスが200のstatus codeを返す結果になりました。

なぜwaitが必要なのか、下記に詳しい解説があります。
Podが終了する処理とServiceがPodの宛先を切り替える処理は並列に実行されるようです。

qiita.com

このような挙動によって、終了しようとしているPodに対して終了直前までアクセスされる可能性があります。
Podの終了する処理において、wait時間を確保して終了直前のアクセスに対しても処理が完了するように制御する必要があります。

まとめ

色々試しつつ、waitするという方法に行き着きました。
SIGTERMプロセスを受け取るようにして正常終了しようと試みましたが見当違いでした。
Kubernetesの仕組み上こうするしかないのかと思いつつ、辛いやり方だなと思います。

参考資料

Docker で node.js を動かすときは PID 1 にしてはいけない - ngzmのブログ

Kubernetes: 詳解 Pods の終了 - Qiita

docker-node/BestPractices.md at master · nodejs/docker-node · GitHub

Pod内のコンテナ間でプロセス名前空間を共有する - Kubernetes

Health Checks and Graceful Shutdown