ビビリフクロウの足跡

とあるインフラSEの勉強&備忘ブログ

StatefulSetのノード障害時の挙動と対応

前回、Kubernetesのノード障害時の挙動について記事を書きました。このときはDeploymentを使って挙動を確かめていたですが、前回の記事を見た方から「StatefulSetだと挙動が違ったはず」とのご意見を頂戴しました。

https://twitter.com/tzkb/status/1116597587186814977

そこでStatefulSetだとノード障害時、どんな挙動になるか試してみました。

クラスタの状態は前回の状態からスタートします。

# kubectl get node
NAME                                 STATUS   ROLES    AGE     VERSION
test-cluster-7ajeznlcoium-master-0   Ready    master   2d22h   v1.14.0
test-cluster-7ajeznlcoium-minion-0   Ready    <none>   2d22h   v1.14.0
test-cluster-7ajeznlcoium-minion-1   Ready    <none>   45h     v1.14.0
test-cluster-7ajeznlcoium-minion-2   Ready    <none>   2d16h   v1.14.0

今回はElasticSearchのステートフルセットをデプロイしてみます。KubernetesのGitリポジトリをクローンします。

# cd ~
# git clone https://github.com/kubernetes/kubernetes

Cluster Add-Onからfluentd-elasticsearchディレクトリに入ります。

# cd kubernetes/cluster/addons/fluentd-elasticsearchds

デフォルトではElasticsearchのStatefulSetはvolumeClaimTemplatesを利用するように書かれていないので、以下のようにes-statefulset.yamlを書き換えます。

# Elasticsearch deployment itself
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: elasticsearch-logging
  namespace: kube-system
  labels:
    k8s-app: elasticsearch-logging
    version: v6.6.1
    kubernetes.io/cluster-service: "true"
    addonmanager.kubernetes.io/mode: Reconcile
spec:
  serviceName: elasticsearch-logging
  replicas: 2
  selector:
    matchLabels:
      k8s-app: elasticsearch-logging
      version: v6.6.1
  template:
    metadata:
      labels:
        k8s-app: elasticsearch-logging
        version: v6.6.1
        kubernetes.io/cluster-service: "true"
    spec:
      serviceAccountName: elasticsearch-logging
      containers:
      - image: gcr.io/fluentd-elasticsearch/elasticsearch:v6.6.1
        name: elasticsearch-logging
        resources:
          # need more cpu upon initialization, therefore burstable class
          limits:
            cpu: 1000m
          requests:
            cpu: 100m
        ports:
        - containerPort: 9200
          name: db
          protocol: TCP
        - containerPort: 9300
          name: transport
          protocol: TCP
        volumeMounts:
        - name: elasticsearch-logging
          mountPath: /data
        env:
        - name: "NAMESPACE"
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
      # Elasticsearch requires vm.max_map_count to be at least 262144.
      # If your OS already sets up this number to a higher value, feel free
      # to remove this init container.
      initContainers:
      - image: alpine:3.6
        command: ["/sbin/sysctl", "-w", "vm.max_map_count=262144"]
        name: elasticsearch-logging-init
        securityContext:
          privileged: true
  volumeClaimTemplates:
  - metadata:
      name: elasticsearch-logging
    spec:
      accessModes:
      - ReadWriteOnce
      resources:
        requests:
          storage: 10Gi

ではおもむろにデプロイしてみます。

# kubectl apply -f es-statefulset.yaml
# kubectl get pod -n kube-system -o wide -l k8s-app=elasticsearch-logging
NAME                      READY   STATUS    RESTARTS   AGE     IP             NODE                                 NOMINATED NODE   READINESS GATES
elasticsearch-logging-0   1/1     Running   0          3m34s   10.100.2.221   test-cluster-7ajeznlcoium-minion-2   <none>           <none>
elasticsearch-logging-1   1/1     Running   0          99s     10.100.0.97    test-cluster-7ajeznlcoium-minion-0   <none>           <none>

ここでtest-cluster-7ajeznlcoium-minion-2のkubeletを停止します。

(test-cluster-7ajeznlcoium-minion-2)
# sudo systemctl stop kubelet

すると。。。

# kubectl get pod -n kube-system -o wide -l k8s-app=elasticsearch-logging
NAME                      READY   STATUS        RESTARTS   AGE     IP             NODE                                 NOMINATED NODE   READINESS GATES
elasticsearch-logging-0   1/1     Terminating   0          11m     10.100.2.221   test-cluster-7ajeznlcoium-minion-2   <none>           <none>
elasticsearch-logging-1   1/1     Running       0          9m56s   10.100.0.97    test-cluster-7ajeznlcoium-minion-0   <none>           <none>

StatefulSetの場合は、PodのTerminate処理が完了してから新しいPodが上がるので、kubeletが反応しない以上Terminate処理が完了できず、stuckしてしまうみたいです。

ではどうするか。。。その際の解の一つがTwitter上でご指摘いただいた方から教わったkube-fencingです。

kube-fencingを使うと、NotReadyなノードが生じた場合、その上のk8sオブジェクトを全て削除してノードをフェンシング(強制再起動)してくれます。k8sオブジェクトの削除対象にはNodeオブジェクトも含まれるので、その上に乗ったPodはTerminateどころかetcdから消えます。したがって新しいPodが間違いなく作られるという仕組みで回避しようというのです。

では早速インストールしてみましょう。ソースコードをDLします。

# cd ~
# git clone https://github.com/kvaps/kube-fencing

manifest群が置いてあるディレクトリに移動します。

# cd kube-fencing/examples

私が管理しているノード群はOpenStack上に乗っているので、openstackコマンドを使ってfencingを実現したいと思います。以下のようにfencing-script.yamlを書き換えます。

apiVersion: v1
kind: ConfigMap
metadata:
  name: fencing-scripts
  namespace: fencing
data:
  fence.sh: |
    #!/bin/bash
    NODE="$1"
    source /openstack/openrc-file
    openstack server reboot --hard $1

fencing-scriptに与えるopenrcファイルを用意し、secretオブジェクトを作成します。Secretオブジェクトを作るために、fencingNamespaceも一緒に作ります。

# kubectl create ns fencing
# kubectl create secret generic openrc-file --from-file=openrc-file=openrc-file -n fencing

作ったSecretをマウントするように、またimageもopenstackコマンドがインストールされたものを指定するようにfencing-agents.yamlを書き換えます。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: fencing-agents
  namespace: fencing
spec:
  replicas: 1
  selector:
    matchLabels:
      app: fencing-agents
  template:
    metadata:
      labels:
        app: fencing-agents
    spec:
      nodeSelector:
        node-role.kubernetes.io/master: ""
      tolerations:
      - key: node-role.kubernetes.io/master
        operator: Exists
        effect: NoSchedule
      hostNetwork: true
      containers:
      - name: fencing-agents
        image: bbrfkr0129/kube-fencing-agents
        command: [ "/usr/bin/tail", "-f", "/dev/null" ]
        volumeMounts:
        - name: scripts
          mountPath: /scripts
        - name: openrc-file
          mountPath: /openstack
      volumes:
      - name: scripts
        configMap:
          name: fencing-scripts
          defaultMode: 0744
      - name: openrc-file
        secret:
          secretName: openrc-file
          defaultMode: 0400

manifestを適用します。

# kubectl apply -f .

ノードがNotReady時にfencingしてほしいノードはlabel付けをしておく必要があります。ワーカーノードをラベル付けしておきます。

# kubectl label node test-cluster-7ajeznlcoium-minion-0 fencing=enabled
# kubectl label node test-cluster-7ajeznlcoium-minion-1 fencing=enabled
# kubectl label node test-cluster-7ajeznlcoium-minion-2 fencing=enabled

これでインストール完了です! 早速kubeletを停止させてみましょう。

(test-cluster-7ajeznlcoium-minion-2)
# sudo systemctl stop kubelet

どうなるかというと。。。今度は新しいPodが上がってきません。。。

NAME                      READY   STATUS     RESTARTS   AGE     IP            NODE                                 NOMINATED NODE   READINESS GATES
elasticsearch-logging-0   1/1     Running    0          3m51s   10.100.0.98   test-cluster-7ajeznlcoium-minion-0   <none>           <none>
elasticsearch-logging-1   0/1     Init:0/1   0          53s     <none>        test-cluster-7ajeznlcoium-minion-0   <none>           <none>

describeでログを見ると当然で、PVが違うインスタンスにアタッチされていて、移動できない旨のメッセージが表示されます。しかもたちの悪いことにノードオブジェクトは削除/再作成が行われているため、自分にPVがアタッチされていることを覚えていません。

ではやはりkube-fencingでもStatefulSetは救えないのか。。。というと、実は最後の砦があります。それは

もう諦めて、StatefulSetにはファイルシステムベース(NFSなど)のPVを使う、ということです。こうすれば、PVの取り合いが起きてもどのノードもPVをアタッチ/マウントできるので問題を回避できます。nfs-clientexternal-storage-provisionerなどを使って、興味がある方は試してみてください。

結論としては

  • kube-fencingでもBlock DeviceベースのStatefulSetは救えない。FileSystemベースなら大丈夫。

ということでした。