KubernetesのStatefulSetを疑ってみたが濡れ衣だった

# 記事を読む前に
* StatefulSetにデータ破損につながる仕様上の問題がないか疑って検証しました
* タイトルにある通り、それ自体は濡れ衣でした
* StatefulSetの挙動についてまとめに記載してあります。そこだけ読んでもOKです
    * ここからまとめの間は延々と疑った内容と検証の内容です

# 前置き
* StatefulSetなるものが目につきました。こいつを使うと冗長構成のDBがKubernetes上に組めるそうです。
* なので調査してみました

# まずは調査
* [訳しながら理解していくKubernetes_ StatefulSets編](https://qiita.com/HirokiSakonju/items/39b86672047509a5b30c)
* [Deploying PostgreSQL Clusters using StatefulSets](https://kubernetes.io/blog/2017/02/postgresql-clusters-kubernetes-statefulsets/)
  * [サンプルyaml](https://github.com/CrunchyData/crunchy-containers/tree/master/examples/kube/statefulset) 
* [Kubernetes PVC と Deployment, StatefulSet の関係を検証してみた](https://qiita.com/sugimount/items/b163b08fe9a3b1b102d9)
  * オートヒーリングするそうです
     * それってまずくないか？:thinking:
     
# オートヒーリングの何が怪しいか
* 障害時、Podが起動状態のまま、同一Podをもう一台起動することがあります。(以降これを多重起動問題と呼びます)
  * [ここ](http://dr-asa.hatenablog.com/entry/2018/04/02/174006)にReplicaSetでのNode障害時の挙動が解説されてます
    * STATUSがUnknownになるとPod立て直しが起きます 
  * kubeletプロセスだけ落ちたりした場合、Pod(Docker上のコンテナ)は動いているにも関わらず、PodのSTATUSがUnknownになりPodの立て直しが起きてしまいます
    * kubelet障害については[ここのおまけ](https://qiita.com/ozota/items/728f0e838817d4ed7399)に書きました
    * DockerのContainerdがハングしてコンテナの状態が取得できないという障害も起きたことがあります
  * DBで多重起動問題が起きると、 同一ボリュームをマウントするDBが2台立つことになります
      * やばそう:thinking:

# 懸念している多重起動問題を図で解説するとこんな感じです
1. 通常時は master は kubeletから状態報告を受け取っています
![image.png](https://qiita-image-store.s3.amazonaws.com/0/304012/0ebd8255-93aa-42ea-b22e-065cc87eca75.png)

2. kubelt障害等で報告が途絶えると、masterはPodの状態を判定できません。実はPodが生きていたとしても知るすべはありません 
![image.png](https://qiita-image-store.s3.amazonaws.com/0/304012/ff3ae25c-5d10-f282-4a58-14c84e60c6cb.png)

3. `STATUS Unknown`判定されると別NodeにPodを立て直します
![image.png](https://qiita-image-store.s3.amazonaws.com/0/304012/3d2e0615-f01d-eeb3-d0bb-f4f15d34cf0f.png)

4. 同一ボリュームをマウントするPodが2台できあがります。アプリによってはこれでデータ破損しかねません
![image.png](https://qiita-image-store.s3.amazonaws.com/0/304012/655d469d-f767-7ab9-4830-ef07333b902a.png)


# 何を確かめればオートヒーリングを信用できるか
* `3.`の多重起動問題は実はStatefulSetでは起きない 

# 検証してみる
## 環境構築
* [[コピペ用]Ubuntu16.04にkubeadmでkubernetesを構築する](https://qiita.com/ozota/items/027befd3fa77a826ea62)

    ```bash:node構成
root@master:~# kubectl get nodes
NAME     STATUS   ROLES    AGE   VERSION
master   Ready    master   26h   v1.12.3
node1    Ready    <none>   25h   v1.12.3
node2    Ready    <none>   25h   v1.12.3
node3    Ready    <none>   25h   v1.12.3
node4    Ready    <none>   25h   v1.12.3
node5    Ready    <none>   24h   v1.12.3
    ```
* [Ubuntu 16.04: NFSサーバを立ち上げる](https://www.hiroom2.com/2016/05/07/ubuntu-16-04%E3%81%ABnfs%E3%82%B5%E3%83%BC%E3%83%90%E3%82%92%E7%AB%8B%E3%81%A1%E4%B8%8A%E3%81%92%E3%82%8B/)

    ```bash:/etc/exports
/opt/nfs1 10.0.0.0/24(rw,sync,no_subtree_check,no_root_squash)
/opt/nfs2 10.0.0.0/24(rw,sync,no_subtree_check,no_root_squash)
/opt/nfs3 10.0.0.0/24(rw,sync,no_subtree_check,no_root_squash)
    ```
## StatefulSet作成
  * [公式ガイド](https://kubernetes.io/docs/tasks/run-application/run-replicated-stateful-application/)に従ってmysqlでやってみます
 
1. PV作成

    ```yaml
    apiVersion: v1
    kind: PersistentVolume
    metadata:
      name: pv1
    spec:
      capacity:
        storage: 11Gi
      volumeMode: Filesystem
      accessModes:
        - ReadWriteOnce
      persistentVolumeReclaimPolicy: Recycle
      storageClassName: slow
      mountOptions:
        - hard
      nfs:
        server: storage
        path: /opt/nfs1
    ```
    * pv2(path: /opt/nfs2), pv3(path: /opt/nfs3)も同様に作成します

2. `kubectl create -f https://k8s.io/examples/application/mysql/mysql-configmap.yaml`
3. `kubectl create -f https://k8s.io/examples/application/mysql/mysql-services.yaml`
4. `kubectl create -f https://k8s.io/examples/application/mysql/mysql-statefulset.yaml`
5. `kubectl get pods -l app=mysql --watch`で作成待機
    * `mysql-0`のStateが`Init:0/2`→`Init:1/2`→`Running` となった後に`mysql-1`が作成され始めます
      * `mysql-2`も同様に`mysql-1`が`Running`になってから作られます
    * すべて完了するのに10分ぐらいかかります

6. 動作確認
    * mysqlコンテナを立てて`mysql-0.mysql`に対してSQL実行します
    * データを流す
    
        ```bash
        kubectl run mysql-client --image=mysql:5.7 -i --rm --restart=Never --\
          mysql -h mysql-0.mysql <<EOF
        CREATE DATABASE test;
        CREATE TABLE test.messages (message VARCHAR(250));
        INSERT INTO test.messages VALUES ('hello');
        EOF
      ```


    * 流したデータを読み取る

    
        ```bash
        root@master:~# kubectl run mysql-client --image=mysql:5.7 -i -t --rm --restart=Never --\
          mysql -h mysql-read -e "SELECT * FROM test.messages"
        If you don't see a command prompt, try pressing enter.
        +---------+
        | message |
        +---------+
        | hello   |
        +---------+
        pod "mysql-client" deleted
        ```    
        * sqlで流した 'hello'が確かに読み取れてます

## 多重起動させてみる
1. Podの配置を確認

    ```bash
root@master:~# kubectl get pods -o wide
NAME      READY   STATUS    RESTARTS   AGE    IP          NODE    NOMINATED NODE
mysql-0   2/2     Running   0          121m   10.44.0.1   node4   <none>
mysql-1   2/2     Running   0          119m   10.42.0.1   node3   <none>
mysql-2   2/2     Running   0          117m   10.35.0.1   node5   <none>
    ```

2. `mysql-0`のいるnode4で`kubelet`を落とす

    ```bash
    root@node4:~# systemctl stop kubelet
    root@node4:~# systemctl status kubelet
    ● kubelet.service - kubelet: The Kubernetes Node Agent
       Loaded: loaded (/lib/systemd/system/kubelet.service; enabled; vendor preset:
      Drop-In: /etc/systemd/system/kubelet.service.d
               └─10-kubeadm.conf
       Active: inactive (dead) since Mon 2018-12-03 05:04:57 UTC; 5s ago 
    ・・・
    ```

3. 待つ
    * デフォルトだとだいたい5分程で`STATUS Unknown`になります
      * [参考](https://fatalfailure.wordpress.com/2016/06/10/improving-kubernetes-reliability-quicker-detection-of-a-node-down/) 

4. 待った結果

    ```bash
root@master:~# kubectl get pods -o wide
NAME      READY   STATUS    RESTARTS   AGE    IP          NODE    NOMINATED NODE
mysql-0   2/2     Unknown   0          142m   10.44.0.1   node4   <none>
mysql-1   2/2     Running   0          140m   10.42.0.1   node3   <none>
mysql-2   2/2     Running   0          138m   10.35.0.1   node5   <none>
    ```
    * STATUS UnknownのままPodが立つ気配はありません
        * どうやら濡れ衣だったようです:sweat_smile:
        
## 仕様を確認してみる
* [Issue](https://github.com/kubernetes/kubernetes/issues/47230#issuecomment-309586908)がありました
  * StatefulSetではPodが`STATUS Unknown`になってもPodを立て直さないよ
  * `kubectl delete pods <pod> --grace-period=0 --force`でUnknownのPodを強制的に消すとPodを立て直すよ
  * これはStatefulSetの仕様なので、自動で立て直ししてほしい場合はDeploymentとか別のやつを使うといいよ 

# まとめ
* 濡れ衣でした！StatefulSet、疑ってすみません:bow_tone1:
* StatefulSetはNode障害等が起きても、管理者がコマンドを打つまではPodの立て直しはしません
  * 言い換えると、Node障害からの復旧は自動化されていません 
  * その代わり多重起動でデータが破損したりすることは防がれています
  * 復旧が自動化されていない点を考えると、StatefulSetは高可用性のためというよりは、障害復旧手順の簡易化と負荷分散がメリットのように思えます
      * mysqlの例だと、読み取りをmysql-1,mysql-2に分散できることです
      * スレーブをマスターに昇格する仕組みを作れば高可用性（ダウンタイム短縮）もいけるかもしれませんが、k8sサービスの切り替えとかいろいろ大変そうです 
* Storageを使う場合で多重起動が起きると困る場合は、StatefulSetを使いましょう
* 自動でPod立て直ししてほしい場合はDeployment等StatefulSet以外を検討しましょう
* さすがGoogle。ちゃんと考えられてます
* （見切り発車せずに先に仕様確認しとけばよかった）