Help us understand the problem. What is going on with this article?

Kubernetes上でMariaDB Galera Clusterを試す

MariaDB Galera Clusterとは?

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

技術的にはGalera Cluster for MySQLを使っているようです。

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

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

OpenShift向けに作られているadfinis-sygroup/openshift-mariadb-galeraがよくできているので、これを参考にすることにします。

Manifest

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

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はここにあります

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

その実体はこちら

特に

/usr/bin/peer-finder -on-start="${CONTAINER_SCRIPTS_DIR}/configure-galera.sh" -service=${K8S_SVC_NAME}

が気になる処理です。

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

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

peer-finderから呼び出される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で構成されるクラスタが完成します。

デプロイしてみる

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

$ 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

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

$ 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コマンドでデータベースとテーブルを作成し、適当なレコードを挿入しておきます。

$ 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します。

$ 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に入っているデータ量に比例して復旧時間がかかるでしょう。

$ 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が一つ再生成されましたが、中のデータは大丈夫でしょうか?

$ 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つになっている状態ではアクセスできるのでしょうか?

試してみましょう。

$ 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つ削除してみます。

(略)
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互換ではあるもののいくつかの制限もあるので、利用の際は十分に検討が必要です。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away