Oct
04
2023
--

What’s New in Percona Toolkit 3.5.5

percona toolkit 3.5.5

Percona Toolkit 3.5.5 was released on October 3, 2023. This blog post covers the main changes in this release.

pt-online-schema-change improvements

After nibble hook

This was contributed by Marcelo HP Ferreira at https://github.com/percona/percona-toolkit/pull/645. The feature allows you to define a custom plugin that will be triggered after copying each nibble.

Here are a few examples of what you can print with this hook:

  • Copy rate
  • Number of rows per nibble
  • Run time per nibble
  • Chunk size when using dynamic chunks

You can find an example plugin in the sample file
t/ptonlineschemachange/samples/plugins/on_copy_rows_after_nibble.pm

Using option –skip-check-slave-lag multiple times

ptonlineschemachange  always supported multiple invocations of option
skipcheckslavelag  but practically used only the first one; other replicas are still checked for the lag as reported at PT-2241. Contribution by PinoCao (PR-622) fixes this issue. Now, this option works as expected. You can specify multiple replicas for which you do not want to check lag.

No failure on replica disconnect

This is one more feature that was broken before. As described in the user manual for the
maxlag  option:

The tool waits forever for replicas to stop lagging. If any replica is stopped, the tool waits forever until the replica is started. The data copy continues when all replicas are running and not lagging too much.

However, this was not the case. In case of replica disconnect,
ptonlineschemachange  just stopped working. This was especially unfortunate if this happened after a few hours of data copying. This behavior was reported at PT-2168 and PT-2096

The reason why this feature did not work lies in a few places. One is a debugging code for bug fix PT-1760 that was not removed. As a result, the tool died in case of replica disconnect instead of handling the error.

But even after removing this debugging code, there exist multiple places in the code where the tool can die. Fix for PT-2168 adds checks for these situations and, depending on the option
failonstoppedreplication , either dies or continues waiting when the replica is available again. Now,
ptonlineschemachange  should die less frequently.

ptonlineschemachange  was failing at replica loss for many years, and users may rely on this behavior. Therefore, the option
failonstoppedreplication  is enabled by default. This means that you need to disable it (
nofailonstoppedreplication ) if you want
ptonlineschemachange  to continue copying data after one of the replicas is down.

ptonlineschemachange  also updates the replica list while waiting. If you use option
recursionmethod , set to processlist or hosts, this happens automatically. If you use
recursionmethod=dsn , you need to update the DSN table manually, so the tool re-reads it and updates the list of replicas it monitors.

K8 support

Percona Toolkit is shipped with the
ptk8sdebugcollector  tool that collects diagnostic data from Percona Kubernetes operators. It collects system information together with the output of
ptmysqlsummary ,
ptmongodbsummary , or
pg_gather  tools, depending on the operator. In version 3.5.5, support for PostgreSQL operator version two has been added. We are planning to add new features to this tool for every release. It will become a Swiss army knife for Percona Kubernetes Operators troubleshooting.

Quality improvements

Perl tools in Percona Toolkit come with a regression test suite that uses its own module that fires up MySQL sandbox (
lib/Sandbox.pm ) and extended Perl module
Test::More  (
lib/PerconaTest.pm ). Unfortunately, in the last few years, not all tests were stable, and the suite was not run at each code change. After noticing this, we started working on fixing existing tests so they do not fail due to improvements in new MySQL versions or environmental issues. Fix for PT-2156: Fix tests for lib finishes this effort. We now run tests locally before releasing the product. We plan to make regression tests part of the release process starting from the next version.

Another move in this direction is standardizing the output of the common options, such as format of the option
version . This is done as a bug fix for PT-2235: pt-mongodb-index-check does not support option –version and PT-2236: pt-secure-collect, pt-pg-summary do not follow PT standard for option –version.

Percona Toolkit also fixed typos and trailing whitespace issues in this release. Viktor Szépe contributed to these changes. He also updated our GitHub workflows, so such issues should not show up in the future.

Quality control is not only about code fixes but also about testing. In the latest releases, we get CVE reports for Go tools from Kushal Haldar. They include more vulnerabilities than Open Source tools that we use, can find, and help us improve the quality of Go-based tools.

Community contributions

Percona Toolkit continuously receives contributions. In the latest releases, we try to accept as many contributions as possible. At the same time, we started requiring minimal quality control, such as adding test cases and running regression tests before accepting the patch. There are still a few pull requests waiting for final resolution, but we hope we will process all of them by the end of the year.

For this release, we want to thank:

  • Kushal Haldar for his reports about vulnerabilities in Go-based tools
  • Viktor Szépe for pull requests, improving the quality of our code
  • Marcelo HP Ferreira for after nibble hook to
    ptonlineschemachange
  • PinoCao for
    skipcheckslavelag  bug fix

Percona Toolkit is a collection of advanced open source command-line tools, developed and used by the Percona technical staff, that are engineered to perform a variety of MySQL, MariaDB, MongoDB, and PostgreSQL server and system tasks that are too difficult or complex to perform manually.

 

Download Percona Toolkit

Apr
12
2023
--

Don’t Auto pt-online-schema-change for Tables With Foreign Keys

pt-online-schema-change

During the early stages of my career, I was captivated by the theories and concepts surrounding foreign keys and how they empowered us to maintain data integrity. However, in practical application, I have found them to be quite challenging to manage. I am sure you’re a champion DBA, but I keep my distance from foreign keys.

With that short story as a background, this post aims to address the dilemma that arises when utilizing the pt-online-schema-change tool on tables that contain foreign keys.

We already know what one of the most used Percona tools pt-online-schema-change is and how pt-online-schema-change handles foreign keys.

When utilizing the pt-online-schema-change tool to alter a table, such as renaming the original table to be replaced as a new one, it’s important to consider how foreign keys are affected. The tool offers two options, rebuild_constraints, and drop_swap, to ensure that foreign keys continue to reference the correct table during the alteration process. Here are the high-level steps involved:

  1. Drop and re-add foreign key constraints (rebuild_constraints)
  • Rename Current Table T1 to T1_old (FKs are pointing to T1_old now)
  • Rename New table with changes, _T1_new to T1 (T1 doesn’t have any tables referencing it)
  • ALTER on child tables to adjust the foreign key to point to T1 rather than T1_old.
  1. Drop the original table and rename the new table as original (drop_swap)
  • Disable foreign key checks for the session (FOREIGN_KEY_CHECKS=0)
  • Drop the original table
  • Rename the new table _T1_new to T1

It’s worth noting that when using the “drop_swap” option, toward the end of the process, the child table’s foreign key will be pointing to the updated table. However, this approach carries some risks due to the following facts:

  • For a short time between dropping the original table and renaming the temporary table, the table to be altered simply does not exist.
  • In the event of an error during the rename operation, rollback is impossible, as the original table has already been dropped. This can result in data loss or other unintended consequences.

The pt-online-schema-change tool also provides a third option, “auto.” When selecting “auto,” the tool makes a decision between “rebuild_constraints” and “drop_swap” based on the size of the child table involved in the alteration process. This allows for flexibility in choosing the appropriate option based on the specific scenario and the size of the affected child table.

Here, pt-online-schema-change knows the time taken for the row copy operation from the old table to the new table. It can estimate if the child table can be altered within –chunk-time (that’s considered fast). If child table ALTERs are fast, pt-online-schema-change uses rebuild_constraints; otherwise, use the drop_swap approach.

To error is human, and hence I could be wrong. Luckily the Percona tools are open source. I encourage you to review the source at the pt-online-schema-change’s git. Following is the code block that describes the “auto” behavior and then calls the determine_alter_fk_method.

# XXX Auto-choose the alter fk method BEFORE swapping/renaming tables
# else everything will break because if drop_swap is chosen, then we
# most NOT rename tables or drop the old table.
if ( $alter_fk_method eq 'auto' ) {
# If chunk time is set, then use the average rate of rows/s
# from copying the orig table to determine the max size of
# a child table that can be altered within one chunk time.
# The limit is a fudge factor. Chunk time won't be set if
# the user specified --chunk-size=N on the cmd line, in which
# case the max child table size is their specified chunk size
# times the fudge factor.
my $max_rows = $o->get('dry-run') ? $o->get('chunk-size') * $limit
: $chunk_time && $avg_rate ? $avg_rate * $chunk_time * $limit
: $o->get('chunk-size') * $limit;
PTDEBUG && _d('Max allowed child table size:', $max_rows);

$alter_fk_method = determine_alter_fk_method(
child_tables => $child_tables,
max_rows => $max_rows,
Cxn => $cxn,
OptionParser => $o,
);

Now, the tool will use drop_swap for the child tables which are larger in data size or a number of rows. 

I want to bring something important to your attention, and that is rebuilding the constraints will be slower for large tables unless it is done “ONLINE.” The online operation will not really need the data copy, and it will be faster.

The documentation reads:

    The INPLACE algorithm is supported when foreign_key_checks is disabled. Otherwise, only the COPY algorithm is supported.

This has already been identified and improved in the pt-online-schema-change version 3.3.1, as noted in the bug report https://jira.percona.com/browse/PT-1327

Thus, in my opinion, while using pt-online-schema-change, it is both safe and efficient to use “rebuild_constraints” as long as the tool is version 3.3.1 or higher. Avoid using “drop_swap,” and surely it makes less sense to use “auto.” Don’t “auto” the pt-online-schema-change for tables with foreign keys; understand the options and take control in your hands. That said, it is really easy to upgrade to the latest version of Percona Toolkit.

Percona Toolkit

Dec
12
2022
--

Using Percona Toolkit to Alter Database Tables Online: A Controlled Approach

Percona Toolkit to Alter Database Tables Online

Percona Toolkit to Alter Database Tables OnlineTable modifications are a common task for database administrators. In this blog, I’ll explain how to alter tables online in a controlled manner that does not disrupt application users or cause application downtime.

One of the tools in Percona Toolkit is pt-online-schema-change, a utility that alters the structure of a table without interfering with the reads or writes. The tool creates an empty copy of the table to alter and modify as desired before copying the rows from the original table into the new one.

When the copying is finished, it removes the original table and replaces it with the new one. Any changes made to data in the original tables during the copy process will be reflected in the new table as the tool creates triggers on the original table to update the corresponding rows in the new table.

How to test the pt-online-schema-change command?

Before running the actual alter using the tool, perform a dry run to ensure the pt-online-schema-change command is functional. The –dry-run option creates and modifies the new table without adding triggers, copying data, or replacing the existing table.

The basic command for modifying a table is as follows, which may need to be tweaked as needed using the variables like –critical-load threads_running –max-load Threads_running –chunk-size –max-lag, –max-flow-ctl (Percona XtraDB Cluster) and so on running in a production environment.

I’m using “ENGINE=InnoDB” for the first test case, which rebuilds the table; this is useful for removing fragmented spaces from the table.

Dry-run test:

$ pt-online-schema-change --dry-run --alter "ENGINE=InnoDB" h=172.31.92.72,D=mytestdb,t=authors;
Operation, tries, wait:
  analyze_table, 10, 1
  copy_rows, 10, 0.25
  create_triggers, 10, 1
  drop_triggers, 10, 1
  swap_tables, 10, 1
  update_foreign_keys, 10, 1
Starting a dry run.  `mytestdb`.`authors` will not be altered.  Specify --execute instead of --dry-run to alter the table.
Creating new table...
Created new table mytestdb._authors_new OK.
Altering new table...
Altered `mytestdb`.`_authors_new` OK.
Not creating triggers because this is a dry run.
Not copying rows because this is a dry run.
Not swapping tables because this is a dry run.
Not dropping old table because this is a dry run.
Not dropping triggers because this is a dry run.
2022-12-09T05:44:23 Dropping new table...
2022-12-09T05:44:23 Dropped new table OK.
Dry run complete.  `mytestdb`.`authors` was not altered.
$

How to run the ALTER TABLE?

It is recommended that you read the documentation before performing the task.

To run the alter, replace the –dry-run option with –execute.

$ pt-online-schema-change --execute --alter "ENGINE=InnoDB" h=172.31.92.72,D=mytestdb,t=authors;
Found 1 slaves:
ip-172-31-90-216.ec2.internal -> ip-172-31-90-216.ec2.internal:socket
Will check slave lag on:
ip-172-31-90-216.ec2.internal -> ip-172-31-90-216.ec2.internal:socket
..
Operation, tries, wait:
  analyze_table, 10, 1
  copy_rows, 10, 0.25
  create_triggers, 10, 1
  drop_triggers, 10, 1
  swap_tables, 10, 1
  update_foreign_keys, 10, 1
Altering `mytestdb`.`authors`...
Creating new table...
Created new table mytestdb._authors_new OK.
Altering new table...
Altered `mytestdb`.`_authors_new` OK.
2022-12-09T05:57:10 Creating triggers...
2022-12-09T05:57:10 Created triggers OK.
2022-12-09T05:57:10 Copying approximately 10023 rows...
2022-12-09T05:57:10 Copied rows OK.
2022-12-09T05:57:10 Analyzing new table...
2022-12-09T05:57:10 Swapping tables...
2022-12-09T05:57:10 Swapped original and new tables OK.
2022-12-09T05:57:10 Dropping old table...
2022-12-09T05:57:10 Dropped old table `mytestdb`.`_authors_old` OK.
2022-12-09T05:57:10 Dropping triggers...
2022-12-09T05:57:10 Dropped triggers OK.
Successfully altered `mytestdb`.`authors`.
$

Can we pause the pt-online-schema-change execution? Yes!

The –pause-file=/tmp/pt-osc.pause option helps you to pause the execution. While the file specified by this parameter is present, execution will be paused and resumed when it is removed.

Note: I shortened the pt-osc log to make the result more readable.

$ pt-online-schema-change --pause-file=/tmp/pt-osc.pause --execute --alter "ENGINE=InnoDB" h=172.31.92.72,D=mytestdb,t=authors;
….
2022-12-10T15:42:01 Copying approximately 10023 rows...
Sleeping 60 seconds because /tmp/pt-osc.pause exists
Sleeping 60 seconds because /tmp/pt-osc.pause exists
Copying `mytestdb`.`authors`:  73% 00:44 remain
2022-12-10T15:44:04 Copied rows OK.
...
Successfully altered `mytestdb`.`authors`.

Can we review the data and tables before swapping them? Yes!

The —no-swap-tables —no-drop-old-table —no-drop-new-table —no-drop-triggers options allow us to do the alter in a controlled manner. 

That is, we will let tools handle the majority of the tasks, such as creating the new table, altering, copying the records, and the remaining table swapping and trigger dropping will be done manually.

Caution:  The —no-swap-tables option does not work if the table has foreign keys with child tables associated.

$ pt-online-schema-change --no-swap-tables --no-drop-triggers --no-drop-old-table --no-drop-new-table --execute --alter "CHARACTER SET = utf8mb4, COLLATE = utf8mb4_general_ci, MODIFY email varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,MODIFY name  varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL" h=172.31.92.72,D=mytestdb,t=authors2;
Found 1 slaves:
ip-172-31-90-216.ec2.internal -> ip-172-31-90-216.ec2.internal:socket
Will check slave lag on:
ip-172-31-90-216.ec2.internal -> ip-172-31-90-216.ec2.internal:socket
Operation, tries, wait:
  analyze_table, 10, 1
  copy_rows, 10, 0.25
  create_triggers, 10, 1
  drop_triggers, 10, 1
  swap_tables, 10, 1
  update_foreign_keys, 10, 1
Altering `mytestdb`.`authors2`...
Creating new table...
Created new table mytestdb._authors2_new OK.
Altering new table...
Altered `mytestdb`.`_authors2_new` OK.
2022-12-09T09:16:28 Creating triggers...
2022-12-09T09:16:28 Created triggers OK.
2022-12-09T09:16:28 Copying approximately 10067 rows...
2022-12-09T09:16:29 Copied rows OK.
Not dropping old table because --no-drop-triggers was specified.
Not dropping triggers because --no-drop-triggers was specified.  To drop the triggers, execute:
DROP TRIGGER IF EXISTS `mytestdb`.`pt_osc_mytestdb_authors2_del`
DROP TRIGGER IF EXISTS `mytestdb`.`pt_osc_mytestdb_authors2_upd`
DROP TRIGGER IF EXISTS `mytestdb`.`pt_osc_mytestdb_authors2_ins`
Not dropping the new table `mytestdb`.`_authors2_new` because --no-drop-new-table was specified.  To drop the new table, execute:
DROP TABLE IF EXISTS `mytestdb`.`_authors2_new`;
Successfully altered `mytestdb`.`authors2`.
$

pt-online-schema-change has done the job and we now have two tables and three triggers. So we can safely review the table structure and data in the _authors2_new table, and once we’re sure everything is in order, we can swap and drop the triggers.

mysql> show tables like '%authors2%';
+---------------------------------+
| Tables_in_mytestdb (%authors2%) |
+---------------------------------+
| _authors2_new                   |
| authors2                        |
+---------------------------------+
2 rows in set (0.00 sec)

Find the TRIGGERS:

mysql> SELECT TRIGGER_NAME,EVENT_MANIPULATION FROM INFORMATION_SCHEMA.TRIGGERS WHERE TRIGGER_SCHEMA='mytestdb' and EVENT_OBJECT_TABLE like '%authors%' \G
*************************** 1. row ***************************
      TRIGGER_NAME: pt_osc_mytestdb_authors2_del
EVENT_MANIPULATION: DELETE
*************************** 2. row ***************************
      TRIGGER_NAME: pt_osc_mytestdb_authors2_upd
EVENT_MANIPULATION: UPDATE
*************************** 3. row ***************************
      TRIGGER_NAME: pt_osc_mytestdb_authors2_ins
EVENT_MANIPULATION: INSERT
3 rows in set (0.00 sec)

Run the following SQL to swap the tables and remove the triggers.

RENAME TABLE
mytestdb.authors2 TO mytestdb._authors2_old,
mytestdb._authors2_new TO mytestdb.authors2;

Lastly, remove the triggers and the old table:

DROP TRIGGER IF EXISTS `mytestdb`.`pt_osc_mytestdb_authors2_del`;
DROP TRIGGER IF EXISTS `mytestdb`.`pt_osc_mytestdb_authors2_upd`;
DROP TRIGGER IF EXISTS `mytestdb`.`pt_osc_mytestdb_authors2_ins`;
DROP TABLE IF EXISTS mytestdb._authors2_old;

Wrap up

pt-online-schema-change is a part of the Percona Toolkit for altering tables online, and we can customize it with various options available based on our needs. MySQL’s online DDL with the direct alter is an option, particularly for dropping indexes and changing metadata, among other things. Where online DDL is not a choice, we can use the pt-online-schema-change.

Caution: It is not recommended to run the tool directly in the replica instance as the PT-OSC operations will not produce a consistent table on the replicas. The statements coming via binlog will not be processed by triggers, so whatever new data is coming in via replication, will be missing in the new table.

Nov
23
2022
--

Online DDL Tools and Metadata Locks

Online DDL Tools and Metadata Locks

Online DDL Tools and Metadata LocksOne thing I commonly hear when working with my clients is “I want to change my DDL strategy in order to avoid locking in my database! The last time I used the same old method I ended up in a metadata lock situation!”

I agree that metadata locks can be painful, but unfortunately, it’s completely unavoidable, and changing from one tool to another won’t help with this. That said, it’s still worth it to examine how metadata locks work and what the impact is for each of the common tools and processes. In doing so we will see that all these tools will require metadata locks, but knowing more about how they work and how the use locking can help us determine the right tool for your specific use case.

Any time you make a change to a table a metadata lock is needed to ensure consistency between the table itself and MySQL’s data dictionary. In order for MySQL to establish this lock it has to wait for any query against the table in question to finish, and in turn, any query on the table that occurs after the lock request has to wait for the long-running query and the lock to process before continuing. This will become a lot more apparent in our first example where we’re going to cover the online DDL method using the instant algorithm. I will also illustrate how this works with other tools such as pt-online-schema-change and gh-ost.

Lab setup

In order to demonstrate each technology, the first thing we have to do is set up the environment. I’ve used my VM lab to create a single host using Percona Server for MySQL 8.0. I created a test database and an example table and then created three dummy records.

root@centos7-1 ~]# mysql

mysql> create database test;
Query OK, 1 row affected (0.00 sec)

mysql> use test;
Database changed

mysql> CREATE TABLE `t1` (
    -> `c1` int unsigned NOT NULL AUTO_INCREMENT,
    -> `c2` int,
    -> `c3` int,
    -> PRIMARY KEY (`c1`)
    -> ) ENGINE=InnoDB;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into t1 (c1, c2, c3) values (1, 1, 1);
Query OK, 1 row affected (0.01 sec)

mysql> insert into t1 (c1, c2, c3) values (2, 2, 2);
Query OK, 1 row affected (0.01 sec)

mysql> insert into t1 (c1, c2, c3) values (3, 3, 3);
Query OK, 1 row affected (0.00 sec)

Online DDL, Algorithm=INSTANT

In order to demonstrate how metadata locks work I have several terminals running against my host.

Terminal One

In the first terminal, I have created a long-running query that will prevent the formation of a metadata lock on the table in question.

mysql> select * from t1 where 1=1 and sleep(100);

Terminal Two

In terminal two, I attempted to run the direct alter against MySQL, which hung.

mysql> ALTER TABLE t1 ADD COLUMN c4 TINYINT NOT NULL DEFAULT '1', ALGORITHM=INSTANT;
# HUNG

Terminal three

In terminal three we can see the processlist which shows the offending query and the alter that is seeking, but unable to obtain, the metadata lock.

mysql> show processlist;
+----+-----------------+-----------+------+---------+------+---------------------------------+------------------------------------------------------------------------------+---------+-----------+---------------+
| Id | User            | Host      | db   | Command | Time | State                           | Info                                                                         | Time_ms | Rows_sent | Rows_examined |
+----+-----------------+-----------+------+---------+------+---------------------------------+------------------------------------------------------------------------------+---------+-----------+---------------+
|  5 | event_scheduler | localhost | NULL | Daemon  |   90 | Waiting on empty queue          | NULL                                                                         |   89866 |         0 |             0 |
|  8 | root            | localhost | test | Query   |   30 | User sleep                      | select * from t1 where 1=1 and sleep(100)                                    |   30212 |         0 |             0 |
|  9 | root            | localhost | test | Query   |   12 | Waiting for table metadata lock | ALTER TABLE t1 ADD COLUMN c4 TINYINT NOT NULL DEFAULT '1', ALGORITHM=INSTANT |   11630 |         0 |             0 |
| 10 | root            | localhost | NULL | Query   |    0 | init                            | show processlist                                                             |       0 |         0 |             0 |
+----+-----------------+-----------+------+---------+------+---------------------------------+------------------------------------------------------------------------------+---------+-----------+---------------+
4 rows in set (0.00 sec)

Terminal four

And in terminal four I have created another select statement against the table, but it hangs because it’s queued behind the metadata lock.

mysql> select * from t1;
#HUNG

Terminal one

So now we have fully recreated the issue. In order to resolve this I have gone back into terminal one to kill the offending long-running query.

mysql> select * from t1 where 1=1 and sleep(100);
^C^C -- query aborted
ERROR 1317 (70100): Query execution was interrupted

Terminal two

In terminal two we can see that the alter table completed now that it was able to get the lock it needed.

mysql> ALTER TABLE t1 ADD COLUMN c4 TINYINT NOT NULL DEFAULT '1', ALGORITHM=INSTANT;
Query OK, 0 rows affected (36.13 sec)

Terminal three

In terminal three we can see that the processlist is now clear.

mysql> show processlist;
+----+-----------------+-----------+------+---------+------+------------------------+------------------+---------+-----------+---------------+
| Id | User            | Host      | db   | Command | Time | State                  | Info             | Time_ms | Rows_sent | Rows_examined |
+----+-----------------+-----------+------+---------+------+------------------------+------------------+---------+-----------+---------------+
|  5 | event_scheduler | localhost | NULL | Daemon  |  120 | Waiting on empty queue | NULL             |  119876 |         0 |             0 |
|  8 | root            | localhost | test | Sleep   |   60 |                        | NULL             |   60222 |         0 |             1 |
|  9 | root            | localhost | test | Sleep   |   42 |                        | NULL             |   41640 |         0 |             0 |
| 10 | root            | localhost | NULL | Query   |    0 | init                   | show processlist |       0 |         0 |             0 |
| 11 | root            | localhost | test | Sleep   |   12 |                        | NULL             |   12092 |         3 |             3 |
+----+-----------------+-----------+------+---------+------+------------------------+------------------+---------+-----------+---------------+

Terminal four

And in terminal four we can see the query that was queued behind the lock request was able to complete.

mysql> select * from t1;

+----+------+------+----+
| c1 | c2   | c3   | c4 |
+----+------+------+----+
|  1 |    1 |    1 |  1 |
|  2 |    2 |    2 |  1 |
|  3 |    3 |    3 |  1 |
+----+------+------+----+
3 rows in set (6.58 sec)

As such we can conclude that Online DDL was impacted by a long-running query given that it was unable to get the metadata lock it needed. Once the long-running query was killed the alter table was able to complete.

Before moving on to the next example, I’ll undo the changes in my lab so we have a fresh start.

Terminal one

mysql> ALTER TABLE t1 DROP COLUMN c4;
Query OK, 0 rows affected (0.04 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> show create table t1 \G
*************************** 1. row ***************************
       Table: t1
Create Table: CREATE TABLE `t1` (
  `c1` int unsigned NOT NULL AUTO_INCREMENT,
  `c2` int DEFAULT NULL,
  `c3` int DEFAULT NULL,
  PRIMARY KEY (`c1`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
1 row in set (0.00 sec)

pt-online-schema-change

Now that we know a little more about metadata locks, I’ll simplify the lab output in this example and the gh-ost example to follow in order to keep things a little more brief and concise.

For pt-online-schema change, there are at least four places where metadata locks are found. One for the creation of each of the three associated triggers, and one for the table swap.

Just as the same as before, in terminal one I created the long-running query against the table in question. In terminal two I ran the following pt-online-schema-change command.

Terminal two

[root@centos7-1 ~]# pt-online-schema-change --print --progress time,10 \
> --no-swap-tables --no-drop-new-table --no-drop-triggers \
> --alter="ADD COLUMN c4 TINYINT NOT NULL DEFAULT '1'" \
> --tries create_triggers:10000:1,drop_triggers:10000:1,copy_rows:10000:1 \
> --set-vars tx_isolation=\'READ-COMMITTED\',lock_wait_timeout=30 \
> h=localhost,D=test,t=t1 \
> --chunk-time=1 \
> --critical-load threads_running=99999 --max-load Threads_running=50 \
> --execute
...
Operation, tries, wait:
  analyze_table, 10, 1
  copy_rows, 10000, 1
  create_triggers, 10000, 1
  drop_triggers, 10000, 1
  swap_tables, 10, 1
  update_foreign_keys, 10, 1
Altering `test`.`t1`...
Creating new table...
CREATE TABLE `test`.`_t1_new` (
  `c1` int unsigned NOT NULL AUTO_INCREMENT,
  `c2` int DEFAULT NULL,
  `c3` int DEFAULT NULL,
  PRIMARY KEY (`c1`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
Created new table test._t1_new OK.
Altering new table...
ALTER TABLE `test`.`_t1_new` ADD COLUMN c4 TINYINT NOT NULL DEFAULT '1'
Altered `test`.`_t1_new` OK.
2022-10-18T07:47:57 Creating triggers...
# HUNG

Terminal three

As you can see, this process is hung on the creation of triggers that will keep the original table in sync with the new table that pt-osc has created. This is because creating triggers on a table changes the table definition, which impacts the data dictionary. We can see this in the processlist as shown below…

mysql> show processlist;
+----+-----------------+-----------+------+---------+------+---------------------------------+------------------------------------------------------------------------------------------------------+---------+-----------+---------------+
| Id | User            | Host      | db   | Command | Time | State                           | Info                                                                                                 | Time_ms | Rows_sent | Rows_examined |
+----+-----------------+-----------+------+---------+------+---------------------------------+------------------------------------------------------------------------------------------------------+---------+-----------+---------------+
|  5 | event_scheduler | localhost | NULL | Daemon  |  577 | Waiting on empty queue          | NULL                                                                                                 |  576835 |         0 |             0 |
| 10 | root            | localhost | NULL | Query   |    0 | init                            | show processlist                                                                                     |       0 |         0 |             0 |
| 11 | root            | localhost | test | Sleep   |  469 |                                 | NULL                                                                                                 |  469051 |         3 |             3 |
| 16 | root            | localhost | test | Query   |   44 | User sleep                      | select * from t1 where 1=1 and sleep(1000)                                                           |   44129 |         0 |             0 |
| 17 | root            | localhost | test | Query   |    7 | Waiting for table metadata lock | CREATE TRIGGER `pt_osc_test_t1_del` AFTER DELETE ON `test`.`t1` FOR EACH ROW BEGIN DECLARE CONTINUE  |    6567 |         0 |             0 |
| 18 | root            | localhost | test | Sleep   |   38 |                                 | NULL                                                                                                 |   38267 |         1 |             1 |
+----+-----------------+-----------+------+---------+------+---------------------------------+------------------------------------------------------------------------------------------------------+---------+-----------+---------------+

Terminal two

Once I killed the offending long-running query, the pt-osc process continued:

2022-10-18T07:47:57 Creating triggers...
-----------------------------------------------------------
Event : DELETE
Name  : pt_osc_test_t1_del
SQL   : CREATE TRIGGER `pt_osc_test_t1_del` AFTER DELETE ON `test`.`t1` FOR EACH ROW BEGIN DECLARE CONTINUE HANDLER FOR 1146 begin end; DELETE IGNORE FROM `test`.`_t1_new` WHERE `test`.`_t1_new`.`c1` <=> OLD.`c1`; END
Suffix: del
Time  : AFTER
-----------------------------------------------------------
-----------------------------------------------------------
Event : UPDATE
Name  : pt_osc_test_t1_upd
SQL   : CREATE TRIGGER `pt_osc_test_t1_upd` AFTER UPDATE ON `test`.`t1` FOR EACH ROW BEGIN DECLARE CONTINUE HANDLER FOR 1146 begin end; DELETE IGNORE FROM `test`.`_t1_new` WHERE !(OLD.`c1` <=> NEW.`c1`) AND `test`.`_t1_new`.`c1` <=> OLD.`c1`; REPLACE INTO `test`.`_t1_new` (`c1`, `c2`, `c3`) VALUES (NEW.`c1`, NEW.`c2`, NEW.`c3`); END
Suffix: upd
Time  : AFTER
-----------------------------------------------------------
-----------------------------------------------------------
Event : INSERT
Name  : pt_osc_test_t1_ins
SQL   : CREATE TRIGGER `pt_osc_test_t1_ins` AFTER INSERT ON `test`.`t1` FOR EACH ROW BEGIN DECLARE CONTINUE HANDLER FOR 1146 begin end; REPLACE INTO `test`.`_t1_new` (`c1`, `c2`, `c3`) VALUES (NEW.`c1`, NEW.`c2`, NEW.`c3`);END
Suffix: ins
Time  : AFTER
-----------------------------------------------------------
2022-10-18T07:49:21 Created triggers OK.
2022-10-18T07:49:21 Copying approximately 3 rows...
INSERT LOW_PRIORITY IGNORE INTO `test`.`_t1_new` (`c1`, `c2`, `c3`) SELECT `c1`, `c2`, `c3` FROM `test`.`t1` LOCK IN SHARE MODE /*pt-online-schema-change 7797 copy table*/
2022-10-18T07:49:21 Copied rows OK.
Not dropping old table because --no-drop-triggers was specified.
Not dropping triggers because --no-drop-triggers was specified.  To drop the triggers, execute:
DROP TRIGGER IF EXISTS `test`.`pt_osc_test_t1_del`
DROP TRIGGER IF EXISTS `test`.`pt_osc_test_t1_upd`
DROP TRIGGER IF EXISTS `test`.`pt_osc_test_t1_ins`
Not dropping the new table `test`.`_t1_new` because --no-drop-new-table was specified.  To drop the new table, execute:
DROP TABLE IF EXISTS `test`.`_t1_new`;
Successfully altered `test`.`t1`.

Terminal two

But you’ll notice in our command that we noted that we would swap tables later using the –no-swap-tables –no-drop-new-table options. So now we have to swap the table. This is impacted by metadata locks as well. In terminal one, I created the long-running query again, and in terminal two I attempted to do the table swap.

mysql> RENAME TABLE test.t1 to test._t1_old,
    -> test._t1_new to test.t1;
# HUNG

Terminal three

This was hung due to waiting for a metadata lock, which we can see in the processlist:

mysql> show processlist;
+----+-----------------+-----------+------+---------+------+---------------------------------+---------------------------------------------------------------+---------+-----------+---------------+
| Id | User            | Host      | db   | Command | Time | State                           | Info                                                          | Time_ms | Rows_sent | Rows_examined |
+----+-----------------+-----------+------+---------+------+---------------------------------+---------------------------------------------------------------+---------+-----------+---------------+
|  5 | event_scheduler | localhost | NULL | Daemon  |  762 | Waiting on empty queue          | NULL                                                          |  762369 |         0 |             0 |
| 10 | root            | localhost | NULL | Query   |    0 | init                            | show processlist                                              |       0 |         0 |             0 |
| 11 | root            | localhost | test | Sleep   |  654 |                                 | NULL                                                          |  654585 |         3 |             3 |
| 16 | root            | localhost | test | Query   |   80 | User sleep                      | select * from t1 where 1=1 and sleep(1000)                    |   79853 |         0 |             0 |
| 20 | root            | localhost | test | Query   |   41 | Waiting for table metadata lock | RENAME TABLE test.t1 to test._t1_old,
test._t1_new to test.t1 |   41043 |         0 |             0 |
+----+-----------------+-----------+------+---------+------+---------------------------------+---------------------------------------------------------------+---------+-----------+---------------+

Terminal three

Once the long-running query was killed, the table swap was able to continue. Further work on this process was not interrupted by long-running queries because all work at this point only happens on the swapped _old table.

mysql> show tables;
+----------------+
| Tables_in_test |
+----------------+
| _t1_old        |
| t1             |
+----------------+
2 rows in set (0.00 sec)

mysql> drop trigger pt_osc_test_t1_ins;
Query OK, 0 rows affected (0.00 sec)

mysql> drop trigger pt_osc_test_t1_upd;
Query OK, 0 rows affected (0.01 sec)

mysql> drop trigger pt_osc_test_t1_del;
Query OK, 0 rows affected (0.00 sec)

mysql> drop table _t1_old;
Query OK, 0 rows affected (0.01 sec)

So, as you can see, pt-osc is impacted by metadata locks as well. In fact, three more than direct alters.

gh-ost

You may think that using gh-ost will help you get around metadata locks, but this isn’t the case. As you will see below, if there is a long-running query, it will hang when it tries to do the table swap similar to how pt-osc swaps tables.

[root@centos7-1 ~]# gh-ost \
> --max-load=Threads_running=25 \
> --critical-load=Threads_running=1000 \
> --chunk-size=1000 \
> --user="root" \
> --password="password" \
> --host=localhost \
> --allow-on-master \
> --database="test" \
> --table="t1" \
> --verbose \
> --alter="ADD COLUMN c4 TINYINT NOT NULL DEFAULT '1'" \
> --switch-to-rbr \
> --allow-master-master \
> --cut-over=default \
> --exact-rowcount \
> --concurrent-rowcount \
> --default-retries=120 \
> --execute
2022-10-18 07:56:38 INFO starting gh-ost 1.1.5
2022-10-18 07:56:38 INFO Migrating `test`.`t1`

...

# Migrating `test`.`t1`; Ghost table is `test`.`_t1_gho`
# Migrating centos7-1.localdomain:3306; inspecting centos7-1.localdomain:3306; executing on centos7-1.localdomain
# Migration started at Tue Oct 18 07:56:38 -0400 2022
# chunk-size: 1000; max-lag-millis: 1500ms; dml-batch-size: 10; max-load: Threads_running=25; critical-load: Threads_running=1000; nice-ratio: 0.000000
# throttle-additional-flag-file: /tmp/gh-ost.throttle
# Serving on unix socket: /tmp/gh-ost.test.t1.sock
Copy: 0/3 0.0%; Applied: 0; Backlog: 0/1000; Time: 0s(total),

...

2022-10-18 07:56:39 INFO Creating magic cut-over table `test`.`_t1_del`
2022-10-18 07:56:39 INFO Magic cut-over table created
2022-10-18 07:56:39 INFO Locking `test`.`t1`, `test`.`_t1_del`
Copy: 3/3 100.0%; Applied: 0; Backlog: 0/1000; Time: 2s(total), 1s(copy); streamer: centos7-1-bin.000003:20639; Lag: 0.04s, HeartbeatLag: 0.05s, State: migrating; ETA: due
Copy: 3/3 100.0%; Applied: 0; Backlog: 0/1000; Time: 3s(total), 1s(copy); streamer: centos7-1-bin.000003:25267; Lag: 0.04s, HeartbeatLag: 0.05s, State: migrating; ETA: due
Copy: 3/3 100.0%; Applied: 0; Backlog: 0/1000; Time: 4s(total), 1s(copy); streamer: centos7-1-bin.000003:29893; Lag: 0.04s, HeartbeatLag: 0.05s, State: migrating; ETA: due
Copy: 3/3 100.0%; Applied: 0; Backlog: 0/1000; Time: 5s(total), 1s(copy); streamer: centos7-1-bin.000003:34518; Lag: 0.04s, HeartbeatLag: 0.05s, State: migrating; ETA: due
Copy: 3/3 100.0%; Applied: 0; Backlog: 0/1000; Time: 6s(total), 1s(copy); streamer: centos7-1-bin.000003:39149; Lag: 0.04s, HeartbeatLag: 0.05s, State: migrating; ETA: due
Copy: 3/3 100.0%; Applied: 0; Backlog: 0/1000; Time: 7s(total), 1s(copy); streamer: centos7-1-bin.000003:43780; Lag: 0.04s, HeartbeatLag: 0.05s, State: migrating; ETA: due
2022-10-18 07:56:45 ERROR Error 1205: Lock wait timeout exceeded; try restarting transaction
2022-10-18 07:56:45 INFO Looking for magic cut-over table
2022-10-18 07:56:45 ERROR Error 1205: Lock wait timeout exceeded; try restarting transaction

And once this lock clears, the process is able to complete.

2022-10-18 07:56:52 INFO Dropping magic cut-over table
2022-10-18 07:56:52 INFO Dropping table `test`.`_t1_del`
2022-10-18 07:56:52 INFO Table dropped
Copy: 3/3 100.0%; Applied: 0; Backlog: 0/1000; Time: 15s(total), 1s(copy); streamer: centos7-1-bin.000003:81553; Lag: 0.04s, HeartbeatLag: 0.05s, State: migrating; ETA: due

...

# Migrating `test`.`t1`; Ghost table is `test`.`_t1_gho`
# Migrating centos7-1.localdomain:3306; inspecting centos7-1.localdomain:3306; executing on centos7-1.localdomain
# Migration started at Tue Oct 18 07:56:38 -0400 2022
# chunk-size: 1000; max-lag-millis: 1500ms; dml-batch-size: 10; max-load: Threads_running=25; critical-load: Threads_running=1000; nice-ratio: 0.000000
# throttle-additional-flag-file: /tmp/gh-ost.throttle
# Serving on unix socket: /tmp/gh-ost.test.t1.sock
Copy: 3/3 100.0%; Applied: 0; Backlog: 0/1000; Time: 20s(total), 1s(copy); streamer: centos7-1-bin.000003:106278; Lag: 0.04s, HeartbeatLag: 0.06s, State: migrating; ETA: due
2022-10-18 07:56:58 INFO Setting RENAME timeout as 3 seconds
2022-10-18 07:56:58 INFO Session renaming tables is 41
2022-10-18 07:56:58 INFO Issuing and expecting this to block: rename /* gh-ost */ table `test`.`t1` to `test`.`_t1_del`, `test`.`_t1_gho` to `test`.`t1`
Copy: 3/3 100.0%; Applied: 0; Backlog: 0/1000; Time: 21s(total), 1s(copy); streamer: centos7-1-bin.000003:111137; Lag: 0.04s, HeartbeatLag: 0.05s, State: migrating; ETA: due
2022-10-18 07:56:59 INFO Found atomic RENAME to be blocking, as expected. Double checking the lock is still in place (though I don't strictly have to)
2022-10-18 07:56:59 INFO Checking session lock: gh-ost.40.lock
2022-10-18 07:56:59 INFO Connection holding lock on original table still exists
2022-10-18 07:56:59 INFO Will now proceed to drop magic table and unlock tables
2022-10-18 07:56:59 INFO Dropping magic cut-over table
2022-10-18 07:56:59 INFO Releasing lock from `test`.`t1`, `test`.`_t1_del`
2022-10-18 07:56:59 INFO Tables unlocked
2022-10-18 07:56:59 INFO Tables renamed
2022-10-18 07:56:59 INFO Lock & rename duration: 5.975493692s. During this time, queries on `t1` were blocked
[2022/10/18 07:56:59] [info] binlogsyncer.go:164 syncer is closing...
2022-10-18 07:57:00 INFO Closed streamer connection. err=<nil>
2022-10-18 07:57:00 INFO Dropping table `test`.`_t1_ghc`
[2022/10/18 07:57:00] [error] binlogstreamer.go:77 close sync with err: sync is been closing...
[2022/10/18 07:57:00] [info] binlogsyncer.go:179 syncer is closed
2022-10-18 07:57:00 INFO Table dropped
2022-10-18 07:57:00 INFO Am not dropping old table because I want this operation to be as live as possible. If you insist I should do it, please add `--ok-to-drop-table` next time. But I prefer you do not. To drop the old table, issue:
2022-10-18 07:57:00 INFO -- drop table `test`.`_t1_del`
2022-10-18 07:57:00 INFO Done migrating `test`.`t1`
2022-10-18 07:57:00 INFO Removing socket file: /tmp/gh-ost.test.t1.sock
2022-10-18 07:57:00 INFO Tearing down inspector
2022-10-18 07:57:00 INFO Tearing down applier
2022-10-18 07:57:00 INFO Tearing down streamer
2022-10-18 07:57:00 INFO Tearing down throttler
# Done

Conclusion

As you can see above, all schema change tools have to work with metadata locks. Some more than others, but this is something that we always need to consider when executing changes. Even if you opt to set a table swap for a later date/time, you still need to be aware that metadata locks can interrupt that process. The key here is to monitor for long-running queries at the time that that change occurs and kill them if they are getting in the way, if that’s possible.

This brings me to the final point: selecting a tool to use for a change. Personally, I do not believe there is such a thing as a single best tool for schema changes. You pick the best tool that will be the lightest weight and the least interruptive for the change you want to make.

Oct
08
2020
--

Dangerous Edge Case Warning for Percona Toolkit and pt-online-schema-change

Percona Toolkit and pt-online-schema-change

Percona Toolkit and pt-online-schema-changeRecently I was dealing with an unexpected issue raised by our Support customer, in which data became inconsistent after a schema change was applied.

After some investigation, it turned out that affected tables had a special word in the comments of some columns, which triggered an already known (and fixed) issue with the TableParser.pm library of Percona Toolkit.  The problem is that the customer was using an outdated Toolkit version, where pt-online-schema-change was using that buggy parser.

This bug applies only to Percona Toolkit versions up to 3.0.10, so if you have already 3.0.11 or newer installed, you can skip the rest of this post as these are no longer affected.

I am writing this post to warn every user of pt-online-schema-change who has not upgraded the toolkit, as the problem is potentially very dangerous and can lead to the silent loss of data. 

The problem can manifest in two ways. The first one, although confusing, is not really dangerous as the operation is canceled. It happens when the columns with culprit comments do not allow NULL values. For example:

CREATE TABLE `test_not_null` (
`id` int NOT NULL,
`add_id` int NOT NULL COMMENT 'my generated test case',
PRIMARY KEY (`id`)
) ENGINE=InnoDB;

Schema change operation on this one will look like this:

$ ./pt-online-schema-change-3.0.10 u=msandbox,p=msandbox,h=localhost,S=/tmp/mysql_sandbox5735.sock,D=test,t=test_not_null --print --alter "engine=InnoDB" --execute
(...)
Altering `test`.`test_not_null`...
Creating new table...
CREATE TABLE `test`.`_test_not_null_new` (
`id` int(11) NOT NULL,
`add_id` int(11) NOT NULL COMMENT 'my generated test case',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
Created new table test._test_not_null_new OK.
Altering new table...
ALTER TABLE `test`.`_test_not_null_new` engine=InnoDB
Altered `test`.`_test_not_null_new` OK.
2020-09-30T21:25:22 Creating triggers...
2020-09-30T21:25:22 Created triggers OK.
2020-09-30T21:25:22 Copying approximately 3 rows...
INSERT LOW_PRIORITY IGNORE INTO `test`.`_test_not_null_new` (`id`) SELECT `id` FROM `test`.`test_not_null` LOCK IN SHARE MODE /*pt-online-schema-change 1438 copy table*/
2020-09-30T21:25:22 Dropping triggers...
DROP TRIGGER IF EXISTS `test`.`pt_osc_test_test_not_null_del`
DROP TRIGGER IF EXISTS `test`.`pt_osc_test_test_not_null_upd`
DROP TRIGGER IF EXISTS `test`.`pt_osc_test_test_not_null_ins`
2020-09-30T21:25:22 Dropped triggers OK.
2020-09-30T21:25:22 Dropping new table...
DROP TABLE IF EXISTS `test`.`_test_not_null_new`;
2020-09-30T21:25:22 Dropped new table OK.
`test`.`test_not_null` was not altered.
2020-09-30T21:25:22 Error copying rows from `test`.`test_not_null` to `test`.`_test_not_null_new`: 2020-09-30T21:25:22 Copying rows caused a MySQL error 1364:
Level: Warning
Code: 1364
Message: Field 'add_id' doesn't have a default value
Query: INSERT LOW_PRIORITY IGNORE INTO `test`.`_test_not_null_new` (`id`) SELECT `id` FROM `test`.`test_not_null` LOCK IN SHARE MODE /*pt-online-schema-change 1438 copy table*/

So the reason for the failed operation may be unclear, but at least no data gets damaged. A much worse result happens when the column with comment allows nulls:

CREATE TABLE `test_null` (
`id` int NOT NULL,
`add_id` int DEFAULT NULL COMMENT 'my generated test case',
PRIMARY KEY (`id`)
) ENGINE=InnoDB;

mysql [localhost:5735] {msandbox} (test) > select * from test_null;
+----+--------+
| id | add_id |
+----+--------+
| 1  |      1 |
| 2  |      2 |
| 3  |      3 |
+----+--------+
3 rows in set (0.01 sec)

For this one, the schema change command runs without any errors:

$ ./pt-online-schema-change-3.0.10 u=msandbox,p=msandbox,h=localhost,S=/tmp/mysql_sandbox5735.sock,D=test,t=test_null --print --alter "engine=InnoDB" --execute
(...)
Altering `test`.`test_null`...
Creating new table...
CREATE TABLE `test`.`_test_null_new` (
`id` int(11) NOT NULL,
`add_id` int(11) DEFAULT NULL COMMENT 'my generated test case',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
Created new table test._test_null_new OK.
Altering new table...
ALTER TABLE `test`.`_test_null_new` engine=InnoDB
Altered `test`.`_test_null_new` OK.
2020-09-30T21:28:11 Creating triggers...
2020-09-30T21:28:11 Created triggers OK.
2020-09-30T21:28:11 Copying approximately 3 rows...
INSERT LOW_PRIORITY IGNORE INTO `test`.`_test_null_new` (`id`) SELECT `id` FROM `test`.`test_null` LOCK IN SHARE MODE /*pt-online-schema-change 3568 copy table*/
2020-09-30T21:28:11 Copied rows OK.
2020-09-30T21:28:11 Analyzing new table...
2020-09-30T21:28:11 Swapping tables...
RENAME TABLE `test`.`test_null` TO `test`.`_test_null_old`, `test`.`_test_null_new` TO `test`.`test_null`
2020-09-30T21:28:11 Swapped original and new tables OK.
2020-09-30T21:28:11 Dropping old table...
DROP TABLE IF EXISTS `test`.`_test_null_old`
2020-09-30T21:28:11 Dropped old table `test`.`_test_null_old` OK.
2020-09-30T21:28:11 Dropping triggers...
DROP TRIGGER IF EXISTS `test`.`pt_osc_test_test_null_del`
DROP TRIGGER IF EXISTS `test`.`pt_osc_test_test_null_upd`
DROP TRIGGER IF EXISTS `test`.`pt_osc_test_test_null_ins`
2020-09-30T21:28:11 Dropped triggers OK.
Successfully altered `test`.`test_null`.

But… the table data is not the same after:

mysql [localhost:5735] {msandbox} (test) > select * from test_null;
+----+--------+
| id | add_id |
+----+--------+
|  1 |   NULL |
|  2 |   NULL |
|  3 |   NULL |
+----+--------+
3 rows in set (0.00 sec)

Summarizing, it is essential to make sure you are using the up to date Percona Toolkit, especially the pt-online-schema-change tool, to avoid potential disaster. The current latest stable release, as of when I am writing the post, is version 3.2.1, and the fixed version for this particular bug, 3.0.11, was released in July 2018.

References:

https://www.percona.com/doc/percona-toolkit/3.0/release_notes.html
https://jira.percona.com/browse/PT-1570

 

Jun
07
2019
--

How pt-online-schema-change Handles Foreign Keys

pt-online-schema-change

pt-online-schema-changeForeign key related issues are very common when dealing with DDL changes in MySQL using Percona toolkit. In this blog post, I will explain how the tool (pt-online-schema-change) handles foreign key constraints when executing a DDL change.

First of all, I would like to explain why foreign keys have to be handled at all before writing more about the “How”. Foreign key constraints are aware of table rename operations. In other words, if the parent table is renamed, the child table automatically knows it and changes the foreign key constraint accordingly. Please have a look at the below example, and you can see the table name is automatically updated in the child table after the rename operation on the parent table:

mysql> show create table prd_details \G
*************************** 1. row ***************************
       Table: prd_details
Create Table: CREATE TABLE `prd_details` (
  `product_id` int(11) NOT NULL AUTO_INCREMENT,
  `product_catalog_id` int(11) unsigned NOT NULL,
  ......
  CONSTRAINT `prd_details_ibfk_1` FOREIGN KEY (`product_catalog_id`) REFERENCES `product_catalog` (`catalog_id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
1 row in set (0.00 sec)i
mysql> RENAME TABLE product_catalog TO product_cat ;
Query OK, 0 rows affected (0.15 sec)
mysql> show create table prd_details \G
*************************** 1. row ***************************
       Table: prd_details
Create Table: CREATE TABLE `prd_details` (
  `product_id` int(11) NOT NULL AUTO_INCREMENT,
  `product_catalog_id` int(11) unsigned NOT NULL,
  ......
  CONSTRAINT `prd_details_ibfk_1` FOREIGN KEY (`product_catalog_id`) REFERENCES `product_cat` (`catalog_id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci

Well, that is indeed very nice and to be expected. But please allow me to explain how this becomes a problem when dealing with DDL changes using pt-online-schema-change. The tool implements the DDL changes as mentioned below. Please keep in mind that these are just to give an idea of how the tool works, as there would be more internal things going on in reality.

Let’s take an example ALTER for this case:

Query:

ALTER TABLE T1 MODIFY COLUMN c1 BIGINT UNSIGNED NOT NULL ;

pt-online-schema-change steps for the above alter:

  1. Create a similar table _T1_new
  2. Modify the column c1 to BIGINT in the table _T1_new
  3. Define triggers on table T1 so that changes to data on the original table will be applied to _T1_new as well.
  4. Copy the data from table T1 to _T1_new.
  5. Swap the tables
  6. Drop triggers.

All looks good so far. Now let’s see why these steps create a problem, with a close look at Step #5 (Swap the tables).

Without foreign keys: Swapping of these tables is done as below, which looks nice.

  • Rename T1 —> T1_old
  • Rename _T1_new –> T1
  • If everything right, drop the table T1_old
  • Drop triggers on the new T1 table

With foreign keys: As I mentioned earlier, if there are any child tables with foreign keys to table T1, after renaming, they would automatically point to T1_old but not the new T1. Pt online schema change has to ensure the child table refers to a correct parent table by the end of this DDL change.

  • Rename T1 —> T1_old       =====? The child table refers to T1_old automatically.
  • Rename _T1_new —> T1

In this case, the foreign keys in the child table are still referring to the old table T1_old which don’t have the schema change in place. If you drop T1_old, child table CT1 ends up pointing to a table that doesn’t exist. That’s a very bad situation. Now let’s talk about how the tool handles this.

How does pt-online-schema-change handle this?

The tool comes up with an option named --alter-foreign-keys-method This option supports two values at a high level and below you can see what are those and how they will work.

alter-foreign-keys-method=drop_swap

With this value, it won’t swap as mentioned in the steps. Rather, it drops the old table and then renames the new table with the change in place.

  • Disable foreign key checks for the session (FOREIGN_KEY_CHECKS=0)
  • Drop the table T1_old
  • Rename the new table _T1_new –> T1

The good thing here is that it is quick, but the bad thing is that it’s not reliable. If something goes wrong with renaming, it ends up with the same problem of referring to an unexisting table.

alter-foreign-keys-method=rebuild_constraints

This is the preferred approach for the reason it maintains the consistency of the schema and its relations. In this approach, before dropping the old table, it runs ALTER on all the child tables to drop existing FK and re-add new FK constraints that points to the columns from the new table (with the schema change in place).  Below sequence of bullet points explains the same.

  • Rename T1 –> T1_old
  • Rename _T1_new –> T1
  • ALTER on child table to adjust the foreign key so that it points to T1 rather T1_old.
ALTER TABLE child_table DROP FOREIGN KEY `fk_name`, ADD CONSTRAINT `_fk_name` FOREIGN KEY  (`child_table_column`) REFERENCES _T1_new (`parent_table_column`)

  • Drop the table T1_old
  • Drop triggers from the new T1 table.

I would like to mention that the current implementation to rebuild the child table can be improved by making use of the INPLACE ALTER which I hope would probably be available in upcoming releases. You can see more information about this in the existing bug report here. I will discuss in brief about the two other options available, which are derived based on the above two. Let’s have a quick look.

auto:  If this value is used, it leaves the decision up to the tool itself to choose from the two (drop_swap/rebuild_constraints) options available. If the number of rows in the child table is small, it uses rebuild_constraints; otherwise, it goes with the drop_swap approach. For this reason, this option should always be chosen carefully as it can end up with unexpected results when choosing drop_swap. Below is an example log snippet which explains this behavior:

# pt-online-schema-change --user=root --password=xxxxxxx --alter-foreign-keys-method=auto  --alter "MODIFY COLUMN header_id BIGINT unsigned NOT NULL AUTO_INCREMENT" D=DB1,t=T1 --execute
...........
Copying `DB1`.`T1`:  75% 00:18 remain
2019-05-28T12:49:41 Copied rows OK.
2019-05-28T12:49:41 Max rows for the rebuild_constraints method: 5588
Determining the method to update foreign keys...
2019-05-28T12:49:41   `DB1`.`child_of_T1`: too many rows: 197076; must use drop_swap
2019-05-28T12:49:41 Drop-swapping tables...
...........

none: If this value is used, it is similar to drop_swap but without swapping. In other words, it just drops the original table and leaves the child tables in a state which they point to a table that doesn’t exist. In this case, DBA’s have need to fix the leftover job.

Photo by Silas Köhler on Unsplash

Mar
21
2017
--

Dropping the Foreign Key Constraint Using pt-online-schema-change

pt-online-schema-change

Foreign KeyIn this blog post, we’ll look at how to get rid of the unused Foreign Key (FK) constraint and/or related columns/keys with the help of pt-online-schema-change and the power of its plugins.

Before we proceed, here is a useful blog post written by Peter Zaitsev on Hijacking Innodb Foreign Keys.

If you are trying to get rid of an unused foreign key (FK) constraint and related columns from versions older than MySQL 5.6, or tables that cannot be executed with

ALTER TABLE ... ALGORITHM=INPLACE

 because of limitations mentioned here (specifically, tables with 5.5 TIMESTAMP formats), you can use

pt-online-schema-change

.

For DROP FOREIGN KEY

constraint_name

  with

pt-online-schema-change

 requires specifying

_constraint_name

 rather than the real

constraint_name

. This is due to a limitation in MySQL:

pt-online-schema-change

 adds a leading underscore to foreign key constraint names when creating the new table. Here’s is a simple example of one such case:

CREATE TABLE `test3` (
  `Id` int(11) NOT NULL DEFAULT '0',
  `Firstname` varchar(32) DEFAULT NULL,
  `City` varchar(32) DEFAULT NULL,
  PRIMARY KEY (`Id`),
  CONSTRAINT `FKID` FOREIGN KEY (`Id`) REFERENCES `test4` (`Id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=latin1

To drop the constraint, we are supposed to add an underscore prior to

constraint_name

 FKID:

[root@siddhant ~]# pt-online-schema-change --user=root --execute --set-vars=foreign_key_checks=0  --alter-foreign-keys-method=rebuild_constraints --alter="DROP FOREIGN KEY _FKID" D=apps02,t=test3 --socket=/tmp/mysql-master5520.sock
Operation, tries, wait:
analyze_table, 10, 1
copy_rows, 10, 0.25
……...Altering `apps02`.`test3`...
Creating new table...
Created new table apps02._test3_new OK.
Altering new table….... …….
2017-02-11T12:45:12 Dropped old table `apps02`.`_test3_old` OK.
2017-02-11T12:45:12 Dropping triggers...
2017-02-11T12:45:12 Dropped triggers OK.
Successfully altered `apps02`.`test3`.

Below is one case where if, for some reason, you already have an FK constraint with an underscore the above method of adding an additional underscore to already underscored _FK will fail with an error while dropping it:

Error altering new table `apps02`.`_test3_new`: DBD::mysql::db do failed: Error on rename of './apps02/_test3_new' to './apps02/#sql2-697-19' (errno: 152) [for Statement "ALTER TABLE `apps02`.`_test3_new` DROP FOREIGN KEY ___FKID"] at /usr/bin/pt-online-schema-change line 9069.

In such cases, we will have to make use of the

--plugin

  option used along with a file that calls the 

pt_online_schema_change_plugin

 class and a hook

after_alter_new_table

 to drop the FK constraint. For example, a table with the FK constraint with an underscore is:

CREATE TABLE `test` (
  `Id` int(11) NOT NULL DEFAULT '0',
  `Firstname` varchar(32) DEFAULT NULL,
  `City` varchar(32) DEFAULT NULL,
  PRIMARY KEY (`Id`),
  CONSTRAINT `___fkId` FOREIGN KEY (`Id`) REFERENCES `test2` (`Id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1

Here we have a table with foreign key

___fkid

 using three underscores. Our plugin for dropping the constraint should be as follows:

[root@siddhant ~]# cat ptosc_plugin_drop_fk.pl
package pt_online_schema_change_plugin;
use strict;
sub new {
   my ($class, %args) = @_;
   my $self = { %args };
   return bless $self, $class;
}
sub after_alter_new_table {
   my ($self, %args) = @_;
   my $new_tbl = $args{new_tbl};
   my $dbh     = $self->{cxn}->dbh;
   my $sth = $dbh->prepare("ALTER TABLE $new_tbl->{name} DROP FOREIGN KEY __fkId");
   $sth->execute();
}
1;

NOTE: DROP FOREIGN KEY CONSTRAINT in the plugin has one underscore less than original foreign key constraint, 

__fkId

 vs.

___fkId

. Also, the alter statement will be NOOP alter (i.e., 

--alter ="ENGINE=INNODB"

).

Here is the

pt-online-schema-change

 execution example with the plugin.

[root@siddhant ~]#  pt-online-schema-change --user=root --execute  --set-vars=foreign_key_checks=0  --alter-foreign-keys-method=rebuild_constraints --alter="ENGINE=INNODB" --plugin=/root/ptosc_plugin_drop_fk.pl  D=apps01,t=test --socket=/tmp/mysql-master5520.sock
Created plugin from /root/ptosc_plugin_drop_fk.pl.
Operation, tries, wait:
  analyze_table, 10, 1
  copy_rows, 10, 0.25
  create_triggers, 10, 1
  drop_triggers, 10, 1
  swap_tables, 10, 1
  update_foreign_keys, 10, 1
Altering `apps01`.`test`...
Creating new table...
Created new table apps01._test_new OK.
Altering new table...
Altered `apps01`.`_test_new` OK.
2017-02-11T11:26:14 Creating triggers...
2017-02-11T11:26:14 Created triggers OK.
2017-02-11T11:26:14 Copied rows OK.
2017-02-11T11:26:14 Swapping tables...
2017-02-11T11:26:14 Swapped original and new tables OK.
2017-02-11T11:26:14 Dropping old table...
2017-02-11T11:26:14 Dropped old table `apps01`.`_test_old` OK.
2017-02-11T11:26:14 Dropping triggers...
2017-02-11T11:26:14 Dropped triggers OK.
Successfully altered `apps01`.`test`.

Nov
04
2016
--

Changing the Tablespace Directory with pt-online-schema-change

Tablespace Directory

Tablespace DirectoryIn this blog, we’ll discuss changing the tablespace directory using pt-online-schema-change.

One of the most annoying situations in the life of a DBA is realizing that the disk where the datadir resides is running out of space. If you’re lucky enough to run over an LVM volume or a RAID (depending on the level, though), it is easy to add disk space. But what if you are not that lucky, and your datadir is running on a single disk? Not so funny!

That is the exact situation we recently faced with a customer, for both the master and slave server. When trying to figure out a solution we saw that:

  • There was enough space on a different partition within the same server.
  • The tables have their own tablespace (innodb_file_per_table = on)
  • The MySQL version was 5.6.

We proceed to move some of the tables to the other partition to make room in the datadir, by using the tablespace placing feature: http://dev.mysql.com/doc/refman/5.6/en/tablespace-placing.html.

One note before we continue: if you are using a version equal or lower than 5.6.29, and

innodb_flush_method = O_DIRECT

, there’s a bug that the

CREATE TABLE....DATA DIRECTORY = '/another/directory/'

 won’t work. See: https://bugs.mysql.com/bug.php?id=79200. This was fixed on 5.6.30.

In the slave, we were able to stop the replication and move the tables. A problem occurred when we wanted to do the same on the master, since no downtime was allowed.

This is where

pt-online-schema-change

 came to the rescue!

We could use

pt-osc

 to do the table placing without downtime, but there’s a catch:

pt-osc

 only works when what you want to do is possible by using an ALTER TABLE statement, and in order to use the

CREATE TABLE....DATA DIRECTORY = '/another/directory'

  you need to use a CREATE TABLE statement.

What to do, then? Add a new feature to

pt-online-schema-change

:

--data-dir="/new/directory"

 

With the help of the main developer of the Percona Toolkit, Carlos Salguero, adding this new feature was possible in record time. Now moving the tablespace to another place without downtime is possible.

The new feature will be available with version 2.2.20 of Percona Toolkit, but until the release the code is available at the GitHub repository: https://raw.githubusercontent.com/percona/percona-toolkit/2.2/bin/pt-online-schema-change

Moving the table is just a matter of executing

pt-online-schema-change  --data-dir="/new/datadir" --execute

Let’s see an example. The following table resides in the default datadir:

mysql> show create table sbtest5;
*************************** 1. row ***************************
Table: sbtest5
Create Table: CREATE TABLE `sbtest5` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`k` int(10) unsigned NOT NULL DEFAULT '0',
`c` char(120) NOT NULL DEFAULT '',
`pad` char(60) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `k_5` (`k`)
) ENGINE=InnoDB AUTO_INCREMENT=1001 DEFAULT CHARSET=latin1 MAX_ROWS=1000000
1 row in set (0.00 sec)

Now, let’s move it to the directory /opt/datadir, which owner is the MySQL user:

[root@ps56-1 percona]# pt-online-schema-change  --data-dir="/opt/datadir" --execute D=percona,t=sbtest5
No slaves found.  See --recursion-method if host ps56-1 has slaves.
Not checking slave lag because no slaves were found and --check-slave-lag was not specified.
Operation, tries, wait:
  analyze_table, 10, 1
  copy_rows, 10, 0.25
  create_triggers, 10, 1
  drop_triggers, 10, 1
  swap_tables, 10, 1
  update_foreign_keys, 10, 1
Altering `percona`.`sbtest5`...
Creating new table...
Created new table percona._sbtest5_new OK.
2016-11-01T19:22:27 Creating triggers...
2016-11-01T19:22:27 Created triggers OK.
2016-11-01T19:22:27 Copying approximately 1000 rows...
2016-11-01T19:22:27 Copied rows OK.
2016-11-01T19:22:27 Analyzing new table...
2016-11-01T19:22:27 Swapping tables...
2016-11-01T19:22:28 Swapped original and new tables OK.
2016-11-01T19:22:28 Dropping old table...
2016-11-01T19:22:28 Dropped old table `percona`.`_sbtest5_old` OK.
2016-11-01T19:22:28 Dropping triggers...
2016-11-01T19:22:28 Dropped triggers OK.
Successfully altered `percona`.`sbtest5`.

Okay, all good. Let’s see the new table definition:

mysql> show create table sbtest5;
*************************** 1. row ***************************
       Table: sbtest5
Create Table: CREATE TABLE `sbtest5` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `k` int(10) unsigned NOT NULL DEFAULT '0',
  `c` char(120) NOT NULL DEFAULT '',
  `pad` char(60) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`),
  KEY `k_5` (`k`)
) ENGINE=InnoDB AUTO_INCREMENT=1001 DEFAULT CHARSET=latin1 MAX_ROWS=1000000 DATA DIRECTORY='/opt/datadir/'

DATA DIRECTORY='/opt/datadir/'

 is in the right place! ?

And from the filesystem, the *.ibd file is in the new directory:

[root@ps56-1 opt]# ls -l /opt/datadir/percona/ | grep sbtest5
-rw-rw---- 1 mysql mysql 344064 Nov  1 19:22 sbtest5.ibd

And in the datadir, we can see the *isl file:

[root@ps56-1 opt]# ls -l /var/lib/mysql/percona/ | grep sbtest5
-rw-rw---- 1 mysql mysql   8632 Nov  1 19:22 sbtest5.frm
-rw-rw---- 1 mysql mysql     32 Nov  1 19:22 sbtest5.isl

And the contents seems fine:

[root@ps56-1 opt]# cat /var/lib/mysql/percona/sbtest5.isl
/opt/datadir/percona/sbtest5.ibd

So, in conclusion, if you need to move a table to another directory without downtime,

pt-online-schema-change

 can do that for you now.

Jul
01
2016
--

Amazon RDS and pt-online-schema-change

Amazon RDS and pt-online-schema-change

Amazon RDS and pt-online-schema-changeIn this blog post, I discuss some of the insights needed when using Amazon RDS and pt-online-schema-change together.

The pt-online-schema-change tool runs DDL queries (ALTER) online so that the table is not locked for reads and writes. It is a commonly used tool by community users and customers. Using it on Amazon RDS requires knowing about some specific details. First, a high-level explanation of how the tool works.

This is an example from the documentation:

pt-online-schema-change --alter "ADD COLUMN c1 INT" D=sakila,t=actor

The tool runs an ALTER on the table “actor” from the database “sakila.” The alter adds a column named “c1” of type “integer.” In the background, the tool creates a new empty table similar to “actor” but with the new column already added. It then creates triggers on the original table to update the corresponding rows in the new table. After, it starts copying rows to the new table (this is the phase that takes the longest amount of time). When the copy is done, the tables are swapped, triggers removed and the old table dropped.

As we can see, it is a tool that uses the basic features of MySQL. You can run it on MySQL, Percona Server, MariaDB, Amazon RDS and so on. But when using Amazon, there is a hidden issue: you don’t have SUPER privileges. This means that if you try to run the tool on an RDS with binary logs enabled, you could get the following error:

DBD::mysql::db do failed: You do not have the SUPER privilege and binary logging is enabled (you might want to use the less safe log_bin_trust_function_creators variable) [for Statement "CREATE TRIGGER `pt_osc_db_table_del` AFTER DELETE ON `db`.`table` FOR EACH ROW DELETE IGNORE FROM `db`.`_table_new` WHERE `db`.`_table_new`.`table_id` <=> OLD.`table_id` AND `db`.`_table_new`.`account_id` <=> OLD.`account_id`"] at /usr/bin/pt-online-schema-change line 10583.

The following documentation page explains the reason for this message:

http://dev.mysql.com/doc/refman/5.7/en/stored-programs-logging.html

The bottom line is creating triggers on a server with binary logs enabled requires a user with SUPER privileges (which is impossible in Amazon RDS). The error message specifies the workaround. We need to enable the variable log_bin_trust_function_creators. Enabling it is like saying to the server:

“I trust regular users’ triggers and functions, and that they won’t cause problems, so allow my users to create them.”

Since the database functionality won’t change, it becomes a matter of trusting your users. log_bin_trust_function_creators is a global variable that can be changed dynamically:

mysql> SET GLOBAL log_bin_trust_function_creators = 1;

Run the tool again. This time, it will work. After the ALTER is done, you can change the variable to 0 again.

After you’re done with the ALTER process, you can change the variable to “0” again.

Jun
30
2016
--

Rescuing a crashed pt-online-schema-change with pt-archiver

crashed pt-online-schema-change

crashed pt-online-schema-changeThis article discusses how to salvage a crashed pt-online-schema-change by leveraging pt-archiver and executing queries to ensure that the data gets accurately migrated. I will show you how to continue the data copy process, and how to safely close out the pt-online-schema-change via manual operations such as RENAME TABLE and DROP TRIGGER commands. The normal process to recover from a crashed pt-online-schema-change is to drop the triggers on your original table and drop the new table created by the script. Then you would restart pt-online-schema-change. In this case, this wasn’t possible.

A customer recently needed to add a primary key column to a very busy table (with around 200 million rows). The table only had a unique key on one column (called our_id below). The customer had concerns about slave lag, and wanted to ensure there was little or no lag. This, as well as the fact that you can’t add a primary key as an online DDL in MySQL and Percona Server 5.6, meant the obvious answer was using pt-online-schema-change.

Due to the sensitivity of their environment, they could only afford one short window for the initial metadata locks, and needed to manually do the drop swap that pt-online-schema-change normally does automatically. This is where no-drop-triggers and no-swap-tables come in. The triggers will theoretically run indefinitely to keep the new and old tables in sync once pt-online-schema-change is complete. We crafted the following command:

pt-online-schema-change
--execute
--alter-foreign-keys-method=auto
--max-load Threads-running=30
--critical-load Threads_running=55
--check-slave-lag mysql-slave1,mysql-slave2,mysql-slave3
--max?lag=10
--chunk-time=0.5
--set-vars=lock_timeout=1
--tries="create_triggers:10:2,drop_triggers:10:2"
--no-drop-new-table
--no-drop-triggers
--no-swap-tables
--chunk-index "our_id"
--alter "ADD newcol BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST"
D=website,t=largetable
--nocheck-plan

You can see some of the specifics of other flags and why we used them in the Percona Toolkit Manual.

Once we ran the command the customer got concerned, as their monitoring tools weren’t showing any work done (which is by design, pt-online-schema-change doesn’t want to hurt your running environment). The customer ran strace -p to verify it was working. This wasn’t a great choice as it crashed pt-online-schema-change.

At this point, we knew that the application (and management) would not allow us to take new metadata locks to create triggers on the table, as we had passed our metadata lock window.

So how do we recover?

First, let’s start with a clean slate. We issued the following commands to create a new table, where __largetable_new is the table created by pt-online-schema-change:

CREATE TABLE mynewlargetable LIKE __largetable_new;
RENAME TABLE __largetable_new TO __largetable_old, mynewlargetable TO __largetable_new;
DROP TABLE __largetable_old;

Now the triggers on the original table, largetable are updating the new empty table that has our new schema.

Now let’s address the issue of actually moving the data that’s already in largetable to __largetable_new. This is where pt-archiver comes in. We crafted the following command:

pt-archiver
--execute
--max-lag=10
--source D=website,t=largetable,i=our_id
--dest D=website,t=__largetable_new
--where "1=1"
--no-check-charset
--no-delete
--no-check-columns
--txn-size=500
--limit=500
--ignore
--statistics

We use pt-archiver to slowly copy records non-destructively to the new table based on our_id and WHERE 1=1 (all records). At this point, we periodically checked the MySQL data directory over the course of a day with ls -l to compare table sizes.

Once the table files were close to the same size, we ran counts on the tables. We noticed something interesting: the new table had thousands more records than the original table.

This concerned us. We wondered if our “hack” was a mistake. At this point we ran some verification queries:

select min(our_id) from __largetable_new;
select max(our_id) from __largetable_new;
select min(our_id) from largetable;
select max(our_id) from largetable;

We learned that there were older records that didn’t exist in the live table. This means that pt-archiver and the DELETE trigger may have missed each other (i.e., pt-archiver was already in a transaction but hadn’t written records to the new table until after the DELETE trigger already fired).

We verified with more queries:

SELECT COUNT(*) FROM largetable l WHERE NOT EXISTS (SELECT our_id FROM __largetable_new n WHERE n.our_id=l.our_id);

They returned nothing.

SELECT COUNT(*) FROM __largetable_new n WHERE NOT EXISTS (SELECT our_id FROM largetable l WHERE n.our_id=l.our_id);

Our result showed 4000 extra records in the new table. This shows that we ended up with extra records that were deleted from the original table. We ran other queries based on their data to verify as well.

This wasn’t a huge issue for our application, and it could have been easily dealt with using a simple DELETE query based on the unique index (i.e., if it doesn’t exist in the original table, delete it from the new one).

Now to complete the pt-online-schema-change actions. All we need to do is the atomic rename or drop swap. This should be done as soon as possible to avoid running in a degraded state, where all writes to the old table are duplicated on the new one.

RENAME TABLE largetable TO __largetable_old , __largetable_new TO largetable;

Then drop the triggers for safety:

DROP TRIGGER pt_osc_website_largetable_ins;
DROP TRIGGER pt_osc_website_largetable_upd;
DROP TRIGGER pt_osc_website_largetable_del;

At this point it is safer to wait for the old table to clear out of the buffer pool before dropping it, just to ensure there is no impact on the server (maybe a week to be safe). You can check information_schema for a more accurate reading on this:

SELECT COUNT(*) FROM INFORMATION_SCHEMA.INNODB_BUFFER_PAGE WHERE TABLE_NAME = '`website`.`__largetable_old`';
+----------+
| count(*) |
+----------+
|   279175 |
+----------+
1 row in set (8.94 sec)

Once this goes to 0 you can issue:

DROP TABLE __largetable_old;

Powered by WordPress | Theme: Aeros 2.0 by TheBuckmaker.com