-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Bug Report: OnlineDDL PK conversion results in table scans #16023
Comments
Thank you, assigned myself to look into it. |
I'd like to clarify a bit. The problem is not with the binlog filter: the filter tells vreplication how to read rows from the source table. We convert the source string to The only scenarios where we don't convert is when both source and target are compatible utf8 flavors or can trivially convert from one to another -- but that's just a side note, let's move on. What needs to change is the way vplayer events are applied to the table. We know vplayer reads all strings as utf8 from the binary log (because MySQL always converts to utf8 in the binary logs). It then needs to un-convert them in the I actually thought that was already done in the past, but I will now take another look. I know for certainty that we do un-convert string values in the |
Logging the statement generated by update _vt_vrp_e0497d27226c11ef89c00a43f95f28a3_20240604122049_ set updated_at=:a_updated_at, created_at=:a_created_at, colstuff=:a_colstuff where id=(convert(:b_id using utf8mb4)) and id2=:b_id2 So it is in |
It defines both what we want from the source and how we want to transform it for the target. It's the latter part that is the issue in this case. Binlog entries are binary, go strings are always unicode/utf8 — perhaps that's what you mean here? My understanding is this... We end up with the update statement, which is a sequence of bytes interpreted in vcopier (go) as a unicode string but then sent over the wire to MySQL as a sequence of bytes again. In MySQL that sequence of bytes is interpreted using the connection charset / character_set_client — and then the individual values converted to the character sets in the table definition for storage as needed. It's not clear to me why we need to explicitly tell MySQL to convert the incoming value to the schema character set. Perhaps we're doing that to deal with any edge cases and any connection charset / character_set_client in use? I'm assuming that there are good reasons and I just don't have them in my mind yet. 🙂
I don't yet see why we need to do any conversions. All strings in go are also unicode/utf8. We do not do any such conversions in VReplication — these are only done when explicitly specified by OnlineDDL today AFAIK (I could of course be wrong). It's certainly possible that there's some VReplication side bug/gap in here, but I don't see that yet. Thanks! |
I argue that this is immaterial. VCopier can do all the conversions it wants, it's just |
Because the target charset is update ... set ... where id=(convert(:b_id using utf8mb4)) and id2=:b_id2 But |
My argument is that MySQL uses schema on write — which includes the character set. Why are we doing the CONVERT in the SQL statement itself?
The vplayer is only doing that because it's been told to in the binlog source definition.
MySQL is only doing that conversion because we're explicitly telling it to. Which is the problem as I see it. 🙂 |
If it's not clear, my main argument is that there's an incoming sequence of bytes defining the SQL statement. MySQL will coerce the individual values in that statement — if possible — from the client/connection charset to the column charset. It's not clear to me why we're trying to explicitly tell MySQL to do anything here when in the end we always want it to do what it should, no? Meaning to convert the value to the charset defined for the column. There may very well be good reason(s), mind you, I'm just trying to understand them. |
@mattlord we read column values from MySQL into golang space. We do that in two ways:
We then have a single logic that applies to those values. When I began writing OnlineDDL for VReplication I found that this logic mostly assumed utf8. The problem is that in (2) you always read utf8 because binary logs. But in (1) you get whatever charset you have on the table. So values were then written to the backend table inconsistently and that was seen in various charset validation test cases for Online DDL, which we originally imported from gh-ost. This is why we now We absolutely have to deal with conversions because the binary log is utf8 irrespective of the real table charset. We furthermore need to overcome the MySQL connection charset transformation (because it does). OK so now that both use utf8, we need to make sure the value is converted back to the real charset in MySQL table columns. We do that correctly when we construct |
OK, issues relating to the connection/client charset ( |
Which of the points? When you read an I can tell you with absolute certainty that form A != form B. You get a different sequence of bytes. Form B, BTW, is correct, because you're reading utf8 into utf8. You now have two different actual binary values, and in both cases you wish to |
@mattlord proof is super easy by the way. Remove the |
Most of the time we're reading ROW events, but sometimes statements. I don't see where the bytes are serialized with a unicode encoding in the MySQL source (I may just be missing it). But that's also immaterial here I think.
OK.
The byte representation for
I still don't understand how we have two different binary values. It's a matter of how those bytes are interpreted at the point of storage or comparison.
I believe that, I'm just trying to understand where the problem really lies and if there's a better way to deal with it today. I'm not disagreeing, I'm merely trying to understand. I'll run a local test that way to try and get a better feel. This may very well end up being considered a real/valid VReplication side bug and I want to understand it better so that I can think about how best to fix it. 🙂 |
However - what I'd like to reexamine is, once we have everything as utf8 in golang space (having read it as utf8 from the original table or from binary log), why do we need to |
Here's an example. I rewrote Jun 04 15:06:26 -21 átesting-binlog átesting-binlog átesting-binlog átesting-binlog átesting-binlog
Jun 04 15:06:26 -22 testátest-binlog testátest-binlog testátest-binlog 🍻😀 átesting-binlog
Jun 04 15:06:26 -23 átesting-bnull átesting-bnull átesting-bnull NULL NULL
Jun 04 15:06:26 +21 átesting-binlog ĂĄtesting-binlog átesting-binlog átesting-binlog átesting-binlog
Jun 04 15:06:26 +22 testátest-binlog testĂĄtest-binlog testátest-binlog 🍻😀 átesting-binlog
Jun 04 15:06:26 +23 átesting-bnull ĂĄtesting-bnull átesting-bnull NULL NULL If I edit
|
I'm not sure what to make of it anymore, this hurts my brain. |
Side note, but maybe more important than this entire discussion: it may be the case that we should reject this type of schema migration, because the source and target table do not share identifiable column values in the PKE. That is, the |
I think you're looking at the results of selecting those rows/values? I'll have to look. What you see on SELECT or generally on "display" is determined by the connection/client charset:
In any event, I'm fine moving this to a VReplication bug and I'll put it in the prioritized queue. I AGREE, it gets very complicated and hairy. That's why I'm trying to look at it with fresh eyes rather than building on determinations/assumptions made years ago (which may of course turn out to be right/true). |
New findings! Consider this table: drop table if exists tbl;
CREATE TABLE `tbl` (
`c1` varchar(12) NOT NULL DEFAULT '',
`c2` varchar(12) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL DEFAULT '',
`ts` datetime DEFAULT current_timestamp(),
UNIQUE KEY `c1c2_uidx` (`c1`, `c2`)
) ENGINE InnoDB,
CHARSET utf8mb4,
COLLATE utf8mb4_unicode_ci;
insert into tbl (c1, c2) values (left(sha1(rand()),12), left(sha1(rand()),12));
insert into tbl (c1, c2) select left(sha1(rand()),12), left(sha1(rand()),12) from tbl;
insert into tbl (c1, c2) select left(sha1(rand()),12), left(sha1(rand()),12) from tbl;
insert into tbl (c1, c2) select left(sha1(rand()),12), left(sha1(rand()),12) from tbl;
insert into tbl (c1, c2) select left(sha1(rand()),12), left(sha1(rand()),12) from tbl;
insert into tbl (c1, c2) select left(sha1(rand()),12), left(sha1(rand()),12) from tbl;
insert into tbl (c1, c2) select left(sha1(rand()),12), left(sha1(rand()),12) from tbl;
insert into tbl (c1, c2) select left(sha1(rand()),12), left(sha1(rand()),12) from tbl;
insert into tbl (c1, c2) select left(sha1(rand()),12), left(sha1(rand()),12) from tbl;
insert into tbl (c1, c2) select left(sha1(rand()),12), left(sha1(rand()),12) from tbl;
insert into tbl (c1, c2) select left(sha1(rand()),12), left(sha1(rand()),12) from tbl;
insert into tbl (c1, c2) select left(sha1(rand()),12), left(sha1(rand()),12) from tbl;
insert into tbl (c1, c2) select left(sha1(rand()),12), left(sha1(rand()),12) from tbl;
insert into tbl (c1, c2) select left(sha1(rand()),12), left(sha1(rand()),12) from tbl;
insert into tbl (c1, c2) select left(sha1(rand()),12), left(sha1(rand()),12) from tbl;
insert into tbl (c1, c2) select left(sha1(rand()),12), left(sha1(rand()),12) from tbl;
insert into tbl (c1, c2) select left(sha1(rand()),12), left(sha1(rand()),12) from tbl;
insert into tbl (c1, c2) select left(sha1(rand()),12), left(sha1(rand()),12) from tbl;
insert into tbl (c1, c2) select left(sha1(rand()),12), left(sha1(rand()),12) from tbl;
insert into tbl (c1, c2) select left(sha1(rand()),12), left(sha1(rand()),12) from tbl;
insert into tbl (c1, c2) select left(sha1(rand()),12), left(sha1(rand()),12) from tbl; Because select /*+ MAX_EXECUTION_TIME(3600000) */ c1, convert(c2 using utf8mb4) as c2, ts from tbl force index (`c1c2_uidx`) order by c1, c2; Let's mysql> explain select /*+ MAX_EXECUTION_TIME(3600000) */ c1, convert(c2 using utf8mb4) as c2, ts from tbl force index (`c1c2_uidx`) order by c1, c2;
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+----------------+
| 1 | SIMPLE | tbl | NULL | ALL | NULL | NULL | NULL | NULL | 1910748 | 100.00 | Using filesort |
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+----------------+
But, why is this query tanking? Didn't we ask it to This is where the fun starts. The problem is that we've aliased mysql> explain select /*+ MAX_EXECUTION_TIME(3600000) */ c1, convert(c2 using utf8mb4), ts from tbl force index (`c1c2_uidx`) order by c1, c2;
+----+-------------+-------+------------+-------+---------------+-----------+---------+------+--------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+-----------+---------+------+--------+----------+-------+
| 1 | SIMPLE | tbl | NULL | index | NULL | c1c2_uidx | 64 | NULL | 950012 | 100.00 | NULL |
+----+-------------+-------+------------+-------+---------------+-----------+---------+------+--------+----------+-------+ So, in the aliased variation, However, that aliasing is really useful, as it hides away the complexity of the conversion. I was thinking we could alias into something else, to then re-alias in code. However, I realized this is easily solved like so: mysql> explain select /*+ MAX_EXECUTION_TIME(3600000) */ c1, convert(c2 using utf8mb4) as c2, ts from tbl force index (`c1c2_uidx`) order by tbl.c1, tbl.c2;
+----+-------------+-------+------------+-------+---------------+-----------+---------+------+---------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+-----------+---------+------+---------+----------+-------+
| 1 | SIMPLE | tbl | NULL | index | NULL | c1c2_uidx | 64 | NULL | 1910748 | 100.00 | NULL |
+----+-------------+-------+------------+-------+---------------+-----------+---------+------+---------+----------+-------+ Qualifying the columns with the table name un-aliases them and uses their original values, and MySQL is then happy to use the index. Which means the fix is to merely qualify the sorting columns with the table name. WDYT? |
To be clear, We could qualify the table name in the |
Agreed. It is relevant to the grand discussion in this issue, but it's not as relevant to the specific solution proposed in #16023 (comment).
This circles back to the greater discussion, and we will need to understand the entire charset/collation issue better.
I'm not sure I follow this specific example, but would like to keep focusing for now on the solution proposed in #16023 (comment).
ASCII is truly the exception to the charset issue, because ASCII is contained within UTF8 and doesn't actually need conversion. So for the purpose of this discussion let's assume a
That is correct. Bummer. Back to the drawing board. |
We're having an internal discussion around this issue and have made some progress. Some highlights:
|
Overview of the Issue
When changing the character set and/or collation of a PK column, OnlineDDL creates a VReplication binlog filter which uses the from (original) charset rather than the to charset. This results in not being able to use the primary key on the shadow table when applying replicated events and can cause vplayer stalls (see #15974).
Reproduction Steps
Example output:
Binary Version
❯ vtgate --version vtgate version Version: 20.0.0-SNAPSHOT (Git revision c666b1442b216413bd36f31926489cc0581af511 branch 'vplayer_batch_trx_timeout') built on Wed May 29 10:36:27 EDT 2024 by matt@pslord.local using go1.22.3 darwin/arm64
Operating System and Environment details
Log Fragments
The text was updated successfully, but these errors were encountered: