As we know, Orchestrator is a MySQL high availability and replication management tool that aids in managing farms of MySQL servers. In this blog post, we discuss how to make the Orchestrator (which manages MySQL) itself fault-tolerant and highly available.
When considering HA for the Orchestrator one of the popular choices will be using the Raft consensus.
Raft is a consensus protocol/ algorithm where multiple nodes composed of a (Leader) and (Followers) agree on the same state and value. The Leaders are decided by the quorum and voting, and it is the responsibility of the Leader Raft to do all the decision-making and changes. The other node just follows or syncs with the Leader without involving any direct changes.
When Raft is used with Orchestrator, it provides high availability, solves network partitioning, and ensures fencing on the Isolated node.
Next, we will see how we can deploy an Orchestrator/Raft based setup with the below topology.
For demo purposes, I am using the same server for both Orchestrator/Raft and MySQL.
|
1 |
172.31.20.60 Node1<br>172.31.16.8 Node2<br>172.31.23.135 Node3<br> |
So, we have the topology below, which we are going to deploy. Each Raft/Orchestrator node has its own separate SQLite database instance.

For this demo, I am installing the packages via Percona distribution. However we can also install the Orchestrator packages from Percona or Openark repositories directly.
|
1 |
shell> sudo yum install -y https://repo.percona.com/yum/percona-release-latest.noarch.rpm <br>shell> sudo percona-release setup pdps-8.0<br>shell> sudo yum install -y percona-orchestrator percona-orchestrator-cli percona-orchestrator-client<br>shell> sudo yum install -y percona-server-server<br> |
Note – Openark is no longer active, and the last update was quite some time ago( “2021”). Therefore, we can rely on the Percona repositories, which have the latest release last pushed on (“2024”).
1) Create database-specific users/tables on the Source database node (Node1).
|
1 |
mysql> CREATE DATABASE meta;<br><br>mysql> CREATE TABLE meta.cluster (<br> anchor TINYINT NOT NULL,<br> cluster_name VARCHAR(128) CHARACTER SET ascii NOT NULL DEFAULT '',<br> cluster_domain VARCHAR(128) CHARACTER SET ascii NOT NULL DEFAULT '',<br> PRIMARY KEY (anchor)<br>) ENGINE=InnoDB DEFAULT CHARSET=utf8;<br><br>mysql> INSERT INTO meta.cluster (anchor, cluster_name, cluster_domain) VALUES (1, 'testcluster', 'example.com');<br> |
Note – Orchestrator will fetch the cluster details from this table.
|
1 |
mysql> CREATE USER 'orchestrator'@'%' IDENTIFIED BY 'Orc@1234';<br>mysql> GRANT SUPER, PROCESS, REPLICATION SLAVE, RELOAD ON *.* TO 'orchestrator'@'%';<br>mysql> GRANT SELECT ON mysql.slave_master_info TO 'orchestrator'@'%';<br>mysql> GRANT DROP ON `_pseudo_gtid_`.* TO 'orchestrator'@'%';<br>mysql> GRANT SELECT ON meta.* TO 'orchestrator'@'%';<br> |
Note – These credentials will be used by the Orchestrator to connect to the MySQL backends.
2) Then, we need to copy the orchestrator template file to the /etc/orchestrator.conf.json and perform the necessary changes in the mentioned sections.
|
1 |
shell> sudo cp /usr/local/orchestrator/orchestrator-sample.conf.json /etc/orchestrator.conf.json<br> |
|
1 |
"MySQLTopologyUser": "orchestrator",<br>"MySQLTopologyPassword": "Orc@1234", |
|
1 |
"MySQLOrchestratorHost": "127.0.0.1",<br>"MySQLOrchestratorPort": 3306,<br>"MySQLOrchestratorDatabase": "orchestrator",<br>"MySQLOrchestratorUser": "orc_server_user",<br>"MySQLOrchestratorPassword": "orc_server_password", |
Create Orchestrator schema and related credentials.
1 mysql> CREATE DATABASE IF NOT EXISTS orchestrator;<br>mysql> CREATE USER 'orchestrator'@'localhost' identified by 'Orc@1234';<br>mysql> GRANT ALL PRIVILEGES ON orchestrator.* TO 'orchestrator'@'localhost';You need to replace the details with Orchestrator managing database (MySQL) information.
1 "MySQLOrchestratorHost": "127.0.0.1",<br>"MySQLOrchestratorPort": 3306,<br>"MySQLOrchestratorDatabase": "orchestrator",<br>"MySQLOrchestratorUser": "orchestrator",<br>"MySQLOrchestratorPassword": "Orc@1234
|
1 |
DetectClusterAliasQuery": "SELECT ifnull(max(cluster_name), '''') as cluster_alias from meta.cluster where anchor=1;", |
|
1 |
"BackendDB": "sqlite",<br>"SQLite3DataFile": "/var/lib/orchestrator/orchestrator.db", |
|
1 |
"RecoverMasterClusterFilters": [<br> "testcluster"<br> ],<br> "RecoverIntermediateMasterClusterFilters": [<br> "testcluster"<br> ],<br> |
RecoverMasterClusterFilters => It defines which cluster should be auto failover/recover.
RecoverIntermediateMasterClusterFilters => It resembles whether recovery/failure for intermediate masters allow. Intermediate masters are the replica hosts, which have their replicas as well.
|
1 |
"DefaultRaftPort": 10008,<br>"RaftAdvertise": "172.31.20.60",<br>"RaftBind": "172.31.20.60",<br>"RaftDataDir": "/var/lib/orchestrator",<br>"RaftEnabled": true,<br>"RaftNodes": [<br> "172.31.20.60",<br> "172.31.16.8",<br> "172.31.23.135"<br> ],<br> |
|
1 |
"DefaultRaftPort": 10008,<br>"RaftAdvertise": "172.31.16.8",<br>"RaftBind": "172.31.16.8",<br>"RaftDataDir": "/var/lib/orchestrator",<br>"RaftEnabled": true,<br>"RaftNodes": [<br> "172.31.20.60",<br> "172.31.16.8",<br> "172.31.23.135"<br> ],<br> |
|
1 |
"DefaultRaftPort": 10008,<br>"RaftAdvertise": "172.31.23.135",<br>"RaftBind": "172.31.23.135",<br>"RaftDataDir": "/var/lib/orchestrator",<br>"RaftEnabled": true,<br>"RaftNodes": [<br> "172.31.20.60",<br> "172.31.16.8",<br> "172.31.23.135"<br> ],<br> |
Note – Here we mainly replace the RaftAdvertise/RaftBind configuration for each node. We need to also make sure the communication between the nodes is allowed on the given Raft port (10008).
3) Then, we can create the Raft data directory on each node.
|
1 |
shell> mkdir -p /var/lib/orchestrator<br> |
4) Finally, we can start the Orchestrator service on each node.
|
1 |
shell> systemctl start orchestrator<br> |
From the Orchestrator UI- http://ec2-54-147-20-38.compute-1.amazonaws.com:3000/web/status directly we can do the initial Node discovery process.

So here is our MySQL topology consisting of all 3 nodes.

As we are using SQLite3, we can use the below way to access the tables and information from the insight of the database.
|
1 |
shell> sqlite3 /var/lib/orchestrator/orchestrator.db<br>SQLite version 3.34.1 2021-01-20 14:10:07<br>Enter ".help" for usage hints.<br><br>sqlite> .tables |
|
1 |
access_token <br>active_node <br>agent_seed <br>agent_seed_state <br>async_request <br>audit <br>blocked_topology_recovery <br>candidate_database_instance <br>cluster_alias <br>…<br>node_health <br>node_health_history <br>orchestrator_db_deployments <br>orchestrator_metadata <br>raft_log <br>raft_snapshot <br>raft_store <br>topology_failure_detection <br>topology_recovery <br>topology_recovery_steps <br> |
Next, we can check the logs of each node to confirm the status.
|
1 |
shell> journalctl -u orchestrator<br> |
We will see some voting and state changing in the below logs. So, Node2(172.31.16.8) becomes the leader while other nodes follow it.
|
1 |
Feb 02 15:09:38 ip-172-31-20-60.ec2.internal orchestrator[18395]: 2025-02-02 15:09:38 DEBUG raft leader is ; state: Candidate<br>Feb 02 15:09:38 ip-172-31-20-60.ec2.internal orchestrator[18395]: 2025/02/02 15:09:38 [WARN] raft: Election timeout reached, restarting election<br>Feb 02 15:09:38 ip-172-31-20-60.ec2.internal orchestrator[18395]: 2025/02/02 15:09:38 [INFO] raft: Node at 172.31.20.60:10008 [Candidate] entering Candidate state<br>Feb 02 15:09:38 ip-172-31-20-60.ec2.internal orchestrator[18395]: 2025/02/02 15:09:38 [DEBUG] raft: Votes needed: 2<br>Feb 02 15:09:38 ip-172-31-20-60.ec2.internal orchestrator[18395]: 2025/02/02 15:09:38 [DEBUG] raft: Vote granted from 172.31.20.60:10008. Tally: 1<br>Feb 02 15:09:39 ip-172-31-20-60.ec2.internal orchestrator[18395]: 2025/02/02 15:09:39 [WARN] raft: Election timeout reached, restarting election<br>Feb 02 15:09:39 ip-172-31-20-60.ec2.internal orchestrator[18395]: 2025/02/02 15:09:39 [INFO] raft: Node at 172.31.20.60:10008 [Candidate] entering Candidate state<br>Feb 02 15:09:40 ip-172-31-20-60.ec2.internal orchestrator[18395]: 2025/02/02 15:09:40 [DEBUG] raft: Votes needed: 2<br>Feb 02 15:09:40 ip-172-31-20-60.ec2.internal orchestrator[18395]: 2025/02/02 15:09:40 [DEBUG] raft: Vote granted from 172.31.20.60:10008. Tally: 1<br>Feb 02 15:09:41 ip-172-31-20-60.ec2.internal orchestrator[18395]: 2025/02/02 15:09:41 [WARN] raft: Election timeout reached, restarting election<br>Feb 02 15:09:41 ip-172-31-20-60.ec2.internal orchestrator[18395]: 2025/02/02 15:09:41 [INFO] raft: Node at 172.31.20.60:10008 [Candidate] entering Candidate state<br>Feb 02 15:09:41 ip-172-31-20-60.ec2.internal orchestrator[18395]: 2025/02/02 15:09:41 [DEBUG] raft: Votes needed: 2<br>...<br>Feb 02 15:33:13 ip-172-31-20-60.ec2.internal orchestrator[18395]: 2025-02-02 15:33:13 DEBUG raft leader is 172.31.16.8:10008; state: Follower<br>Feb 02 15:33:18 ip-172-31-20-60.ec2.internal orchestrator[18395]: 2025-02-02 15:33:18 DEBUG raft leader is 172.31.16.8:10008; state: Follower<br> |
We can also use the below curl command to get the status.
|
1 |
shell> curl http://localhost:3000/api/status|jq .<br> |
|
1 |
{<br> "Code": "OK",<br> "Message": "Application node is healthy",<br> "Details": {<br> "Healthy": true,<br> "Hostname": "ip-172-31-16-8.ec2.internal",<br> "Token": "52c01a982d4169dc145b7693d0f86100a952949f6a83d7ef4db6ad5dafe45a8a",<br> "IsActiveNode": true,<br> "ActiveNode": {<br> "Hostname": "172.31.16.8:10008",<br> "Token": "",<br> "AppVersion": "",<br> "FirstSeenActive": "",<br> "LastSeenActive": "",<br> "ExtraInfo": "",<br> "Command": "",<br> "DBBackend": "",<br> "LastReported": "0001-01-01T00:00:00Z"<br> },<br> "Error": null,<br> "AvailableNodes": [<br> {<br> "Hostname": "ip-172-31-16-8.ec2.internal",<br> "Token": "52c01a982d4169dc145b7693d0f86100a952949f6a83d7ef4db6ad5dafe45a8a",<br> "AppVersion": "3.2.6-15",<br> "FirstSeenActive": "2025-02-02T17:49:18Z",<br> "LastSeenActive": "2025-02-04T18:01:12Z",<br> "ExtraInfo": "",<br> "Command": "",<br> "DBBackend": "/var/lib/orchestrator/orchestrator.db",<br> "LastReported": "0001-01-01T00:00:00Z"<br> }<br> ],<br> "RaftLeader": "172.31.16.8:10008",<br> "IsRaftLeader": true,<br> "RaftLeaderURI": "http://172.31.16.8:3000",<br> "RaftAdvertise": "172.31.16.8",<br> "RaftHealthyMembers": [<br> "172.31.23.135",<br> "172.31.20.60",<br> "172.31.16.8"<br> ]<br> }<br>}<br> |
In the Orchestrator UI itself we can check the Raft details.

Now consider the current raft-leader Node1(172.31.20.60).
|
1 |
shell> orchestrator-client -c raft-leader<br> |
|
1 |
172.31.20.60:10008<br> |
If we stop Node1 we can see that one of the follower nodes (Node2) becomes the new leader.
|
1 |
Shell > systemctl stop orchestrator<br> |
|
1 |
Feb 02 18:04:11 ip-172-31-16-8.ec2.internal orchestrator[1854]: 2025/02/02 18:04:11 [DEBUG] raft: Failed to contact 172.31.20.60:10008 in 1m48.352338846s<br>lines 3028-3073/3073 |
|
1 |
Feb 02 18:02:22 ip-172-31-23-135.ec2.internal orchestrator[1859]: 2025/02/02 18:02:22 [ERR] raft: Failed to make RequestVote RPC to 172.31.20.60:10008: dial tcp 172.><br>Feb 02 18:02:22 ip-172-31-23-135.ec2.internal orchestrator[1859]: 2025/02/02 18:02:22 [DEBUG] raft: Vote granted from 172.31.23.135:10008. Tally: 1<br>Feb 02 18:02:25 ip-172-31-23-135.ec2.internal orchestrator[1859]: 2025-02-02 18:02:25 DEBUG raft leader is 172.31.16.8:10008; state: Follower |
So, the new leader is Node2(172.31.16.8) now.
|
1 |
shell> orchestrator-client -c raft-leader<br> |
|
1 |
172.31.16.8:10008 |
We can also manually trigger the switchover using the command raft-elect-leader from the current leader node.
|
1 |
shell> orchestrator-client -c raft-elect-leader -hostname ip-172-31-20-60.ec2.internal<br> |
|
1 |
ip-172-31-20-60.ec2.internal |
Basically Raft leader node is responsible for making all topology related changes and recovery. Other nodes just sync/exchange information.
Once we stop the Source database Node3(172.31.23.135) the auto failover happens automatically. These are the logs from the Leader Raft node.
|
1 |
Feb 2 18:16:21 ip-172-31-16-8 orchestrator[1854]: 2025-02-02 18:16:21 INFO topology_recovery: Running PostFailoverProcesses hook 1 of 1: echo '(for all types) Recovered from DeadMaster on ip-172-31-23-135.ec2.internal:3306. Failed: ip-172-31-23-135.ec2.internal:3306; Successor: ip-172-31-16-8.ec2.internal:3306' >> /tmp/recovery.log<br>Feb 2 18:16:21 ip-172-31-16-8 orchestrator[1854]: [martini] Started GET /api/audit-recovery-steps/1738518664101788458:8540cbf99f297864b4e719eb05aac13b2588e1be79ac8b56467e8758c13f1c08 for 103.164.24.70:17284<br>Feb 2 18:16:21 ip-172-31-16-8 orchestrator[1854]: [martini] Completed 200 OK in 782.708µs<br>Feb 2 18:16:21 ip-172-31-16-8 orchestrator[1854]: [martini] Started GET /api/maintenance for 103.164.24.70:15464<br>Feb 2 18:16:21 ip-172-31-16-8 orchestrator[1854]: [martini] Completed 200 OK in 2.342996ms<br>Feb 2 18:16:21 ip-172-31-16-8 orchestrator[1854]: 2025-02-02 18:16:21 DEBUG orchestrator/raft: applying command 650: write-recovery-step<br>Feb 2 18:16:21 ip-172-31-16-8 orchestrator[1854]: 2025-02-02 18:16:21 INFO CommandRun(echo '(for all types) Recovered from DeadMaster on ip-172-31-23-135.ec2.internal:3306. Failed: ip-172-31-23-135.ec2.internal:3306; Successor: ip-172-31-16-8.ec2.internal:3306' >> /tmp/recovery.log,[]) |
In this blog post, we explored one of the ways of setting up high availability for the Orchestrator tool. The Raft mechanism has the advantage that it comes with automatic fencing and fault tolerance by voting/consensus mechanism. The leader will be elected and the sole responsible for all changes and recoveries. In a production environment, we should have at least three nodes (odd number) to have a quorum/voting. Also, there are some other ways that exist for configuring HA in an orchestrator using (Semi HA and HA by the shared backend) which we can explore in some other blog posts.
Resources
RELATED POSTS