After upgrading some of our slaves to latest 5.7, I have found what looks like a serious regression introduced in MySQL 5.7.
A couple weeks ago I noticed that the error log file of one of our clusters, where I had implemented my in place transparent compression of binary logs, was literally flooded by the following error:
[ERROR] Binlog has bad magic number; It's not a binary log file that can be used by this version of MySQL
In the above setup this is an harmless error, and it should only happen at server startup, where mysqld opens and reads all available binary log files. The error is due to the fact that since files are now compressed, mysqld doesn't recognize them as valid - not an issue, as only older files are compressed, and only after ensuring all slaves already got them.
But I couldn't understand why the error was now hitting every minute or so, after upgrading from 5.6 to 5.7.
After a short investigation I was able to relate the errors to the exact moment where a binary log was closed and a new one created, this normally happens when the binary log file size reaches the value you have set in max_binlog_size variable. But this indicated that mysqld was continuously trying to open and read all the available binary logs, each time a file was rotated.
Ouch! This wasn't a nice finding.
On this server we have in excess of 3,500 binary log files, and I know about installations which have much more (mind you, we have a retention of 4 days only here). We like to keep binlog files small, we have them sized at 100 MB as this makes easier to peruse them when troubleshooting with mysqlbinlog. Anyways this wouldn't change anything, I mean, having 3,500 100M files or 350 1G files would generate exactly the same amount of I/O when you wanted to read them all....
Anyways, I had just found that every time a binary log is rotated, mysqld needs to open all of the files. What a waste of I/O and performances, I thought. So I decided to have a look at the code to understand what mysqld was actually trying to do with those binlogs.
It wasn't too difficult to narrow down the binlog scan with the function purge_logs_before_date()
that expires logs based on time, as set by the expire_logs_days variable.
But reading the binary log files shouldn't be really needed when you're only looking to expire them, so this looked weird to me.
Further digging into the source code shows that mysqld is actually looking for "lost GTIDs" in the binlog files, and does it every time a binary log is closed for rotation. I haven't looked deeper into what a "lost" GTID means in the code, however whatever the reason for this search is, for sure it shouldn't be done repeatedly at every binlog rotation.
The weird thing is, we aren't even using GTIDs!
But still mysqld opens and reads ALL the available binary log files all the time. This is a serious regression compared to 5.6 where this wasn't happening, and it can hit you hard when you have a large number of binary log files. Caveat emptor.
I have raised a bug for the above, and really hope Oracle gives it the needed attention.
Showing posts with label bug. Show all posts
Showing posts with label bug. Show all posts
Monday, December 11, 2017
Monday, November 6, 2017
when xtrabackup fails with 'read' returned OS error 0 after upgrading to 5.7
Here's something that has puzzled me for several weeks.
Right after migrating MySQL from 5.6 to 5.7, we started experiencing random xtrabackup failures on some, but not all, of our slaves.
The failures were only happening when taking an incremental backup, and it would always fail on the same table on each slave, with errors similar to the following:
171106 13:00:33 [01] Streaming ./gls/C_GLS_IDS_AUX.ibd
InnoDB: 262144 bytes should have been read. Only 114688 bytes read. Retrying for the remaining bytes.
InnoDB: 262144 bytes should have been read. Only 114688 bytes read. Retrying for the remaining bytes.
InnoDB: 262144 bytes should have been read. Only 114688 bytes read. Retrying for the remaining bytes.
InnoDB: 262144 bytes should have been read. Only 114688 bytes read. Retrying for the remaining bytes.
InnoDB: 262144 bytes should have been read. Only 114688 bytes read. Retrying for the remaining bytes.
InnoDB: 262144 bytes should have been read. Only 114688 bytes read. Retrying for the remaining bytes.
InnoDB: 262144 bytes should have been read. Only 114688 bytes read. Retrying for the remaining bytes.
InnoDB: 262144 bytes should have been read. Only 114688 bytes read. Retrying for the remaining bytes.
InnoDB: 262144 bytes should have been read. Only 114688 bytes read. Retrying for the remaining bytes.
InnoDB: 262144 bytes should have been read. Only 114688 bytes read. Retrying for the remaining bytes.
InnoDB: Retry attempts for reading partial data failed.
InnoDB: Tried to read 262144 bytes at offset 0, but was only able to read 114688
InnoDB: File (unknown): 'read' returned OS error 0. Cannot continue operation
InnoDB: Cannot continue operation.
Searching the web didn't yield any useful result.
The error was happening on one slave but not on another, OS, MySQL and Xtrabackup versions being the same:
I even tried to dump the offending schema from a failing slave and import it into a non failing slave, but on the non-failing slave the same table in that schema backed up just fine.
This has been going on for a while, where I used to run without incremental backups on the migrated slaves, until today, when it happened again on yet another slave that we had just migrated, and I had an intuition: since the failure of the incremental backup was only happening after the migration, of course it had to be related to the migration itself. So I started suspecting some incompatibility between 5.7 and/or xtrabackup, and tablespaces created with MySQL 5.6.
Verifying my suspect was as easy as running:
ALTER TABLE C_GLS_IDS_AUX Engine=InnoDB
Et voilĂ !! The rebuilt tablespace was not crashing the incremental backup anymore.
Eventually, another table(space) would crash the backup again later, but running a NULL alter table on it and relaunching the backup would fix that. Wash, rinse, repeat until the incremental completes, and you're done.
Hope this helps someone saving some time..
And if you are looking for xtrabackup based backup scripts, feel free to check my own set here on my GitHub page.
ADDENDUM
Looks like I have been a little overenthusiastic about this.
In fact, the problem reappeared the day after writing this blog post - apparently, the null alter table only fixes it temporarily.
Turns out this has to be some incompatibility between Xtrabackup 2.4.x and MySQL 5.7.x as I was able to reproduce the issue. Please see this bug if you are interested in the details.
Right after migrating MySQL from 5.6 to 5.7, we started experiencing random xtrabackup failures on some, but not all, of our slaves.
The failures were only happening when taking an incremental backup, and it would always fail on the same table on each slave, with errors similar to the following:
171106 13:00:33 [01] Streaming ./gls/C_GLS_IDS_AUX.ibd
InnoDB: 262144 bytes should have been read. Only 114688 bytes read. Retrying for the remaining bytes.
InnoDB: 262144 bytes should have been read. Only 114688 bytes read. Retrying for the remaining bytes.
InnoDB: 262144 bytes should have been read. Only 114688 bytes read. Retrying for the remaining bytes.
InnoDB: 262144 bytes should have been read. Only 114688 bytes read. Retrying for the remaining bytes.
InnoDB: 262144 bytes should have been read. Only 114688 bytes read. Retrying for the remaining bytes.
InnoDB: 262144 bytes should have been read. Only 114688 bytes read. Retrying for the remaining bytes.
InnoDB: 262144 bytes should have been read. Only 114688 bytes read. Retrying for the remaining bytes.
InnoDB: 262144 bytes should have been read. Only 114688 bytes read. Retrying for the remaining bytes.
InnoDB: 262144 bytes should have been read. Only 114688 bytes read. Retrying for the remaining bytes.
InnoDB: 262144 bytes should have been read. Only 114688 bytes read. Retrying for the remaining bytes.
InnoDB: Retry attempts for reading partial data failed.
InnoDB: Tried to read 262144 bytes at offset 0, but was only able to read 114688
InnoDB: File (unknown): 'read' returned OS error 0. Cannot continue operation
InnoDB: Cannot continue operation.
Searching the web didn't yield any useful result.
The error was happening on one slave but not on another, OS, MySQL and Xtrabackup versions being the same:
- CentOS release 6.8 (Final)
- xtrabackup version 2.4.8 based on MySQL server 5.7.13 Linux (x86_64) (revision id: 97330f7)
- mysqld Ver 5.7.19-17 for Linux on x86_64 (Percona Server (GPL), Release 17, Revision e19a6b7b73f)
I even tried to dump the offending schema from a failing slave and import it into a non failing slave, but on the non-failing slave the same table in that schema backed up just fine.
This has been going on for a while, where I used to run without incremental backups on the migrated slaves, until today, when it happened again on yet another slave that we had just migrated, and I had an intuition: since the failure of the incremental backup was only happening after the migration, of course it had to be related to the migration itself. So I started suspecting some incompatibility between 5.7 and/or xtrabackup, and tablespaces created with MySQL 5.6.
Verifying my suspect was as easy as running:
ALTER TABLE C_GLS_IDS_AUX Engine=InnoDB
Et voilĂ !! The rebuilt tablespace was not crashing the incremental backup anymore.
Eventually, another table(space) would crash the backup again later, but running a NULL alter table on it and relaunching the backup would fix that. Wash, rinse, repeat until the incremental completes, and you're done.
Hope this helps someone saving some time..
And if you are looking for xtrabackup based backup scripts, feel free to check my own set here on my GitHub page.
ADDENDUM
Looks like I have been a little overenthusiastic about this.
In fact, the problem reappeared the day after writing this blog post - apparently, the null alter table only fixes it temporarily.
Turns out this has to be some incompatibility between Xtrabackup 2.4.x and MySQL 5.7.x as I was able to reproduce the issue. Please see this bug if you are interested in the details.
Monday, July 3, 2017
SET PASSWORD will lock you out of your slave in a mixed 5.6/5.7 MySQL topology
Long time no post.... :-)
Here's something interesting.
Last week I decided to give MySQL 5.7 a try (yes, I am kinda conservative DBA...) and the very same day that I installed my first 5.7 replica I noticed that, after changing my own password on the 5.6 master, I could no longer connect to the 5.7 slave.
Very annoying, to say the least! So I went and dug out the root password (which we do not normally use) and when I connected to the slave I was surprised to see that my password's hash on the 5.7 slave was different than the hash on the 5.6 master. No wonder I couldn't connect....
A bit of research on the MySQL documentation and I understood that 5.7 introduced few changes around the way you work with users' passwords. SET PASSWORD is now deprecated in favour of ALTER USER: see MySQL 5.7 Reference Manual page.
So far so good, but it's obvious that when Oracle implemented those changes, they didn't consider upgrade paths, or mixed 5.6/5.7 topologies that are commonly found during a major MySQL version upgrade, like in this case.
Turns out there is a bug first reported by Gillian Gunson back in September 2016 (SET PASSWORD replicates incorrectly between 5.6 and 5.7) which Oracle acknowledged but didn't care to fix at the time of this blog post. Sorry for the rant, but I really feel this to be a real showstopper to any migration where there are many database users and where a password change may happen quite often.
As Gillian pointed out in the above bug, what really happens is that the password is hashed twice, and on the replica we get an hash of the already hashed password - no wonder it doesn't work :-)
The good news is that this is really an easy fix. I was able to patch this myself in few minutes, and the patch is one line of code.... you can find the diff below, this was applied to 5.7.18 Percona source code but I believe it will be the same in Oracle MySQL, regardless the minor version.
Basically, I added a check to avoid hashing the password if the thread is a slave thread and the command is a SET PASSWORD. This check may break the use of SET PASSWORD when master will be on 5.7, I didn't check that - but hey, SET PASSWORD is deprecated anyways... and I will only use my patched version of 5.7 during the migration.
Enjoy!
Here's something interesting.
Last week I decided to give MySQL 5.7 a try (yes, I am kinda conservative DBA...) and the very same day that I installed my first 5.7 replica I noticed that, after changing my own password on the 5.6 master, I could no longer connect to the 5.7 slave.
Very annoying, to say the least! So I went and dug out the root password (which we do not normally use) and when I connected to the slave I was surprised to see that my password's hash on the 5.7 slave was different than the hash on the 5.6 master. No wonder I couldn't connect....
A bit of research on the MySQL documentation and I understood that 5.7 introduced few changes around the way you work with users' passwords. SET PASSWORD is now deprecated in favour of ALTER USER: see MySQL 5.7 Reference Manual page.
So far so good, but it's obvious that when Oracle implemented those changes, they didn't consider upgrade paths, or mixed 5.6/5.7 topologies that are commonly found during a major MySQL version upgrade, like in this case.
Turns out there is a bug first reported by Gillian Gunson back in September 2016 (SET PASSWORD replicates incorrectly between 5.6 and 5.7) which Oracle acknowledged but didn't care to fix at the time of this blog post. Sorry for the rant, but I really feel this to be a real showstopper to any migration where there are many database users and where a password change may happen quite often.
As Gillian pointed out in the above bug, what really happens is that the password is hashed twice, and on the replica we get an hash of the already hashed password - no wonder it doesn't work :-)
The good news is that this is really an easy fix. I was able to patch this myself in few minutes, and the patch is one line of code.... you can find the diff below, this was applied to 5.7.18 Percona source code but I believe it will be the same in Oracle MySQL, regardless the minor version.
Basically, I added a check to avoid hashing the password if the thread is a slave thread and the command is a SET PASSWORD. This check may break the use of SET PASSWORD when master will be on 5.7, I didn't check that - but hey, SET PASSWORD is deprecated anyways... and I will only use my patched version of 5.7 during the migration.
Enjoy!
*** sql/auth/sql_user.cc Mon Jul 3 16:18:55 2017
--- sql/auth/sql_user.cc.patch Mon Jul 3 16:07:01 2017
***************
*** 565,570 ****
--- 565,574 ----
*/
if ((Str->uses_identified_by_clause ||
(Str->auth.length == 0 && !user_exists))
+ // Rick Pizzi: don't change to hash if thread is a replication thread and a
+ // SET PASSWORD was issued on master, to avoid double hash
+ // (fix for https://bugs.mysql.com/bug.php?id=82979)
+ && (!thd->slave_thread || thd->lex->sql_command != SQLCOM_SET_OPTION))
{
st_mysql_auth *auth= (st_mysql_auth *) plugin_decl(plugin)->info;
inbuf= Str->auth.str;
Thursday, February 23, 2017
When order of appearance of indexes matters in MySQL
Sometimes MySQL surprises you in ways you would have never imagined.
Would you think that the order in which the indexes appear in a table matters?
It does. Mind you, not the order of the columns - the order of the indexes.
MySQL optimizer can, in specific circumstances, take different paths, sometimes with nefarious effects.
Please consider the following table:
CREATE TABLE `mypartitionedtable ` (
`HASH_ID` char(64) NOT NULL,
`RAW_DATA` mediumblob NOT NULL,
`EXPIRE_DATE` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY `EXPIRE_DATE_IX` (`EXPIRE_DATE`),
KEY `HASH_ID_IX` (`HASH_ID`)
) ENGINE=TokuDB DEFAULT CHARSET=latin1 ROW_FORMAT= TOKUDB_ UNCOMPRESSED
/*!50100 PARTITION BY RANGE (UNIX_TIMESTAMP (EXPIRE_ DATE))
(PARTITION p2005 VALUES LESS THAN (1487847600) ENGINE = TokuDB,
PARTITION p2006 VALUES LESS THAN (1487851200) ENGINE = TokuDB,
PARTITION p2007 VALUES LESS THAN (1487854800) ENGINE = TokuDB,
PARTITION p2008 VALUES LESS THAN (1487858400) ENGINE = TokuDB,
[ ... ]
This is a table where we store key-value data for a cache. The table is partitioned by range on the cache expire date, so we can actually apply retention to the cache data.
Now, consider the following DML:
Would you think that the order in which the indexes appear in a table matters?
It does. Mind you, not the order of the columns - the order of the indexes.
MySQL optimizer can, in specific circumstances, take different paths, sometimes with nefarious effects.
Please consider the following table:
CREATE TABLE `mypartitionedtable ` (
`HASH_ID` char(64) NOT NULL,
`RAW_DATA` mediumblob NOT NULL,
`EXPIRE_DATE` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY `EXPIRE_DATE_IX` (`EXPIRE_DATE`),
KEY `HASH_ID_IX` (`HASH_ID`)
) ENGINE=TokuDB DEFAULT CHARSET=latin1 ROW_FORMAT=
/*!50100 PARTITION BY RANGE (UNIX_TIMESTAMP
(PARTITION p2005 VALUES LESS THAN (1487847600) ENGINE = TokuDB,
PARTITION p2006 VALUES LESS THAN (1487851200) ENGINE = TokuDB,
PARTITION p2007 VALUES LESS THAN (1487854800) ENGINE = TokuDB,
PARTITION p2008 VALUES LESS THAN (1487858400) ENGINE = TokuDB,
[ ... ]
This is a table where we store key-value data for a cache. The table is partitioned by range on the cache expire date, so we can actually apply retention to the cache data.
Now, consider the following DML:
DELETE FROM mypartitionedtable WHERE HASH_ID = 'somehash' AND EXPIRE_DATE > NOW() ;
One would think that the plan for the query above is optimal: it goes by HASH_ID_IX index and also prunes partitions, by only looking at partitions with non-expired data.
In reality, it depends.
Statistics for partitioned tables in MySQL are only computed on the largest partition.
This works well most of the times, *except* when the largest partition becomes one of the expired ones.
Let's see this in detail...
We have several partitions, each partition contains a different day worth of data, based on values of the EXPIRE_DATE column. For the purpose of this post, let's assume partition p654 contains yesterday's data, and partition p666 contains data with an expire date which falls tomorrow.
If we explain a query for a partition that contains valid cache data, i.e. a partition that has all rows with EXPIRE_DATE in the future, everything looks good: you can see that the cost for a search by HASH_ID is much less than the cost for a search by EXPIRE_DATE, as highlighted by the value of the "rows" column:
mysql>EXPLAIN SELECT COUNT(*) FROM BIG_STORAGE_INNO PARTITION(p666) WHERE EXPIRE_DATE > NOW();
+----+-------------+------------------+-------+----------------+----------------+---------+------+--------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------------+-------+----------------+----------------+---------+------+--------+--------------------------+
| 1 | SIMPLE | BIG_STORAGE_INNO | range | EXPIRE_DATE_IX | EXPIRE_DATE_IX | 5 | NULL | 291978 | Using where; Using index |
+----+-------------+------------------+-------+----------------+----------------+---------+------+--------+--------------------------+
1 row in set (0.00 sec)
mysql>EXPLAIN SELECT COUNT(*) FROM BIG_STORAGE_INNO PARTITION(p666) WHERE HASH_ID = 'any hash value';
+----+-------------+------------------+------+---------------+---------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------------+------+---------------+---------+---------+-------+------+--------------------------+
| 1 | SIMPLE | BIG_STORAGE_INNO | ref | HASH_ID | HASH_ID | 64 | const | 1 | Using where; Using index |
+----+-------------+------------------+------+---------------+---------+---------+-------+------+--------------------------+
1 row in set (0.01 sec)
But what happens if we explain the same query for a partition which has all rows with an EXPIRE_DATE in the past?
mysql>EXPLAIN SELECT COUNT(*) FROM BIG_STORAGE_INNO PARTITION(p654) WHERE HASH_ID = 'any hash value';
+----+-------------+------------------+------+---------------+---------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------------+------+---------------+---------+---------+-------+------+--------------------------+
| 1 | SIMPLE | BIG_STORAGE_INNO | ref | HASH_ID | HASH_ID | 64 | const | 1 | Using where; Using index |
+----+-------------+------------------+------+---------------+---------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)
mysql>EXPLAIN SELECT COUNT(*) FROM BIG_STORAGE_INNO PARTITION(p654) WHERE EXPIRE_DATE > NOW();
+----+-------------+------------------+-------+----------------+----------------+---------+------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------------+-------+----------------+----------------+---------+------+------+--------------------------+
| 1 | SIMPLE | BIG_STORAGE_INNO | range | EXPIRE_DATE_IX | EXPIRE_DATE_IX | 5 | NULL | 1 | Using where; Using index |
+----+-------------+------------------+-------+----------------+----------------+---------+------+------+--------------------------+
1 row in set (0.01 sec)
Whoa, cost is the same for both queries now!
This has a very bad effect, as the optimizer will choose to use EXPIRE_DATE to select our rows and disregard HASH_ID completely. And it will do this for all partitions, not just this one (where the plan actually would be ok).
Our DELETE above becomes now a locking scan of all partitions having EXPIRE_DATE > NOW(), this means (in our case) almost the entire table.
Ouch!!
This is in my opinion a bug - the fact that the optimizer only looks at the largest partition is ... sub-optimal, if you ask me. But in this case, the optimizer should also evaluate that EXPIRE_DATE is a partitioning key and choose the other index for the query, which would be correct.
Now for the fun part of this post.
I was puzzled about what was causing the optimizer to pick one index and not the other, considering that they had identical cost. Unfortunately, explain wasn't telling me anything useful, so I decided to instrument some debugging to find out.
Debugging the MySQL server actually involves either recompiling from source with debugging option enabled, or (for lazy DBAs like myself) installing the debug package from your vendor of choice.
I was glad to find out that the stock Oracle distribution (Community edition) already includes the debugging binary of the server, called mysqld-debug. So I stopped the server, replaced mysqld with mysqld-debug, and restarted.
This was not enough, however, to have any debug output. A quick look at the manual and I found that there is a variable that needs instrumented, called simply debug. More information about debugging MySQL is here, however all it's needed is to set this global variable as follows:
mysql>set global debug='d:L:F:O';
This will cause debugging output to go to the MySQL error log. It will print debugging output prefixed by source code file and line , useful if you want to look at source code to understand what's going on.
So I ran the explain for the DELETE on the partition with yesterday data, and here is what I got (uninteresting debugging output removed for clarity):
opt_range.cc: 264: get_key_scans_params: opt: range_scan_alternatives: starting struct opt_range.cc: 264: get_key_scans_params: opt: (null): starting struct opt_range.cc: 299: get_key_scans_params: opt: index: "EXPIRE_DATE_IX" opt_range.cc: 9541: check_quick_select: exit: Records: 4 opt_range.cc: 264: get_key_scans_params: opt: ranges: starting struct opt_range.cc: 299: get_key_scans_params: opt: (null): "2017-02-22 17:28:51 < EXPIRE_DATE" opt_range.cc: 279: get_key_scans_params: opt: ranges: ending struct opt_range.cc: 313: get_key_scans_params: opt: index_dives_for_eq_ranges: 1 opt_range.cc: 313: get_key_scans_params: opt: rowid_ordered: 0 opt_range.cc: 313: get_key_scans_params: opt: using_mrr: 0 opt_range.cc: 313: get_key_scans_params: opt: index_only: 0 opt_range.cc: 336: get_key_scans_params: opt: rows: 4 opt_range.cc: 347: get_key_scans_params: opt: cost: 5.81 opt_range.cc: 313: get_key_scans_params: opt: chosen: 1 opt_range.cc: 279: get_key_scans_params: opt: (null): ending struct opt_range.cc: 264: get_key_scans_params: opt: (null): starting struct opt_range.cc: 299: get_key_scans_params: opt: index: "HASH_ID" opt_range.cc: 9541: check_quick_select: exit: Records: 4 opt_range.cc: 264: get_key_scans_params: opt: ranges: starting struct opt_range.cc: 299: get_key_scans_params: opt: (null): "925445622fe6c9da0a432b3e686b807557c215f04233b59a94b7eff1cb6ef3d9 <= HASH_ID <= 925445622fe6c9da0a432b3e686b807557c215f04233b59a94b7eff1cb6ef3d9" opt_range.cc: 279: get_key_scans_params: opt: ranges: ending struct opt_range.cc: 313: get_key_scans_params: opt: index_dives_for_eq_ranges: 1 opt_range.cc: 313: get_key_scans_params: opt: rowid_ordered: 1 opt_range.cc: 313: get_key_scans_params: opt: using_mrr: 0 opt_range.cc: 313: get_key_scans_params: opt: index_only: 0 opt_range.cc: 336: get_key_scans_params: opt: rows: 4 opt_range.cc: 347: get_key_scans_params: opt: cost: 5.81 opt_range.cc: 313: get_key_scans_params: opt: chosen: 0 opt_range.cc: 299: get_key_scans_params: opt: cause: "cost" opt_range.cc: 279: get_key_scans_params: opt: (null): ending struct opt_range.cc: 13830: print_sel_tree: info: SEL_TREE: 0x7f58c00b1fd8 (ROR scans) scans: HASH_ID opt_range.cc: 5676: get_key_scans_params: info: Returning range plan for key EXPIRE_DATE_IX, cost 5.81, records 4
By looking at the output above, it was confirmed that the optimizer was giving the two indexes the same exact cost: 5.81. The index on EXPIRE_DATE was selected (chosen: 1) and the other one discarded (cause: "cost").
To me, this looked quite like an arbitrary decision to pick the first index of the set, all costs being equal.
Makes sense?
Well, it does, if cost is really identical, but in my case, I think the optimizer should have considered that one of the two indexes is a partitioning key, and pick the other.
I said this was an arbitrary decision.
But, the same decision could have been: pick the *last* index of the set, instead of the first.
Hey, let's patch the source code, I thought. But then an idea came to my mind. What if the order of appearance of the indexes in the table was reversed? After all, the code is simply scanning all available indexes, one at a time, evaluating them and assigning each of them a cost.
I quickly created another identical table, but with the order of the two indexes reversed:
CREATE TABLE `BIG_STORAGE_INNO` (
`HASH_ID` char(64) NOT NULL,
`SERIALIZATION_TYPE` enum('JAVA','PROTOSTUFF') NOT NULL,
`COMPRESSION_TYPE` enum('DEFLATE','LZ4','NONE','GZIP') NOT NULL,
`RAW_DATA` mediumblob NOT NULL,
`LAST_UPDATE` datetime NOT NULL,
`EXPIRE_DATE` datetime NOT NULL,
KEY `HASH_ID_IX` (`HASH_ID`),
KEY `EXPIRE_DATE_IX` (`EXPIRE_DATE`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
/*!50100 PARTITION BY RANGE (TO_DAYS(EXPIRE_DATE))
(PARTITION p654 VALUES LESS THAN (736741) ENGINE = InnoDB,
PARTITION p655 VALUES LESS THAN (736742) ENGINE = InnoDB,
PARTITION p656 VALUES LESS THAN (736743) ENGINE = InnoDB,
PARTITION p657 VALUES LESS THAN (736744) ENGINE = InnoDB,
PARTITION p658 VALUES LESS THAN (736745) ENGINE = InnoDB,
[ .... ]
You can see that the order of the columns is the same as before, but the order of the two indexes is reversed - now HASH_ID_IX comes first.
Guess what happens now in the optimizer?
opt_range.cc: 264: get_key_scans_params: opt: range_scan_alternatives: starting struct
opt_range.cc: 264: get_key_scans_params: opt: (null): starting struct
opt_range.cc: 299: get_key_scans_params: opt: index: "HASH_ID_IX"
opt_range.cc: 9541: check_quick_select: exit: Records: 4
opt_range.cc: 264: get_key_scans_params: opt: ranges: starting struct
opt_range.cc: 299: get_key_scans_params: opt: (null): "925445622fe6c9da0a432b3e686b807557c215f04233b59a94b7eff1cb6ef3d9 <= HASH_ID <= 925445622fe6c9da0a432b3e686b807557c215f04233b59a94b7eff1cb6ef3d9"
opt_range.cc: 279: get_key_scans_params: opt: ranges: ending struct
opt_range.cc: 313: get_key_scans_params: opt: index_dives_for_eq_ranges: 1
opt_range.cc: 313: get_key_scans_params: opt: rowid_ordered: 1
opt_range.cc: 313: get_key_scans_params: opt: using_mrr: 0
opt_range.cc: 313: get_key_scans_params: opt: index_only: 0
opt_range.cc: 336: get_key_scans_params: opt: rows: 4
opt_range.cc: 347: get_key_scans_params: opt: cost: 5.81
opt_range.cc: 313: get_key_scans_params: opt: chosen: 1
opt_range.cc: 279: get_key_scans_params: opt: (null): ending struct
opt_range.cc: 264: get_key_scans_params: opt: (null): starting struct
opt_range.cc: 299: get_key_scans_params: opt: index: "EXPIRE_DATE_IX"
opt_range.cc: 9541: check_quick_select: exit: Records: 4
opt_range.cc: 264: get_key_scans_params: opt: ranges: starting struct
opt_range.cc: 299: get_key_scans_params: opt: (null): "2017-02-22 18:20:05 < EXPIRE_DATE"
opt_range.cc: 279: get_key_scans_params: opt: ranges: ending struct
opt_range.cc: 313: get_key_scans_params: opt: index_dives_for_eq_ranges: 1
opt_range.cc: 313: get_key_scans_params: opt: rowid_ordered: 0
opt_range.cc: 313: get_key_scans_params: opt: using_mrr: 0
opt_range.cc: 313: get_key_scans_params: opt: index_only: 0
opt_range.cc: 336: get_key_scans_params: opt: rows: 4
opt_range.cc: 347: get_key_scans_params: opt: cost: 5.81
opt_range.cc: 313: get_key_scans_params: opt: chosen: 0
opt_range.cc: 299: get_key_scans_params: opt: cause: "cost"
opt_range.cc: 279: get_key_scans_params: opt: (null): ending struct
opt_range.cc: 13830: print_sel_tree: info: SEL_TREE: 0x7f7da4095838 (ROR scans) scans: HASH_ID_IX
opt_range.cc: 5676: get_key_scans_params: info: Returning range plan for key HASH_ID_IX, cost 5.81, records 4
Bingo!! The right index comes first now, and is chosen in every situation.
My problem is solved - well, I call this a workaround, although an interesting one.
I have raised a bug on the subject, I really believe that in this situation, the fact that one of the two identically expensive indexes is a partitioning key should be taken into account.
For sure this has been an amusing troubleshooting... and an interesting finding!
ADDENDUM:
I have just discovered that the order of appearance of indexes also matters (a lot) when using row based replication and tables without a primary key!
The sql thread will use the first index in order of appearance to scan the table if it needs to search rows for an update .... this may result in very long time spent searching for rows if the first index in the table is not very selective. You can spot that because the slave thread will spend lot of time in Update_rows_log_event::find_row state for each update statement on the table without the PK.
The fix is to make sure a very selective index appears first in the table. This may be a life saver (it was in my case!)
Hope this helps!
Rick
I have just discovered that the order of appearance of indexes also matters (a lot) when using row based replication and tables without a primary key!
The sql thread will use the first index in order of appearance to scan the table if it needs to search rows for an update .... this may result in very long time spent searching for rows if the first index in the table is not very selective. You can spot that because the slave thread will spend lot of time in Update_rows_log_event::find_row state for each update statement on the table without the PK.
The fix is to make sure a very selective index appears first in the table. This may be a life saver (it was in my case!)
Hope this helps!
Rick
Tuesday, September 27, 2016
If enforcing data retention via DROP PARTITION, better watch your auto_increment counter
One of the main tasks that any DBA has to deal with is certainly data retention. Especially when your developers like to store logging and tracking information inside the database. This is not something that I would recommend (don't try it at home!) but when you're late to the party (read: someone had taken this path before you joined) , then you'll have to live with it.
Data retention in MySQL is usually applied by partitioning the table by RANGE on a selected column, usually containing the row creation timestamp. New partitions are created in advance for the coming days or weeks or months, and a script is used that will run nightly and drop partitions that are older than a set retention.
There are many tools that can be used to automate data retention; I have chosen the excellent pdb-parted, a nice and cozy perl script that you can find in the PalominoDB repository on GitHub (since PalominoDB is no longer in existence, I have forked the repository under my own account, you can also get the script here).
It goes without saying that when I first joined, although there were many tables already containing millions of rows of logging, statistical and tracking data, there were no pruners in effect, i.e. no retention at all was applied. Over the next weeks and months I managed to get the required information from the various dev teams and started pruning the bigger tables. After a couple of months of strict diet, I had recovered about 250 GB of space out of our main cluster, or about 30% of total dataset size at that time. Not bad! Backup and restore times improved, and also their footprint was much smaller.
Over the next several months the company kept growing very fast. A DWH Team was set up and they started ingesting data from MySQL inside their own systems (Vertica based). Some of the data that was ingested was coming from these pruned tables.
Everything went well for some time, then I received a visit from one of the DWH guys with a strange issue: they were seeing duplicate keys in the data ingested from one of the partitioned tables. I knew that, in theory, this should not be possible, as each and every table of that type has an auto increment primary key, and we had no reports of duplicate keys in production when data was first inserted.
At first I tried to blame the ETLs that they use to import data from MySQL to Vertica, but soon I realised that the table that they were having the issue with had a notable difference: it was really small in size, only few hundreds rows, while all other tables contained hundreds of millions. So I asked them for the MAX value of the auto increment key they had in their systems, and to my surprise, it was higher than the MAX value we had in MySQL!!! D'oh.
After some investigation it turned out that this particular table was only used for debugging, therefore the application was not writing to it unless the debug mode was activated. Usually, debug was only turned on for small amount of time (e.g. hours) and then turned off again, and the saved data was used for troubleshooting.
Anyways, it was clear that somehow, the auto_increment in this table was getting reset at some point; it was the only possibility for having an higher value in DWH than what we had in MySQL, as DWH was basically ingesting our rows directly, without any modification whatsoever.
It was easy enough for me to point the finger here, as the only statement that was issued against this table, apart from INSERT, was.... DROP PARTITION.
You will object that DROP PARTITION does not reset the auto_increment counter of a table. Of course I agree, but there was no other possibility here, so it had to be some kind of bug.
It was easy enough to reproduce the behaviour and to confirm that, if the partition you are dropping leaves no other rows in the table, the auto_increment counter is, in fact, reset to 1. You can find the steps in the bug I have submitted to Oracle (please see below).
In my opinion, the behaviour of auto_increment in this situation is not consistent:
Data retention in MySQL is usually applied by partitioning the table by RANGE on a selected column, usually containing the row creation timestamp. New partitions are created in advance for the coming days or weeks or months, and a script is used that will run nightly and drop partitions that are older than a set retention.
There are many tools that can be used to automate data retention; I have chosen the excellent pdb-parted, a nice and cozy perl script that you can find in the PalominoDB repository on GitHub (since PalominoDB is no longer in existence, I have forked the repository under my own account, you can also get the script here).
It goes without saying that when I first joined, although there were many tables already containing millions of rows of logging, statistical and tracking data, there were no pruners in effect, i.e. no retention at all was applied. Over the next weeks and months I managed to get the required information from the various dev teams and started pruning the bigger tables. After a couple of months of strict diet, I had recovered about 250 GB of space out of our main cluster, or about 30% of total dataset size at that time. Not bad! Backup and restore times improved, and also their footprint was much smaller.
Over the next several months the company kept growing very fast. A DWH Team was set up and they started ingesting data from MySQL inside their own systems (Vertica based). Some of the data that was ingested was coming from these pruned tables.
Everything went well for some time, then I received a visit from one of the DWH guys with a strange issue: they were seeing duplicate keys in the data ingested from one of the partitioned tables. I knew that, in theory, this should not be possible, as each and every table of that type has an auto increment primary key, and we had no reports of duplicate keys in production when data was first inserted.
At first I tried to blame the ETLs that they use to import data from MySQL to Vertica, but soon I realised that the table that they were having the issue with had a notable difference: it was really small in size, only few hundreds rows, while all other tables contained hundreds of millions. So I asked them for the MAX value of the auto increment key they had in their systems, and to my surprise, it was higher than the MAX value we had in MySQL!!! D'oh.
After some investigation it turned out that this particular table was only used for debugging, therefore the application was not writing to it unless the debug mode was activated. Usually, debug was only turned on for small amount of time (e.g. hours) and then turned off again, and the saved data was used for troubleshooting.
Anyways, it was clear that somehow, the auto_increment in this table was getting reset at some point; it was the only possibility for having an higher value in DWH than what we had in MySQL, as DWH was basically ingesting our rows directly, without any modification whatsoever.
It was easy enough for me to point the finger here, as the only statement that was issued against this table, apart from INSERT, was.... DROP PARTITION.
You will object that DROP PARTITION does not reset the auto_increment counter of a table. Of course I agree, but there was no other possibility here, so it had to be some kind of bug.
It was easy enough to reproduce the behaviour and to confirm that, if the partition you are dropping leaves no other rows in the table, the auto_increment counter is, in fact, reset to 1. You can find the steps in the bug I have submitted to Oracle (please see below).
In my opinion, the behaviour of auto_increment in this situation is not consistent:
- TRUNCATE TABLE (DDL) empties the table, counter is reset => OK
- DELETE * FROM TABLE (DML) empties the table, counter is NOT reset => OK
- DROP PARTITION (DDL) does NOT empty the table, merely drops some data - counter is reset or not depending on content of other partitions => WEIRD
I really feel that the auto_increment should be left alone when DROP PARTITION is issued, regardless what the content of the other partitions is.
I invite you to check the bug content for some discussion that I had with the Oracle bugs team, where they tried to convince me that this behaviour was correct at first, then acknowledged, but there was not much activity after that.
Monday, September 26, 2016
SHOW WARNINGS can stall your MySQL server if compressed protocol is used
If there is something that I love about information technology is the incredible amount of unpredictable ways that bugs can bite you where and when you less expect it.
This one I'm about to describe is a serious one, yet there has been a very quiet response from Percona bug team at Launchpad, where I first submitted it (because we use Percona server here). If you have read my other posts you know by now that I am a laz... err, busy guy so bear with me if only today I have verified that this is, in fact, an upstream bug and have raised a bug to Oracle too.
It affects 5.6.33, latest version at the time of this article.
Anyways, the bug started to manifest itself in the form of complete stalls of our main cluster, which was happening at different times and apparently in a way that was unrelated to the amount of traffic hitting the master. When stalling, system CPU time was topping 100% of total available CPUs, no wonder that the server was unresponsive...
Complete stalls are not nice: everything is frozen and you can't even check what's going on, as MySQL would not respond to any command. In some cases this just lasted few seconds, in some other we had stalls which lasted well over 1 minute .
We have the habit to run a script that takes snapshots of "SHOW ENGINE INNODB STATUS" every 10 seconds on each master, and saves them to files; we use these to do post-mortem analysis of incidents that hit the production (traffic spikes and the like). But this time, the snapshots didn't give any useful information: the DB was just running smoothly, low traffic, no sign of anything suspicious. Few second later, total stall. Then smooth again. Weird.
I had to find a way to catch the bug while it was biting. I decided to craft to a small script to enable the general log for 60 seconds, then turn it off again and run pt-query-digest on the generated log, printing a digest of top offenders. Then I waited for the next occurrence, patiently watching sysstat output. As soon as the system CPU time skyrocketed again, I ran my script that captured 60 seconds of full database traffic.
I was really surprised to find that in the sampled time, the top query by number of invocations was SHOW WARNINGS, with about 10,000 invocations per second.
Whoa!!! What the hell is that? Why are production applications executing SHOW WARNINGS in the first place? And not only that, but the amount of them was INSANE!!
Well, it was now obvious that the server stall was actually caused by a flood of SHOW WARNINGS. Not so obvious was the reason for such large amount of this statement to arrive on our master.
Lo and behold, turns out that there is a neat feature in Hibernate, the ORM that we use here, that automatically causes a SHOW WARNINGS to be issued after each and every statement that is executed. To have this enabled it is sufficient to set the Hibernate log level to "warning". D'oh!!!
Of course, Hibernate's setting was the source of all troubles. We asked the devs for a quick fix and after bringing Hibernate's log level back to "error", the problem disappeared.
Still, the behaviour of MySQL when a large amount of SHOW WARNINGS is received was not proper: it would be very easy for anyone to stall any server by just flooding it with these statements, which do not require any special privilege. I felt that this is a serious bug (after all, it has caused downtime in production for us) so I decided to find a way to reproduce it, and to raise a bug as soon as I succeeded.
So I went to our 32 core test server and set up a sysbench test using this simple piece of LUA code:
Then I ran sysbench in order to try and reproduce the stall. The command line I used at first was the following:
This one I'm about to describe is a serious one, yet there has been a very quiet response from Percona bug team at Launchpad, where I first submitted it (because we use Percona server here). If you have read my other posts you know by now that I am a laz... err, busy guy so bear with me if only today I have verified that this is, in fact, an upstream bug and have raised a bug to Oracle too.
It affects 5.6.33, latest version at the time of this article.
Anyways, the bug started to manifest itself in the form of complete stalls of our main cluster, which was happening at different times and apparently in a way that was unrelated to the amount of traffic hitting the master. When stalling, system CPU time was topping 100% of total available CPUs, no wonder that the server was unresponsive...
Complete stalls are not nice: everything is frozen and you can't even check what's going on, as MySQL would not respond to any command. In some cases this just lasted few seconds, in some other we had stalls which lasted well over 1 minute .
We have the habit to run a script that takes snapshots of "SHOW ENGINE INNODB STATUS" every 10 seconds on each master, and saves them to files; we use these to do post-mortem analysis of incidents that hit the production (traffic spikes and the like). But this time, the snapshots didn't give any useful information: the DB was just running smoothly, low traffic, no sign of anything suspicious. Few second later, total stall. Then smooth again. Weird.
I had to find a way to catch the bug while it was biting. I decided to craft to a small script to enable the general log for 60 seconds, then turn it off again and run pt-query-digest on the generated log, printing a digest of top offenders. Then I waited for the next occurrence, patiently watching sysstat output. As soon as the system CPU time skyrocketed again, I ran my script that captured 60 seconds of full database traffic.
I was really surprised to find that in the sampled time, the top query by number of invocations was SHOW WARNINGS, with about 10,000 invocations per second.
Whoa!!! What the hell is that? Why are production applications executing SHOW WARNINGS in the first place? And not only that, but the amount of them was INSANE!!
Well, it was now obvious that the server stall was actually caused by a flood of SHOW WARNINGS. Not so obvious was the reason for such large amount of this statement to arrive on our master.
Lo and behold, turns out that there is a neat feature in Hibernate, the ORM that we use here, that automatically causes a SHOW WARNINGS to be issued after each and every statement that is executed. To have this enabled it is sufficient to set the Hibernate log level to "warning". D'oh!!!
Of course, Hibernate's setting was the source of all troubles. We asked the devs for a quick fix and after bringing Hibernate's log level back to "error", the problem disappeared.
Still, the behaviour of MySQL when a large amount of SHOW WARNINGS is received was not proper: it would be very easy for anyone to stall any server by just flooding it with these statements, which do not require any special privilege. I felt that this is a serious bug (after all, it has caused downtime in production for us) so I decided to find a way to reproduce it, and to raise a bug as soon as I succeeded.
So I went to our 32 core test server and set up a sysbench test using this simple piece of LUA code:
pathtest = string.match(test, "(.*/)") or ""
dofile(pathtest .. "./sysbench/tests/db/common.lua")
function thread_init(thread_id)
set_vars()
end
function event(thread_id)
db_query("SHOW WARNINGS")
end
Then I ran sysbench in order to try and reproduce the stall. The command line I used at first was the following:
./sysbench/sysbench --mysql-host=localhost --mysql-user=sysbench --mysql-password=sbtest --mysql-db=sbtest --test=show_warnings.lua --max_requests=0 --max_time=20 --num-threads=30 --mysql-engine-trx=yes run
To my dismay, this did not produce the desired effect: there was no stall and the server was able to perform approximately 560,000 SHOW WARNINGS per second using 30 cores out of the 32 available on the server (keep in mind I was also running sysbench on the same machine).
This was really head scratching to me, as I was pretty sure that this statement was the root cause of our incidents. So I decided to look at the connection string used by the offending application and noticed that there was link compression enabled. This was a common setup throughout the company, and was in place for historical reasons, to save bandwidth when we were on the old non-gigabit infrastructure.
I decided to add compression to the sysbench test and try again:
./sysbench/sysbench --mysql-host=localhost --mysql-user=sysbench --mysql-password=sbtest --mysql-db=sbtest --test=show_warnings.lua --max_requests=0 --max_time=20 --num-threads=30 --mysql-engine-trx=yes --mysql-compression=on run
Needless to say, that did the trick!! System CPU time skyrocketed and used over 80% of the 32 cores of the test server. Now, this is absolutely weird - what can protocol compression have to do with SHOW WARNINGS? Moreover, throughput fell down from 560k to 48k qps.
I have filed this as a bug on Percona launchpad long ago, but there has been very little activity on it, as you can see.
I feel that this bug is a serious one, as Hibernate is widely used at MySQL based shops, and there may be other ORMs that have similar options that can flood poor MySQL with thousands of SHOW WARNINGS per second.
Since I have verified that this is an upstream bug, today I have filed it on bugs.mysql.com as well, hopefully it will receive the attention it deserves.
Saturday, September 24, 2016
Replication from MySQL 5.6 to MariaDB 10 may fail when using fractional seconds
One of the most interesting features introduced in MariaDB 10 was without doubt multi source replication, or the ability for a slave to have multiple masters.
Tired of hearing complaints from developers who couldn't join tables because they were on different servers, I decided to give MariaDB a try to see if I could leverage this neat feature.
At the time, we had 5 main clusters, classic multi-master active/standby configuration, with some slaves under each of them. I wanted to create a "super slave" that would contain the dataset from all the five clusters, so that developers could connect to it and join at will.
The initial creation of the MariaDB superslave was easy. After installing the binaries, I just bootstrapped it with an xtrabackup copy of our main cluster and set up replication. All went just fine as expected.
Suddendly I realized that I couldn't use xtrabackup to bring the datasets from other masters in the superslave, as the neat xtrabackup tool (which I love) only works at tablespace level. So, armed with lots of patience and time, I imported each dataset into the superslave using mysqldump (which can also save the master position) and after approximately two weeks, I had the superslave running with a impressive (at least for me) 7 TB dataset.
Everything ran just fine for some days, then suddendly I started getting replication errors for queries coming from a particular master. Basically, the slave couldn't find a row that on the master was deleted just fine. My first aid response was to run a pt-table-checksum (yes, you can run that with a multisource slave if you are careful) and I was surprised to find hundreds chunks with differences for that table, considering that we had no failures whatsoever since the superslave was bootstrapped.
I decided to sync the table anyways.
To my surprise, another checksum ran shortly after the sync reported still a large amount of differences. This was really head scratching, especially considering that the problem only appeared on one specific table out of thousands we had imported from the various clusters.
I started to suspect that some odd bug was biting me and this table, for some reason, was the only one triggering it. It was your average small logging table, nothing fancy, but I noticed that there was a datetime column as part of the primary key. In any case, I had no idea what could cause the inconsistency between the master and the slave for this particular table by the look of it.
I decided to peruse the binary logs to see what was passed from master to slave.
At that time, we were using MIXED replication format, which is by the way the default.
Using mysqlbinlog -v I printed out INSERT statements from one of the most recent binary log files from the master, with the intention of comparing the values passed via replication with the actual values as found on the MariaDB slave.
I grepped the mysqlbinlog output for the INSERT statements on this table, and noticed that all of them contained fractional seconds. This is actually quite common with Java ORMs and libraries, they like to pass millisecond information, however I knew that the DATETIME and TIMESTAMP columns in MySQL do not save this information by default: you have to create the column including the desired precision for that, otherwise the fractional part is discarded.
At least, this is what I was believing before actually comparing rows of this table between the master and the slave. ...
Comparing the MySQL master and the MariaDB slave, I found that, for some of the rows, there was a one second difference in the datetime column, which was part of the multicolumn primary key.
That explained the replication errors, as of course, the PK on master and slave were, in fact, different and as a result, delete statements on the master couldn't find the row on the slave!
It was easy enough to correlate this data drift to a different behaviour between MySQL master and MariaDB slave regarding the fractional seconds handling: by looking at some of the rows on both master and slave, it was clear that the difference only existed when the fractional part was greater than 500 - the master was rounding to next second, while the slave was always truncating.
A quick test confirmed the above:
master>insert into test_datetime values ('2015-10-05 10:00:00.501');
Query OK, 1 row affected (0.00 sec)
master>select * from test_datetime;
+---------------------+
| dt |
+---------------------+
| 2015-10-05 10:00:01 |
+---------------------+
1 row in set (0.00 sec)
MariaDB slave [rick]> select * from test_datetime;
+---------------------+
| dt |
+---------------------+
| 2015-10-05 10:00:00 |
+---------------------+
1 row in set (0.00 sec)
D'oh!
I realized then that the problem, although only showing up in this unfortunate situation, was actually much bigger than that, as Java was the main technology used in the Company and I was pretty sure that almost all inserted datetimes and timestamps contained the damn milliseconds!
Some googling suggested that, although MariaDB 10 had some new and neat features backported from 5.7, and almost all the features from 5.6, was actually based on MySQL 5.5 - and the fractional seconds rounding in MySQL was only introduced in 5.6.4.
Anyways, I raised a bug to MariaDB back in 2015 when I first discovered this, as it should at least be clearly documented that replicating from MySQL 5.6 into MariaDB 10 will lead to inconsistent slaves if fractional seconds are used. You can check the status of the bug yourself...
I have actually found a workaround to this bug, which basically consists in switching to ROW based replication. when using ROW based replication the fractional seconds thing is no longer in play, as the binary log will contain the actual row content from the master, instead of a statement to insert the row values.
My experience is based on MariaDB 10.0.21, but judging by the attention that the above bug has received, I believe this may still be valid today.
Of course, it goes without saying that I had to spend 2 weeks running table checksum/table sync on all 7 TB of data in order to bring the superslave back to consistency, after setting replication format to ROW. But it has been running flawlessly since.
Subscribe to:
Posts (Atom)