なになれ

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

KubernetesでResource requestsとlimitsを最適な値にする方法

Kubernetesでは、ContainerのCPUやMemoryの使用量を設定するためのResource requestsとlimitsの機能があります。
これらの値をどのように決めるかはなかなか難しいのではないかと思います。
ただKubernetesの運用において、Resource requestsとlimitsの設定は重要です。
設定値が低すぎると実際のリソース使用量は予定以上になってしまい、リソースが不足することでPodが正常に起動しなくなる可能性があります。
設定値が高すぎると実際のリソース使用量は余裕がある状態になり、リソースの無駄が発生します。
Resource requestsとlimitsを最適な値にする必要があります。

要約

  • Vertical Pod Autoscaler(VPA)が提案する推奨値をPodのResource requestsとlimitsに設定する

概要

VPAはPodのリソース使用量を自動でスケールアップするための機能です。
ただ現時点では自動でPodのスケールアウトを行うHorizontal Pod Autoscaler(HPA)と併用ができないです。
Podのスケールアウトを利用しないのは可用性の観点からして現実的ではないので、HPAを利用しつつ、最適なリソース使用量を手動で設定するのが望ましいのではないかと思います。
VPAを実運用で利用するのではなく、最適なPodのリソース使用量を確認するためのものとして利用します。

方法

前提条件

Vertical Pod Autoscalerをインストールする

VPAのリポジトリを取得する。

$ git clone https://github.com/kubernetes/autoscaler.git

VPAをインストールする。

$ ./hack/vpa-up.sh

Goldilocksをインストールする

GoldilocksはVPAによる推奨値の確認を支援するためのツールです。

Goldilocksのリポジトリを取得する。

$ git clone https://github.com/FairwindsOps/goldilocks.git

Goldilocksをインストールする。

$ kubectl create namespace goldilocks
$ kubectl -n goldilocks apply -f hack/manifests/controller
$ kubectl -n goldilocks apply -f hack/manifests/dashboard

Goldilocksを適用するnamespaceを設定します。
ここでは、defaultのnamespaceを指定しています。

$ kubectl label ns default goldilocks.fairwinds.com/enabled=true

Resourceの推奨値を確認する

ここでは例として、Deploymentを以下のように用意します。

hamster.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hamster
spec:
  selector:
    matchLabels:
      app: hamster
  replicas: 2
  template:
    metadata:
      labels:
        app: hamster
    spec:
      containers:
        - name: hamster
          image: k8s.gcr.io/ubuntu-slim:0.1
          resources:
            requests:
              cpu: 100m
              memory: 50Mi
          command: ["/bin/sh"]
          args:
            - "-c"
            - "while true; do timeout 0.5s yes >/dev/null; sleep 0.5s; done"

applyします

$ kubectl apply -f hamster.yaml

Goldilocksのダッシュボードを表示して、推奨値を確認します。

$ kubectl -n goldilocks port-forward svc/goldilocks-dashboard 8080:80
$ open http://localhost:8080/

f:id:hi1280:20200105221750p:plain:w800

Suggested Changesの欄で推奨値をコピーすることができます。

※ここでは、単にPodが動いている状態での値を推奨値としていますが、WebサービスのPod等は想定される負荷をかけて推奨値を確認する必要があると思われます

Resourceの推奨値を設定する

推奨値をdeploymentのyamlにコピペします。

hamster.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hamster
spec:
  selector:
    matchLabels:
      app: hamster
  replicas: 2
  template:
    metadata:
      labels:
        app: hamster
    spec:
      containers:
        - name: hamster
          image: k8s.gcr.io/ubuntu-slim:0.1
          resources:
            limits:
              cpu: 587m
              memory: 262144k
            requests:
              cpu: 587m
              memory: 262144k
          command: ["/bin/sh"]
          args:
            - "-c"
            - "while true; do timeout 0.5s yes >/dev/null; sleep 0.5s; done"

applyします

$ kubectl apply -f hamster.yaml

ダッシュボードを確認すると値が同一になっていることが確認できます。
f:id:hi1280:20200105223419p:plain:w800

ここまでの手順でResourceの推奨値に設定することができました。

まとめ

今回の方法は実はGoldilocksというツールの利用方法をなぞっただけです。
GoldilocksというツールはResource requestsとlimitsをどのような値に設定すれば良いかという課題から生まれたツールのようです。
現状はVPAとHPAが併用できないため、Resource requestsとlimitsを手動で設定しなければならず、今回のような方法になっています。
近い将来、Kubernetesの仕組みとして自動でResourceの値が決まるようになるかもしれません。

参考

autoscaler/vertical-pod-autoscaler at master · kubernetes/autoscaler · GitHub

GitHub - FairwindsOps/goldilocks: Get your resource requests "Just Right"

Introducing Goldilocks : A Tool for Recommending Resource Requests

2019年で学んだ技術

2019年を振り返る内容です。
半年過ぎたころからAWSメインのインフラ業務が多くなってきたので、そのあたりで学ぶことが変わったりしました。

AWS

Webバックエンド

Webフロントエンド

インフラ

コンテナ

チーム開発

まとめ

来年も引き続きAWSKubernetesあたりを中心に学習していく所存です

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