StatefulSetでActive-Standbyなデータベースを動かしてみた話

本記事は[Kubernetes Advent Calendar 2018](https://qiita.com/advent-calendar/2018/kubernetes)の6日目です。
昨日は@nnao45さんの[「kubectl-completion-zshを車輪の再発明した」](https://medium.com/@n4sekai5y/kubectl-completion-zsh%E3%82%92%E8%BB%8A%E8%BC%AA%E3%81%AE%E5%86%8D%E7%99%BA%E6%98%8E%E3%81%97%E3%81%9F-da6c5345263f)でした。
コマンド補完、いいですよね。あれやこれや一々打つの面倒くさいし！

さて私は[「PostgreSQL on Kubernetes 2018（全部俺）」](https://qiita.com/advent-calendar/2018/postgresonk8s)のAdvent Calenderを執筆中ですが、そこでデータベースをStatefulSetで動かしています。
その際に起きた問題や調べたことを書いていきたいと思います。

#TL;DR

+ PostgreSQLをStatefulSetで動かしたら、Failoverしなかったよ。
+ 良く調べたらそれが仕様。
+ operatorを使えるようになるとつよい。

#PostgreSQL on Kubernetes
[こちら](https://qiita.com/tzkoba/items/56fdc60126d4b2350ad5)で述べたように、PostgreSQLをContainer-Nativeな分散ストレージ：[Rook](https://rook.io/)の上で動かすという構成を作ってみました。

![pg-rook-archi.PNG](https://qiita-image-store.s3.amazonaws.com/0/54853/4521ab5d-3e90-34dc-87d3-19e6f6cdcec7.png)

このとき、PostgreSQLのワークロードにはStatefulSetを使っていますが、Active-Standbyの構成を意識し、Replicasは1、つまり起動しているデータベースは常に1台という形にしています。

この構成の特徴は以下となります。

+ PostgreSQLインスタンスは常に1つ、ノード障害時は副系でインスタンスを起動する。
+ データはRookが展開するCephのブロックデバイスに格納。ノードを跨ぐFOにも対応。
+ PostgreSQLのポッドがrecreateされても、Serviceを介することで一意な名称でアクセス

## 問題点：StatefulSetはFailoverしない
既に使っている人はご存知のことかと思うのですが、StatefulSetの基本的な考え方として、ポッドが稼動しているノードがダウンした際に**別ノードでポッドを再起動する**、ということをしません。

つまり、Active-Standbyな構成では必須のクラスタリソースのFailoverがされず、下図のようにPostgreSQLインスタンスが**1つも起動されない状態になります**。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/54853/98c706fc-8913-f322-a158-1fde05d3826d.png)

## 仕様（のよう）です
同じようなことに悩む人は多いようで、本家Kubernetesのgithubにも当問題に関するissueがありました。

[*StatefulSet pod is never evicted from shutdown node*](https://github.com/kubernetes/kubernetes/issues/54368)がそれで、問題に対応したプルリクまで作成されたようですが、それに対して[このようなコメント](https://github.com/kubernetes/kubernetes/issues/54368#issuecomment-339378164)がされています。

>This is by design. When a node goes "down", the master does not know whether it was a safe down (deliberate shutdown) or a network partition. 
>If the master said "ok, the pod is deleted" then the pod could actually be running somewhere on the cluster, thus violating the guarantees of stateful sets only having one pod.
>（Google翻訳）
> これは設計によるものです。ノードが「ダウン」状態になると、マスタは安全ダウン（意図的なシャットダウン）かネットワークパーティションかを認識しません。
>マスターが「OK、ポッドが削除されました」と言った場合、ポッドは実際にクラスタ上のどこかで動作している可能性があり、ポッドが1つしかないステートフルなセットの保証に違反します。

[こちらのブログ](https://blog.thecodeteam.com/2017/08/16/technical-dive-statefulsets-deployments-kubernetes/)でも同じことを言っています。

> Kubernetes doesn’t want a Pod to be immediately restarted on another node to reduce a possible split-brain scenario. 
>However, if the storage provider is capable, pod.Spec.TerminationGracePeriodSeconds can be added to the StatefulSet and the Controller will automatically restart the Pod on a new host after it’s gone into an “Unknown” state.
> （Google翻訳）
>Kubernetesは、スプリットブレインシナリオの可能性を減らすために、別のノードでポッドをすぐに再起動することは望ましくありません。
>ただし、ストレージプロバイダが対応可能であれば、pod.Spec.TerminationGracePeriodSecondsをStatefulSetに追加することができます。コントローラは、「Unknown」状態になった後、自動的に新しいホスト上でPodを再起動します。


## Failoverさせるには
では、このような状態に陥ったらどうすべきかを考えてみましょう。
ポッドが稼動していたノードが停止してしまった場合、以下のような状況になります。

```shell

$ kubectl get node
NAME       STATUS     ROLES     AGE    VERSION
node001    NotReady   worker    15d    v1.10.5
node002    Ready      worker    15d    v1.10.5

$ kubectl get pod
NAME            READY    STATUS     RESTARTS    AGE
pg-rook-sf-0    1/1      Unknown    0           15m
```

ノード：node001が**NotReady**になっており、その上で動いていたポッド：pg-rook-sf-0のSTATUSが**Unknown**になってしまいます。
これを放っておいても、node002でpg-rook-sf-0のポッドが起動することはありません。

node002でpg-rook-sf-0を起動させる方法の一つとして、強制削除する方法があります。

```shell

$ kubectl delete pod pg-rook-sf-0 --force --grace-period=0
warning: Immediate deletion does not wait for confirmation that the running resource has been terminated. The resource may continue to run on the cluster indefinitely.
pod "pg-rook-sf-0" force deleted

$ kubectl get node
NAME       STATUS     ROLES     AGE    VERSION
node001    NotReady   worker    15d    v1.10.5
node002    Ready      worker    15d    v1.10.5

$ kubectl get pod -o wide
NAME            READY   STATUS     RESTARTS   AGE   IP            NODE
pg-rook-sf-0    1/1     Running    0          5s    10.42.6.21    node002
```

ポイントはdeleteコマンドに付けられた **--force --grace-period=0** の2つのオプションです。
このオプションを付けることでKubernetesはポッドにいきなりSIGKILLを送信します。
これにより下記のように動作しているように見えます（詳細はちょっと違うかも）。

+ 応答できないノード上のポッドが強制削除され、管理情報（etcd？）も更新される。
+ ポッド数が0となったことで、ReadyなノードでPostgreSQLインスタンスが起動される。
+ 結果として、Failoverが完了する。

もう一つは上述のブログでも紹介されているしたやり方で、YAMLの中で**pod.Spec.TerminationGracePeriodSecondsに0をセットしておく**ことです。
そうすることでノード障害時に自動でFalioverするようになりますが、こちらはこちらで困ったことになります。
つまり、今回のPostgreSQL on Kubernetesでは**常にデータベースがSIGKILLで即時に落とされてしまう**ことになります（kubectl deleteやapplyでも）。
この方法は[公式ドキュメントでも非推奨](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#deployment-and-scaling-guarantees)となっています。

と言いながら、一応こちらのパラメータも検証してみました。
興味のある方は[こちら](https://qiita.com/tzkoba/items/b412ccd0fd3c7f6a4ab4)をお読み下さい。

## 実際のところはこうすべき

+ データベースなどのステートフルなアプリではStatefulSetを使いましょう。

とは良く言われることですが、これまで書いてきたように課題もあります。（他の課題は別途[Advent Calender](https://qiita.com/advent-calendar/2018/postgresonk8s)内で紹介できればと思っています。）
では、LinuxのHA構成でこれまでCluterProやPacemakerがやってくれていた複雑な処理はどこで扱えばよいのでしょうか。

一つの回答は「**[operator](https://coreos.com/operators/)で実装しましょう**」になります。
StatefulSetで（DeploymentやDaemonsetでも）足りない部分はCustom Controlerで補いましょう、という考え方です。

例えば、Kubecon ChinaでもTiDBという分散データベースの紹介で同じ悩みが述べられています。（資料のPDFは[こちら](https://schd.ws/hosted_files/kccncchina2018english/ab/TiDB%20on%20Kubernetes%20-%20KubeCon.pdf)）

また、mysqlですとOracle謹製の[mysql-operator](https://github.com/oracle/mysql-operator)というものがあります。
とあるコミュニュティで教えて頂いて未利用ですが、backup/restoreなども実装されているようです。
PostgreSQLでもHA構成ではなくレプリケーション構成ですが、Crunchy Dataという会社がoperatorを作っているようです。（紹介資料のPDFは[こちら](https://www.pgconf.asia/JA/2017/wp-content/uploads/sites/2/2017/12/D2-B4.pdf)）

#まとめ
Kubernetesの初心者としては、StatefulSetでFailoverしないという事象は正直バグだと思いました。
しかし、確かに分散システムでsplit brainを恐れる気持ちはDBエンジニアとして良く理解できます。

operatorやCRDによるKubernetesの拡張の必要性も今回の事象を通して強く認識できたので、次はoperator開発にチャレンジしてみようかと思います。

ありがとうございました。