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 になる


参考