Skip to content

Instantly share code, notes, and snippets.

@aamir814
Last active November 7, 2023 23:10
Show Gist options
  • Select an option

  • Save aamir814/3dcea3e31bd98831c0daa5a5021582e4 to your computer and use it in GitHub Desktop.

Select an option

Save aamir814/3dcea3e31bd98831c0daa5a5021582e4 to your computer and use it in GitHub Desktop.
MySQL Multi-source Replication POC

MySQL Multi-source Replication POC

Overview

Assuming you have two Cloud SQL MySQL instances, I1 and I2 with following databases:

I1: a, b, c, d, e, f, g, h
I2: i, j, k, l

You want to move database g and h from I1 to I2 with minimal downtime. So the end state should look like this:

I1: a, b, c, d, e, f
I2: i, j, k, l, g, h

Cloud SQL for MySQL doesn't support multi-source replication. So in order to achive this we need to create an intermediate MySQL instance on a VM (T1) and replicate that back to Cloud SQL as a new instance (I3). Following diagram demostrate the workflow:

image

In this gist we will cover step-by-step instructions on how to perform multi-source replicaiton.

Deploy MySQL 8 (Codelab) VM - T1

Deploy the VM on the same VPC. Also select the zone that is closest to the Cloud SQL instance.

https://console.cloud.google.com/marketplace/product/click-to-deploy-images/mysql8-book?project=aamir-playground

Username: root
Password: Daf.x9Aa

image

Make sure master_info_repository and relay_log_info_repository are set to TABLE

#To SSH to VM run this:
gcloud compute ssh mysql8-book-1-vm --tunnel-through-iap --zone=us-central1-f

PASSWORD=Daf.x9Aa
mysql -u root -p$PASSWORD

mysql> show global variables like 'master_info%';
+------------------------+-------+
| Variable_name          | Value |
+------------------------+-------+
| master_info_repository | TABLE |
+------------------------+-------+
1 row in set (0.00 sec)

mysql> show global variables like 'relay_log_info%';
+---------------------------+----------------+
| Variable_name             | Value          |
+---------------------------+----------------+
| relay_log_info_file       | relay-log.info |
| relay_log_info_repository | TABLE          |
+---------------------------+----------------+
2 rows in set (0.00 sec)

Turn ON GTID_MODE

mysql> show global variables like '%gtid%';
+----------------------------------+-------+
| Variable_name                    | Value |
+----------------------------------+-------+
| binlog_gtid_simple_recovery      | ON    |
| enforce_gtid_consistency         | OFF   |
| gtid_executed                    |       |
| gtid_executed_compression_period | 0     |
| gtid_mode                        | OFF   |
| gtid_owned                       |       |
| gtid_purged                      |       |
| session_track_gtids              | OFF   |
+----------------------------------+-------+
8 rows in set (0.00 sec)

Change GTID_MODE to ON

Must run these statements in sequence.

mysql> SET @@GLOBAL.ENFORCE_GTID_CONSISTENCY = ON;
Query OK, 0 rows affected (0.00 sec)

mysql> SET @@GLOBAL.GTID_MODE = OFF_PERMISSIVE;
Query OK, 0 rows affected (0.04 sec)

mysql> SET @@GLOBAL.GTID_MODE = ON_PERMISSIVE;
Query OK, 0 rows affected (0.02 sec)

mysql> SET @@GLOBAL.GTID_MODE = ON;
Query OK, 0 rows affected (0.02 sec)

Update my.cnf file

File is located in /etc/mysql/my.cnf and must be modified as root user

[mysqld]
server-id=3
gtid_mode=ON
enforce_gtid_consistency=ON
log-replica-updates=ON
replicate-ignore-db=mysql,a,b,c,d,e,f
binlog-format=ROW
log_bin=mysql-bin
binlog_expire_logs_seconds=172800
read_only=ON

Restart mysql as root user

root@mysql8-book-1-vm:/home/aamirharoon# service mysql stop
root@mysql8-book-1-vm:/home/aamirharoon# service mysql status
● mysql.service - MySQL Community Server
   Loaded: loaded (/lib/systemd/system/mysql.service; enabled; vendor preset: enabled)
   Active: inactive (dead) since Tue 2023-05-23 16:26:46 UTC; 4s ago
     Docs: man:mysqld(8)
           http://dev.mysql.com/doc/refman/en/using-systemd.html
  Process: 431 ExecStart=/usr/sbin/mysqld (code=exited, status=0/SUCCESS)
 Main PID: 431 (code=exited, status=0/SUCCESS)
   Status: "Server shutdown complete"

May 23 14:06:31 mysql8-book-1-vm systemd[1]: Starting MySQL Community Server...
May 23 14:06:42 mysql8-book-1-vm systemd[1]: Started MySQL Community Server.
May 23 16:26:44 mysql8-book-1-vm systemd[1]: Stopping MySQL Community Server...
May 23 16:26:46 mysql8-book-1-vm systemd[1]: mysql.service: Succeeded.
May 23 16:26:46 mysql8-book-1-vm systemd[1]: Stopped MySQL Community Server.

root@mysql8-book-1-vm:/home/aamirharoon# service mysql start

root@mysql8-book-1-vm:/home/aamirharoon# service mysql status
● mysql.service - MySQL Community Server
   Loaded: loaded (/lib/systemd/system/mysql.service; enabled; vendor preset: enabled)
   Active: active (running) since Tue 2023-05-23 16:27:05 UTC; 3min 39s ago
     Docs: man:mysqld(8)
           http://dev.mysql.com/doc/refman/en/using-systemd.html
  Process: 19026 ExecStartPre=/usr/share/mysql-8.0/mysql-systemd-start pre (code=exited, status=0/SUCCESS)
 Main PID: 19061 (mysqld)
   Status: "Server is operational"
    Tasks: 39 (limit: 4669)
   Memory: 378.3M
   CGroup: /system.slice/mysql.service
           └─19061 /usr/sbin/mysqld

May 23 16:27:04 mysql8-book-1-vm systemd[1]: Starting MySQL Community Server...
May 23 16:27:05 mysql8-book-1-vm systemd[1]: Started MySQL Community Server.

Verify GTID_MODE is ON

mysql> show global variables like '%gtid%';
+----------------------------------+-------+
| Variable_name                    | Value |
+----------------------------------+-------+
| binlog_gtid_simple_recovery      | ON    |
| enforce_gtid_consistency         | ON    |
| gtid_executed                    |       |
| gtid_executed_compression_period | 0     |
| gtid_mode                        | ON    |
| gtid_owned                       |       |
| gtid_purged                      |       |
| session_track_gtids              | OFF   |
+----------------------------------+-------+
8 rows in set (0.00 sec)

Create user for replication

CREATE USER 'rep_user'@'%' IDENTIFIED BY 'DoiT1234';
GRANT REPLICATION SLAVE ON *.* TO 'rep_user'@'%';
GRANT SELECT, SHOW VIEW, EXECUTE, REPLICATION CLIENT, RELOAD, TRIGGER ON *.* TO 'rep_user'@'%';

Instance1 - I1

Create mysql8-i1 instance

This step is optional and duplicating existing customer setup. If you have existing Cloud SQL for MySQL instance then skip this step.

MYIP=$(dig +short myip.opendns.com @resolver1.opendns.com)
MYPROJECT=aamir-playground
I1PASSWORD=Passw0rd1234

gcloud sql instances create mysql8-i1 --database-version=MYSQL_8_0 --cpu=2 --memory=4GB --enable-bin-log --network=projects/$MYPROJECT/global/networks/default --authorized-networks=$MYIP --region=us-central1 --root-password=$I1PASSWORD

Create user for replication

I1IP=$(gcloud sql instances describe mysql8-i1 --format='value(ipAddresses[0].ipAddress)')
mysql -h $I1IP -u root -p$I1PASSWORD

CREATE USER 'rep_user'@'%' IDENTIFIED BY 'DoiT1234';
GRANT REPLICATION SLAVE ON *.* TO 'rep_user'@'%';

Create database and tables on I1

This step is optional and duplicating existing customer setup. If you have existing Cloud SQL for MySQL instance then skip this step.

create database IF NOT EXISTS a;
use a;
DROP TABLE IF EXISTS a1;
CREATE TABLE IF NOT EXISTS a1 (id SERIAL PRIMARY KEY, data text);
INSERT INTO a1 (data) VALUES ('apple'), ('banana'), ('cherry');


create database IF NOT EXISTS b;
use b;
DROP TABLE IF EXISTS b1;
CREATE TABLE IF NOT EXISTS b1 (id SERIAL PRIMARY KEY, data text);
INSERT INTO b1 (data) VALUES ('apple'), ('banana'), ('cherry');


create database IF NOT EXISTS c;
use c;
DROP TABLE IF EXISTS c1;
CREATE TABLE IF NOT EXISTS c1 (id SERIAL PRIMARY KEY, data text);
INSERT INTO c1 (data) VALUES ('apple'), ('banana'), ('cherry');


create database IF NOT EXISTS d;
use d;
DROP TABLE IF EXISTS d1;
CREATE TABLE IF NOT EXISTS d1 (id SERIAL PRIMARY KEY, data text);
INSERT INTO d1 (data) VALUES ('apple'), ('banana'), ('cherry');

create database IF NOT EXISTS e;
use e;
DROP TABLE IF EXISTS e1;
CREATE TABLE IF NOT EXISTS e1 (id SERIAL PRIMARY KEY, data text);
INSERT INTO e1 (data) VALUES ('apple'), ('banana'), ('cherry');

create database IF NOT EXISTS f;
use f;
DROP TABLE IF EXISTS f1;
CREATE TABLE IF NOT EXISTS f1 (id SERIAL PRIMARY KEY, data text);
INSERT INTO f1 (data) VALUES ('apple'), ('banana'), ('cherry');

create database IF NOT EXISTS g;
use g;
DROP TABLE IF EXISTS g1;
CREATE TABLE IF NOT EXISTS g1 (id SERIAL PRIMARY KEY, data text);
INSERT INTO g1 (data) VALUES ('apple'), ('banana'), ('cherry');

create database IF NOT EXISTS h;
use h;
DROP TABLE IF EXISTS h1;
CREATE TABLE IF NOT EXISTS h1 (id SERIAL PRIMARY KEY, data text);
INSERT INTO h1 (data) VALUES ('apple'), ('banana'), ('cherry');

Instance2 - i2

Create mysql8-i2 instance

This step is optional and duplicating existing customer setup. If you have existing Cloud SQL for MySQL instance then skip this step.

MYIP=$(dig +short myip.opendns.com @resolver1.opendns.com)
MYPROJECT=aamir-playground
I2PASSWORD=Passw0rd1234

gcloud sql instances create mysql8-i2 --database-version=MYSQL_8_0 --cpu=2 --memory=4GB --enable-bin-log --network=projects/$MYPROJECT/global/networks/default --authorized-networks=${MYIP} --region=us-central1 --root-password=$I2PASSWORD

Create user for replication

I2IP=$(gcloud sql instances describe mysql8-i2 --format='value(ipAddresses[0].ipAddress)')
mysql -h $I2IP -u root -p$I2PASSWORD

CREATE USER 'rep_user'@'%' IDENTIFIED BY 'DoiT1234';
GRANT REPLICATION SLAVE ON *.* TO 'rep_user'@'%';

Create databases and table on I2

This step is optional and duplicating existing customer setup. If you have existing Cloud SQL for MySQL instance then skip this step.

create database IF NOT EXISTS i;
use i;
DROP TABLE IF EXISTS i1;
CREATE TABLE IF NOT EXISTS i1 (id SERIAL PRIMARY KEY, data text);
INSERT INTO i1 (data) VALUES ('apple'), ('banana'), ('cherry');

create database IF NOT EXISTS j;
use j;
DROP TABLE IF EXISTS j1;
CREATE TABLE IF NOT EXISTS j1 (id SERIAL PRIMARY KEY, data text);
INSERT INTO j1 (data) VALUES ('apple'), ('banana'), ('cherry');

create database IF NOT EXISTS k;
use k;
DROP TABLE IF EXISTS k1;
CREATE TABLE IF NOT EXISTS k1 (id SERIAL PRIMARY KEY, data text);
INSERT INTO k1 (data) VALUES ('apple'), ('banana'), ('cherry');

create database IF NOT EXISTS l;
use l;
DROP TABLE IF EXISTS l1;
CREATE TABLE IF NOT EXISTS l1 (id SERIAL PRIMARY KEY, data text);
INSERT INTO l1 (data) VALUES ('apple'), ('banana'), ('cherry');

Replicate I1 and I2 to T1

Export data from I1 - g and h databases only

Run on VM. Use mysql8-i1 private IP

I1PIP=10.113.128.33
I1PASSWORD=Passw0rd1234

mysqldump -h $I1PIP -uroot -p$I1PASSWORD --single-transaction --triggers --routines --events --hex-blob --set-gtid-purged=COMMENTED --databases g h > dumpI1.sql

Export data from I2 - i, j, k and l databases only

Run on VM. Use mysql8-i2 private IP

I2PIP=10.113.128.250
I2PASSWORD=Passw0rd1234

mysqldump -h $I2PIP -uroot -p$I2PASSWORD --single-transaction --triggers --routines --events --hex-blob --set-gtid-purged=COMMENTED --databases i j k l > dumpI2.sql

Following warnings can be ignored:

mysqldump: [Warning] Using a password on the command line interface can be insecure.
Warning: A partial dump from a server that has GTIDs will by default include the GTIDs of all transactions, even those that changed suppressed parts of the database. If you don't want to restore GTIDs, pass --set-gtid-purged=OFF. To make a complete dump, pass --all-databases --triggers --routines --events.

Record the gtid_purged value that mysqldump added to each of the dump files.

I1_POS=$(cat dumpI1.sql | grep GTID_PURGED | cut -f2 -d'+' | cut -f1 -d$'\'')
I2_POS=$(cat dumpI2.sql | grep GTID_PURGED | cut -f2 -d'+' | cut -f1 -d$'\'')
echo $I1_POS,$I2_POS
b816afbd-fa71-11ed-b1cf-42010a800fc4:1-27, c77fd021-fa71-11ed-b1dc-42010a800fc5:1-41

Import each dump file into T1

PASSWORD=Daf.x9Aa
mysql -uroot -p$PASSWORD < dumpI1.sql > restoreI1.log 
echo $?

mysql -uroot -p$PASSWORD < dumpI2.sql > restoreI2.log
echo $?

Reset Master to clear the GTID

On the replica, issue RESET MASTER to clear the GTID execution history (assuming, as explained above, that all the dump files have been imported and that there are no wanted transactions with GTIDs on the replica). Then issue a SET @@GLOBAL.gtid_purged statement to set the gtid_purged value to the union of all the GTID sets from all the dump files, as recorded in previous steps.

mysql -uroot -p$PASSWORD -e 'RESET MASTER; SET @@GLOBAL.gtid_purged = "'${I1_POS}','${I2_POS}'"'

Execute the CHANGE MASTER TO command

Make source of I1 over a channel named i-1.

Make source of I2 over a channel named i-2. You can name it anything

I1PIP=10.113.128.33
mysql -uroot -p$PASSWORD -e 'CHANGE REPLICATION SOURCE TO SOURCE_HOST="'${I1PIP}'", SOURCE_USER="rep_user", SOURCE_PASSWORD="DoiT1234", SOURCE_AUTO_POSITION=1 FOR CHANNEL "i-1"';

I2PIP=10.113.128.250
mysql -uroot -p$PASSWORD -e 'CHANGE REPLICATION SOURCE TO SOURCE_HOST="'${I2PIP}'", SOURCE_USER="rep_user", SOURCE_PASSWORD="DoiT1234", SOURCE_AUTO_POSITION=1 FOR CHANNEL "i-2"';

Apply Replication Filter

THIS IS TEMPORARY. RESTART WILL RESET IT. To make the replica replicate only database db1 from source1, and only database db2 from source2, use the mysql client to issue the CHANGE REPLICATION FILTER statement for each channel, like this:

mysql -uroot -p$PASSWORD -e 'CHANGE REPLICATION FILTER REPLICATE_DO_DB = (g, h) FOR CHANNEL "i-1"';
mysql -uroot -p$PASSWORD -e 'CHANGE REPLICATION FILTER REPLICATE_DO_DB = (i, j, k, l) FOR CHANNEL "i-2"';

Starting Multi-Source Replicas

mysql -uroot -p$PASSWORD
mysql> START REPLICA FOR CHANNEL "i-1";
mysql> START REPLICA FOR CHANNEL "i-2";

Verify Replication Status

mysql> SHOW REPLICA STATUS FOR CHANNEL "i-1"\G
mysql> SHOW REPLICA STATUS FOR CHANNEL "i-2"\G

Replicate T1 to I3

Set up a source representation instance

Create the request data

The request data contains basic information about your external server in JSON format. The request data can be configured for a Cloud SQL replica on a public or private network and should contain this information:

Do not perform on VM. Should do it on local PC with gcloud auth setup

hostPort is VM IP address

cat mysql8-source.json
{
   "name": "mysql8-vm",
   "region": "us-central1",
   "databaseVersion": "MYSQL_8_0",
   "onPremisesConfiguration": {
     "hostPort": "35.232.36.120:3306",
      "username": "rep_user",
      "password": "DoiT1234"
    }
}

Create a source representation instance

gcloud auth login
ACCESS_TOKEN="$(gcloud auth print-access-token)"
curl --header "Authorization: Bearer ${ACCESS_TOKEN}" \
     --header 'Content-Type: application/json' \
     --data @./mysql8-source.json \
     -X POST \
https://sqladmin.googleapis.com/sql/v1beta4/projects/aamir-playground/instances

OUTPUT:

{
  "kind": "sql#operation",
  "targetLink": "https://sqladmin.googleapis.com/sql/v1beta4/projects/aamir-playground/instances/mysql8-vm",
  "status": "DONE",
  "user": "[email protected]",
  "insertTime": "2023-05-23T22:59:54.083Z",
  "endTime": "2023-05-23T22:59:54.091Z",
  "operationType": "CREATE",
  "name": "a0da48b6-5085-434e-a8a2-099b00000032",
  "targetId": "mysql8-vm",
  "selfLink": "https://sqladmin.googleapis.com/sql/v1beta4/projects/aamir-playground/operations/a0da48b6-5085-434e-a8a2-099b00000032",
  "targetProject": "aamir-playground"
}

Set up a Cloud SQL replica - I3

Create the request data

The request data contains basic information about your external server and Cloud SQL replica in JSON format. The request data can be configured for a Cloud SQL replica on a public or private network and should contain this information. For more information click here.

cat mysql8-i3.json
{
  "settings": {
    "tier": "db-custom-2-4096",
    "dataDiskSizeGb": "20",
    "ipConfiguration": {
        "privateNetwork": "projects/aamir-playground/global/networks/default"
     }
  },
  "masterInstanceName": "mysql8-vm",
  "region": "us-central1",
  "databaseVersion": "MYSQL_8_0",
  "name": "mysql8-i3"
}

Create the Cloud SQL replica

Before you start this step, create a JSON file that contains your replica request data. Then, to create a Cloud SQL replica, open a Cloud Shell terminal and run these commands:

gcloud auth login
ACCESS_TOKEN="$(gcloud auth print-access-token)"
curl --header "Authorization: Bearer ${ACCESS_TOKEN}" \
     --header 'Content-Type: application/json' \
     --data @./mysql8-i3.json \
     -X POST https://sqladmin.googleapis.com/sql/v1beta4/projects/aamir-playground/instances

OUTPUT:

{
  "kind": "sql#operation",
  "targetLink": "https://sqladmin.googleapis.com/sql/v1beta4/projects/aamir-playground/instances/mysql8-i3",
  "status": "PENDING",
  "user": "[email protected]",
  "insertTime": "2023-05-24T14:41:18.228Z",
  "operationType": "CREATE_REPLICA",
  "name": "60b072c0-3e60-45b2-8ecd-cd0b00000032",
  "targetId": "mysql8-i3",
  "selfLink": "https://sqladmin.googleapis.com/sql/v1beta4/projects/aamir-playground/operations/60b072c0-3e60-45b2-8ecd-cd0b00000032",
  "targetProject": "aamir-playground"
}

Get the Cloud SQL replica's outgoing IP address

You can use the outgoing IP address of the Cloud SQL replica to create a secure connection between the external server and the Cloud SQL replica. You won't be charged for this IP address. image

Allow incoming connections on the external server

The Cloud SQL replica needs to connect to the external server for replication to succeed. You must configure the network firewall for your external server to accept connections from the Cloud SQL replica's outgoing IP address if the following conditions apply:

  • The external server is behind a firewall or some other network restriction.
  • Your Cloud SQL replica is using a public IP.

To connect to the Cloud SQL replica, you use the replica's primary IP address. This IP address is displayed in the Google Cloud console.

Verify your replication settings

gcloud auth login
ACCESS_TOKEN="$(gcloud auth print-access-token)"
curl --header "Authorization: Bearer ${ACCESS_TOKEN}" \
     --header 'Content-Type: application/json' \
     --data '{
         "syncMode": "online"
       }' \
     -X POST \
https://sqladmin.googleapis.com/sql/v1beta4/projects/aamir-playground/instances/mysql8-i3/verifyExternalSyncSettings

OUTPUT:

{
  "kind": "sql#externalSyncSettingErrorList"
}

Seed the Cloud SQL Replica

Using managed import service. This will start replication on the new Cloud SQL instance, i3.

gcloud auth login
ACCESS_TOKEN="$(gcloud auth print-access-token)"
curl --header "Authorization: Bearer ${ACCESS_TOKEN}" \
     --header 'Content-Type: application/json' \
     --data '{
         "syncMode": "online"
       }' \
     -X POST \
https://sqladmin.googleapis.com/sql/v1beta4/projects/aamir-playground/instances/mysql8-i3/startExternalSync

OUTPUT:

{
  "kind": "sql#operation",
  "targetLink": "https://sqladmin.googleapis.com/sql/v1beta4/projects/aamir-playground/instances/mysql8-i3",
  "status": "PENDING",
  "user": "[email protected]",
  "insertTime": "2023-05-24T17:33:35.503Z",
  "operationType": "START_EXTERNAL_SYNC",
  "name": "13c44942-c80b-4db4-9527-8c4700000032",
  "targetId": "mysql8-i3",
  "selfLink": "https://sqladmin.googleapis.com/sql/v1beta4/projects/aamir-playground/operations/13c44942-c80b-4db4-9527-8c4700000032",
  "targetProject": "aamir-playground"
}

During cut-over

Stop i1,i2 write traffic

  1. Stop writes to i1 & i2
  2. Verify t1 has all replication events
  3. Verify i3 has all necessary replication events

Promote Replica

When ready to cut-over, promote replica. image

Now i3 has g, h, i, j, k and l databases image

Enabling Writing to new i3 and existing i1 (excluding g,h)

  • Stop i2 or disable network access.
  • Drop schemas i1 (g,h). WARNING: DO NOT DO THIS ON mysql8-i3
  • Point your application for i2 traffic to the new endpoint on i3
  • Point your application for i1 traffic to new endpoint of i3 for databases (g, h)
  • Re-enable application writes
  • Validate data being written to i1 and i3 correctly.

Cleanup

  • Snapshot and delete i2, t1

Links

https://dev.mysql.com/doc/refman/8.0/en/replication-multi-source.html https://cloud.google.com/sql/docs/mysql/replication/configure-external-replica#configure_the_primary_instance https://cloud.google.com/sql/docs/mysql/replication/configure-replication-from-external#setup-source-instance https://cloud.google.com/sql/docs/mysql/replication/managed-import-replication-from-external#verify-replication

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment