Posted
over 4 years
ago
by
Bhuvanesh R
When you installed the Debezium MySQL connector, then it’ll start read your historical data and push all of them into the Kafka topics. This setting can we changed via snapshot.mode parameter in the connector. But if you are going to start a new
... [More]
sync, then Debezium will load the existing data its called Snapshot. Unfortunately, if you have a busy transactional MySQL database, then it may lead to some performance issues. And your DBA will never agree to read the data from Master Node.[Disclaimer: I’m a DBA :) ]. So I was thinking of figuring out to take the snapshot from the Read Replica, once the snapshot is done, then start read the realtime data from the Master. I found this useful information in a StackOverflow answer.
If your binlog uses GTID, you should be able to make a CDC tool like Debezium read the snapshot from the replica, then when that’s done, switch to the master to read the binlog. But if you don’t use GTID, that’s a little more tricky. The tool would have to know the binlog position on the master corresponding to the snapshot on the replica.
Source: https://stackoverflow.com/a/58250791/6885516
Then I tried to implement in a realtime scenario and verified the statement is true. Yes, we made this in our system. Here is the step by step details from our PoC.
Requirements:
Master and Slave should be enabled with GTID.
Debezium Connector Node can talk to both master and slave.
log-slave-updates must be enabled on the slave(anyhow for GTID its requires).
A user account for Debezium with respective permissions.
Install Debezium connector.
Sample data:
Create a new database to test this sync and insert some values.
create database bhuvi;
use bhuvi;
create table rohi (
id int,
fn varchar(10),
ln varchar(10),
phone int);
insert into rohi values (1, 'rohit', 'last',87611);
insert into rohi values (2, 'rohit', 'last',87611);
insert into rohi values (3, 'rohit', 'last',87611);
insert into rohi values (4, 'rohit', 'last',87611);
insert into rohi values (5, 'rohit', 'last',87611);
Create the MySQL Connector Config:
File Name: mysql.json
{
"name": "mysql-connector-db01",
"config": {
"name": "mysql-connector-db01",
"connector.class": "io.debezium.connector.mysql.MySqlConnector",
"database.server.id": "1",
"tasks.max": "1",
"database.history.kafka.bootstrap.servers": "YOUR-BOOTSTRAP-SERVER:9092",
"database.history.kafka.topic": "schema-changes.mysql",
"database.server.name": "mysql-db01",
"database.hostname": "IP-OF-READER-NODE",
"database.port": "3306",
"database.user": "bhuvi",
"database.password": "****",
"database.whitelist": "bhuvi",
"snapshot.mode": "initial",
"snapshot.locking.mode": "none",
"key.converter": "org.apache.kafka.connect.json.JsonConverter",
"value.converter": "org.apache.kafka.connect.json.JsonConverter",
"key.converter.schemas.enable": "false",
"value.converter.schemas.enable": "false",
"internal.key.converter": "org.apache.kafka.connect.json.JsonConverter",
"internal.value.converter": "org.apache.kafka.connect.json.JsonConverter",
"internal.key.converter.schemas.enable": "false",
"internal.value.converter.schemas.enable": "false",
"transforms": "unwrap",
"transforms.unwrap.add.source.fields": "ts_ms",
"tombstones.on.delete": "false",
"transforms.unwrap.type": "io.debezium.transforms.ExtractNewRecordState
}
}
Watch the status of the connector:
Open three terminal windows and start listening to the following topics.
NOTE: change the bootstrap-server as per your cluster’s IP.
connect-configs
connect-status
--Terminal-1
kafka-console-consumer –bootstrap-server localhost:9092 –topic connect-configs –from-beginning
--Terminal-2
kafka-console-consumer –bootstrap-server localhost:9092 –topic connect-status –from-beginning
Install the Connector:
curl -X POST -H "Accept: application/json" -H "Content-Type: application/json" http://localhost:8083/connectors -d @mysql.json
Once you installed, from your connect-configs topic, you will get the following output.
{"properties":{"connector.class":"io.debezium.connector.mysql.MySqlConnector","snapshot.locking.mode":"none","database.user":"bhuvi","database.server.id":"1","tasks.max":"1","database.history.kafka.bootstrap.servers":"172.31.40.132:9092","database.history.kafka.topic":"schema-changes.mysql","database.server.name":"mysql-db01","internal.key.converter.schemas.enable":"false","database.port":"3306","key.converter.schemas.enable":"false","internal.key.converter":"org.apache.kafka.connect.json.JsonConverter","task.class":"io.debezium.connector.mysql.MySqlConnectorTask","database.hostname":"172.31.25.99","database.password":"*****","internal.value.converter.schemas.enable":"false","name":"mysql-connector-db01","value.converter.schemas.enable":"false","internal.value.converter":"org.apache.kafka.connect.json.JsonConverter","value.converter":"org.apache.kafka.connect.json.JsonConverter","database.whitelist":"bhuvi","key.converter":"org.apache.kafka.connect.json.JsonConverter","snapshot.mode":"initial"}}
{"tasks":1}
And then from your connect-statustopic, you’ll get the status of your MySQL connector.
{"state":"RUNNING","trace":null,"worker_id":"172.31.36.115:8083","generation":2}
{"state":"RUNNING","trace":null,"worker_id":"172.31.36.115:8083","generation":3}
Snapshot Status from the log file:
By default, the Kafka connector’s logs will go to syslog. You can customize this log location. So wherever you have the log file, you can see the snapshot progress there.
[2019-12-28 11:06:04,246] INFO Step 7: scanning contents of 1 tables while still in transaction (io.debezium.connector.mysql.SnapshotReader)
[2019-12-28 11:06:04,252] INFO Step 7: - scanning table 'bhuvi.rohi' (1 of 1 tables) (io.debezium.connector.mysql.SnapshotReader)
[2019-12-28 11:06:04,252] INFO For table 'bhuvi.rohi' using select statement: 'SELECT * FROM `bhuvi`.`rohi`' (io.debezium.connector.mysql.SnapshotReader)
[2019-12-28 11:06:04,264] INFO Step 7: - Completed scanning a total of 31 rows from table 'bhuvi.rohi' after 00:00:00.012 (io.debezium.connector.mysql.SnapshotReader)
[2019-12-28 11:06:04,265] INFO Step 7: scanned 5 rows in 1 tables in 00:00:00.018 (io.debezium.connector.mysql.SnapshotReader)
[2019-12-28 11:06:04,265] INFO Step 8: committing transaction (io.debezium.connector.mysql.SnapshotReader)
[2019-12-28 11:06:04,267] INFO Completed snapshot in 00:00:01.896 (io.debezium.connector.mysql.SnapshotReader)
[2019-12-28 11:06:04,348] WARN [Producer clientId=connector-producer-mysql-connector-db01-0] Error while fetching metadata with correlation id 7 : {mysql-db01.bhuvi.rohi=LEADER_NOT_AVAILABLE} (org.apache.kafka.clients.NetworkClient)
[2019-12-28 11:06:04,460] INFO Transitioning from the snapshot reader to the binlog reader (io.debezium.connector.mysql.ChainedReader)
[2019-12-28 11:06:04,492] INFO GTID set purged on server: 88726004-2734-11ea-ae86-0e7687279b85:1-7 (io.debezium.connector.mysql.BinlogReader)
[2019-12-28 11:06:04,492] INFO Attempting to generate a filtered GTID set (io.debezium.connector.mysql.MySqlTaskContext)
[2019-12-28 11:06:04,492] INFO GTID set from previous recorded offset: 88726004-2734-11ea-ae86-0e7687279b85:1-11 (io.debezium.connector.mysql.MySqlTaskContext)
[2019-12-28 11:06:04,492] INFO GTID set available on server: 88726004-2734-11ea-ae86-0e7687279b85:1-11 (io.debezium.connector.mysql.MySqlTaskContext)
[2019-12-28 11:06:04,492] INFO Final merged GTID set to use when connecting to MySQL: 88726004-2734-11ea-ae86-0e7687279b85:1-11 (io.debezium.connector.mysql.MySqlTaskContext)
[2019-12-28 11:06:04,492] INFO Registering binlog reader with GTID set: 88726004-2734-11ea-ae86-0e7687279b85:1-11 (io.debezium.connector.mysql.BinlogReader)
Snapshot Complete:
Once your’ snapshot process is done, then the connect-offsets topic will have the binlog information of till where it’s consumed.
{"file":"ip-172-31-25-99-bin.000001","pos":1234,"gtids":"88726004-2734-11ea-ae86-0e7687279b85:1-11"}
Then it’ll start applying the ongoing replication changes as well.
{"ts_sec":1577531225,"file":"ip-172-31-25-99-bin.000001","pos":1299,"gtids":"88726004-2734-11ea-ae86-0e7687279b85:1-11","row":1,"server_id":1,"event":2}
Now we have verified that the database’s snapshot has been done. Its time to swap the nodes. We’ll start consuming from the Master.
If you enable the Monitoring for the Debezium connector, then you see the lag from the JMX or Premetheus metrics.
Reference: Configuring monitoring for Debezium MySQL Connector.
curl localhost:7071 | grep debezium_metrics_SecondsBehindMaster
debezium_metrics_SecondsBehindMaster{context="binlog",name="mysql-db01",plugin="mysql",} 299.577536699E9
Sometimes the metrics take a few more minutes to update. So once you are able to see the last binlog information from the connet-offsets and the lag <10, then the snapshot is done.
Switch to Master:
The main important thing is to STOP the slave thread in your Read replica. This will prevent the changing the GTID in your connect-offsets topic.
mysql-slave> STOP SLAVE;
To simulate the sync, we can add 1 new row in our MySQL table. So this will never replicate to your slave. But once you switch the node, it should start reading from this row.
mysql-master> insert into rohi values (6, 'rohit', 'last','87611');
We need to update the existing MySQL connector’s config and just change the "database.hostname" parameter.
Note: this JSON file format is different from the one which we used to register the connector. So make sure the syntax.
{
"connector.class": "io.debezium.connector.mysql.MySqlConnector",
"snapshot.locking.mode": "none",
"tasks.max": "3",
"database.history.kafka.topic": "schema-changes.mysql",
"transforms": "unwrap",
"internal.key.converter.schemas.enable": "false",
"transforms.unwrap.add.source.fields": "ts_ms",
"tombstones.on.delete": "false",
"transforms.unwrap.type": "io.debezium.transforms.ExtractNewRecordState",
"value.converter": "org.apache.kafka.connect.json.JsonConverter",
"database.whitelist": "bhuvi",
"key.converter": "org.apache.kafka.connect.json.JsonConverter",
"database.user": "bhuvi",
"database.server.id": "1",
"database.history.kafka.bootstrap.servers": "YOUR-KAFKA-BOOTSTRAP-SERVER:9092",
"database.server.name": "mysql-db01",
"database.port": "3306",
"key.converter.schemas.enable": "false",
"internal.key.converter": "org.apache.kafka.connect.json.JsonConverter",
"database.hostname": "MASTER-IP-ADDRESS",
"database.password": "****",
"internal.value.converter.schemas.enable": "false",
"name": "mysql-connector-db01",
"value.converter.schemas.enable": "false",
"internal.value.converter": "org.apache.kafka.connect.json.JsonConverter",
"snapshot.mode": "initial"
}
Run the below command to update the config file.
File Name: mysql-update.json
curl -X PUT -H "Accept: application/json" -H "Content-Type: application/json" http://localhost:8083/connectors/mysql-connector-db01/config -d @mysql-update.json
Once its updated, from the connect-offsets topic, you can see that the Debezium starts reading the data from the Next GTID.
{"ts_sec":1577531276,"file":"mysql-bin.000008","pos":1937,"gtids":"88726004-2734-11ea-ae86-0e7687279b85:1-13","row":1,"server_id":1,"event":2}
Also from your topic, you can see the last row has been pushed.
kafka-console-consumer --bootstrap-server localhost:9092 --topic mysql-db01.bhuvi.rohi --from-beginning
{"id":1,"fn":"rohit","ln":"last","phone":87611,"__ts_ms":0}
{"id":2,"fn":"rohit","ln":"last","phone":87611,"__ts_ms":0}
{"id":3,"fn":"rohit","ln":"last","phone":87611,"__ts_ms":0}
{"id":4,"fn":"rohit","ln":"last","phone":87611,"__ts_ms":0}
{"id":5,"fn":"rohit","ln":"last","phone":87611,"__ts_ms":0}
{"id":6,"fn":"rohit","ln":"last","phone":87611,"__ts_ms":1577531276000}
This method helped us to sync the historical data from the Read replica to the Kafka topic without affecting the transactions on the Master node. Still, we are exploring this for more scenarios. I’ll keep posting new articles about this.
[Less]
|
Posted
over 4 years
ago
by
parvez alam
try this
ID
|
Posted
over 4 years
ago
by
parvez alam
yes, you can add
|
Posted
over 4 years
ago
by
Sri Sakthivel Durai Pandian
MySQL InnoDB Cluster has introduced by the MySQL team for the High Availability ( HA ) purpose . It provides a complete high availability solution for MySQL.
Alright, I am planning to write the series of the blogs about the InnoDB Cluster
... [More]
configurations / Management with MySQL Shell / Monitoring etc …
In this blog I am going to show the InnoDB Cluster configuration with three nodes .
What is InnoDB Cluster ?
MySQL InnoDB Cluster is the Combination of,
MySQL shell
Group Replication ( GR )
MySQL Router
Lab Environment :
I have prepared my lab with three servers,
OS : Centos 7.7
MySQL 8.0.18 ( latest version )
The server details are ,
192.168.91.11 ( hostname : sakthilabs11 )
192.168.91.12 ( hostname : sakthilabs12 )
192.168.91.13 ( hostname : sakthilabs13 )
Step 1 :
Need to allow the complete communication between the cluster nodes based on the hostname and IP . The below entry needs to be made on all the cluster nodes individually .
[root@sakthilabs11 ~]# cat /etc/hosts | grep 192
192.168.33.11 sakthilabs11 sakthilabs11
192.168.33.12 sakthilabs12 sakthilabs12
192.168.33.13 sakthilabs13 sakthilabs13
Step 2 :
In this step, we need to prepare the MySQL server for the InnoDB Cluster . The below step needs to be individually executed on all the cluster nodes .
cmd : dba.configureLocalInstance("username@userhost:3306");
When executing the above command , it will print the informations and ask the actions to configure the InnoDB Cluster . I just highlight them in the below output section .
output ;
MySQL localhost:33060+ ssl JS > dba.configureLocalInstance("root@localhost:3306");Please provide the password for 'root@localhost:3306': *Save password for 'root@localhost:3306'? [Y]es/[N]o/Ne[v]er (default No): yConfiguring local MySQL instance listening at port 3306 for use in an InnoDB cluster…
1) Create remotely usable account for 'root' with same grants and password
2) Create a new admin account for InnoDB cluster with minimal required grants
3) Ignore and continue
4) Cancel
Please select an option [1]: 2
Please provide an account name (e.g: icroot@%) to have it created with the necessary
privileges or leave empty and press Enter to cancel.
Account Name: InnoDBCluster
Password for new account: ***
Confirm password: ***
NOTE: Some configuration options need to be fixed:+--------------------------+---------------+----------------+--------------------------------------------------+| Variable | Current Value | Required Value | Note |+--------------------------+---------------+----------------+--------------------------------------------------+| binlog_checksum | CRC32 | NONE | Update the server variable || enforce_gtid_consistency | OFF | ON | Update read-only variable and restart the server || gtid_mode | OFF | ON | Update read-only variable and restart the server || server_id | 1 | | Update read-only variable and restart the server |+--------------------------+---------------+----------------+--------------------------------------------------+
Some variables need to be changed, but cannot be done dynamically on the server.
Do you want to perform the required configuration changes? [y/n]: y
Do you want to restart the instance after configuring it? [y/n]: y
Cluster admin user 'InnoDBCluster'@'%' created.Configuring instance…The instance 'localhost:3306' was configured for InnoDB cluster usage.Restarting MySQL…NOTE: MySQL server at localhost:3306 was restarted.
Step 3 :
After prepare all the nodes , need to login any one of the MySQL Shell with the InnoDB Cluster account ( which was created during the preparing phase ) .
cmd : shell.connect('[email protected]:3306');
output :
MySQL localhost:33060+ ssl JS > shell.connect('[email protected]:3306');
Creating a session to '[email protected]:3306'
Please provide the password for '[email protected]:3306': ***
Save password for '[email protected]:3306'? [Y]es/[N]o/Ne[v]er (default No): y
Fetching schema names for autocompletion… Press ^C to stop.
Closing old connection…
Your MySQL connection id is 9
Server version: 8.0.18 MySQL Community Server - GPL
No default schema selected; type \use to set one.
MySQL 192.168.33.11:3306 ssl JS >
Step 4 :
Create your first node of the InnoDB Cluster .
cmd : cluster = dba.createCluster('first_InnoDB_cluster');
output :
MySQL 192.168.33.11:3306 ssl JS > cluster = dba.createCluster('first_InnoDB_cluster');
A new InnoDB cluster will be created on instance '192.168.33.11:3306'.
Validating instance at 192.168.33.11:3306…
This instance reports its own address as sakthilabs11:3306
Instance configuration is suitable.
Creating InnoDB cluster 'first_InnoDB_cluster' on '192.168.33.11:3306'…
Adding Seed Instance…
Cluster successfully created. Use Cluster.addInstance() to add MySQL instances.
At least 3 instances are needed for the cluster to be able to withstand up to
one server failure.
MySQL 192.168.33.11:3306 ssl JS >
Step 5 :
Now we have successfully created the single node cluster , Have to add the other nodes as well . When adding the other nodes, it will ask the recovery method, we need to choose them . Clone plugin is the default one .
cmd : cluster.addInstance('[email protected]:3306');
output :
MySQL 192.168.33.11:3306 ssl JS > cluster.addInstance('[email protected]:3306');
Please provide the password for '[email protected]:3306': ***
Save password for '[email protected]:3306'? [Y]es/[N]o/Ne[v]er (default No): y
Please select a recovery method [C]lone/[I]ncremental recovery/[A]bort (default Clone): clone
Validating instance at 192.168.33.12:3306…
This instance reports its own address as sakthilabs12:3306
Instance configuration is suitable.A new instance will be added to the InnoDB cluster. Depending on the amount ofdata on the cluster this might take from a few seconds to several hours.
Adding instance to the cluster…
Monitoring recovery process of the new cluster member. Press ^C to stop monitoring and let it continue in background.Clone based state recovery is now in progress.
NOTE: A server restart is expected to happen as part of the clone process. If theserver does not support the RESTART command or does not come back after awhile, you may need to manually start it back.
Waiting for clone to finish…NOTE: 192.168.33.12:3306 is being cloned from sakthilabs11:3306** Stage DROP DATA: Completed** Clone TransferFILE COPY ############################################################ 100% CompletedPAGE COPY ############################################################ 100% CompletedREDO COPY ############################################################ 100% Completed** Stage RECOVERY: \NOTE: 192.168.33.12:3306 is shutting down…
Waiting for server restart… ready
sakthilabs12:3306 has restarted, waiting for clone to finish…
Clone process has finished: 59.55 MB transferred in about 1 second (~59.55 MB/s)
Incremental distributed state recovery is now in progress.
Waiting for distributed recovery to finish…NOTE: '192.168.33.12:3306' is being recovered from 'sakthilabs11:3306'
Distributed recovery has finished
The instance '192.168.33.12:3306' was successfully added to the cluster.
Similarly , I have added the third node as well .
Finally ,
We can check the cluster status with the below command .
cmd : cluster.status();
output :
MySQL 192.168.33.11:3306 ssl JS > cluster.status();
{
"clusterName": "first_InnoDB_cluster",
"defaultReplicaSet": {
"name": "default",
"primary": "sakthilabs11:3306",
"ssl": "REQUIRED",
"status": "OK",
"statusText": "Cluster is ONLINE and can tolerate up to ONE failure.",
"topology": {
"sakthilabs11:3306": {
"address": "sakthilabs11:3306",
"mode": "R/W",
"readReplicas": {},
"replicationLag": null,
"role": "HA",
"status": "ONLINE",
"version": "8.0.18"
},
"sakthilabs12:3306": {
"address": "sakthilabs12:3306",
"mode": "R/O",
"readReplicas": {},
"replicationLag": null,
"role": "HA",
"status": "ONLINE",
"version": "8.0.18"
},
"sakthilabs13:3306": {
"address": "sakthilabs13:3306",
"mode": "R/O",
"readReplicas": {},
"replicationLag": null,
"role": "HA",
"status": "ONLINE",
"version": "8.0.18"
}
},
"topologyMode": "Single-Primary"
},
"groupInformationSourceMember": "sakthilabs11:3306"
}
Hope this blog will helps someone who is trying to learning the MySQL InnoDB Cluster . I will be coming back with new blog soon .
Thanks !!
[Less]
|
Posted
over 4 years
ago
by
MySQL Performance Blog
This blog discusses a few concepts about Docker and how we can use it to run a MySQL async replication environment. Docker is a tool designed to make it easier for developers and sysadmins to create/develop, configure, and run applications with
... [More]
containers. The container allows us to package all parts of the application it needs, such as libraries, dependencies like code, configurations, and runtime engine. Docker runtime containers are platform-independent so the package created can be shipped one platform to another platform.
Dockerhub is the repository where you can find containerized docker images for applications like MySQL, Percona Server for MySQL, and MariaDB. Using the example below, I will show you how to set up a docker container from the Pecona Server for MySQL docker image that I download from the repository.
Custom Network Instead of the Default
First, I will create a network that my docker instances will use. I will be using a user-defined network instead of the default one. It is recommended to use the user-defined bridge networks to control which containers can communicate with each other. Docket daemon automatically takes care of DNS name resolution. By creating your own network, every single container using that network will have DNS resolution automagically.
MacBook-Pro: $ docker network create --driver bridge isolated_nw
f8cd8f09b4042b39b04a6a43fd9dc71507af22dfd6ee2817fa80360577b24d6f
MacBook-Pro: $
Storage for Persisting the Data
The second step is to provision the storage which my docker instances will be using. In docker, storage can be provisioned in two ways, by using a bind mount or by using a docker volume. Bind mounts are dependent on the directory structure of the host machine while docker volumes are completely managed by Docker. In my example, I am using bind mounts for the primary instance and docker volumes for the replica to illustrate how either of these options can be used.
MacBook-Pro: $ mkdir /Users/myuser/mysql_primary_data
Configuration File for the Primary Instance
I will proceed with the creation of the configuration file which my primary instance will be using.
MacBook-Pro: $ cat /Users/myuser/primaryconf/my-custom.cnf
[mysqld]
server-id = 100
log_bin
Provisioning the Primary Instance
In the fourth step, we will provision the primary instance. The docker run command will download the latest percona server image if the image does not already exist in the local repository. In this example, we already have the downloaded image so the docker run does not need to download it again.
MacBook-Pro: $ docker run --network=isolated_nw -v /Users/myuser/mysql_primary_data:/var/lib/mysql -v /Users/myuser/primaryconf:/etc/percona-server.conf.d/ -p 3308:3306 -p 33061:33060 --name percona_primary -e MYSQL_ROOT_HOST='%' -e MYSQL_ROOT_PASSWORD=MysqlTestInstance -d percona --default-authentication-plugin=mysql_native_password
deb40d6941db74d845cbc5ec550572d37d4763740e3a72c015ada0c7520a0fd7
MacBook-Pro: $
I intend to set up an async replication environment, so I will get the binary log details from the primary instance.
MacBook-Pro: $ mysql -h127.0.0.1 -uroot -p -P3308 -e "show master status;"
Enter password:
+-------------------------+----------+--------------+------------------+-------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+-------------------------+----------+--------------+------------------+-------------------+
| deb40d6941db-bin.000003 | 154 | | | |
+-------------------------+----------+--------------+------------------+-------------------+
MacBook-Pro: $
For setting the replication we need a user and the below command connects the primary instance and grants the privilege.
MacBook-Pro: $ mysql -h127.0.0.1 -uroot -p -P3308 -e "GRANT REPLICATION SLAVE ON *.* TO 'repl_user'@'%' IDENTIFIED BY 'replpass';"
Enter password:
MacBook-Pro: $
Docker Volume for Replication Storage
Before I create my replica instance, I will provision the docker volume which my replica instance will use for storage.
MacBook-Pro: $ docker volume create mysqlreplicavol
mysqlreplicavol
MacBook-Pro: $
Configuration File for the Replica Instance
Create a custom MySQL configuration file for the replica instance.
MacBook-Pro: $ cat /Users/myuser/replicaconf/my-custom.cnf
[mysqld]
server-id = 101
Provisioning the Replica Instance
The next step is to set up a replica instance using the docker volume that I created above.
MacBook-Pro: $ docker run --network=isolated_nw -v mysqlreplicavol:/var/lib/mysql -v /Users/myuser/replicaconf:/etc/percona-server.conf.d/ -p 3309:3306 -p 33062:33060 --name percona_replica -e MYSQL_ROOT_HOST='%' -e MYSQL_ROOT_PASSWORD=MysqlTestInstance -d percona --default-authentication-plugin=mysql_native_password
98c109998a522a51c4ceca7d830a1f9af2abdd408c5e1d6d12cfd55af13d170d
MacBook-Pro: $
To set up the replication, apply the change master command in the replica instance and start the replica.
MacBook-Pro: $ mysql -h127.0.0.1 -uroot -p -P3309 -e "CHANGE MASTER TO MASTER_HOST='percona_primary',MASTER_USER='repl_user', MASTER_PASSWORD='replpass', MASTER_LOG_FILE='deb40d6941db-bin.000003', MASTER_LOG_POS=154;"
Enter password:
MacBook-Pro: $ mysql -h127.0.0.1 -uroot -p -P3309 -e "start slave";
Enter password:
MacBook-Pro: $
Verify the Replication Status
MacBook-Pro: $ mysql -h127.0.0.1 -uroot -p -P3309 -e "show slave status \G" |egrep 'Running|Master' | egrep -v 'SSL|TLS'
Enter password:
Master_Host: percona_primary
Master_User: repl_user
Master_Port: 3306
Master_Log_File: deb40d6941db-bin.000003
Read_Master_Log_Pos: 434
Relay_Master_Log_File: deb40d6941db-bin.000003
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Exec_Master_Log_Pos: 434
Seconds_Behind_Master: 0
Master_Server_Id: 100
Master_UUID: 49451bba-2627-11ea-be09-0242ac120002
Master_Info_File: /var/lib/mysql/master.info
Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates
Master_Retry_Count: 86400
Master_Bind:
MacBook-Pro: $
Create a test DB to confirm it is replicating to the replica instance.
MacBook-Pro: $ mysql -h127.0.0.1 -uroot -p -P3308 -e "create database mytestdb;"
Enter password:
MacBook-Pro: $
MacBook-Pro: $ mysql -h127.0.0.1 -uroot -p -P3309 -e "show databases like 'mytestdb';"
Enter password:
+---------------------+
| Database (mytestdb) |
+---------------------+
| mytestdb |
+---------------------+
MacBook-Pro: $
To check the logs we can use the docker logs command, for example :
MacBook-Pro: $ docker logs percona_primary
Initializing database
2019-12-24T08:27:54.395857Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).
...
....
2019-12-24T08:28:19.792475Z 0 [Note] mysqld: ready for connections.
Version: '5.7.26-29-log' socket: '/var/lib/mysql/mysql.sock' port: 3306 Percona Server (GPL), Release 29, Revision 11ad961
2019-12-24T08:45:04.071744Z 7 [Note] Start binlog_dump to master_thread_id(7) slave_server(101), pos(deb40d6941db-bin.000003, 154)
MacBook-Pro: $
The setup that I have shown above can be used as a quick setup of an async replication configuration to run demos or test experiments. Such a configuration is not suitable for production and will not be supported for production use by Docker.
Note: Percona Monitoring and Management (PMM) is also distributed as an appliance in the form of a docker image.
[Less]
|
Posted
over 4 years
ago
by
MySQL Performance Blog
This blog discusses a few concepts about Docker and how we can use it to run a MySQL async replication environment. Docker is a tool designed to make it easier for developers and sysadmins to create/develop, configure, and run applications with
... [More]
containers. The container allows us to package all parts of the application it needs, such as libraries, dependencies like code, configurations, and runtime engine. Docker runtime containers are platform-independent so the package created can be shipped one platform to another platform.
Dockerhub is the repository where you can find containerized docker images for applications like MySQL, Percona Server for MySQL, and MariaDB. Using the example below, I will show you how to set up a docker container from the Pecona Server for MySQL docker image that I download from the repository.
Custom Network Instead of the Default
First, I will create a network that my docker instances will use. I will be using a user-defined network instead of the default one. It is recommended to use the user-defined bridge networks to control which containers can communicate with each other. Docket daemon automatically takes care of DNS name resolution. By creating your own network, every single container using that network will have DNS resolution automagically.
MacBook-Pro: $ docker network create --driver bridge isolated_nw
f8cd8f09b4042b39b04a6a43fd9dc71507af22dfd6ee2817fa80360577b24d6f
MacBook-Pro: $
Storage for Persisting the Data
The second step is to provision the storage which my docker instances will be using. In docker, storage can be provisioned in two ways, by using a bind mount or by using a docker volume. Bind mounts are dependent on the directory structure of the host machine while docker volumes are completely managed by Docker. In my example, I am using bind mounts for the primary instance and docker volumes for the replica to illustrate how either of these options can be used.
MacBook-Pro: $ mkdir /Users/myuser/mysql_primary_data
Configuration File for the Primary Instance
I will proceed with the creation of the configuration file which my primary instance will be using.
MacBook-Pro: $ cat /Users/myuser/primaryconf/my-custom.cnf
[mysqld]
server-id = 100
log_bin
Provisioning the Primary Instance
In the fourth step, we will provision the primary instance. The docker run command will download the latest percona server image if the image does not already exist in the local repository. In this example, we already have the downloaded image so the docker run does not need to download it again.
MacBook-Pro: $ docker run --network=isolated_nw -v /Users/myuser/mysql_primary_data:/var/lib/mysql -v /Users/myuser/primaryconf:/etc/percona-server.conf.d/ -p 3308:3306 -p 33061:33060 --name percona_primary -e MYSQL_ROOT_HOST='%' -e MYSQL_ROOT_PASSWORD=MysqlTestInstance -d percona --default-authentication-plugin=mysql_native_password
deb40d6941db74d845cbc5ec550572d37d4763740e3a72c015ada0c7520a0fd7
MacBook-Pro: $
I intend to set up an async replication environment, so I will get the binary log details from the primary instance.
MacBook-Pro: $ mysql -h127.0.0.1 -uroot -p -P3308 -e "show master status;"
Enter password:
+-------------------------+----------+--------------+------------------+-------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+-------------------------+----------+--------------+------------------+-------------------+
| deb40d6941db-bin.000003 | 154 | | | |
+-------------------------+----------+--------------+------------------+-------------------+
MacBook-Pro: $
For setting the replication we need a user and the below command connects the primary instance and grants the privilege.
MacBook-Pro: $ mysql -h127.0.0.1 -uroot -p -P3308 -e "GRANT REPLICATION SLAVE ON *.* TO 'repl_user'@'%' IDENTIFIED BY 'replpass';"
Enter password:
MacBook-Pro: $
Docker Volume for Replication Storage
Before I create my replica instance, I will provision the docker volume which my replica instance will use for storage.
MacBook-Pro: $ docker volume create mysqlreplicavol
mysqlreplicavol
MacBook-Pro: $
Configuration File for the Replica Instance
Create a custom MySQL configuration file for the replica instance.
MacBook-Pro: $ cat /Users/myuser/replicaconf/my-custom.cnf
[mysqld]
server-id = 101
Provisioning the Replica Instance
The next step is to set up a replica instance using the docker volume that I created above.
MacBook-Pro: $ docker run --network=isolated_nw -v mysqlreplicavol:/var/lib/mysql -v /Users/myuser/replicaconf:/etc/percona-server.conf.d/ -p 3309:3306 -p 33062:33060 --name percona_replica -e MYSQL_ROOT_HOST='%' -e MYSQL_ROOT_PASSWORD=MysqlTestInstance -d percona --default-authentication-plugin=mysql_native_password
98c109998a522a51c4ceca7d830a1f9af2abdd408c5e1d6d12cfd55af13d170d
MacBook-Pro: $
To set up the replication, apply the change master command in the replica instance and start the replica.
MacBook-Pro: $ mysql -h127.0.0.1 -uroot -p -P3309 -e "CHANGE MASTER TO MASTER_HOST='percona_primary',MASTER_USER='repl_user', MASTER_PASSWORD='replpass', MASTER_LOG_FILE='deb40d6941db-bin.000003', MASTER_LOG_POS=154;"
Enter password:
MacBook-Pro: $ mysql -h127.0.0.1 -uroot -p -P3309 -e "start slave";
Enter password:
MacBook-Pro: $
Verify the Replication Status
MacBook-Pro: $ mysql -h127.0.0.1 -uroot -p -P3309 -e "show slave status \G" |egrep 'Running|Master' | egrep -v 'SSL|TLS'
Enter password:
Master_Host: percona_primary
Master_User: repl_user
Master_Port: 3306
Master_Log_File: deb40d6941db-bin.000003
Read_Master_Log_Pos: 434
Relay_Master_Log_File: deb40d6941db-bin.000003
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Exec_Master_Log_Pos: 434
Seconds_Behind_Master: 0
Master_Server_Id: 100
Master_UUID: 49451bba-2627-11ea-be09-0242ac120002
Master_Info_File: /var/lib/mysql/master.info
Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates
Master_Retry_Count: 86400
Master_Bind:
MacBook-Pro: $
Create a test DB to confirm it is replicating to the replica instance.
MacBook-Pro: $ mysql -h127.0.0.1 -uroot -p -P3308 -e "create database mytestdb;"
Enter password:
MacBook-Pro: $
MacBook-Pro: $ mysql -h127.0.0.1 -uroot -p -P3309 -e "show databases like 'mytestdb';"
Enter password:
+---------------------+
| Database (mytestdb) |
+---------------------+
| mytestdb |
+---------------------+
MacBook-Pro: $
To check the logs we can use the docker logs command, for example :
MacBook-Pro: $ docker logs percona_primary
Initializing database
2019-12-24T08:27:54.395857Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).
...
....
2019-12-24T08:28:19.792475Z 0 [Note] mysqld: ready for connections.
Version: '5.7.26-29-log' socket: '/var/lib/mysql/mysql.sock' port: 3306 Percona Server (GPL), Release 29, Revision 11ad961
2019-12-24T08:45:04.071744Z 7 [Note] Start binlog_dump to master_thread_id(7) slave_server(101), pos(deb40d6941db-bin.000003, 154)
MacBook-Pro: $
The setup that I have shown above can be used as a quick setup of an async replication configuration to run demos or test experiments. Such a configuration is not suitable for a typical production install and you need to test it well before any other use.
Note: Percona Monitoring and Management (PMM) is also distributed as an appliance in the form of a docker image.
[Less]
|
Posted
over 4 years
ago
by
MySQL Performance Blog
One of our support customers approached us with the following problem the other day:
mysql> CREATE TABLE child_table (
`id` int unsigned auto_increment,
`column1` varchar(64) NOT NULL,
parent_id int unsigned NOT NULL,
PRIMARY KEY (`id`)
... [More]
,
CONSTRAINT FOREIGN KEY (parent_id) REFERENCES parent_table (id));
ERROR 1215 (HY000): Cannot add foreign key constraint
They could not create a table with an FK relation! So, of course, we asked to see the parent table definition, which was:
CREATE TABLE `parent_table` (
`id` int unsigned auto_increment,
`column1` varchar(64) COLLATE utf8_bin NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB
PARTITION BY HASH (id)
PARTITIONS 4;
The parent table is partitioned! This immediately explained the problem; partitioned tables can not be part of an FK relationship, as described (in point 10) here – MySQL Error Code 1215: “Cannot add foreign key constraint”.
Quoting the official MySQL manual for completeness:
Partitioned tables using the InnoDB storage engine do not support foreign keys. More specifically, this means that the following two statements are true:
No definition of an InnoDB table employing user-defined partitioning may contain foreign key references; no InnoDB table whose definition contains foreign key references may be partitioned.
No InnoDB table definition may contain a foreign key reference to a user-partitioned table; no InnoDB table with user-defined partitioning may contain columns referenced by foreign keys.
So, after verifying it was impossible to guarantee referential integrity using CONSTRAINTs, we turned to an old alternative from MyISAM era of MySQL: using a set of triggers that would intercept the DML statements before they execute, and verify if the parent row actually exists.
So for this, we would create child_table without the constraint:
CREATE TABLE child_table (
`id` int unsigned auto_increment,
`column1` varchar(64) NOT NULL,
parent_id int unsigned NOT NULL,
PRIMARY KEY (`id`));
And then we create 4 triggers: BEFORE INSERT and BEFORE UPDATE on the child table, and BEFORE UPDATE and BEFORE DELETE on the parent table.
DELIMITER //
DROP TRIGGER IF EXISTS PARTITIONED_TABLE_CHECK_INS //
CREATE TRIGGER PARTITIONED_TABLE_CHECK_INS BEFORE INSERT ON child_table FOR EACH ROW
BEGIN
DECLARE fk_check INT;
DECLARE fk_error_msg VARCHAR(200);
IF (@DISABLE_TRIGGERS IS NULL) THEN
SELECT COUNT(*) FROM parent_table WHERE id=new.parent_id INTO fk_check;
IF fk_check < 1 THEN
SELECT CONCAT("Foreign key constraint fails for table `parent_table`, can't find row matching `id='", new.parent_id, "`") INTO fk_error_msg;
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = fk_error_msg;
END IF;
END IF;
END
//
DROP TRIGGER IF EXISTS PARTITIONED_TABLE_CHECK_UPD //
CREATE TRIGGER PARTITIONED_TABLE_CHECK_UPD BEFORE UPDATE ON child_table FOR EACH ROW
BEGIN
DECLARE fk_check INT;
DECLARE fk_error_msg VARCHAR(200);
IF (@DISABLE_TRIGGERS IS NULL) THEN
SELECT COUNT(*) FROM parent_table WHERE id=new.parent_id INTO fk_check;
IF fk_check < 1 THEN
SELECT CONCAT("Foreign key constraint fails for table `child_table`, can't find row matching `id='", new.parent_id, "`") INTO fk_error_msg;
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = fk_error_msg;
END IF;
END IF;
END
//
-- DELETE is checked the other way around and the trigger has to be attached to parent_table (I kept naming the same for consistency)
DELIMITER //
DROP TRIGGER IF EXISTS PARTITIONED_TABLE_CHECK_DEL //
CREATE TRIGGER PARTITIONED_TABLE_CHECK_DEL BEFORE DELETE ON parent_table FOR EACH ROW
BEGIN
DECLARE fk_check INT;
DECLARE fk_error_msg VARCHAR(200);
IF (@DISABLE_TRIGGERS IS NULL) THEN
SELECT COUNT(*) FROM child_table WHERE parent_id=old.id INTO fk_check;
IF fk_check > 0 THEN
SELECT CONCAT("Foreign key constraint fails for table `parent_table`, child table has ", fk_check," row(s) matching condition `parent_id='", old.id, "`") INTO fk_error_msg;
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = fk_error_msg;
END IF;
END IF;
END
//
-- Check UPDATE on parent_id.id; If PK differs we must check it's not referenced
DELIMITER //
DROP TRIGGER IF EXISTS PARTITIONED_TABLE_CHECK_PARENT_PK_UPDATE //
CREATE TRIGGER PARTITIONED_TABLE_CHECK_PARENT_PK_UPDATE BEFORE UPDATE ON parent_table FOR EACH ROW
BEGIN
DECLARE fk_check INT;
DECLARE fk_error_msg VARCHAR(200);
IF (@DISABLE_TRIGGERS IS NULL) THEN
IF old.id <> new.id THEN
SELECT COUNT(*) FROM child_table WHERE parent_id=old.id INTO fk_check;
IF fk_check > 0 THEN
SELECT CONCAT("Foreign key constraint fails for table `parent_table`, child table has ", fk_check," row(s) matching condition `parent_id='", old.id, "`") INTO fk_error_msg;
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = fk_error_msg;
END IF;
END IF;
END IF;
END
//
DELIMITER ;
Testing the Triggers:
Populate parent_table:
mysql> INSERT INTO parent_table (id, column1) VALUES (1, "column1");
Query OK, 1 row affected (0.03 sec)
Test insert:
-- Insert is valid
mysql> INSERT INTO child_table (id, column1, parent_id) VALUES (null, "value1", 1);
Query OK, 1 row affected (0.01 sec)
-- Insert fails with FK check
mysql> INSERT INTO child_table (id, column1, parent_id) VALUES (null, "value2", 2);
ERROR 1644 (45000): Foreign key constraint fails for table `parent_table`, can't find row matching `id='2`
So far so good! For valid child ids, inserts are accepted, and for invalid child ids, trigger rejects the insert.
Test Update:
--Test invalid update on child:
mysql> UPDATE child_table SET parent_id='2' WHERE parent_id='1';
ERROR 1644 (45000): Foreign key constraint fails for table `child_table`, can't find row matching `id='2`
-- Test invalid update on parent
mysql> UPDATE parent_table SET id=5;
ERROR 1644 (45000): Foreign key constraint fails for table `parent_table`, child table has 1 row(s) matching condition `parent_id='1`
-- Test valid update on parent and child
mysql> INSERT INTO parent_table VALUES (10, "column1");
Query OK, 1 row affected (0.00 sec)
mysql> INSERT INTO child_table VALUES (10, "column1", 1);
Query OK, 1 row affected (0.01 sec)
mysql> UPDATE parent_table SET id = 9 WHERE id = 10;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> UPDATE child_table SET parent_id = 9 WHERE id = 10;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
Test Delete:
-- Invalid delete
mysql> DELETE FROM parent_table WHERE id=1 ;
ERROR 1644 (45000): Foreign key constraint fails for table `parent_table`, child table has 1 row(s) matching condition `parent_id='1`
For both delete and update, we also tested trigger is working as expecting and checking FK integrity.
Insert new row on parent_table which we should be able to delete without failing the “constraint” (as it will have no child rows) :
-- Test valid delete
INSERT INTO parent_table (id, column1) VALUES (2, "column2");
Query OK, 1 row affected (0.03 sec)
mysql> DELETE FROM parent_table WHERE id=2 ;
Query OK, 1 row affected (0.03 sec)
Unfortunately, the non-standard REPLACE INTO is not compatible with the above method, as it actually consists of two operations – a DELETE and a subsequent INSERT INTO, and doing the DELETE on the parent table for a referenced row would trigger the FK error:
mysql> REPLACE INTO parent_table (id, column1) VALUES (1, "column2");
ERROR 1644 (45000): Foreign key constraint fails for table `parent_table`, child table has 1 row(s) matching condition `parent_id='1`
REPLACE INTO the child_table should work without issues.
On the other hand, INSERT…ON DUPLICATE KEY CHECK will work as expected as the trigger on the UPDATE will work correctly and prevent breaking referential integrity.
For convenience FK triggers can be disabled on the session; This would be the equivalent of SET foreign_key_checks=0. You can disable by setting the following variable:
mysql > SET @DISABLE_TRIGGERS=1;
Query OK, 0 rows affected (0.00 sec)
Disclaimer:
The above is a proof of concept and while it should work for the vast majority of uses, there are two cases that are not checked by the triggers and will break referential integrity: TRUNCATE TABLE parent_table and DROP TABLE parent_table, as it will not execute the DELETE trigger and hence will allow all child rows to become invalid at once.
And in general, DDL operations which can break referential integrity (for example ALTER TABLE modifying column type or name) are not handled as these operations don’t fire TRIGGERs of any kind, and also it relies on you writing the correct query to find the parent rows (for example if you have a parent table with a multi-column primary key, you must check all the columns in the WHERE condition of the triggers)
Also, keep in mind added performance impact; Triggers will add overhead, so please make sure to measure impact on the response time of the DML in these two tables. Please test thoroughly before deploying to production!
[Less]
|
Posted
over 4 years
ago
by
MySQL Performance Blog
One of our support customers approached us with the following problem the other day:
mysql> CREATE TABLE child_table (
`id` int unsigned auto_increment,
`column1` varchar(64) NOT NULL,
parent_id int unsigned NOT NULL,
PRIMARY KEY (`id`)
... [More]
,
CONSTRAINT FOREIGN KEY (parent_id) REFERENCES parent_table (id));
ERROR 1215 (HY000): Cannot add foreign key constraint
They could not create a table with an FK relation! So, of course, we asked to see the parent table definition, which was:
CREATE TABLE `parent_table` (
`id` int unsigned auto_increment,
`column1` varchar(64) COLLATE utf8_bin NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB
PARTITION BY HASH (id)
PARTITIONS 4;
The parent table is partitioned! This immediately explained the problem; partitioned tables can not be part of an FK relationship, as described (in point 10) here – MySQL Error Code 1215: “Cannot add foreign key constraint”.
Quoting the official MySQL manual for completeness:
Partitioned tables using the InnoDB storage engine do not support foreign keys. More specifically, this means that the following two statements are true:
No definition of an InnoDB table employing user-defined partitioning may contain foreign key references; no InnoDB table whose definition contains foreign key references may be partitioned.
No InnoDB table definition may contain a foreign key reference to a user-partitioned table; no InnoDB table with user-defined partitioning may contain columns referenced by foreign keys.
So, after verifying it was impossible to guarantee referential integrity using CONSTRAINTs, we turned to an old alternative from MyISAM era of MySQL: using a set of triggers that would intercept the DML statements before they execute, and verify if the parent row actually exists.
So for this, we would create child_table without the constraint:
CREATE TABLE child_table (
`id` int unsigned auto_increment,
`column1` varchar(64) NOT NULL,
parent_id int unsigned NOT NULL,
PRIMARY KEY (`id`));
And then we create 4 triggers: BEFORE INSERT and BEFORE UPDATE on the child table, and BEFORE UPDATE and BEFORE DELETE on the parent table.
DELIMITER //
DROP TRIGGER IF EXISTS PARTITIONED_TABLE_CHECK_INS //
CREATE TRIGGER PARTITIONED_TABLE_CHECK_INS BEFORE INSERT ON child_table FOR EACH ROW
BEGIN
DECLARE fk_check INT;
DECLARE fk_error_msg VARCHAR(200);
IF (@DISABLE_TRIGGERS IS NULL) THEN
SELECT COUNT(*) FROM parent_table WHERE id=new.parent_id INTO fk_check;
IF fk_check < 1 THEN
SELECT CONCAT("Foreign key constraint fails for table `parent_table`, can't find row matching `id='", new.parent_id, "`") INTO fk_error_msg;
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = fk_error_msg;
END IF;
END IF;
END
//
DROP TRIGGER IF EXISTS PARTITIONED_TABLE_CHECK_UPD //
CREATE TRIGGER PARTITIONED_TABLE_CHECK_UPD BEFORE UPDATE ON child_table FOR EACH ROW
BEGIN
DECLARE fk_check INT;
DECLARE fk_error_msg VARCHAR(200);
IF (@DISABLE_TRIGGERS IS NULL) THEN
SELECT COUNT(*) FROM parent_table WHERE id=new.parent_id INTO fk_check;
IF fk_check < 1 THEN
SELECT CONCAT("Foreign key constraint fails for table `child_table`, can't find row matching `id='", new.parent_id, "`") INTO fk_error_msg;
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = fk_error_msg;
END IF;
END IF;
END
//
-- DELETE is checked the other way around and the trigger has to be attached to parent_table (I kept naming the same for consistency)
DELIMITER //
DROP TRIGGER IF EXISTS PARTITIONED_TABLE_CHECK_DEL //
CREATE TRIGGER PARTITIONED_TABLE_CHECK_DEL BEFORE DELETE ON parent_table FOR EACH ROW
BEGIN
DECLARE fk_check INT;
DECLARE fk_error_msg VARCHAR(200);
IF (@DISABLE_TRIGGERS IS NULL) THEN
SELECT COUNT(*) FROM child_table WHERE parent_id=old.id INTO fk_check;
IF fk_check > 0 THEN
SELECT CONCAT("Foreign key constraint fails for table `parent_table`, child table has ", fk_check," row(s) matching condition `parent_id='", old.id, "`") INTO fk_error_msg;
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = fk_error_msg;
END IF;
END IF;
END
//
-- Check UPDATE on parent_id.id; If PK differs we must check it's not referenced
DELIMITER //
DROP TRIGGER IF EXISTS PARTITIONED_TABLE_CHECK_PARENT_PK_UPDATE //
CREATE TRIGGER PARTITIONED_TABLE_CHECK_PARENT_PK_UPDATE BEFORE UPDATE ON parent_table FOR EACH ROW
BEGIN
DECLARE fk_check INT;
DECLARE fk_error_msg VARCHAR(200);
IF (@DISABLE_TRIGGERS IS NULL) THEN
IF old.id <> new.id THEN
SELECT COUNT(*) FROM child_table WHERE parent_id=old.id INTO fk_check;
IF fk_check > 0 THEN
SELECT CONCAT("Foreign key constraint fails for table `parent_table`, child table has ", fk_check," row(s) matching condition `parent_id='", old.id, "`") INTO fk_error_msg;
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = fk_error_msg;
END IF;
END IF;
END IF;
END
//
DELIMITER ;
Testing the Triggers:
Populate parent_table:
mysql> INSERT INTO parent_table (id, column1) VALUES (1, "column1");
Query OK, 1 row affected (0.03 sec)
Test insert:
-- Insert is valid
mysql> INSERT INTO child_table (id, column1, parent_id) VALUES (null, "value1", 1);
Query OK, 1 row affected (0.01 sec)
-- Insert fails with FK check
mysql> INSERT INTO child_table (id, column1, parent_id) VALUES (null, "value2", 2);
ERROR 1644 (45000): Foreign key constraint fails for table `parent_table`, can't find row matching `id='2`
So far so good! For valid child ids, inserts are accepted, and for invalid child ids, trigger rejects the insert.
Test Update:
--Test invalid update on child:
mysql> UPDATE child_table SET parent_id='2' WHERE parent_id='1';
ERROR 1644 (45000): Foreign key constraint fails for table `child_table`, can't find row matching `id='2`
-- Test invalid update on parent
mysql> UPDATE parent_table SET id=5;
ERROR 1644 (45000): Foreign key constraint fails for table `parent_table`, child table has 1 row(s) matching condition `parent_id='1`
-- Test valid update on parent and child
mysql> INSERT INTO parent_table VALUES (10, "column1");
Query OK, 1 row affected (0.00 sec)
mysql> INSERT INTO child_table VALUES (10, "column1", 1);
Query OK, 1 row affected (0.01 sec)
mysql> UPDATE parent_table SET id = 9 WHERE id = 10;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> UPDATE child_table SET parent_id = 9 WHERE id = 10;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
Test Delete:
-- Invalid delete
mysql> DELETE FROM parent_table WHERE id=1 ;
ERROR 1644 (45000): Foreign key constraint fails for table `parent_table`, child table has 1 row(s) matching condition `parent_id='1`
For both delete and update, we also tested trigger is working as expecting and checking FK integrity.
Insert new row on parent_table which we should be able to delete without failing the “constraint” (as it will have no child rows) :
-- Test valid delete
INSERT INTO parent_table (id, column1) VALUES (2, "column2");
Query OK, 1 row affected (0.03 sec)
mysql> DELETE FROM parent_table WHERE id=2 ;
Query OK, 1 row affected (0.03 sec)
Unfortunately, the non-standard REPLACE INTO is not compatible with the above method, as it actually consists of two operations – a DELETE and a subsequent INSERT INTO, and doing the DELETE on the parent table for a referenced row would trigger the FK error:
mysql> REPLACE INTO parent_table (id, column1) VALUES (1, "column2");
ERROR 1644 (45000): Foreign key constraint fails for table `parent_table`, child table has 1 row(s) matching condition `parent_id='1`
REPLACE INTO the child_table should work without issues.
On the other hand, INSERT…ON DUPLICATE KEY CHECK will work as expected as the trigger on the UPDATE will work correctly and prevent breaking referential integrity.
For convenience FK triggers can be disabled on the session; This would be the equivalent of SET foreign_key_checks=0. You can disable by setting the following variable:
mysql > SET @DISABLE_TRIGGERS=1;
Query OK, 0 rows affected (0.00 sec)
Disclaimer:
The above is a proof of concept and while it should work for the vast majority of uses, there are two cases that are not checked by the triggers and will break referential integrity: TRUNCATE TABLE parent_table and DROP TABLE parent_table, as it will not execute the DELETE trigger and hence will allow all child rows to become invalid at once.
And in general, DDL operations which can break referential integrity (for example ALTER TABLE modifying column type or name) are not handled as these operations don’t fire TRIGGERs of any kind, and also it relies on you writing the correct query to find the parent rows (for example if you have a parent table with a multi-column primary key, you must check all the columns in the WHERE condition of the triggers)
Also when using “ON CASCADE DELETE” FKs, triggers won’t be fired on delete child rows. Keep this in mind if you have complex FK + triggers tables.
Last, keep in mind added performance impact; Triggers will add overhead, so please make sure to measure impact on the response time of the DML in these two tables.
Because of all the above, triggers are NOT suggested to be used to maintain data consistency, but using triggers is the only possible workaround when using partitioned tables with InnoDB. Please test thoroughly and be aware of the caveats before deploying to production!
[Less]
|
Posted
over 4 years
ago
by
MinervaDB
MySQL query rewrite for ClickHouse using ProxySQL
Introduction
ProxySQL in September 2017 announced support for ClickHouse as backend. ProxySQL is a popular open source, high performance and protocol-aware proxy server for MySQL and its
... [More]
forks. ClickHouse is an open source column-oriented database management system capable of real time generation of analytical data reports using SQL queries. To support ClickHouse as a backend, ProxySQL acts as a data bridge between MySQL protocol and ClickHouse protocol, allowing MySQL clients to execute queries in ClickHouse through it. ClickHouse’s SQL query syntax is different than MySQL’s syntax, and migrating application from MySQL to ClickHouse isn’t just a matter of changing connections endpoint but it also requires modifying some queries. This needs development time, but not always possible. One of ProxySQL most widely used feature is indeed the ability of rewriting queries, so often it is just a matter of writing the right query rules. In this blog post we have explained step-by-step MySQL query rewrite for ClickHouse using ProxySQL:
How to implement ProxySQL query rewrite for ClickHouse ?
The below is MySQL query we are considering for query rewrite:
SELECT COUNT(`id`), FROM_UNIXTIME(`created`, '%Y-%m') AS `date` FROM `tablename` GROUP BY FROM_UNIXTIME(`created`, '%Y-%m')
ClickHouse doesn’t support FROM_UNIXTIME, but it supports toDate and toTime.
ClickHouse also supports toYear and toMonth , useful to format the date the same FROM_UNIXTIME does.
Therefore, it is possible to rewrite the query as:
SELECT COUNT(`id`), concat(toString(toYear(toDate(created))), '-', toString(toMonth(toDate(created)))) AS `date`
FROM `tablename`
GROUP BY toYear(toDate(created)), toMonth(toDate(created));
To perform the above rewrite, we will need two rules, one for the first FROM_UNIXTIME, and one for the second one. Or we can just use one rewrite rules to replace FROM_UNIXTIME(created, ‘%Y-%m’) no matter if on the retrieved fields or in the GROUP BY clause, generating the following query:
SELECT COUNT(`id`), concat(toString(toYear(toDate(created))), '-', toString(toMonth(toDate(created)))) AS `date`
FROM `tablename`
GROUP BY concat(toString(toYear(toDate(created))), '-', toString(toMonth(toDate(created))));
Does it look great? No, not yet!
For the month of March, concat(toString(toYear(toDate(created))), ‘-‘, toString(toMonth(toDate(created)))) will return 2018-3 : not what the application was expecting, as MySQL would return 2018-03 . The same applies for all the first 9 months of each year.
Finally, we rewrote the query as the follow, and the application was happy:
SELECT COUNT(`id`), substring(toString(toDate(created)),1,7) AS `date`
FROM `tablename`
GROUP BY substring(toString(toDate(created)),1,7);
Note: because of the datatypes conversions that ClickHouse needs to perform in order to execute the above query, its execution time is about 50% slower than executing the following query:
SELECT COUNT(`id`), concat(toString(toYear(toDate(created))), '-', toString(toMonth(toDate(created)))) AS `date`
FROM `tablename`
GROUP BY toYear(toDate(created)), toMonth(toDate(created));
Architecting using two ProxySQL
Great, we now know how to rewrite the query!
Although, the ClickHouse module in ProxySQL doesn’t support query rewrite. The ClickHouse module in ProxySQL is only responsible to transform data between MySQL and ClickHouse protocol, and viceversa.
Therefore the right way of achieving this solution is to configure two ProxySQL layers, one instance responsible for rewriting the query and sending the rewritten query to the second ProxySQL instance, this one responsible for executing the query (already modified) on ClickHouse.
Architecting using only one ProxySQL
Does the above architecture seems complex? Not really, it is reasonable straightforward.
Can it be improved?
As you can see from the previous chart, the ClickHouse module and the MySQL module listen on different ports. The first ProxySQL instance is receiving traffic on port 6033, and sending traffic on the second PorxySQL instance on port 6090.
Are two instances really required? The answer is no.
In fact, a single instance can receive MySQL traffic on port 6033, rewrite the query, and send the rewritten query to itself on port 6090, to finally execute the rewritten query on ClickHouse.
This diagram describes the architecture:
Configuration
For reference, below is the step to configure one single ProxySQL to send traffic to ClickHouse, and use itself as a backend.
Create ClickHouse user:
INSERT INTO clickhouse_users (username,password) VALUES ('clicku','clickp');
LOAD CLICKHOUSE USERS TO RUNTIME;
SAVE CLICKHOUSE USERS TO DISK;
Create MySQL user (same as ClickHouse):
INSERT INTO mysql_users(username,password) SELECT username, password FROM clickhouse_users;
LOAD MYSQL USERS TO RUNTIME;
SAVE MYSQL USERS TO DISK;
Configure ProxySQL itself as a backend for MySQL traffic:
INSERT INTO mysql_servers(hostname,port) VALUES ('127.0.0.1',6090);
SAVE MYSQL SERVERS TO DISK;
LOAD MYSQL SERVERS TO RUNTIME;
Create a query rule for rewriting queries:
INSERT INTO mysql_query_rules (active,match_pattern,replace_pattern,re_modifiers) VALUES
(1,"FROM_UNIXTIME\(`created`, '%Y-%m'\)", 'substring(toString(toDate(created)),1,7)',"CASELESS,GLOBAL");
LOAD MYSQL QUERY RULES TO RUNTIME;
SAVE MYSQL QUERY RULES TO DISK;
This is a very simple example to demonstrate how to perform query rewrite from MySQL to ClickHouse using just one ProxySQL instance. In a real world scenarios you will need to create more rules based on your own queries.
Conclusion
Not only ProxySQL allows to send queries to ClickHouse, but it also allows to rewrite queries to solve issues related to different SQL syntax and available functions.
To achieve this, ProxySQL uses its ability to use itself as a backend: rewrite the query in the MySQL module, and execute it in the ClickHouse module.
References
https://www.proxysql.com/blog/clickhouse-and-proxysql-queries-rewrite
https://minervadb.com/index.php/2019/12/25/how-to-use-proxysql-to-work-on-clickhouse-like-mysql/
The post ClickHouse and ProxySQL queries rewrite (Cross-post from ProxySQL) appeared first on The WebScale Database Infrastructure Operations Experts.
[Less]
|
Posted
over 4 years
ago
by
MinervaDB
Use ClickHouse like MySQL with ProxySQL
Introduction
We have several customers on ClickHouse now for both columnar database analytics and archiving MySQL data, You can access data from ClickHouse with clickhouse-client but this involves some
... [More]
learning and also limitations technically. Our customers are very comfortable using MySQL so they always preferred a MySQL client for ClickHouse query analysis and reporting, Thankfully ProxySQL works as a optimal bridge between ClickHouse and MySQL client, This indeed was a great news for us and our customers worldwide. This blog post is about how we can use MySQL client with ClickHouse.
Installation
https://github.com/sysown/proxysql/releases/ (**Download the package starting with clickhouse )
Dependencies installation:
yum -y install perl-DBD-MySQL
Start ProxySQL once completed installation successfully.
# The default configuration file is this:
/etc/proxysql.cnf
# There is no such data directory by default:
mkdir / var / lib / proxysql
# start up
proxysql --clickhouse-server
# ProxySQL will default to daemon mode in the background
Creating ClickHouse user
Create a user for ClickHouse in the ProxySQL with password, The password is not configured for ClickHouse but for accessing ProxySQL:
# ProxySQL port is 6032, the default username and password are written in the configuration file
[email protected]: / root # mysql -h 127.0.0.1 -P 6032 -uadmin -padmin
Welcome to the MariaDB monitor. Commands end with; or \ g.
Your MySQL connection id is 3
Server version: 5.6.81 (ProxySQL Admin Module)
Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
Type 'help;' or '\ h' for help. Type '\ c' to clear the current input statement.
MySQL [(none)]> INSERT INTO clickhouse_users VALUES ('chuser', 'chpasswd', 1,100);
Query OK, 1 row affected (0.00 sec)
MySQL [(none)] > select * from clickhouse_users;
+ ---------- + ---------- + -------- + ----------------- +
| username | password | active | max_connections |
+ ---------- + ---------- + -------- + ----------------- +
| chuser | chpasswd | 1 | 100 |
+ ---------- + ---------- + -------- + ----------------- +
1 row in set (0.00 sec)
MySQL [(none)]> LOAD CLICKHOUSE USERS TO RUNTIME;
Query OK, 0 rows affected (0.00 sec)
MySQL [(none)]> SAVE CLICKHOUSE USERS TO DISK;
Query OK, 0 rows affected (0.00 sec)
Connecting to ClickHouse from MySQL Client
By default ProxySQL opens the port 6090 to receive user access to ClickHouse:
# Use username and password above
# If it is a different machine, remember to change the IP
[email protected]: / root # mysql -h 127.0.0.1 -P 6090 -uclicku -pclickp --prompt "ProxySQL-To-ClickHouse>"
Welcome to the MariaDB monitor. Commands end with; or \ g.
Your MySQL connection id is 64
Server version: 5.6.81 (ProxySQL ClickHouse Module)
Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
Type 'help;' or '\ h' for help. Type '\ c' to clear the current input statement.
ProxySQL-To-ClickHouse >
Querying ClickHouse like MySQL
MySQL [(none)] > select version ();
+ ------------------- +
| version |
+ ------------------- +
| 5.6.81-clickhouse |
+ ------------------- +
1 row in set (0.00 sec)
MySQL [(none)] > select now ();
+ --------------------- +
| now () |
+ --------------------- +
| 2019-12-25 20:17:14 |
+ --------------------- +
1 row in set (0.00 sec)
MySQL [(none)] > select today ();
+ ------------ +
| today () |
+ ------------ +
| 2019-12-25 |
+ ------------ +
1 row in set (0.00 sec)
# Our table is over 55 billion
ProxySQL-To-ClickHouse > select count (*) from mysql_audit_log_data;
+ ------------- +
| count () |
+ ------------- +
| 539124837571 |
+ ------------- +
1 row in set (8.31 sec)
Limitations
This ProxySQL solution works only when it is on the local ClickHouse (Note.- ClickHouse instance cannot have password in this ecosystem / recommended solution)
ProxySQL query rewrite limitations – The simple queries work seamless, The complex query rewrite are quite expense and there might some levels of SQL semantics limitations
Conclusion – ProxySQL Version 2.0.8 new features and enhancements
Changed default max_allowed_packet from 4M to 64M
Added support for mysqldump 8.0 and Admin #2340
Added new variable mysql-aurora_max_lag_ms_only_read_from_replicas : if max_lag_ms is used and the writer is in the reader hostgroup, the writer will be excluded if at least N replicas are good candidates.
Added support for unknown character set , and for collation id greater than 255 #1273
Added new variable mysql-log_unhealthy_connections to suppress messages related to unhealty clients connections being closed
Reimplemented rules_fast_routing using khash
Added support for SET CHARACTERSET #1692
Added support for same node into multiple Galera clusters #2290
Added more verbose output for error 2019 (Can’t initialize character set) #2273
Added more possible values for mysql_replication_hostgroups.check_type #2186
read_only | innodb_read_only
read_only & innodb_read_only
Added support and packages for RHEL / CentOS 8
References:
http://jackpgao.github.io/2017/12/19/Using-ClickHouse-like-MySQL-by-ProxySQL/
https://www.proxysql.com/blog/clickhouse-and-proxysql-queries-rewrite
https://www.altinity.com/blog/2018/7/13/clickhouse-and-proxysql-queries-rewrite
https://github.com/sysown/proxysql/releases
The post How to use ProxySQL to work on ClickHouse like MySQL ? appeared first on The WebScale Database Infrastructure Operations Experts.
[Less]
|