MySQLの非同期レプリケーション環境におけるHAの実現方法としては一例として以下のようなソフトウェアがあります。
この内MHA、mysqlfailoverはすでに開発が停止しており、MaxScaleはBSLライセンスであるため気軽に利用するには敷居が高いように思います。
Orchestratorは現在も開発が活発であり、ユーザも多いかと思います。
一方で多機能な分設定が複雑であったり、バックエンドのデータベースを必要とするなど、小規模なMySQLクラスタで気軽に利用するにはToo muchな印象を受けます。
そこで今回は、MySQL ReplicaSetと、MySQL Shellの機能を利用して小規模な環境でのHA機能の実装を試してみたいと思います。
なお、記事中に紹介した内容はあくまで実験的なものです。
MySQL ReplicaSet
MySQL ReplicaSet は、これまでの一般的な非同期レプリケーションの環境を、MySQL InnoDB ClusterのようにMySQL Routerと統合し、MySQL Shell から管理可能とした複合的なソリューションです。
データベース部分はこれまでの非同期レプリケーションと何ら変わり無いものです。
MySQL ReplicaSetはMySQL Routerと連携することにより、プライマリ・セカンダリ間の負荷分散やプライマリへのルーティングが可能ですが、MySQL Routerには自動フェイルオーバ機能は提供されていません。
https://dev.mysql.com/doc/refman/8.0/ja/mysql-innodb-replicaset-introduction.html
MySQL レプリケーションに基づいている場合、InnoDB ReplicaSet には単一のプライマリがあり、複数のセカンダリインスタンスにレプリケートされます。
InnoDB ReplicaSet では、自動フェイルオーバーやマルチプライマリモードなど、InnoDB クラスタ が提供するすべての機能が提供されるわけではありません。
ただし、インスタンスの構成、追加、削除などの機能も同様にサポートされています。
たとえば、障害が発生した場合は、セカンダリインスタンスに手動でスイッチオーバーまたはフェイルオーバーできます。
そのため、もし自動的にMySQL Routerの接続をセカンダリサーバに切り替えたいとなった場合、何らかの仕組みをユーザ側で実装する必要が発生します。
MySQL Shellの setPrimaryInstance(), forcePrimaryInstance() について
MySQL Shell APIには、 setPrimaryInstance()
, forcePrimaryInstance()
という、MySQL ReplicaSetのプライマリを他のセカンダリに切り替えるための関数が存在します。
このコマンドは、レプリカセットのPRIMARYの安全な切り替えを実行します。
現在のPRIMARYはSECONDARYに降格されて読み取り専用になり、プロモートされたインスタンスは読み取り/書き込みマスターになります。
他のすべてのSECONDARYインスタンスは、新しいPRIMARYから複製するように更新されます。スイッチオーバー中に、プロモートされたインスタンスは古いPRIMARYと同期され、トポロジの変更がコミットされる前に、PRIMARYに存在するすべてのトランザクションが確実に適用されます。
その同期ステップに時間がかかりすぎるか、SECONDARYインスタンスのいずれかで不可能な場合、スイッチは中止されます。
これらの問題のあるSECONDARYインスタンスは、フェイルオーバーを可能にするために、修復するか、レプリカセットから削除する必要があります。安全なスイッチオーバーを可能にするには、すべてのレプリカセットインスタンスがシェルから到達可能であり、一貫したトランザクションセットを持っている必要があります。
PRIMARYが使用できない場合は、代わりにforce_primary_instance()を使用して強制フェイルオーバーを実行する必要があります。
このコマンドは、現在のPRIMARYが使用できず、復元できない災害シナリオで、レプリカセットのPRIMARYの強制フェイルオーバーを実行します。
指定された場合、ターゲットインスタンスはPRIMARYにプロモートされ、他の到達可能なSECONDARYインスタンスは新しいPRIMARYに切り替えられます。
ターゲットインスタンスには、到達可能なインスタンスの中で最新のGTID_EXECUTEDが設定されている必要があります。
そうでない場合、操作は失敗します。
ターゲットインスタンスが指定されていない(またはnullである)場合、最新のインスタンスが自動的に選択されてプロモートされます。強制フェイルオーバー後、古いPRIMARYは新しいPRIMARYによって無効と見なされ、レプリカセットの一部にすることはできなくなります。
インスタンスがまだ使用可能な場合は、レプリカセットから削除して、新しいインスタンスとして再度追加する必要があります。
フェイルオーバー中に新しいPRIMARYに切り替えることができなかったSECONDARYインスタンスがあった場合、それらも無効と見なされます。注意
強制フェイルオーバーは潜在的に破壊的なアクションであり、最後の手段としてのみ使用する必要があります。
古いPRIMARYには、プロモートされているSECONDARYにまだ複製されていないトランザクションが含まれている可能性があるため、
フェイルオーバー後にデータが失われる可能性があります。
さらに、障害が発生したと推定されたインスタンスが引き続き更新を処理できる場合、たとえば、インスタンスが配置されているネットワークはまだ機能しているが、
シェルから到達できないため、プロモートされたクラスターから分岐し続けます。
分岐したトランザクションセットを回復または再調整するには、手動による介入が必要であり、障害が発生したMySQLサーバーを回復できたとしても、実行できない場合があります。
多くの場合、強制フェイルオーバーが必要な災害から回復するための最も速くて簡単な方法は、そのような分岐したトランザクションを破棄し、
新しく昇格したPRIMARYから新しいインスタンスを再プロビジョニングすることです。
要約すると、setPrimaryInstance はプライマリをセカンダリに降格してからread_only=1とし、プライマリ候補のセカンダリのトランザクションが適用されるのを待機してからプライマリに昇格するという動作になり、 forcePrimaryInstanceは、現在のプライマリをレプリカセットから破棄し、プライマリ候補のセカンダリのトランザクションが適用されるのを待機してからプライマリに昇格する という動作になります。
前者はプライマリに接続して設定を行う必要があるためスイッチオーバ用、後者はフェイルオーバ用の関数と言えます。
まずはこれらの関数を試してみましょう。
検証環境について
今回の検証環境は以下の構成です。
OSにはCentOS8を使用しました。
レプリカセットの構築については、以前の記事でご紹介していますので、参考にしていただければと思います。
MySQL の InnoDB ReplicaSet を構築してみる
以下が初期状態です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
$ mysqlsh --uri icadmin@node-0 -e "print(dba.getReplicaSet().status())" You are connected to a member of replicaset 'myCluster'. { "replicaSet": { "name": "myCluster", "primary": "node-0:3306", "status": "AVAILABLE", "statusText": "All instances available.", "topology": { "node-0:3306": { "address": "node-0:3306", "instanceRole": "PRIMARY", "mode": "R/W", "status": "ONLINE" }, "node-1:3306": { "address": "node-1:3306", "instanceRole": "SECONDARY", "mode": "R/O", "replication": { "applierStatus": "APPLIED_ALL", "applierThreadState": "Waiting for an event from Coordinator", "applierWorkerThreads": 4, "receiverStatus": "ON", "receiverThreadState": "Waiting for master to send event", "replicationLag": null }, "status": "ONLINE" } }, "type": "ASYNC" } |
プライマリダウン時にsetPrimaryInstance()を実行
node-0(PRIMARY)
で systemctl stop mysqld
を実行すると、statusがUNREACHABLEとなり、node-1(SECONDARY)
ではレプリケーションソースへ接続不可のエラーが発生していますが、instanceRoleは切り替えられていないことが確認できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
{ "replicaSet": { "name": "myCluster", "primary": "node-0:3306", "status": "UNAVAILABLE", "statusText": "PRIMARY instance is not available, but there is at least one SECONDARY that could be force-promoted.", "topology": { "node-0:3306": { "address": "node-0:3306", "connectError": "Could not open connection to 'node-0:3306': Can't connect to MySQL server on 'node-0:3306' (111)", "fenced": null, "instanceRole": "PRIMARY", "mode": null, "status": "UNREACHABLE" }, "node-1:3306": { "address": "node-1:3306", "fenced": true, "instanceErrors": [ "ERROR: Replication I/O thread (receiver) has stopped with an error." ], "instanceRole": "SECONDARY", "mode": "R/O", "replication": { "applierStatus": "APPLIED_ALL", "applierThreadState": "Waiting for an event from Coordinator", "applierWorkerThreads": 4, "expectedSource": "node-0:3306", "receiverLastError": "error reconnecting to master 'mysql_innodb_rs_2969750834@node-0:3306' - retry-time: 60 retries: 1 message: Can't connect to MySQL server on 'node-0:3306' (111)", "receiverLastErrorNumber": 2003, "receiverLastErrorTimestamp": "2021-06-25 09:59:07.372598", "receiverStatus": "ERROR", "receiverThreadState": "", "replicationLag": null, "source": "node-0:3306" }, "status": "ERROR", "transactionSetConsistencyStatus": null } }, "type": "ASYNC" } |
MySQL Routerは接続先をinstanceRoleにより振り分けるため、PRIMARYへの接続(6446ポート)はエラーになります。
SECONDARYへの接続(6447ポート)は接続可能です。
1 2 3 4 5 6 7 8 |
$ mysql -uicadmin -p -h127.0.0.1 -P6446 -e "select @@hostname" ERROR 2003 (HY000): Can't connect to remote MySQL server for client connected to '0.0.0.0:6446' $ mysql -uicadmin -p -h127.0.0.1 -P6447 -e "select @@hostname" +------------+ | @@hostname | +------------+ | node-1 | +------------+ |
この状態からMySQL Shellでセカンダリに接続し、 setPrimaryInstance()
を実行してみましたが、やはりプライマリへの操作が行えないためにエラーとなりました。
エラーメッセージの内容の通り、このような障害ケースでは forcePrimaryInstance()
を使いましょう。
1 2 3 4 5 6 7 |
MySQL JS> var rs = dba.getReplicaSet() MySQL JS> rs.setPrimaryInstance("icadmin@node-1") ERROR: Unable to connect to the PRIMARY of the replicaset myCluster: MySQL Error 2003: Could not open connection to 'node-0:3306': Can't connect to MySQL server on 'node-0:3306' (111) Cluster change operations will not be possible unless the PRIMARY can be reached. If the PRIMARY is unavailable, you must either repair it or perform a forced failover. See \help forcePrimaryInstance for more information. ReplicaSet.setPrimaryInstance: PRIMARY instance is unavailable (MYSQLSH 51118) |
プライマリダウン時にforcePrimaryInstance()を実行
では引き続き forcePrimaryInstance() を実行します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
MySQL JS> rs.forcePrimaryInstance("icadmin@node-1") * Connecting to replicaset instances ** Connecting to node-1:3306 * Waiting for all received transactions to be applied ** Waiting for received transactions to be applied at node-1:3306 node-1:3306 will be promoted to PRIMARY of the replicaset and the former PRIMARY will be invalidated. * Checking status of last known PRIMARY NOTE: node-0:3306 is UNREACHABLE * Checking status of promoted instance NOTE: node-1:3306 has status ERROR * Checking transaction set status * Promoting node-1:3306 to a PRIMARY... * Updating metadata... node-1:3306 was force-promoted to PRIMARY. NOTE: Former PRIMARY node-0:3306 is now invalidated and must be removed from the replicaset. * Updating source of remaining SECONDARY instances Failover finished successfully. |
成功したようです。
node-0(旧PRIMARY)
のstatusがINVALIDATEDになり、node-1(旧SECONDARY)
のinstanceRoleがPRIMARYになりました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
MySQL JS> rs.status() { "replicaSet": { "name": "myCluster", "primary": "node-1:3306", "status": "AVAILABLE_PARTIAL", "statusText": "The PRIMARY instance is available, but one or more SECONDARY instances are not.", "topology": { "node-0:3306": { "address": "node-0:3306", "connectError": "Could not open connection to 'node-0:3306': Can't connect to MySQL server on 'node-0:3306' (111)", "fenced": null, "instanceRole": null, "mode": null, "status": "INVALIDATED" }, "node-1:3306": { "address": "node-1:3306", "instanceRole": "PRIMARY", "mode": "R/W", "status": "ONLINE" } }, "type": "ASYNC" } } |
これによってMySQL Routerの6446への接続もnode-1にルーティングされるはずです。
1 2 3 4 5 6 |
$ mysql -uicadmin -p -h127.0.0.1 -P6446 -e "select @@hostname" +------------+ | @@hostname | +------------+ | node-1 | +------------+ |
想定通りの動作となりました。
なお、このように一度INVALIDATED
となったインスタンスは再度接続可能な状態とした上で、rejoinInstanceで再度クラスタに参加させる必要が発生します。
一度正常な状態に戻すためにrejoinInstance()を実行します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
MySQL JS> rs.rejoinInstance("node-0") You are connected to a member of replicaset 'myCluster'. * Validating instance... This instance reports its own address as node-0:3306 node-0:3306: Instance configuration is suitable. ** Checking transaction state of the instance... The safest and most convenient way to provision a new instance is through automatic clone provisioning, which will completely overwrite the state of 'node-0:3306' with a physical snapshot from an existing replicaset member. To use this method by default, set the 'recoveryMethod' option to 'clone'. WARNING: It should be safe to rely on replication to incrementally recover the state of the new instance if you are sure all updates ever executed in the replicaset were done with GTIDs enabled, there are no purged transactions and the new instance contains the same GTID set as the replicaset or a subset of it. To use this method by default, set the 'recoveryMethod' option to 'incremental'. Incremental state recovery was selected because it seems to be safely usable. * Rejoining instance to replicaset... ** Configuring node-0:3306 to replicate from node-1:3306 ** Checking replication channel status... ** Waiting for rejoined instance to synchronize with PRIMARY... * Updating the Metadata... The instance 'node-0:3306' rejoined the replicaset and is replicating from node-1:3306. |
スクリプトによる実装
forcePrimaryInstance()
を利用することで以下のbashスクリプト(myfailover.sh)のように簡単にフェイルオーバ用プログラムを作成することができました。
スクリプト内では以下のソフトウェアを使用しています。
- jq
- MySQL Shell
以下の仕様としました。
- 起動時myfailover.statusファイルにcandidates(接続候補)の内最初に接続できたノードでdba.getReplicaSet.status()の結果を出力
- myfailover.statusファイルから現在のプライマリノードのIPアドレスを取得
- “select 1” をPrimaryに実行し成功したら一定時間SLEEPし、2から繰り返し
- 3が失敗した場合にSECONDARYのノードに接続しforcePrimaryInstance()を実行しプログラムを終了
- 4が失敗したらエラー終了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
#!/bin/bash mysql_user=icadmin # 接続ユーザ mysql_passwd=MySQL8.0 # パスワード candidates=(node-0 node-1) # 接続候補 interval=10 # 監視間隔 status_file=${0//.sh/}.status # ステータス記録用ファイル log_file=${0//.sh/}.log # ログファイル log_err(){ echo $(date "+%F %T") $* >&2 ;} replicaset_status(){ for node in ${candidates[@]} do mysqlsh ${mysql_user}:${mysql_passwd}@${node} -e 'print(dba.getReplicaSet().status())' [ $? -eq 0 ] && return 0 done return 1 } get_primary_address(){ jq -r '.replicaSet.topology[]|select(.instanceRole == "PRIMARY")|.address' ${status_file}; } get_secondary_addresses() { jq -r '.replicaSet.topology[]|select(.instanceRole == "SECONDARY")|.address' ${status_file}; } health_check() { mysqlsh --sql ${mysql_user}:${mysql_passwd}@${1} -e "select 1" 2>&1 >/dev/null; } do_failover(){ mysqlsh ${mysql_user}:${mysql_passwd}@${1} -e 'dba.getReplicaSet().forcePrimaryInstance()'; } main(){ replicaset_status > ${status_file} [ $? -ne 0 ] && { log_err "Unable to retrieve replica set information."; exit 1; } while : do prim=$(get_primary_address) health_check ${prim} if [ $? -ne 0 ]; then for sec in $(get_secondary_addresses) do health_check ${sec} if [ $? -eq 0 ];then do_failover ${sec} case $? in 0) log_err "Failover succeed. Now PRIMARY is ${sec}" rm -f ${status_file} exit 0 ;; *) log_err "Failover failed" exit 1 ;; esac fi done log_err "There are no valid node" exit 1 fi log_err "health check ok. PRIMARY is ${prim}" sleep ${interval} done } main &>> ${log_file} & |
実行してみましょう。
1 |
$ ./myfailover.sh |
現在のプライマリはnode-0です。
1 2 3 4 5 |
$ tail -f myfailover.log 2021-06-25 15:02:01 health check ok. PRIMARY is node-0:3306 2021-06-25 15:03:02 health check ok. PRIMARY is node-0:3306 2021-06-25 15:04:01 health check ok. PRIMARY is node-0:3306 : |
それではフェイルオーバをテストします。
MySQL Routerの6446ポートに接続し続ける処理を実行します。
1 2 3 4 5 6 |
$ export MYSQL_PWD=MySQL.8.0 $ while :;do mysql -NB -uicadmin -h127.0.0.1 -P6446 -e "select @@hostname";sleep 1;done node-0 node-0 node-0 : |
node-0のmysqldを停止します。
1 |
$ systemctl stop mysqld |
以下のように接続エラーが発生します。
1 |
ERROR 2003 (HY000): Can't connect to remote MySQL server for client connected to '0.0.0.0:6446' |
myfailover.logでは、接続エラーをきっかけにフェイルオーバが実行されたログが出力されました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
MySQL Error 2003 (HY000): Can't connect to MySQL server on 'node-0:3306' (111) WARNING: Using a password on the command line interface can be insecure. You are connected to a member of replicaset 'myCluster'. * Connecting to replicaset instances ** Connecting to node-1:3306 * Waiting for all received transactions to be applied ** Waiting for received transactions to be applied at node-0:3306 * Searching instance with the most up-to-date transaction set node-1:3306 has GTID set c21d6ba3-d598-11eb-b9b7-02001700f622:1-64, c22abf25-d598-11eb-acbd-020017007f98:1-3209 node-1:3306 will be promoted to PRIMARY of the replicaset and the former PRIMARY will be invalidated. * Checking status of last known PRIMARY NOTE: node-0:3306 is UNREACHABLE * Checking status of promoted instance NOTE: node-1:3306 has status ERROR * Checking transaction set status * Promoting node-1:3306 to a PRIMARY... * Updating metadata... node-1:3306 was force-promoted to PRIMARY. NOTE: Former PRIMARY node-0:3306 is now invalidated and must be removed from the replicaset. * Updating source of remaining SECONDARY instances Failover finished successfully. 2021-06-25 15:06:02 Failover succeed. Now PRIMARY is node-1:3306 |
接続テストの方では無事MySQL Routerが新プライマリに接続を向けてくれました。
1 2 3 4 |
ERROR 2003 (HY000): Can't connect to remote MySQL server for client connected to '0.0.0.0:6446' ERROR 2003 (HY000): Can't connect to remote MySQL server for client connected to '0.0.0.0:6446' node-1 node-1 |
その後のヘルスチェックも正常に行われたようです。
1 2 3 |
2021-06-25 15:47:02 Failover succeed. Now PRIMARY is node-1:3306 You are connected to a member of replicaset 'myCluster'. 2021-06-25 15:48:01 health check ok. PRIMARY is node-1:3306 |
色々と考慮すべき点はあるものの、100行にも満たないスクリプトで簡単に自動フェイルオーバ機能を実装できました。
まとめ
フルスタックなHAソフトウェアを採用する場合、構築や技術習得に一定の時間が必要です。
しっかりとしたHA基盤を利用するのが最も良いことは言うまでもありませんが、現在何の仕組みも無く運用されている場合には、まずはシンプルなスクリプトを利用するのも良いかもしれません。
また、例えば以下のPercona社が開発したPacemaker用Resource Agentのプログラムがありますが、MySQL Routerがフェイルオーバを行い、MySQL Shell APIがレプリケーションを考慮したプライマリのプロモートを行ってくれる現在では、もっと簡単にResource Agentを書き直せるでしょう。
https://github.com/Percona-Lab/pacemaker-replication-agents/blob/master/doc/PRM-setup-guide.rst
ぜひMySQL Shell APIをお楽しみください!