In this post, I will go to explain how to set up a master-master replication system between two MySQL to replicate an existing database.
The MySQL master-master configuration is a master-slave configuration in both directions and allows us to get a high availability configuration because you have the same data in both servers and if one goes down you can still work with the live server.
Table of Contents
Machines
For the test, I have two virtual machines running Debian 9 in server version (you can download the virtual image from this link (Spanish version)).
The IP of this two virtual machine will be:
- Server 1
- 192.168.0.201
- Server 2
- 192.168.0.202
It is important to configure the virtual machine (Virtualbox in my case) network in “bridge” mode so that the machines see each other, as this way they have access to the local network. This does not happen in physical machines or in provisioned virtual machines.
Creating the server 1
To create the server 1 you have to import the appliance. Go to “File”->”Import Appliance…”:
Now you have to change the network. Over the virtual machine, select “Settings”:
And change the network configuration, using the “Bridged Adapter” option.
Creating the server 2
You have to make the same steps to create the server 2:
And you have to change the network adapter to “Bridged adapter”:
Now you have two virtual machines ready to work.
Starting the machines
Now you have to start the two virtual machines. To do this, you have to select each virtual machine and click on the “Start” button.
Changing the network configuration
Now you have to update the network address of each VM. Login in each machine with these credentials:
- Username: root
- Password: toor
Server 1
To check the address configuration you have to execute this command:
$ ip addr show
To add a new IP you have to edit the /etc/network/interfaces file:
$ sudo vi /etc/network/interfaces auto lo iface lo inet lookpack auto enp0s3 iface enp0s3 inet static address 192.168.0.201 netmask 255.255.255.0 gateway 192.168.0.1 dns-nameservers 1.1.1.1 dns-nameservers 9.9.9.9
Once you save the file, you have to bring the interface down.
$ sudo ifdown enp0s3
Then bring the interface up:
$ sudo ifup enp0s3
And check the change:
$ sudo ip addr
Server 2
You have to do the same in the server 2 to add the IP 192.168.0.202. The unique change is to replace 201 with 202 in the /etc/network/interfaces file.
Check the change:
$ sudo ip addr
Try to ping to the other machine to check the connectivity.
From server 1 execute:
$ ping 192.168.0.202
And from server 2 execute:
$ ping 192.168.0.201
Updating the packages
To update the packages you have to execute in each machine:
$ sudo apt update && sudo apt upgrade -y
Installing MySQL 5.7
Debian 9 installs MariaDB by default (more information on this link and on this), so you have to add a new repo, then reload the sources and finally install the software.
You have to install MySQL 5.7 server on both servers. Execute the same process in both servers.
Go to the download page https://dev.mysql.com/downloads/repo/apt/ and get the filename of the executable (now is mysql-apt-config_0.8.13-1_all.deb). Maybe when you download it, the version will be a bigger one.
Download the version using wget or curl:
$ wget https://dev.mysql.com/get/mysql-apt-config_0.8.13-1_all.deb
Install the package:
$ sudo dpkg -i mysql-apt-config_0.8.7-1_all.deb
Add the MySQL 5.7 repo because the default repo is the MySQL 8.0.
Update the package information with the new repo:
$ sudo apt update
And install the MySQL server:
$ sudo apt install mysql-server
Enter the root password for the MySQL server:
Testing the MySQL server
Try in both servers that the server works well locally. To do this, try to connect using the root user:
$ mysql -u root -p Enter password: Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 2 Server version: 5.7.28 MySQL Community Server (GPL) Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql>
To check that you can execute a command, show the databases that MySQL creates by default, with the command “show databases;”
mysql> show databases; +--------------------+ | Database | +--------------------+ |information_schema | | mysql | | performance_schema | | sys | +--------------------+ 4 rows in set (0.00 sec)
To exit the MySQL client console run the “exit” command:
mysql> exit Bye
Execute this test on both servers.
Securing MySQL
If you’re going to use these servers in production, it’s interesting that you secure them. Also, (I’m not going to explain how to do it) it is highly recommended that you only allow remote access to MySQL from the other MySQL servers. To filter this traffic you can use a firewall as iptables.
To secure the server, execute the command “mysql_secure_installation”, which guided you to adjust the security of the MySQL server. After the command I leave the result of my execution to show the values I have used when the script asks me something.
$ sudo mysql_secure_installation Securing the MySQL server deployment. Enter password for user root: VALIDATE PASSWORD PLUGIN can be used to test passwords and improve security. It checks the strength of password and allows the users to set only those passwords which are secure enough. Would you like to setup VALIDATE PASSWORD plugin? Press y|Y for Yes, any other key for No: y There are three levels of password validation policy: LOW Length >= 8 MEDIUM Length >= 8, numeric, mixed case, and special characters STRONG Length >= 8, numeric, mixed case, special characters and dictionary file Please enter 0 = LOW, 1 = MEDIUM and 2 = STRONG: 2 Using existing password for root. Estimated strength of the password: 50 Change the password for root ? ((Press y|Y for Yes, any other key for No) : n ... skipping. By default, a MySQL installation has an anonymous user, allowing anyone to log into MySQL without having to have a user account created for them. This is intended only for testing, and to make the installation go a bit smoother. You should remove them before moving into a production environment. Remove anonymous users? (Press y|Y for Yes, any other key for No) : y Success. Normally, root should only be allowed to connect from 'localhost'. This ensures that someone cannot guess at the root password from the network. Disallow root login remotely? (Press y|Y for Yes, any other key for No) : y Success. By default, MySQL comes with a database named 'test' that anyone can access. This is also intended only for testing, and should be removed before moving into a production environment. Remove test database and access to it? (Press y|Y for Yes, any other key for No) : y - Dropping test database... Success. - Removing privileges on test database... Success. Reloading the privilege tables will ensure that all changes made so far will take effect immediately. Reload privilege tables now? (Press y|Y for Yes, any other key for No) : y Success. All done!
Then I restart the server:
$ sudo systemctl restart mysql.service
Execute this command on both servers.
Interface configuration
Next, I put MySQL to listen in all the IP, changing in the file /etc/mysql/mysql.conf.d/mysqld.cnf the line
bind-address = 127.0.0.1
with
bind-address = 0.0.0.0
Then I have to restart the service:
$ sudo systemctl restart mysql.service
Change this configuration on both servers. Remember to protect the server with a firewall in production.
To test that the MySQL port is open to other machines, I use nmap from one virtual machine to test the other and viceversa.
$ # Server 1 $ sudo apt install nmap -y $ sudo nmap -p 3306 192.168.0.202 Starting Nmap 7.40 ( https://nmap.org ) at 2019-10-23 13:24 CEST Nmap scan report for 192.168.0.202 Host is up (-0.18s latency). PORT STATE SERVICE 3306/tcp open mysql MAC Address: 08:00:27:44:DB:B4 (Oracle VirtualBox virtual NIC) Nmap done: 1 IP address (1 host up) scanned in 0.46 seconds $ # Server 2 $ nmap -p 3306 192.168.0.201 Starting Nmap 7.40 ( https://nmap.org ) at 2019-10-23 13:24 CEST Nmap scan report for 192.168.0.201 Host is up (0.00027s latency). PORT STATE SERVICE 3306/tcp open mysql MAC Address: 08:00:27:BD:0C:DB (Oracle VirtualBox virtual NIC) Nmap done: 1 IP address (1 host up) scanned in 0.47 seconds
Installing WordPress in the server 1
I will go to install a WordPress database to get a clean database to replicate. To do this I create a new database, a new user and grant access to this user to the new database:
$ # Server 1 $ mysql -u root -p mysql> CREATE DATABASE wordpress CHARACTER SET utf8 COLLATE utf8_general_ci; mysql> CREATE USER 'wordpress'@'localhost' identified by 'my_password'; mysql> GRANT ALL PRIVILEGES ON wordpress.* TO wordpress@localhost; mysql> FLUSH PRIVILEGES; mysql> exit
Now you have to install the WP-CLI to make a fast WordPress install:
$ sudo apt install php php7.0-mysql -y $ wget https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar $ php wp-cli.phar --info $ chmod +x wp-cli.phar $ sudo mv wp-cli.phar /usr/local/bin/wp $ wp --info OS: Linux 4.9.0-11-amd64 #1 SMP Debian 4.9.189-3+deb9u1 (2019-09-20) x86_64 Shell: /bin/bash PHP binary: /usr/bin/php7.0 PHP version: 7.0.33-0+deb9u5 php.ini used: /etc/php/7.0/cli/php.ini WP-CLI root dir: phar://wp-cli.phar/vendor/wp-cli/wp-cli WP-CLI vendor dir: phar://wp-cli.phar/vendor WP-CLI phar path: /root WP-CLI packages dir: WP-CLI global config: WP-CLI project config: WP-CLI version: 2.3.0
To install WordPress you have to execute these commands:
$ cd /var/www/html $ wp core download $wp config create \
--dbname=wordpress \
--dbuser=wordpress \
--dbpass=
my_password $wp core install --url=wordpress.test \
--title="Testing WordPress" \
--admin_user=manager_wordpress \
--admin_password="my_long_and_secure_password" \
--admin_email=hello@wordpress.test
Configuring the replication
Now you have to configure the replication in the MySQL configuration.
Server 1
You have to edit the file /etc/mysql/myssql.conf.d/mysqld.conf to add the next lines:
server-id = 1 log_bin = /var/log/mysql/mysql-bin.log expire_logs_days = 10 max_binlog_size = 100M binlog_do_db = wordpress
Then you have to restart the service to update the configuration:
$ sudo systemctl restart mysql.service
To check the service you can execute
$ netstat -ntpl | grep mysql tcp 0 0 0.0.0.0:3306 0.0.0.0:* LISTEN 17592/mysqld
Server 2
You have to make the same change in the same file in the server 2, changing the server-id from 1 to 2:
server-id = 2 log_bin = /var/log/mysql/mysql-bin.log expire_logs_days = 10 max_binlog_size = 100M binlog_do_db = wordpress
Then you have to restart the service to update the configuration:
$ sudo systemctl restart mysql.service
To check the service you can execute:
$ netstat -ntpl | grep mysql tcp 0 0 0.0.0.0:3306 0.0.0.0:* LISTEN 5996/mysqld
Creating the users for the replication
You need to create one MySQL user in each server for the replication, so you have to create the user and grant replication permissions.
Server 1
Open the MySQL command line, create the user and grant the correct permissions:
> CREATE USER 'replicationuser'@'%' identified by 'my_password'; > GRANT REPLICATION SLAVE ON *.* TO 'replicationuser'@'%';
Server 2
Open the MySQL command line, create the user and grant the correct permissions:
> CREATE USER 'replicationuser'@'%' identified by 'my_password'; > GRANT REPLICATION SLAVE ON *.* TO 'replicationuser'@'%';
Testing the users
You need to test that the users can access to the other server:
Server 1
Check that the replication user can access to the server 2:
$ mysql -u replicationuser -p -h 192.168.0.202 Enter password: Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 84 Server version: 5.7.28-log MySQL Community Server (GPL) Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql>
Server 2
Check that the replication user can access to the server 1:
$ mysql -u replicationuser -p -h 192.168.0.201 Enter password: Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 16 Server version: 5.7.28-log MySQL Community Server (GPL) Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql>
Export the database from server 1 to server 2
To start the replication both servers must have the same copy of the database, so you have to stop all applications or other elements that can change the content of the databases until we start the replication.
Server 1
In the server 1, I have the database, so I will go to dump it and then I will copy it to the server 2 using scp:
$ mysqldump wordpress > wordpress.sql $ scp wordpress.sql user@192.168.0.202:/home/user
Server 2
In the server 2, I have to create the database, a user to access to it and then I restore the database content with the dump from the server 1:
$ mysql -u root -p mysql> CREATE DATABASE wordpress CHARACTER SET utf8 COLLATE utf8_general_ci; mysql> CREATE USER 'wordpress'@'localhost' identified by 'my_password'; mysql> GRANT ALL PRIVILEGES ON wordpress.* TO wordpress@localhost; mysql> FLUSH PRIVILEGES; mysql> exit $ cd /home/user $ mysql -u root -p wordpress < wordpress.sql
Start the replication
Now you have to tell each server that the other server is its master and the point from which the replication will start:
Server 1
Open the MySQL command line and check the master status:
mysql> show master status\G; * 1. row * File: mysql-bin.000004 Position: 617 Binlog_Do_DB: wordpress Binlog_Ignore_DB: Executed_Gtid_Set: 1 row in set (0,00 sec)
You need two values:
- The file name: mysql-bin.000004
- The position: 617
Go to the server 2, open the MySQL command line, stop the slave:
> stop slave; Query OK, 0 rows affected, 1 warning (0,00 sec)
Change the master configuration:
> CHANGE MASTER TO MASTER_HOST = '192.168.0.201', MASTER_USER = 'replicationuser', MASTER_PASSWORD = 'my_password', MASTER_LOG_FILE = 'mysql-bin.000004', MASTER_LOG_POS = 617; Query OK, 0 rows affected, 2 warnings (0,20 sec)
And start the slave:
> start slave;
Server 2
Open the MySQL command line and check the master status:
> show master status\G; * 1. row * File: mysql-bin.000004 Position: 38321 Binlog_Do_DB: wordpress Binlog_Ignore_DB: Executed_Gtid_Set: 1 row in set (0,00 sec)
You need two values:
- The file name: mysql-bin.000004
- The position: 38321
Go to server 1, open the MySQL command line, stop the slave:
> stop slave; Query OK, 0 rows affected, 1 warning (0,00 sec)
Change the master configuration:
> CHANGE MASTER TO MASTER_HOST = '192.168.0.202', MASTER_USER = 'replicationuser', MASTER_PASSWORD = 'toor', MASTER_LOG_FILE = 'mysql-bin.000004', MASTER_LOG_POS = 38321; Query OK, 0 rows affected, 2 warnings (0,20 sec)
And start the slave:
> start slave;
Test the replication
Replication from server 1 to server 2
Server 1
To test the replication from server 1 to server 2 I will change the “thread_comments_depth” option in the database from “5”, the current value, to “6”, using the WP-CLI:
$ wp option update thread_comments_depth 6
Server 2
To see if the value has changed on the server 2, open the MySQL command line and check the value using an SQL query:
> use wordpress; > select * from wp_options where option_name='thread_comments_depth'\G; * 1. row * option_id: 72 option_name: thread_comments_depth option_value: 6 autoload: yes 1 row in set (0,00 sec)
You can see that the value has changed.
Replication from server 1 to server 2
Server 2
Now I will check the synchronization from server 2 to server 1. To test the replication I will change the “wp_page_for_privacy_policy” option in the database from “3”, the current value, to “4”, using an SQL query:
> update wp_options set option_value=4 where option_name='wp_page_for_privacy_policy'; Query OK, 1 row affected (0,03 sec) Rows matched: 1 Changed: 1 Warnings: 0
Server 1
To see if the value has changed on the server 1, I check the value using the WP-CLI:
$ wp option get wp_page_for_privacy_policy
4
You can see that the value has changed.