昨日構成したCentOS 8上でのMySQL 8.0 Active-Standby構成を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である。
また、前回記事と同じくフェールオーバーの切り替えの自動化には触れていない。それでも手順は十分機械的なので、少し頑張るくらいでフェールオーバー自動化は可能だろう(「フェールオーバーしてみる」の手順2~5が自動化できれば良いのだ)。
構成
OpenShiftクラスターの作成については以下の記事を参照。最近Master3台構成に作り替えたのだが、まあ似たようなものだ。
https://qiita.com/rk05231977/items/ec1626c1223dddffc98a
今回の構成ではMySQLサーバーの開閉塞はNodePortで行うのだが、bastionサーバー経由でないとクラスターにアクセスできないため、やはりHAProxyは使用する。
ストレージは、簡単だったのでbastionサーバー上のNFSストレージを使用している。使えるならmysql1、2のPodが稼働するノードのローカルストレージか、iSCSI等のブロックストレージを使った方が良いだろう。
シェアードナッシングなのでmysql1とmysql2が実行される物理ノードは遠隔に配置できる。マルチAZ単一クラスター構成なら本記事に書いてあるままで、DRでもちょっと頑張れば動作するだろう。
nfsストレージとインターナルレジストリを準備する
1.以下の記事のうち、「NFSサーバーを作る」、「Image Registryを設定する」を参照し、それらを構成する。
https://qiita.com/rk05231977/items/59027862b59207a5e61f
2.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クラスターを作る
1.bastionサーバーにログインし、mysqlクラスターのyamlファイルを作る。
Infrastructure as Codeの世界へようこそ。
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
2.yamlファイルをOpenShiftクラスターにデプロイする。
# oc create -f mysql.yaml
とりあえず、mysqlネームスペースにmysql1とmysql2という2つのStatefulSetが出来上がる。
インターナルレジストリにmysql-80のイメージをpushしていないので、上記をデプロイした直後にPodは起動しない。この後すぐの手順でイメージは配置する。
外部からのアクセスにNodePortが必要だが、それはまだ作っていない。
3.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
4.mysqlのPodを削除し、作り直す。
# oc delete pod -n mysql mysql1-0 mysql2-0
5.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を構成する
1.何もデータが無いのもつまらないので、国データベースを初期データとしてロードしようか。
# 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
2.以降、oc exec経由でmysql1のインスタンスを構成するが、長たらしいコマンドを避けるため短縮コマンドを定義する。
# mysql1="oc exec -n mysql mysql1-0 -- mysql -u root"
3.MySQL Workbenchから繋ぐ用のadminユーザーを作る。
# $mysql1 -e "CREATE USER 'admin'@'%' IDENTIFIED BY 'password';"
# $mysql1 -e "GRANT ALL ON *.* TO 'admin'@'%';"
4.ここからがレプリケーション用の構成。
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'@'%';"
5.レプリケーション用の初期データを用意し、レプリケーションの開始点を確認する。
# $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サーバー構成する
1.mysql2を構成するための短縮コマンドを定義する。
# mysql2="oc exec -n mysql mysql2-0 -- mysql -u root"
2.MySQL Workbenchから繋ぐ用のadminユーザーを作る。
# $mysql2 -e "CREATE USER 'admin'@'%' IDENTIFIED BY 'password';"
# $mysql2 -e "GRANT ALL ON *.* TO 'admin'@'%';"
3.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'@'%';"
3.初期データとして、mysql1のバックアップをロードする。
# oc exec -i -n mysql mysql2-0 -- mysql -u root < dbdump.db
4.レプリケーションを開始する。
# 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を作る
1.OpenShiftクラスター作成時に、bastionサーバー上にHAproxyを導入したと思うので、それを流用する。
haproxy.cfgのファイル末尾に以下を追加する。
node1、node2、node3は、OpenShiftクラスターを構成するノードと、そのIPアドレスである。
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
2.haproxyをreloadし、firewalldを設定する。
# systemctl reload haproxy
# firewall-cmd --add-service=mysql --zone=external --permanent
# firewall-cmd --reload
3.mysql1に接続するNodePortを作る。
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に繋ぐ
1.手元のPCにMySQL Workbenchをダウンロードしてインストールする。
https://www.mysql.com/jp/products/workbench/
2.HAproxyのIPアドレスに向けたConnectionを作って繋ぐ。
3.レコードを追加してみよう。
Schemas > world > Tables > countryのテーブルを開き、以下の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');
フェールオーバーしてみる
1.mysql1が稼働しているnode1を電源オフしてもいいのだが、さすがに何なのでStatefulSetのレプリカ数を0にする。
# oc scale sts -n mysql mysql1 --replicas=0
2.MySQL Workbenchで表がロードできなくなった事を確認する。
3.念のため、mysqlへのアクセスを閉塞する。
NodePortを消せばいい。
# oc delete svc -n mysql mysql
4.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に変更する。
5.mysql2に向けたNodePortを作成する。
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
6.MySQL Workbenchで再度、表をロードする。
繋がるようになっているはずである。ついでに、先にmysql1で追加したレコードがmysql2にもデータ反映されている事を確認しよう。
7.mysql2がActiveサーバーになっている段階で、もう一つレコードを追加してみる。
MySQL Workbenchで以下の文を実行(⚡)する。
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');
元のActiveサーバーに切り戻す
mysql1サーバーに切り戻すところもやってみよう。
1.mysql1のレプリカ数を1に戻す。
# oc scale sts -n mysql mysql1 --replicas=1
# oc logs -f -n mysql mysql1-0
→ 「ready for connections.~」が表示されるのを待つ。
2.まずは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;"
3.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サーバーとして復帰するための準備完了である。
4.mysql2へのアクセスを閉塞する。
# oc delete svc -n mysql mysql
5.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;"
6.master1に振るNodePortを作成し、MySQLへのアクセスを再開する。
# oc create -f nodeport-mysql1.yaml
7.MySQL Workbenchで再度、表をリロードする。表がリロードされ、master2側に追加したレコードがmaster1にも反映されていることを確認する。
8.最後に、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」となっていることを確認する。
(おまけ1)何回か往復したい時の追加データ
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');
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');
(おまけ2)今回構成の消費メモリ
2回往復後の消費メモリ。
mysql1、mysql2の2台合わせて、大体1GBといったところか。
(おまけ3)きれいさっぱり消し去る
1.mysqlネームスペースを消す。
# oc delete namespace mysql
2.PV、StorageClassを消す。
# oc delete pv mysql1 mysql2
# oc delete sc nfs-mysql
3.イメージプルーニングを行い、使われなくなったmysql-80イメージを消す。
# oc adm prune images
4.nfsのストレージも消す。
# sed -i '/\/export.*/d' /etc/exports
# exportfs -r
# rm -rf /export
5.何なら、HAproxyの定義も消す。
(ファイル最後の以下を削除)
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
これで本記事の手順を1からやり直すことが出来る。
やってみよう。