Kubernetes上でMariaDB Galera Clusterを試す

# MariaDB Galera Clusterとは？

- [What is MariaDB Galera Cluster? - MariaDB Knowledge Base](https://mariadb.com/kb/en/library/what-is-mariadb-galera-cluster/)

MariaDB Galera Clusterは同期式のマルチマスタ構成のMariaDBクラスタです。MariaDB10.1以降でデフォルトで利用できる機能です。

技術的には[Galera Cluster for MySQL](http://galeracluster.com/)を使っているようです。

# Kubernetes上にGalera Clusterをデプロイする

Kubernetes上にGalera Clusterをデプロイして、その動作を確認してみます。

OpenShift向けに作られている[adfinis-sygroup/openshift-mariadb-galera](https://github.com/adfinis-sygroup/openshift-mariadb-galera)がよくできているので、これを参考にすることにします。

# Manifest

[adfinis-sygroup/openshift-mariadb-galera](https://github.com/adfinis-sygroup/openshift-mariadb-galera)に書かれているマニフェストは少し古いので、Kubernetes 1.13系でも動くように変更しました。
またpodのaffinityを設定し、なるべく同じNodeにはPodが配置されないようにしています。
加えてPodDisruptionBudgetsを設定し、drain時に一気にPodが削除されないようにしています。

```yaml
apiVersion: v1
kind: Namespace
metadata:
  name: galera
---
apiVersion: v1
kind: Service
metadata:
  name: galera
  namespace: galera
  labels:
    app: galera
spec:
  ports:
  - port: 3306
    name: galera
  clusterIP: None
  publishNotReadyAddresses: true
  selector:
    app: galera
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: galera
  namespace: galera
spec:
  serviceName: "galera"
  replicas: 3
  selector:
    matchLabels:
      app: galera
  template:
    metadata:
      labels:
        app: galera
    spec:
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
              labelSelector:
                matchExpressions:
                - key: app
                  operator: In
                  values:
                  - galera
              topologyKey: kubernetes.io/hostname
      securityContext:
        runAsUser: 27
        fsGroup: 27
      containers:
      - name: galera
        image: adfinissygroup/k8s-mariadb-galera-centos:v004
        imagePullPolicy: Always
        securityContext:
          runAsNonRoot: true
        ports:
        - containerPort: 3306
          name: mysql
        - containerPort: 4444
          name: sst
        - containerPort: 4567
          name: replication
        - containerPort: 4568
          name: ist
        resources:
          requests:
            cpu: 500m
            memory: 4Gi
        readinessProbe:
          exec:
            command:
              - /bin/sh
              - -ec
              - >-
                mysql -u${MYSQL_USER} -p${MYSQL_PASSWORD} -h localhost -e "show status where Variable_name = 'wsrep_ready' and Value = 'ON'"
          initialDelaySeconds: 15
          timeoutSeconds: 5
        volumeMounts:
        - name: datadir
          mountPath: /var/lib/mysql
          subPath: data
        env:
          - name: MYSQL_USER
            value: user
          - name: MYSQL_PASSWORD
            value: password
          - name: POD_NAMESPACE
            valueFrom:
              fieldRef:
                apiVersion: v1
                fieldPath: metadata.namespace
      volumes:
      - name: datadir
        emptyDir: {}
---
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
  name: galera
  namespace: galera
spec:
  maxUnavailable: 1
  selector:
    matchLabels:
      app: mysql
```

# adfinissygroup/k8s-mariadb-galera-centos をみてみる

今回利用しているDocker Imageである、adfinissygroup/k8s-mariadb-galera-centosの動作についてもう少しみていきましょう。

Dockerfileはここにあります

- [openshift-mariadb-galera/k8s-mariadb-galera-centos at master · adfinis-sygroup/openshift-mariadb-galera · GitHub](https://github.com/adfinis-sygroup/openshift-mariadb-galera/tree/master/k8s-mariadb-galera-centos)

その中を見るとENTRYPOINTとして`/usr/bin/container-entrypoint.sh`を呼び出しています。

その実体は[こちら](https://github.com/adfinis-sygroup/openshift-mariadb-galera/blob/master/k8s-mariadb-galera-centos/root/usr/bin/container-entrypoint.sh)

特に

```sh
/usr/bin/peer-finder -on-start="${CONTAINER_SCRIPTS_DIR}/configure-galera.sh" -service=${K8S_SVC_NAME}
```
が気になる処理です。

ここで利用しているpeer-finderは

- [contrib/peer-finder at master · kubernetes/contrib · GitHub](https://github.com/kubernetes/contrib/tree/master/peer-finder)

これです。
これは、StatefulSetと組み合わせて、クラスタリングを行う時に便利なアプリケーションです。
peer-finder実行時に引数のon-startに指定したスクリプトにPeerの情報が送られます。
他にもon-changeなど指定することができますが、ここではon-startのみを使っているようです。

peer-finderから呼び出される[configure-galera.sh](https://github.com/adfinis-sygroup/openshift-mariadb-galera/blob/master/k8s-mariadb-galera-centos/root/usr/share/container-scripts/mysql/configure-galera.sh)をみてみます。

このスクリプトは、標準入力から渡ってくるPeerの情報を読み込み、Galeraのクラスタ設定情報(`/etc/my.cnf.d/galera.cnf`)を更新しています。

Replicaが3のときの挙動を例に説明すると

- はじめのPod(galera-0)が起動するとき、Peerは`galera-0`のみ
- 2つ目Pod(galera-1)が起動するとき、Peerは`galera-0,galera-1`
- 3つ目Pod(galera-2)が起動するとき、Peerは`galera-0,galera-1,galera-2`

ということで、それぞれのPodに設定されるPeerの情報が徐々に増えていくようになります。
Galeraは、その設定されたPeerの設定を元にクラスタを組むため、最終的に、3つのPodで構成されるクラスタが完成します。

# デプロイしてみる

先ほどのマニフェストをデプロイします。
完全にデプロイできると次のようになります

```console
$ kubectl get pods -n galera
NAME       READY   STATUS    RESTARTS   AGE
galera-0   1/1     Running   0          24m
galera-1   1/1     Running   0          22m
galera-2   1/1     Running   0          21m
```
図にするとこんな感じです
![image.png](https://qiita-image-store.s3.amazonaws.com/0/100375/325a6ae3-bd9c-a314-cff8-f776e9d39574.png)

データベースにアクセスするには　Serviceで指定した名前のホスト`galera`にアクセスします。
rootのパスワードは無指定となっています。

```console
$ kubectl run client -n galera --image=mariadb:10.1 -it --rm --restart=Never -- mysql -u root -h galera -e "show databases;"
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
+--------------------+
pod "client" deleted
```

# データを書き込む
galera構成のMariaDBは、MySQL互換です。ということで、普通にmysqlコマンドでデータベースとテーブルを作成し、適当なレコードを挿入しておきます。

```console
$ kubectl run client -n galera --image=mariadb:10.1 -it --rm --restart=Never bash
If you don't see a command prompt, try pressing enter.
root@client:/# mysql -u root -h galera
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 275
Server version: 10.1.31-MariaDB MariaDB Server

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]> create database inajob;
Query OK, 1 row affected (0.00 sec)

MariaDB [(none)]> use inajob
Database changed
MariaDB [inajob]> create table hoge(id int auto_increment not null primary key, value varchar(20));
Query OK, 0 rows affected (0.00 sec)
MariaDB [inajob]> insert hoge(value) values ("hello");
Query OK, 1 row affected (0.00 sec)

MariaDB [inajob]> insert hoge(value) values ("world");
Query OK, 1 row affected (0.00 sec)

MariaDB [inajob]> select * from hoge;
+----+-------+
| id | value |
+----+-------+
|  5 | hello |
|  8 | world |
+----+-------+
2 rows in set (0.00 sec)

MariaDB [inajob]> exit
Bye
root@client:/# exit
exit
pod "client" deleted
```

# Podを削除する

3個あるうちのPodを一つDeleteします。

```console
$ kubectl get pods -n galera
NAME       READY   STATUS    RESTARTS   AGE
galera-0   1/1     Running   0          51m
galera-1   1/1     Running   0          50m
galera-2   1/1     Running   0          49m

$ kubectl delete pods -n galera  galera-0
pod "galera-0" deleted
```

Kubernetesのセルフヒーリングにより、Podは再生成されます。
下のように`-w`をつけてwatchしていると、Podがdeleteされたのちに、再生成されている様子がわかります。
再生成後はデータを持っているPodから自動的にデータをコピーしてくるため、DBに入っているデータ量に比例して復旧時間がかかるでしょう。

```console
$ kubectl get pods -n galera -w
NAME       READY   STATUS    RESTARTS   AGE
galera-0   1/1     Running   0          52m
galera-1   1/1     Running   0          50m
galera-2   1/1     Running   0          49m
galera-0   1/1   Terminating   0     52m
galera-0   0/1   Terminating   0     52m
galera-0   0/1   Terminating   0     52m
galera-0   0/1   Terminating   0     52m
galera-0   0/1   Pending   0     1s
galera-0   0/1   Pending   0     1s
galera-0   0/1   ContainerCreating   0     1s
galera-0   0/1   Running   0     4s
galera-0   1/1   Running   0     46s
```

# 書き込んだデータが残っているか確認する

Podが一つ再生成されましたが、中のデータは大丈夫でしょうか？

```console
$ kubectl run client -n galera --image=mariadb:10.1 -it --rm --restart=Never -- mysql -u root -h galera -e "select * from hoge;" inajob
+----+-------+
| id | value |
+----+-------+
|  5 | hello |
|  8 | world |
+----+-------+
pod "client" deleted
```

同じデータがかえってきました。
3つあるうちの1つのPodが再生成されてもデータが消えていないようです。Galera構成では3つのPodでデータをそれぞれ持っており、新しくクラスタに参加するPodに対しては、まずデータを同期することを行うからです。

# Pod削除、復旧までの間DBにアクセスできるのか？

単にPodを削除し、再生成後、アクセスできることはわかりましたが、2つになっている状態ではアクセスできるのでしょうか？

試してみましょう。

```console
$ kubectl run client -n galera --image=mariadb:10.1 -it --rm --restart=Never bash
If you don't see a command prompt, try pressing enter.
root@client:/# while :; do date;mysql -e "select count(*) from hoge;" -h galera -u root inajob; sleep 1; done
Wed Feb 13 05:04:26 UTC 2019
+----------+
| count(*) |
+----------+
|        2 |
+----------+
Wed Feb 13 05:04:27 UTC 2019
+----------+
| count(*) |
+----------+
|        2 |
+----------+
...
(以下繰り返し)
```

このように継続的にアクセスをしている状態で、Podを1つ削除してみます。


```console
（略）
Wed Feb 13 05:05:40 UTC 2019
+----------+
| count(*) |
+----------+
|        2 |
+----------+
Wed Feb 13 05:05:41 UTC 2019  # <---- PodをDeleteした瞬間たまに接続が失敗することがある
ERROR 2013 (HY000): Lost connection to MySQL server at 'reading initial communication packet', system error: 104 "Connection reset by peer"
Wed Feb 13 05:05:44 UTC 2019
+----------+
| count(*) |
+----------+
|        2 |
+----------+
Wed Feb 13 05:05:45 UTC 2019
+----------+
| count(*) |
+----------+
|        2 |
+----------+
Wed Feb 13 05:05:46 UTC 2019
+----------+
| count(*) |
+----------+
|        2 |
+----------+
Wed Feb 13 05:05:57 UTC 2019  # <--- ここだけすごく時間がかかっている
+----------+
| count(*) |
+----------+
|        2 |
+----------+
Wed Feb 13 05:05:58 UTC 2019 # <---- このタイミングでは、Podは2つしか存在しないが問題なくアクセスができている
+----------+
| count(*) |
+----------+
|        2 |
+----------+
（略）
```

ログを抜粋しましたが、このように、PodをDeleteした瞬間にコネクションが切れることが確認できました。これはアプリケーション側にリトライの処理を書いておくことで対応できそうです。

また、PodがDeleteしてから10秒間ほどアクセスできない時間が発生しています。これは、Podが1ついなくなることで、Glaeraクラスタがクラスタからメンバーを1つ削除すべき　と判断するためにかかる時間のようで、各種パラメータの調整で改善させることもできると思われます。（http://galeracluster.com/documentation-webpages/recovery.html#detecting-single-node-failures）

1つPodがDeleteされ、合計2つのPodになった際も、DBにはアクセスできています。
また、Podが再生成され、データの同期をしている間は、データベースがサービスを開始しないため、readinessProbeで指定したコマンドが失敗します。そうするとKubernetesのServiceによるロードバランス対象には含まれないので、準備ができていないPodにアクセスが行くようなことはないです。

# まとめ

このようにGalera構成を使うことで、1つのPodが落ちてもデータが消えないRDBMSを実現することができます。
この仕組みをうまく使うことで、（数秒間の停止は伴いますが、）Podのローリングアップデートを実現したり、マシンの障害時でもデータを失わないようなデータベースを実現することができます。
Kubernetesでデータを保持するアプリケーションの運用は難しいイメージがありますが、データの同期周りのややこしい部分をGalera構成に任せることで、ある程度は簡単になるのではないかと考えています。

実際的なデータを扱う際には、Podを消して新しく作るたびに、既存のPodからデータを同期することが時間的にもネットワーク帯域的にもコストになるため、PersistentVolumeなどのデータ永続化手法と組み合わせて運用することになります。
また、MySQL互換ではあるものの[いくつかの制限](https://mariadb.com/kb/en/library/mariadb-galera-cluster-known-limitations/#limitations-from-codershipcom)もあるので、利用の際は十分に検討が必要です。