kubernetes

Kubernetes: 複数の Node を安全に停止する (kubectl drain + PodDisruptionBudget)

More than 1 year has passed since last update.

OS のアップデートやスケールダウンなどで複数の Node を安全に停止したい場合のメモです。この記事は Kubernetes 1.7.3 で確認した情報を元に記載しています。

TL;DR

  • Node を停止するためにはkubectl drain コマンドを利用して停止準備を行います
    • drain は Node を Pod のスケジュール対象から外し (unschedulable に設定) Node 上の Pod を退去させることで停止可能な状態にします
    • しかし複数の Node を停止させる場合、アプリケーションの Pod が一つも動作していない状態 (ready な Pod が 0)がありえるので注意が必要です :fearful:
  • PodDisruptionBudget を定義することで安全な Pod 数を保ったまま複数 Node の drain を行うことができるようになります :smiley:
    • 安全な Pod 数が確保できるまで kubectl drain が Pod の退去を待ってくれるようになります

Node から Pod を退去させる (kubectl drain)

OS のアップデートやスケールダウンなどで Node を停止したい場合、その Node にスケジュールされている Pod に退去してもらう必要があります。この操作は kubctl drain <NODE> というコマンドで簡単に行うことができます。

下記のように Node 名を指定して kubectl drain を実行します。

$ kubectl drain --ignore-daemonsets --force gke-cluster-2-default-pool-0fb8a591-71rf
# Node が unschedulable に設定される (kubectl cordon と同等)
node "gke-cluster-2-default-pool-0fb8a591-71rf" cordoned
WARNING: Deleting pods not managed by ReplicationController, ReplicaSet, Job, DaemonSet or StatefulSet: kube-proxy-gke-cluster-2-default-pool-0fb8a591-71rf; Ignoring DaemonSet-managed pods: fluentd-gcp-v2.0-tt6mf
# この Node にある Pod が退去させられていく
pod "myapp-1243928920-bz02h" evicted
pod "heapster-v1.4.0-1718439912-xmg0k" evicted
# drain が完了
node "gke-cluster-2-default-pool-0fb8a591-71rf" drained

ノードが正しく SchedulingDisabled (unschedulable) になっていることを確認します。動作中の Pod がなくなり、新たに Pod がスケジュールされることもないため、このノードは停止することができます。

$ kubectl get nodes
NAME                                       STATUS                     AGE       VERSION
gke-cluster-2-default-pool-0fb8a591-71rf   Ready,SchedulingDisabled   20m       v1.7.3 # SchedulingDisabled になった
gke-cluster-2-default-pool-0fb8a591-cmcl   Ready                      20m       v1.7.3
gke-cluster-2-default-pool-0fb8a591-d6pn   Ready                      20m       v1.7.3
gke-cluster-2-default-pool-0fb8a591-n1sr   Ready                      20m       v1.7.3

kubectl drain の動作

kubectl drain では主に以下の処理が行われています。

  • Node に新規 Pod がスケジュールされないように unschedulable の設定を行う
  • Node に紐付けられている Pod 一覧を取得して、Pod 群に退去 (evict) の処理が行われる
    • 詳しくは後述の Eviction API をご覧ください
    • evict がサポートされない場合(旧バージョンの API Server など)は単に削除処理になる

動作例

例えば Node が 4台あり、以下のように myapp というアプリケーションが 2 つのノードにある状態だとします。myapp は ReplicaSet (レプリカ数 2) で管理されているとします。

image.png

node-1 に対して kubectl drain を行うと、下記のような順序で処理が行われます。

  1. node-1 を unschedulrable に設定
  2. node-1 に割り当てられた Pod (ここでは myapp-1) が退去(evict)させられます
    • PodDisruptionBudget が設定されていない場合、退去(evict) は単純に Pod が削除されます
    • kubectl の drain はここで終了します
  3. ReplicaSet の働きによって node-3 に新たに Pod が作成され、レプリカ数 2 が保たれます。

image.png

:warning: 注意が必要な場合

続けて node-2 を drain したい場合注意が必要です。新しく作成された myapp-3 の Pod 作成に時間がかかった場合は、node-2 を drain することによって一つも Pod が ready になっていない状況が起こり得ます。

image.png

PodDisruptionBudget による安全な drain

PodDisruptionBudget とは Node を計画的に停止したい場合に、Pod の状況を見ながら退去 (evict) させる機能です。基本的にアプリケーションは ReplicaSet によって複数の Pod が保持されているため冗長性があると考えられます。PodDisruptionBudget は停止状態 (Disruption) として許容できる Pod 数を予算 (Budget) として定義して、その予算内で退去させていきます。(Error Budget 的な名前で洒落ていますね)。

PodDiruptionBudget では以下のどちらかの設定が可能です。

  • .spec.minAvailable: 少なくとも有効であるべき Pod 数。パーセンテージによる指定も可能
  • .spec.maxUnavailable: 最大無効であってもよい Pod 数。パーセンテージによる指定も可能 (v1.7 以上で利用可能)

以下のように PodDisruptionBudget リソースを定義して利用します。kubectl create poddisruptionbudget というサブコマンドによる作成も可能です。

apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
  name: myapp
spec:
  # 最大で無効状態な Pod は 1 に設定。これを超えては退去させられない
  maxUnavailable: 1
  # 対象 Pod のセレクタ
  selector:
    matchLabels:
      run: myapp

これを設定すると前述の例の Node-2 の退去は以下の動作になります。

  1. node-2 を unschedulrable に設定
  2. node-2 に割り当てられた Pod (ここでは myapp-1) を退去(evict)しようとする
    • すでに unavailable の Pod があり PodDisruptionBudget を超えてしまうため退去を待ちます (5 秒間隔でリトライ)

image.png

node-3 の Pod が ready になり PodDisruptionBudget を満たすようになると node-2 の Pod は削除されます。

image.png

PodDisruptionBudget の状況を見る

PodDisruptionBudget リソースには管理されている Pod の状態が記述されています。kubectl get pdb で設定状況と現在停止が許容される Pod 数 (ALLOWED-DISRUPTIONS) を見ることができます。

$ kubectl get pdb
NAME      MIN-AVAILABLE   MAX-UNAVAILABLE   ALLOWED-DISRUPTIONS   AGE
myapp     N/A             1                 1                     2m

詳細な状況は下記のように見ることができます。

$ kubectl get pdb -o yaml myapp
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"policy/v1beta1","kind":"PodDisruptionBudget","metadata":{"annotations":{},"name":"myapp","namespace":"default"},"spec":{"maxUnavailable":1,"selector":{"matchLabels":{"run":"myapp"}}}}
  creationTimestamp: 2017-08-28T07:53:13Z
  generation: 1
  name: myapp
  namespace: default
  resourceVersion: "8859"
  selfLink: /apis/policy/v1beta1/namespaces/default/poddisruptionbudgets/myapp
  uid: f14ed83b-8bc5-11e7-862c-42010af00125
spec:
  maxUnavailable: 1
  selector:
    matchLabels:
      run: myapp
status:
  # ready 状態の Pod 数
  currentHealthy: 10
  # この数を下回っては Evicton できない
  desiredHealthy: 9
  disruptedPods: null
  # 現在停止が許される Pod 数。currentHealthy - desiredHealthy で計算される
  disruptionsAllowed: 1
  expectedPods: 10
  observedGeneration: 1

補足: Eviction (退避) API

kubectl drain では内部的には Pod の削除(DELETE)ではなく、Eviction API という Pod のサブリソースを呼び出して Node に紐づく Pod の退避を行っています。Pod の削除との違いは PodDiruptionBudget を参照し予算を満たさない場合は削除せずに 429 Too Many Requests を返す点です。

Eviction サブリソースは Pod のサブリソースとして以下のように定義されています。

/api/v1/namespaces/<NAMESPACE>/pods/<POD>/eviction

この API に対して以下のような Eviction サブリソース を POST します。

{
  "kind": "Eviction",
  "apiVersion": "policy/v1beta1",
  "metadata": {
    "name": "myapp-1243928920-bz02h", # POD 
    "namespace": "default",
    "creationTimestamp": null
  },
  "deleteOptions": {}
}

Eviction API は PodDiruptionBudget を参照し、定義した予算を満たしている場合は Pod の削除して HTTP の 201 Created、満たしていない場合は削除せず HTTP の 429 Too Many Requests を返します。

# kubectl の verbose ログ
I0828 14:12:38.049411   29825 round_trippers.go:405] POST https://35.194.250.38/api/v1/namespaces/default/pods/myapp-1243928920-cpmcr/eviction 429 Too Many Requests in 39 milliseconds

kubectl drain では 429 Too Many Requests のときは 5 秒ごとにリトライする実装になっており、予算を満たすかタイムアウト(--timeout の値)になるまで待ち続けます。(参考: drain.go#L500)

PodDisruptionBudget の動作を試してみる

PodDisruptionBudget を実際に試してみます。ここでは ready になるまでに時間がかかる(2分)アプリケーションをデプロイし、maxUnavailable を 1 に設定しています。(使用した Deployment と PodDisruptionBudget のマニフェスト)。下記のように Pod が 2 つの Node にデプロイされている状態から始めます。Node は全体で 4 つあります。

$ kubectl get pods -o wide
NAME                     READY     STATUS    RESTARTS   AGE       IP         NODE
myapp-1871887261-k5v4s   1/1       Running   0          2m        10.8.0.7   gke-cluster-1-default-pool-94337df7-lnc5
myapp-1871887261-x23z5   1/1       Running   0          2m        10.8.2.6   gke-cluster-1-default-pool-94337df7-sfp4

Pod がデプロイされている 2 Node に対して同時に drain を実行してみます。

$ kubectl drain --ignore-daemonsets --force gke-cluster-1-default-pool-94337df7-lnc5
$ kubectl drain --ignore-daemonsets --force gke-cluster-1-default-pool-94337df7-sfp4

ReplicaSet を watch して drain 中の Pod の状態を見てます。想定どおり PodDisruptionBudget のおかげで ready の Pod が常にある状態を保っています。Node の一つは drain がすぐに終わり、もう一つの Node は drain が PodDisruptionBudget で定義した Pod のready を待つために約 2 分間かかりました。

# DESIRED : ReplicaSet で定義したレプリカ数
# CURRENT : Pod 数 (ステータスは関係なし)
# READY   : ready 状態の Pod 数

$ kubectl get rs -w
NAME               DESIRED   CURRENT   READY     AGE
myapp-1871887261   2         2         2         3m # drain 実行前
myapp-1871887261   2         1         1         3m # ひとつめの Node の drain によって Pod が減る
myapp-1871887261   2         2         1         3m # ReplicaSet によって Pod が増えるが ready にはならない
# ready を待つためにここで約 2分間かかる
myapp-1871887261   2         2         2         5m # 2 つready になったので ふたつめの Node も drain できる
myapp-1871887261   2         1         1         5m # ふたつめの Node の drain によって Pod が減る
myapp-1871887261   2         2         1         5m # ReplicaSet によって Pod が増えるが ready にはならない
myapp-1871887261   2         2         2         7m # 2 分たってすべてが ready になる

参考