OpenShift4+MySQL8.0のActive-Standby構成

[昨日構成したCentOS 8上でのMySQL 8.0 Active-Standby構成](https://qiita.com/rk05231977/items/486f98abda0d3dcf93a3)をOpenShift 4の上でやってみる。
まあ、昨日の記事の焼き直しだが新規性はこちらの方があるだろう。

改めて、今回説明する構成の良いところは使用しているサーバー側コンポーネントがすべてOpenShift 4がサポートするカバー範囲内となる点にある。
→ OpenShiftのサポート範囲に「RHEL Software Collections and RHT SSO Common Service」が含まれている。
https://docs.openshift.com/container-platform/4.6/welcome/oke_about.html
→ RHEL Software CollectionsにMySQL 8.0が含まれている。
https://developers.redhat.com/products/softwarecollections/overview

なお、今回使用するMySQLの細かいバージョンは、昨日の記事と同じ8.0.21である。

また、前回記事と同じくフェールオーバーの切り替えの自動化には触れていない。それでも手順は十分機械的なので、少し頑張るくらいでフェールオーバー自動化は可能だろう（「フェールオーバーしてみる」の手順２～５が自動化できれば良いのだ）。

# 構成
OpenShiftクラスターの作成については以下の記事を参照。最近Master３台構成に作り替えたのだが、まあ似たようなものだ。
https://qiita.com/rk05231977/items/ec1626c1223dddffc98a

今回の構成ではMySQLサーバーの開閉塞はNodePortで行うのだが、bastionサーバー経由でないとクラスターにアクセスできないため、やはりHAProxyは使用する。

ストレージは、簡単だったのでbastionサーバー上のNFSストレージを使用している。使えるならmysql1、2のPodが稼働するノードのローカルストレージか、iSCSI等のブロックストレージを使った方が良いだろう。

シェアードナッシングなのでmysql1とmysql2が実行される物理ノードは遠隔に配置できる。マルチAZ単一クラスター構成なら本記事に書いてあるままで、DRでもちょっと頑張れば動作するだろう。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146381/6624da8f-8002-f17c-6bfe-7e5c4d238d44.png)

# nfsストレージとインターナルレジストリを準備する

１．以下の記事のうち、「NFSサーバーを作る」、「Image Registryを設定する」を参照し、それらを構成する。
https://qiita.com/rk05231977/items/59027862b59207a5e61f

２．mysql用のnfsストレージを作る。

```
# mkdir -p /export/mysql1 /export/mysql2
# chmod 777 /export/mysql1 /export/mysql2
# echo "/export 192.168.1.0/24(rw,no_root_squash)" >> /etc/exports
# exportfs -r
```

# MysqlのActive-Standbyクラスターを作る

１．bastionサーバーにログインし、mysqlクラスターのyamlファイルを作る。
Infrastructure as Codeの世界へようこそ。

```yaml:mysql.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-mysql
provisioner: kubernetes.io/no-provisioner
reclaimPolicy: Delete
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: mysql1
  labels:
    instance-for: mysql1
spec:
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteOnce
  volumeMode: Filesystem
  persistentVolumeReclaimPolicy: Delete
  storageClassName: nfs-mysql
  mountOptions:
    - hard
    - nfsvers=4.1
  nfs:
    path: /export/mysql1
    server: 192.168.1.1
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: mysql2
  labels:
    instance-for: mysql2
spec:
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteOnce
  volumeMode: Filesystem
  persistentVolumeReclaimPolicy: Delete
  storageClassName: nfs-mysql
  mountOptions:
    - hard
    - nfsvers=4.1
  nfs:
    path: /export/mysql2
    server: 192.168.1.1
---
apiVersion: v1
kind: Namespace
metadata:
  name: mysql
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql1
  namespace: mysql
spec:
  accessModes:
    - ReadWriteOnce
  volumeMode: Filesystem
  resources:
    requests:
      storage: 5Gi
  storageClassName: nfs-mysql
  selector:
    matchLabels:
      instance-for: mysql1
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql2
  namespace: mysql
spec:
  accessModes:
    - ReadWriteOnce
  volumeMode: Filesystem
  resources:
    requests:
      storage: 5Gi
  storageClassName: nfs-mysql
  selector:
    matchLabels:
      instance-for: mysql2
---
apiVersion: v1
kind: Service
metadata:
  name: mysql1
  namespace: mysql
  labels:
    app: mysql1
spec:
  ports:
  - port: 3306
    name: mysql
  clusterIP: None
  selector:
    app: mysql1
---
apiVersion: v1
kind: Service
metadata:
  name: mysql2
  namespace: mysql
  labels:
    app: mysql2
spec:
  ports:
  - port: 3306
    name: mysql
  clusterIP: None
  selector:
    app: mysql2
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql1
  namespace: mysql
spec:
  selector:
    matchLabels:
      app: mysql1
  serviceName: mysql1
  replicas: 1
  template:
    metadata:
      labels:
        app: mysql1
    spec:
      terminationGracePeriodSeconds: 10
      containers:
      - name: mysql1
        image: image-registry.openshift-image-registry.svc:5000/mysql/mysql-80
        imagePullPolicy: Always
        ports:
        - containerPort: 3306
          name: mysql
        env:
        - name: MYSQL_ROOT_PASSWORD
          value: password
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql/data
      volumes:
      - name: data
        persistentVolumeClaim:
          claimName: mysql1
      nodeSelector:
        kubernetes.io/hostname: node1
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql2
  namespace: mysql
spec:
  selector:
    matchLabels:
      app: mysql2
  serviceName: mysql2
  replicas: 1
  template:
    metadata:
      labels:
        app: mysql2
    spec:
      terminationGracePeriodSeconds: 10
      containers:
      - name: mysql2
        image: image-registry.openshift-image-registry.svc:5000/mysql/mysql-80
        imagePullPolicy: Always
        env:
        - name: MYSQL_ROOT_PASSWORD
          value: password
        ports:
        - containerPort: 3306
          name: mysql
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql/data
      volumes:
      - name: data
        persistentVolumeClaim:
          claimName: mysql2
      nodeSelector:
        kubernetes.io/hostname: node2
```

２．yamlファイルをOpenShiftクラスターにデプロイする。

```
# oc create -f mysql.yaml
```

とりあえず、mysqlネームスペースにmysql1とmysql2という２つのStatefulSetが出来上がる。

インターナルレジストリにmysql-80のイメージをpushしていないので、上記をデプロイした直後にPodは起動しない。この後すぐの手順でイメージは配置する。

外部からのアクセスにNodePortが必要だが、それはまだ作っていない。

３．mysql-80のコンテナイメージをインターナルレジストリに配置する。
以下、説明は雑だが、registry.redhat.ioからイメージをpullしてインターナルレジストリに配置する。
https://catalog.redhat.com/software/containers/rhel8/mysql-80/5ba0ad4cdd19c70b45cbf48c

```
# podman login registry.redhat.io
# podman pull registry.redhat.io/rhel8/mysql-80

# oc login -u admin -p admin --server=https://api.ocp.example.com:6443
# oc port-forward svc/image-registry -n openshift-image-registry 5000 &

# podman login -u admin -p $(oc whoami -t) localhost:5000 --tls-verify=false
# podman tag registry.redhat.io/rhel8/mysql-80 localhost:5000/mysql/mysql-80
# podman push localhost:5000/mysql/mysql-80 --tls-verify=false
```

４．mysqlのPodを削除し、作り直す。

```
# oc delete pod -n mysql mysql1-0 mysql2-0
```

５．mysqlが起動したのを確認する。
Podのログの最後に「ready for connections. Version: '8.0.21'」と表示されることを確認する。

```
# oc logs -f -n mysql mysql1-0
# oc logs -f -n mysql mysql2-0

（実行結果）
[root@bastion mysql]# oc logs -f -n mysql mysql1-0
...
2021-01-24T12:01:42.398021Z 0 [System] [MY-010931] [Server] /usr/libexec/mysqld: ready for connections. Version: '8.0.21'  socket: '/var/lib/mysql/mysql.sock'  port: 3306  Source distribution.
```


# mysql1を構成する

１．何もデータが無いのもつまらないので、国データベースを初期データとしてロードしようか。

```
# curl -k -O -L https://downloads.mysql.com/docs/world.sql.gz
# gunzip world.sql.gz
# oc exec -i -n mysql mysql1-0 -- mysql -u root < world.sql
```

２．以降、oc exec経由でmysql1のインスタンスを構成するが、長たらしいコマンドを避けるため短縮コマンドを定義する。

```
# mysql1="oc exec -n mysql mysql1-0 -- mysql -u root"
```

３．MySQL Workbenchから繋ぐ用のadminユーザーを作る。

```
# $mysql1 -e "CREATE USER 'admin'@'%' IDENTIFIED BY 'password';"
# $mysql1 -e "GRANT ALL ON *.* TO 'admin'@'%';"
```

４．ここからがレプリケーション用の構成。
https://dev.mysql.com/doc/refman/8.0/en/binlog-replication-configuration-overview.html

MySQLにserver-idを設定し、レプリケーション用のユーザーを作る。
server_idは、前回記事を引き継いで201を設定しようか。
レプリケーション元のIPアドレスを特定することが難しいので、replのホスト名は%である。
あと、仮想マシンで試したときは要らなかったと思うがコンテナだと「WITH mysql_native_password」が無いとレプリケーションがエラーを吐く。何でだ。

```
# $mysql1 -e "SET PERSIST server_id = 201;"

# $mysql1 -e "CREATE USER 'repl'@'%' IDENTIFIED WITH mysql_native_password BY 'password';"
# $mysql1 -e "GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';"
```

５．レプリケーション用の初期データを用意し、レプリケーションの開始点を確認する。

```
# $mysql1 -e "FLUSH TABLES WITH READ LOCK;"
# $mysql1 -e "SHOW MASTER STATUS\G;" > master-status.txt
# oc exec -n mysql mysql1-0 -- mysqldump -u root --all-databases --master-data > dbdump.db
# $mysql1 -e "UNLOCK TABLES;"
# cat master-status.txt

（実行結果 - SHOW MASTER STATUS）
[root@bastion mysql]# cat master-status.txt
*************************** 1. row ***************************
             File: binlog.000002
         Position: 736399
     Binlog_Do_DB:
 Binlog_Ignore_DB:
Executed_Gtid_Set:
```

「binlog.～」と「Position」の値がStandby側でレプリケーションを開始するのに必要となる。

# mysql2サーバー構成する

１．mysql2を構成するための短縮コマンドを定義する。

```
# mysql2="oc exec -n mysql mysql2-0 -- mysql -u root"
```

２．MySQL Workbenchから繋ぐ用のadminユーザーを作る。

```
# $mysql2 -e "CREATE USER 'admin'@'%' IDENTIFIED BY 'password';"
# $mysql2 -e "GRANT ALL ON *.* TO 'admin'@'%';"
```

３．mysql2にレプリケーション用の設定を行う。
server_idを202に設定する。
レプリケーション用のユーザーを作成する。

```
# $mysql2 -e "SET PERSIST server_id = 202;"

# $mysql2 -e "CREATE USER 'repl'@'%' IDENTIFIED WITH mysql_native_password BY 'password';"
# $mysql2 -e "GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';"
```

３．初期データとして、mysql1のバックアップをロードする。

```
# oc exec -i -n mysql mysql2-0 -- mysql -u root < dbdump.db
```

４．レプリケーションを開始する。

```
# MASTER_LOG_FILE=$(grep -oP '(?<=File: ).*' master-status.txt)
# MASTER_LOG_POS=$(grep -oP '(?<=Position: ).*' master-status.txt)

# $mysql2 -e "CHANGE MASTER TO MASTER_HOST='mysql1', \
MASTER_USER='repl', MASTER_PASSWORD='password', \
MASTER_LOG_FILE='${MASTER_LOG_FILE}', MASTER_LOG_POS=${MASTER_LOG_POS};"
# $mysql2 -e "START SLAVE;"
# $mysql2 -e "SHOW SLAVE STATUS\G;"
```

「Slave_IO_State: Waiting for master to send event」、「Last_IO_Error:」、「Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates」となっていることを確認する。


# HAproxyを設定し、NodePortを作る

１．OpenShiftクラスター作成時に、bastionサーバー上にHAproxyを導入したと思うので、それを流用する。
haproxy.cfgのファイル末尾に以下を追加する。
node1、node2、node3は、OpenShiftクラスターを構成するノードと、そのIPアドレスである。

```:/etc/haproxy/haproxy.cfg

frontend mysql
    bind *:3306
    mode tcp
    default_backend mysql
backend mysql
    balance source
    mode tcp
    server node1 192.168.1.3:30306 check
    server node2 192.168.1.4:30306 check
    server node3 192.168.1.5:30306 check
```

２．haproxyをreloadし、firewalldを設定する。

```
# systemctl reload haproxy
# firewall-cmd --add-service=mysql --zone=external --permanent
# firewall-cmd --reload
```

３．mysql1に接続するNodePortを作る。

```yaml:nodeport-mysql1.yaml
apiVersion: v1
kind: Service
metadata:
  name: mysql
  namespace: mysql
spec:
  type: NodePort
  ports:
    - name: mysql
      protocol: TCP
      port: 3306
      targetPort: 3306
      nodePort: 30306
  selector:
    app: mysql1
```

```
# oc create -f nodeport-mysql1.yaml
```

# MySQL WorkbenchからMySQLに繋ぐ

１．手元のPCにMySQL Workbenchをダウンロードしてインストールする。
https://www.mysql.com/jp/products/workbench/

２．HAproxyのIPアドレスに向けたConnectionを作って繋ぐ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146381/50cdfe69-0ce1-dbf1-8f72-2586fe014b5b.png)

３．レコードを追加してみよう。
Schemas > world > Tables > countryのテーブルを開き、以下のSQL文を実行（⚡）する。

```sql:
INSERT INTO `world`.`country` (`Code`, `Name`, `Continent`, `Region`, `SurfaceArea`, `Population`, `LocalName`, `GovernmentForm`, `Code2`) VALUES ('ASR', 'Asura Kingdom', 'Europe', 'Central Region', '100000.00', '1000000', 'Asura Kingdom', 'Monarchy', 'AA');
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146381/d3e931d6-596b-207e-c8b3-2cca3301aad4.png)

レコードが追加されただろうか。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146381/bced0c56-0377-4f0d-8a9d-0ba02a7d0d7e.png)

# フェールオーバーしてみる

１．mysql1が稼働しているnode1を電源オフしてもいいのだが、さすがに何なのでStatefulSetのレプリカ数を0にする。

```
# oc scale sts -n mysql mysql1 --replicas=0
```

２．MySQL Workbenchで表がロードできなくなった事を確認する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146381/e6e9f3f2-512b-2472-5a6b-7e12ffedc13c.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146381/da0bbb55-f03a-fa4c-e724-6a8d40259ef1.png)

３．念のため、mysqlへのアクセスを閉塞する。
NodePortを消せばいい。

```
# oc delete svc -n mysql mysql
```

４．mysql2でレプリケーションを停止し、Activeサーバーになっても良い様に準備する。

```
# $mysql2 -e "SHOW SLAVE STATUS\G;"
　→ 「Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates」となっているのを確認する。
# $mysql2 -e "STOP SLAVE;"
# $mysql2 -e "RESET SLAVE ALL;"

# $mysql2 -e "FLUSH TABLES WITH READ LOCK;"
# $mysql2 -e "SHOW MASTER STATUS\G;" > master-status.txt
# $mysql2 -e "UNLOCK TABLES;"
# cat master-status.txt
```

この時点でmysql2は新しいActiveサーバーとして機能しているので、HAproxyの振り分けをmaster2に変更する。

５．mysql2に向けたNodePortを作成する。

```yaml:nodeport-mysql2.yaml
apiVersion: v1
kind: Service
metadata:
  name: mysql
  namespace: mysql
spec:
  type: NodePort
  ports:
    - name: mysql
      protocol: TCP
      port: 3306
      targetPort: 3306
      nodePort: 30306
  selector:
    app: mysql2
```

```
# oc create -f nodeport-mysql2.yaml
```

６．MySQL Workbenchで再度、表をロードする。
繋がるようになっているはずである。ついでに、先にmysql1で追加したレコードがmysql2にもデータ反映されている事を確認しよう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146381/5f14d832-f5eb-37dd-aba0-35daf8ffc5ed.png)

７．mysql2がActiveサーバーになっている段階で、もう一つレコードを追加してみる。
MySQL Workbenchで以下の文を実行（⚡）する。

```sql:
INSERT INTO `world`.`country` (`Code`, `Name`, `Continent`, `Region`, `SurfaceArea`, `Population`, `LocalName`, `GovernmentForm`, `Code2`) VALUES ('MLS', 'Holy Country of Millis', 'South America', 'Southern Millis', '100000', '1000000', 'Millis', 'Church State', 'MI');
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146381/2484f750-b256-d218-d3c8-11ee0946f9e2.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146381/8466aa2c-ce21-a854-ef59-2c1757bf407e.png)

# 元のActiveサーバーに切り戻す

mysql1サーバーに切り戻すところもやってみよう。

１．mysql1のレプリカ数を1に戻す。

```
# oc scale sts -n mysql mysql1 --replicas=1
# oc logs -f -n mysql mysql1-0
　→ 「ready for connections.～」が表示されるのを待つ。
```

２．まずはmysql1を、mysql2のレプリカ（Standby）として構成する。シャットダウン中のデータ更新に追いつかせるためである。

```
# MASTER_LOG_FILE=$(grep -oP '(?<=File: ).*' master-status.txt)
# MASTER_LOG_POS=$(grep -oP '(?<=Position: ).*' master-status.txt)

# $mysql1 -e "CHANGE MASTER TO MASTER_HOST='mysql2', \
MASTER_USER='repl', MASTER_PASSWORD='password', \
MASTER_LOG_FILE='${MASTER_LOG_FILE}', MASTER_LOG_POS=${MASTER_LOG_POS};"
# $mysql1 -e "START SLAVE;"
```

３．mysql1サーバーで以下のコマンドを実行し、レプリケーションが追いつくのを待つ。

```
# $mysql1 -e "SHOW SLAVE STATUS\G;"
```

「Slave_IO_State: Waiting for master to send event」、「Last_IO_Error:」、「Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates」となっていることを確認する。

レプリケーションが追いついたらmysql1をActiveサーバーとして復帰するための準備完了である。

４．mysql2へのアクセスを閉塞する。

```
# oc delete svc -n mysql mysql
```

５．mysql1サーバーのレプリケーションを止め、Activeに復帰させる準備をする。

```
# $mysql1 -e "SHOW SLAVE STATUS\G;"
　→ 「Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates」となっているのを確認する。
# $mysql1 -e "STOP SLAVE;"
# $mysql1 -e "RESET SLAVE ALL;"

# $mysql1 -e "FLUSH TABLES WITH READ LOCK;"
# $mysql1 -e "SHOW MASTER STATUS\G;" > master-status.txt
# $mysql1 -e "UNLOCK TABLES;"
```

６．master1に振るNodePortを作成し、MySQLへのアクセスを再開する。

```
# oc create -f nodeport-mysql1.yaml
```

７．MySQL Workbenchで再度、表をリロードする。表がリロードされ、master2側に追加したレコードがmaster1にも反映されていることを確認する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146381/389a3130-2ccc-8aa6-07c4-b64d51d1cd3b.png)

８．最後に、mysql2サーバーが改めてStandbyとなるように再構成する。

```
# MASTER_LOG_FILE=$(grep -oP '(?<=File: ).*' master-status.txt)
# MASTER_LOG_POS=$(grep -oP '(?<=Position: ).*' master-status.txt)

# $mysql2 -e "CHANGE MASTER TO MASTER_HOST='mysql1', \
MASTER_USER='repl', MASTER_PASSWORD='password', \
MASTER_LOG_FILE='${MASTER_LOG_FILE}', MASTER_LOG_POS=${MASTER_LOG_POS};"
# $mysql2 -e "START SLAVE;"
# $mysql2 -e "SHOW SLAVE STATUS\G;"
```

「Slave_IO_State: Waiting for master to send event」、「Last_IO_Error:」、「Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates」となっていることを確認する。


# （おまけ１）何回か往復したい時の追加データ

```sql:
INSERT INTO `world`.`country` (`Code`, `Name`, `Continent`, `Region`, `SurfaceArea`, `Population`, `LocalName`, `GovernmentForm`, `Code2`) VALUES ('RAN', 'Ranoa Kingdom', 'Europe', 'Northern Central', '10000', '100000', 'Ranoa', 'Monarchy', 'RA');
```

```sql:
INSERT INTO `world`.`country` (`Code`, `Name`, `Continent`, `Region`, `SurfaceArea`, `Population`, `LocalName`, `GovernmentForm`, `Code2`) VALUES ('KDK', 'King Dragon Kingdom', 'Europe', 'Southern Central', '50000', '500000', 'King Dragon Kingdom', 'Monarchy', 'KD');
```

# （おまけ２）今回構成の消費メモリ
２回往復後の消費メモリ。
mysql1、mysql2の２台合わせて、大体1GBといったところか。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146381/8dbeb870-319e-97b4-553c-f32febdec92e.png)


# （おまけ３）きれいさっぱり消し去る

１．mysqlネームスペースを消す。

```
# oc delete namespace mysql
```

２．PV、StorageClassを消す。

```
# oc delete pv mysql1 mysql2
# oc delete sc nfs-mysql
```

３．イメージプルーニングを行い、使われなくなったmysql-80イメージを消す。

```
# oc adm prune images
```

４．nfsのストレージも消す。

```
# sed -i '/\/export.*/d' /etc/exports
# exportfs -r
# rm -rf /export
```

５．何なら、HAproxyの定義も消す。

```:/etc/haproxy/haproxy.cfg
（ファイル最後の以下を削除）

frontend mysql
    bind *:3306
    mode tcp
    default_backend mysql
backend mysql
    balance source
    mode tcp
    server node1 192.168.1.3:30306 check
    server node2 192.168.1.4:30306 check
    server node3 192.168.1.5:30306 check
```

```
# systemctl reload haproxy
# firewall-cmd --remove-service=mysql --zone=external --permanent
# firewall-cmd --reload
```

これで本記事の手順を１からやり直すことが出来る。
やってみよう。