In a previous post, MySQL High Availability: Stale Reads and How to Fix Them, I’ve talked about the challenges of scaling out reads, where some types of applications cannot tolerate reading stale data. One of the ways of fixing it is by using ProxySQL Binlog Reader.
Long story short, binlog reader is a lightweight binary that keeps reading binlogs and informing ProxySQL – in real-time – about what has been applied on the said server. Since MySQL 5.7, as part of the OK_PACKET, the server will also send back information about the generated GTID event to clients. Knowing which GTID each server has applied and what was the last generated GTID the client connection received from the OK_PACKET, ProxySQL can route the following-up reads to a server that has already applied the said GTID.
At the time of this writing, you will need to compile binlog reader yourself.
Compile Binlog Reader:
To compile it on Centos7 you will need a few packets and repos pre-installed:
1 2 | yum install -y make cmake gcc gcc-c++ epel-release https://dev.mysql.com/get/mysql80-community-release-el7-2.noarch.rpm git wget zlib-devel openssl-devel yum -y --disablerepo=mysql80-community --enablerepo=mysql57-community install mysql-community-libs-compat-5.7.27-1.el7.x86_64 boost-devel.x86_64 mysql-community-devel-5.7.27-1.el7.x86_64 mysql-community-common-5.7.27-1.el7.x86_64 mysql-community-libs-5.7.27-1.el7.x86_64 |
Add the hash header file into include directory:
1 2 3 | cd /usr/include/mysql MYSQL_VERSION=$(rpm -qa | grep mysql-community-devel | awk -F'-' '{print $4}') wget https://raw.githubusercontent.com/mysql/mysql-server/mysql-${MYSQL_VERSION}/include/hash.h cd |
Compile libslave and ProxySQL Binlog Reader:
1 2 3 4 5 6 7 8 | cd git clone https://github.com/sysown/proxysql_mysqlbinlog.git cd proxysql_mysqlbinlog/libslave/ cmake . && make cd .. ln -s /usr/lib64/mysql/libmysqlclient.a /usr/lib64/libmysqlclient.a make chmod +x proxysql_binlog_reader |
Running Binlog Reader:
In order to run binlog reader, your mysql server must have boost installed:
1 2 | yum install epel-release -y yum install boost-system -y |
GTID must be enabled and session_track_gtids configured to OWN_GTID:
1 2 3 | gtid_mode=ON enforce_gtid_consistency session_track_gtids=OWN_GTID |
To start the binary, you need to specify a port for it to bind to. This port will be later configured on ProxySQL:
1 | ./proxysql_binlog_reader -h 127.0.0.1 -u root -psekret -P 3306 -l 3307 -L /tmp/binlogreader.log |
At this moment, if you inspect the error log, you should see something like:
1 2 3 4 5 6 | Starting ProxySQL MySQL Binlog Sucessfully started Angel process started ProxySQL MySQL Binlog process 795 2020-02-07 16:15:32 [INFO] Initializing client... 655dfbcb-49c2-11ea-a325-00163ecf1f9c:1-1 2020-02-07 16:15:32 [INFO] Reading binlogs... |
Please note, if you find the below error, it means you haven’t executed any event that generated a GTID on the server (#Issue7):
1 2 3 4 5 6 7 | Starting ProxySQL MySQL Binlog Sucessfully started Angel process started ProxySQL MySQL Binlog process 898 2020-02-07 17:41:34 [INFO] Initializing client... Error in initializing slave: basic_string::erase 2020-02-07 17:41:34 [INFO] Exiting... Shutdown angel process |
Configuring ProxySQL:
Now it’s time to inform proxysql that:
- The servers have proxysql binlog reader running on port X. Assuming you already have your mysql_servers table populated, all you will have to do is update each entry to inform the GTID port:12UPDATE mysql_servers SET gtid_port = 3307;LOAD MYSQL SERVERS TO RUNTIME; SAVE MYSQL SERVERS TO DISK;
Please note, if none of the slaves has received the GTID you are requesting, you want the master to serve this request. For this reason, it is important that your master is part of your read HG (it can be with the lowest possible weight). You can achieve that by setting mysql-monitor_writer_is_also_reader to true (default value). - A particular query rule should enforce GTID from the writer HG. Most of the cases, this will be the rule that matches the SELECT queries:12345678910111213141516171819202122232425mysql> SELECT rule_id, match_digest, destination_hostgroup, gtid_from_hostgroup FROM mysql_query_rules;+---------+---------------------+-----------------------+---------------------+| rule_id | match_digest | destination_hostgroup | gtid_from_hostgroup |+---------+---------------------+-----------------------+---------------------+| 200 | ^SELECT.*FOR UPDATE | 10 | NULL || 201 | ^SELECT | 11 | NULL |+---------+---------------------+-----------------------+---------------------+2 rows in set (0.00 sec)mysql> UPDATE mysql_query_rules SET gtid_from_hostgroup = 10 WHERE rule_id = 201;Query OK, 1 row affected (0.00 sec)mysql> LOAD MYSQL QUERY RULES TO RUNTIME; SAVE MYSQL QUERY RULES TO DISK;Query OK, 0 rows affected (0.00 sec)Query OK, 0 rows affected (0.25 sec)mysql> SELECT rule_id, match_digest, destination_hostgroup, gtid_from_hostgroup FROM mysql_query_rules;+---------+---------------------+-----------------------+---------------------+| rule_id | match_digest | destination_hostgroup | gtid_from_hostgroup |+---------+---------------------+-----------------------+---------------------+| 200 | ^SELECT.*FOR UPDATE | 10 | NULL || 201 | ^SELECT | 11 | 10 |+---------+---------------------+-----------------------+---------------------+2 rows in set (0.00 sec)
Network Traffic:
One of the advantages of this feature is that you can serve proxysql server with minimal impact on your network since proxysql binlog read will read only a portion of each binlog event to extract the GTID and only it will be sent over the network. This is unlike a normal slave, where all of the binlog events is sent. A GTID event will be classified into three categories and the size will vary depending on each state (below names are not official):
- Full12345678marcelo-altmann-PU-replication-1.lxd.opsession-prxy > marcelo-altmann-PU-proxysql-1.lxd.41726: Flags [P.], cksum 0x5dfd (incorrect -> 0xc8cb), seq 1:49, ack 1, win 510, options [nop,nop,TS val 792525047 ecr 430181741], length 480x0000: 4500 0064 61e3 4000 4006 7b0a ac10 025f E..da.@.@.{...._0x0010: ac10 0327 0ceb a2fe c48c 6725 bf6c 4c56 ...'......g%.lLV0x0020: 8018 01fe 5dfd 0000 0101 080a 2f3c f8f7 ....]......./<..0x0030: 19a4 0d6d 5354 3d64 6131 6262 3030 302d ...mST=da1bb000-0x0040: 3563 3963 2d31 3165 392d 3965 6537 2d30 5c9c-11e9-9ee7-00x0050: 3031 3633 6566 6162 6163 303a 312d 3535 0163efabac0:1-550x0060: 3239 390a 299.
When ProxySQL detects this, it is a completely new GTID (for a new server_uuid). In the above example, we are sending 48 bytes corresponding to the full GTID set of da1bb000-5c9c-11e9-9ee7-00163efabac0:1-55299. This is normally sent when proxysql does not have any track of GTIDs for this server. - Delta1234567801:30:26.066393 IP (tos 0x0, ttl 64, id 25060, offset 0, flags [DF], proto TCP (6), length 94)marcelo-altmann-PU-replication-1.lxd.opsession-prxy > marcelo-altmann-PU-proxysql-1.lxd.41726: Flags [P.], cksum 0x5df7 (incorrect -> 0x93b9), seq 49:91, ack 1, win 510, options [nop,nop,TS val 792557032 ecr 430181741], length 420x0000: 4500 005e 61e4 4000 4006 7b0f ac10 025f E..^a.@.@.{...._0x0010: ac10 0327 0ceb a2fe c48c 6755 bf6c 4c56 ...'......gU.lLV0x0020: 8018 01fe 5df7 0000 0101 080a 2f3d 75e8 ....]......./=u.0x0030: 19a4 0d6d 4931 3d64 6131 6262 3030 3035 ...mI1=da1bb00050x0040: 6339 6331 3165 3939 6565 3730 3031 3633 c9c11e99ee7001630x0050: 6566 6162 6163 303a 3535 3330 300a efabac0:55300.
When ProxySQL has already sent a full GTID event, it will send only the delta event, compounded by the full server_uuid + incremental id. In the above example, we are sending 42 bytes (instead of the initial 48) and the GTID event is da1bb000-5c9c-11e9-9ee7-00163efabac0:1-55300. - Incremental12345601:30:44.388537 IP (tos 0x0, ttl 64, id 25061, offset 0, flags [DF], proto TCP (6), length 61)marcelo-altmann-PU-replication-1.lxd.opsession-prxy > marcelo-altmann-PU-proxysql-1.lxd.41726: Flags [P.], cksum 0x5dd6 (incorrect -> 0xa8c8), seq 91:100, ack 1, win 510, options [nop,nop,TS val 792575354 ecr 430213726], length 90x0000: 4500 003d 61e5 4000 4006 7b2f ac10 025f E..=a.@.@.{/..._0x0010: ac10 0327 0ceb a2fe c48c 677f bf6c 4c56 ...'......g..lLV0x0020: 8018 01fe 5dd6 0000 0101 080a 2f3d bd7a ....]......./=.z0x0030: 19a4 8a5e 4932 3d35 3533 3031 0a ...^I2=55301.
All events after the Delta will contain only the incremental part of the GTID (it will omit the server_uuid). On the above example, we are sending only 9 bytes and sending the GTID 55301 (complete GTID is da1bb000-5c9c-11e9-9ee7-00163efabac0:1-55301).
Testing:
To simulate the issue, we will use a simple PHP script:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | <?php date_default_timezone_set('UTC'); $mysqli = new mysqli('127.0.0.1', 'root', 'sekret', 'test', 6033); if ($mysqli->connect_error) { die('Connect Error (' . $mysqli->connect_errno . ') ' . $mysqli->connect_error); } /* Setup */ echo date('Y-m-d H:i:s') . " Starting to SETUP the testn"; $mysqli->query("DROP TABLE IF EXISTS joinit"); $mysqli->query("CREATE TABLE IF NOT EXISTS `test`.`joinit` ( `i` bigint(11) NOT NULL AUTO_INCREMENT, `s` char(255) DEFAULT NULL, `t` datetime NOT NULL, `g` bigint(11) NOT NULL, KEY(`i`, `t`), PRIMARY KEY(`i`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;"); $date1=date('Y-m-d H:i:s'); $mysqli->query("INSERT INTO test.joinit VALUES (NULL, uuid(), time('$date1'), (FLOOR( 1 + RAND( ) *60 )));"); $mysqli->query("INSERT INTO test.joinit SELECT NULL, uuid(), time('$date1'), (FLOOR( 1 + RAND( ) *60 )) FROM test.joinit;"); $mysqli->query("INSERT INTO test.joinit SELECT NULL, uuid(), time('$date1'), (FLOOR( 1 + RAND( ) *60 )) FROM test.joinit;"); $mysqli->query("INSERT INTO test.joinit SELECT NULL, uuid(), time('$date1'), (FLOOR( 1 + RAND( ) *60 )) FROM test.joinit;"); $mysqli->query("INSERT INTO test.joinit SELECT NULL, uuid(), time('$date1'), (FLOOR( 1 + RAND( ) *60 )) FROM test.joinit;"); $mysqli->query("INSERT INTO test.joinit SELECT NULL, uuid(), time('$date1'), (FLOOR( 1 + RAND( ) *60 )) FROM test.joinit;"); $mysqli->query("INSERT INTO test.joinit SELECT NULL, uuid(), time('$date1'), (FLOOR( 1 + RAND( ) *60 )) FROM test.joinit;"); $mysqli->query("INSERT INTO test.joinit SELECT NULL, uuid(), time('$date1'), (FLOOR( 1 + RAND( ) *60 )) FROM test.joinit;"); $mysqli->query("INSERT INTO test.joinit SELECT NULL, uuid(), time('$date1'), (FLOOR( 1 + RAND( ) *60 )) FROM test.joinit;"); $mysqli->query("INSERT INTO test.joinit SELECT NULL, uuid(), time('$date1'), (FLOOR( 1 + RAND( ) *60 )) FROM test.joinit;"); $mysqli->query("INSERT INTO test.joinit SELECT NULL, uuid(), time('$date1'), (FLOOR( 1 + RAND( ) *60 )) FROM test.joinit;"); $mysqli->query("INSERT INTO test.joinit SELECT NULL, uuid(), time('$date1'), (FLOOR( 1 + RAND( ) *60 )) FROM test.joinit;"); $mysqli->query("INSERT INTO test.joinit SELECT NULL, uuid(), time('$date1'), (FLOOR( 1 + RAND( ) *60 )) FROM test.joinit;"); $mysqli->query("INSERT INTO test.joinit SELECT NULL, uuid(), time('$date1'), (FLOOR( 1 + RAND( ) *60 )) FROM test.joinit;"); $mysqli->query("INSERT INTO test.joinit SELECT NULL, uuid(), time('$date1'), (FLOOR( 1 + RAND( ) *60 )) FROM test.joinit;"); echo date('Y-m-d H:i:s') . " Starting to RUN the testn"; $result = $mysqli->query("SELECT MAX(i) FROM joinit"); $row = $result->fetch_row(); sleep(2); $date2=date('Y-m-d H:i:s'); for ($i=1; $i<$row[0]; $i++) { $result = $mysqli->query("SELECT i FROM joinit WHERE i = $i"); if($result->num_rows == 0) continue; $mysqli->query("UPDATE joinit SET t = '$date2' WHERE i = $i"); $result = $mysqli->query("SELECT i FROM joinit WHERE t = '$date2' AND i = $i"); if($result->num_rows == 0) { echo date('Y-m-d H:i:s') . " Dirty Read Detected on i $i . . ."; usleep(500000); $result = $mysqli->query("SELECT i FROM joinit WHERE t = '$date2' AND i = $i"); echo " After 500ms rows found $result->num_rows n"; } else { echo date('Y-m-d H:i:s') . " i $i is okn"; } } $mysqli->close(); ?> |
The script will:
- Populate a table with some random data.
- For each row, it will update a datetime column with the current time.
- Immediately after the update, it will try to query one of the slaves using the current time as a parameter. If the slave has not processed the update from step 2, it will return 0 rows. The script will report a dirty read and try the same select again after 0.5 seconds and report the number of returned rows.
- If step 3 succeeded, it will report as ok.
Example run without GTID consistent reads:
1 2 3 4 5 6 7 8 9 10 11 12 13 | 2020-02-07 17:13:52 Starting to SETUP the test 2020-02-07 17:13:53 Starting to RUN the test 2020-02-07 17:13:55 i 1 is ok 2020-02-07 17:13:55 Dirty Read Detected on i 2 . . . After 500ms rows found 1 2020-02-07 17:13:55 i 3 is ok 2020-02-07 17:13:55 Dirty Read Detected on i 4 . . . After 500ms rows found 1 2020-02-07 17:13:56 Dirty Read Detected on i 6 . . . After 500ms rows found 1 2020-02-07 17:13:56 i 7 is ok 2020-02-07 17:13:56 i 8 is ok 2020-02-07 17:13:56 i 9 is ok 2020-02-07 17:13:56 i 13 is ok 2020-02-07 17:13:56 i 14 is ok 2020-02-07 17:13:56 Dirty Read Detected on i 15 . . . After 500ms rows found 1 |
Example run with GTID consistent reads:
1 2 3 4 5 6 7 8 9 10 11 12 13 | 2020-02-07 17:15:27 Starting to SETUP the test 2020-02-07 17:15:28 Starting to RUN the test 2020-02-07 17:15:30 i 1 is ok 2020-02-07 17:15:30 i 2 is ok 2020-02-07 17:15:30 i 3 is ok 2020-02-07 17:15:30 i 4 is ok 2020-02-07 17:15:30 i 6 is ok 2020-02-07 17:15:30 i 7 is ok 2020-02-07 17:15:30 i 8 is ok 2020-02-07 17:15:30 i 9 is ok 2020-02-07 17:15:30 i 13 is ok 2020-02-07 17:15:30 i 14 is ok 2020-02-07 17:15:30 i 15 is ok |
Monitoring:
To monitor if a backend server is sending GTID event, you can query stats_mysql_gtid_executed table:
1 2 3 4 5 6 7 8 | mysql> SELECT * FROM stats_mysql_gtid_executed; +---------------+------+-----------------------------------------------+--------+ | hostname | port | gtid_executed | events | +---------------+------+-----------------------------------------------+--------+ | 10.126.47.251 | 3306 | 278a1c44-51c8-11ea-a1ad-00163e1e8e27:1-344029 | 72091 | | 10.126.47.170 | 3306 | 278a1c44-51c8-11ea-a1ad-00163e1e8e27:1-344029 | 72089 | +---------------+------+-----------------------------------------------+--------+ 2 rows in set (0.01 sec) |
In order to verify if queries are been served using this feature, you can then query stats_mysql_connection_pool table and look for the Queries_GTID_sync column:
1 2 3 4 5 6 7 8 9 | mysql> SELECT * FROM stats_mysql_connection_pool; +-----------+--------------+----------+--------+----------+----------+--------+---------+-------------+---------+-------------------+-----------------+-----------------+------------+ | hostgroup | srv_host | srv_port | status | ConnUsed | ConnFree | ConnOK | ConnERR | MaxConnUsed | Queries | Queries_GTID_sync | Bytes_data_sent | Bytes_data_recv | Latency_us | +-----------+--------------+----------+--------+----------+----------+--------+---------+-------------+---------+-------------------+-----------------+-----------------+------------+ | 11 | 10.126.47.79 | 3306 | ONLINE | 0 | 2 | 2 | 0 | 1 | 1327 | 1309 | 59965 | 7866 | 196 | | 11 | 10.126.47.39 | 3306 | ONLINE | 0 | 2 | 2 | 0 | 1 | 18 | 0 | 806 | 77 | 632 | | 10 | 10.126.47.79 | 3306 | ONLINE | 0 | 1 | 2 | 0 | 1 | 602 | 10 | 39383 | 84 | 196 | +-----------+--------------+----------+--------+----------+----------+--------+---------+-------------+---------+-------------------+-----------------+-----------------+------------+ 3 rows in set (0.01 sec) |
Summary:
There are a few caveats to consider if you want to adopt this feature:
- In order for this feature to work, the read query must be executed in the same connection as the write query. Or, if you are using proxysql version 2.0.9 onwards, you can send a query comment specifying the minimum GTID which a server must have in order to serve the read.
- Some write queries don’t generate GTID. For example, an UPDATE that returns 0 affected rows in this case (either due to a condition not matching any row in the table or updated columns being equal to current values), proxysql will not use the feature and redirect the following reads to any slave not updating Queries_GTID_sync.
- This feature doesn’t work with Galera replication.
- If you don’t have your master as part of your read hostgroup (that is done by default with proxysql variable mysql-monitor_writer_is_also_reader), ProxySQL will behave like it was executing SELECT WAIT_FOR_EXECUTED_GTID_SET, where the execution of reading query will stall until the slave has received the requested ID or proxysql mysql-connect_timeout_server_max has elapsed.
Moving from a centralized architecture into a distributed one can bring some challenges, and a delay in replication causing slaves to provide stale data is one of them. ProxySQL binlog readers can help mitigate this issue.
Please note: This feature and binary (proxysql_binlog_reader) are relatively new and are not considered GA as of now. We highly advise you to extensively test it before implementing it in production.
I tried that year ago on one of our Percona MySQL 5.7 clusters and was severely affected by degraded replication performance bug, which probably still didn’t fixed – https://bugs.mysql.com/bug.php?id=92964
I didn’t try it on 8.0 though.