| Bug #115988 | Too Much Disk Read on Startup, penalizing deployments with many tables (1M+). | ||
|---|---|---|---|
| Submitted: | 3 Sep 2024 15:03 | Modified: | 3 Sep 14:42 |
| Reporter: | Jean-François Gagné | Email Updates: | |
| Status: | Closed | Impact on me: | |
| Category: | MySQL Server: InnoDB storage engine | Severity: | S5 (Performance) |
| Version: | 8.0.39, 8.4.2, 9.0.1 | OS: | Any |
| Assigned to: | CPU Architecture: | Any | |
| Tags: | Contribution, regression | ||
[3 Sep 2024 15:13]
Jean-François Gagné
Updating title (sorry for small mistake): - now: Too Much Disk Read on Startup, penalizing deployments with many tables (1M+) - was: Too Much Disk Read on Startup, penalizing deployments many tables (1M+)
[3 Sep 2024 15:26]
J-F Aiven Gagné
More about this contribution in https://github.com/jfg956/mysql-server/pull/14 (*) I confirm the code being submitted is offered under the terms of the OCA, and that I am authorized to contribute it.
Contribution: bug#115988.patch (application/octet-stream, text), 15.98 KiB.
[3 Sep 2024 15:29]
J-F Aiven Gagné
Some notes about my above contribution extracted from https://github.com/jfg956/mysql-server/pull/14 This PR merges on 9.0.1, and the corresponding patch file applies to 8.4.2 and 8.0.39. Because this PR significantly improves MySQL startup, I think it should be back ported to 8.0 and 8.4, and if it is considered a "breaking change", with feature flags / global variables to enable the optimizations, disabled by default. This PR introduces two testing flags / global variables, disabled by default: innodb_tablespace_startup_testing_fadvise and innodb_tablespace_startup_testing_light. They allow enabling the optimizations introduced by this PR. When merging in 9.0, I assumed it did not make sense to keep these, hence "testing" naming and limited documentation. If it is decided they are needed, they should be renamed and documented adequately, which I can do if asked by Oracle. While doing this work, I "played with fire" and refactored a few things. They are identified as "I / JFG had a visceral reaction ..." as comments in the code. These and other comments are in the code for the clarity of the PR, they should be removed / adjusted when merging.
[4 Sep 2024 10:23]
MySQL Verification Team
Hello Jean-François, Thank you for the report and contribution. regards, Umesh
[6 Oct 2025 12:45]
Jean-François Gagné
Hello, it has been more than a year since I contributed a patch for this verified performance bug. Any blocker on merging my work ? Anything I can do to speed-up merging ? Thanks, Jean-François Gagné
[20 Aug 17:44]
Jean-François Gagné
While looking into the fix of Bug#116619, I realized that my suggestion / contribution described here looks at least partially implemented. Digging into this, I found below commit (Bug #37022679 - Too Much Disk Read on Startup, penalizing deployments with many tables (1M+)). Note that the comment of this commit exactly matches this bug report ! https://github.com/mysql/mysql-server/commit/64c9a68cc3aaf65994f9597feb9f0fcb06b57420 Above commit looks included in MySQL 8.0.42, 8.4.5 and 9.3.0 (2025-07-22), but I did not find a reference to Bug#37022679 in the release notes. https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-42.html https://dev.mysql.com/doc/relnotes/mysql/8.4/en/news-8-4-5.html https://dev.mysql.com/doc/relnotes/mysql/9.7/en/news-9-3-0.html Above commit set the read data size to UNIV_SECTOR_SIZE, and used O_DIRECT to bypass the cache. So this looks like implementing both the light and fadvise optimizations of this report, but in a different way. However, the amount of read data is set back to srv_page_size in below commit (Bug #37022676), the justification being to eventually be able to validate the page checksum. So the only the fadvise optimization is really implemented. https://github.com/mysql/mysql-server/commit/5421675991e4de51e5dd85079a34102d85c490be I re-ran tests with different versions. The results below confirm the fadvise optimization being implemented. I could change the status of this report to Closed, but I will not for more than one reasons. First, the light optimization is not implemented. Second, I (and other contributors) deserve an explanation on why this report stayed open, and that my Oct 2025 comment stayed unanswered (even if this was partially implemented). And third, it does not feel right to close this without pointing to a release note comment. And one last thing: a re-implementation of my contribution is in 9.3 but not in below. I have mixed feeling about this ! https://blogs.oracle.com/mysql/mysql-93-thank-you-for-your-contributions ####### # Tests # Below is done on an AWS m6id.large instance (2 vcpus, 1 cores, 8 GB RAM and 118 GB SSD). n=100000 for mv in 8.0.41 8.0.42 8.0.46 8.4.4 8.4.5 8.4.11 9.2.0 9.3.0 9.7.2 26.7.0; do cd; dbdeployer deploy single mysql_$mv > /dev/null cd ~/sandboxes/msb_mysql_${mv//./_} create_tables; stop; start stop; cd; rm -rf ~/sandboxes/msb_mysql_${mv//./_} done 8.0.41 creating: 0:07:46 8.0.41 start: 0:00:32 8.0.41 diskstats: 6686034 sector read. 8.0.42 creating: 0:07:46 8.0.42 start: 0:00:21 8.0.42 diskstats: 3462082 sector read. 8.0.46 creating: 0:07:47 8.0.46 start: 0:00:21 8.0.46 diskstats: 3475530 sector read. 8.4.4 creating: 0:08:21 8.4.4 start: 0:00:33 8.4.4 diskstats: 6683010 sector read. 8.4.5 creating: 0:08:20 8.4.5 start: 0:00:22 8.4.5 diskstats: 3481706 sector read. 8.4.11 creating: 0:08:20 8.4.11 start: 0:00:20 8.4.11 diskstats: 3474554 sector read. 9.2.0 creating: 0:05:46 9.2.0 start: 0:00:34 9.2.0 diskstats: 6678826 sector read. 9.3.0 creating: 0:05:47 9.3.0 start: 0:00:22 9.3.0 diskstats: 3481130 sector read. 9.7.2 creating: 0:05:46 9.7.2 start: 0:00:20 9.7.2 diskstats: 3478034 sector read. 26.7.0 creating: 0:05:19 26.7.0 start: 0:00:19 26.7.0 diskstats: 3476282 sector read.
[21 Aug 20:48]
MySQL Admin
Posted by developer: Bug status updated to 'Documenting'
[1 Sep 12:40]
Edward Gilmore
Posted by developer: Added the following note to the MySQL Server 8.0.42, 8.4.5, and 9.3.0 release notes: Server startup could be significantly slower for deployments with very large numbers of InnoDB tables because MySQL read each tablespace file to identify its space ID. Startup now reads only the required header sector from each .ibd file and uses direct I/O where available, reducing disk-read overhead. If direct I/O is unavailable, MySQL can fall back without failing startup. Our thanks to Jean-François Gagné for the contribution. Our sincerest apologies for the delay documenting this bug. An internal synchronization issue caused a problem with the bug statuses.

Description: Hi, On MySQL startup, the first page of each table is read to get the Space Id. But reading the first page is not only fetching 16 kilobytes from disk, it is fetching 32 kilobytes, which is problematic with many tables. See How to repeat for details. This can be fixed by what I call the "fadvise" optimization, more about this in Suggested fix and contributed patch (I will add the contribution soon). Above applies to 8.0.39, 8.4.2 and 9.0.1 on Debian 12 with kernel 6.1.99. It might not be reproducible on other distributions / kernels as prefetching / readahead might be configured differently. However, this does not change the next part of this bug report. Reading the whole first page of a table is not needed for getting the Space Id. By optimizing the reading logic, the amount of data read can be lowered to 4 kilobytes, which is a big win on startup with many tables. This is what I call the "light" optimization, more about this in Suggested fix and contributed patch (I will add the contribution soon). With the fadvise and light optimizations, MySQL startup time on an AWS vm with local SSDs is reduced from 2:39 to 1:09 (1 minute 9 seconds), see Suggested fix for testing details. Setting this report as a Serious / S2 because reading too much from disk or a long MySQL startup are not minor things. Some could argue that this is an "optimization" / feature request, but I think there is also an argument to this being considered a serious bug and even a regression from 5.7 because, as shown in How to repeat, 5.7 reads fewer data from disk than 8.0 and starts faster. And one last thing: the "light" optimization will probably not work on filesystems that have a 16-kilobyte block size (like a deployment on zfs). This means that when avoiding the overhead of the double write buffer, one will have to pay an overhead on startup. As usual, every optimization has a tradeoff ¯\_(ツ)_/¯. Many thanks for looking into this, Jean-François Gagné How to repeat: # These tests are done on an AWS m6id.xlarge instance (4 vcpus, 2 cores, 16 GB RAM and 220 GB SSD). # Linux Kernel version (might be important for fadvise results). uname -a Linux ip-172-31-15-73 6.1.0-23-cloud-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.99-1 (2024-07-15) x86_64 GNU/Linux # Debian version (might be important for fadvise results). cat /etc/debian_version 12.6 # A few utility functions: # - status_string; # - start; # - stop; # - create_tables. function status_string() { printf "%6s %9s" $mv $1 } # For diskstats: https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats function start() { sudo sync sudo /usr/bin/bash -c "echo 3 > /proc/sys/vm/drop_caches" # I have a dedicated mount point for putting MySQL data dirs. # Adjust below to where your data dir is, ideally not having # other things in there that would influence diskstats. local dev=$(mount | grep /mnt/jgagne_sandboxes | cut -d " " -f 1 | cut -d "/" -f 3) local diskstats_before="$(cat /proc/diskstats | grep $dev)" ( ./start > /dev/null & ) while sleep 1; do test -e data/mysql_sandbox*.pid && break; done | pv -t -N "$(status_string start)" local diskstats_after="$(cat /proc/diskstats | grep $dev)" diskstats="$( echo -e "$diskstats_before\n$diskstats_after" | awk '{print $6-sector_read, "sector read."; sector_read=$6}' | tail -n 1)" echo "$(status_string "diskstats"): $diskstats" } function stop() { local pid=$(cat data/mysql_sandbox*.pid) ( ./stop > /dev/null & ) while sleep 1; do ps -p $pid | grep -q $pid || break; done } # The quickest way to create tables in 5.7 is by reducing durability. # The quickest way to create tables in 8.0+ is by reducing durability and disabling redo logging. function create_tables() { local sql_pre="set global sync_binlog=0, innodb_flush_log_at_trx_commit=2;" local sql_post="set global innodb_fast_shutdown = 0;" sql_post="$sql_post FLUSH BINARY LOGS; do sleep(2); purge binary logs before now();" test ${mv:0:1} = "5" || { sql_pre="$sql_pre ALTER INSTANCE DISABLE INNODB REDO_LOG;" sql_post="$sql_post ALTER INSTANCE ENABLE INNODB REDO_LOG;" } ./use <<< "create database test_jfg; $sql_pre" seq -f "CREATE TABLE t%06.0f(i int);" 1 $n | ./use test_jfg | pv -t -N "$(status_string creating)" ./use <<< "$sql_post" } # The actual test: 1M tables with 5.7 to 9.0. n=1000000 for mv in 5.7.44 8.0.39 8.4.2 9.0.1; do cd; dbdeployer deploy single mysql_$mv > /dev/null cd ~/sandboxes/msb_mysql_${mv//./_} create_tables; stop; start stop; cd; rm -rf ~/sandboxes/msb_mysql_${mv//./_} done # Full results. 5.7.44 creating: 0:37:43 5.7.44 start: 0:00:50 5.7.44 diskstats: 2809958 sector read. 8.0.39 creating: 0:39:38 8.0.39 start: 0:02:43 8.0.39 diskstats: 65783020 sector read. 8.4.2 creating: 0:42:07 8.4.2 start: 0:02:44 8.4.2 diskstats: 65685484 sector read. 9.0.1 creating: 0:31:26 9.0.1 start: 0:02:43 9.0.1 diskstats: 65688772 sector read. # Diskstats results (without the creating and start details). # For 8.0.39, 8.4.2 and 9.0.1, 65+ million sectors are read while 32 per table is expected (one sector is 512 bytes). # Below also shows the regression from 5.7 to 8.0 needing to read 20x sectors from disk. 5.7.44 diskstats: 2809958 sector read. 8.0.39 diskstats: 65783020 sector read. 8.4.2 diskstats: 65685484 sector read. 9.0.1 diskstats: 65688772 sector read. # Start results (without the creating and diskstats details). # Below shows the regression from 5.7 to 8.0 needing 3x time to start. 5.7.44 start: 0:00:50 8.0.39 start: 0:02:43 8.4.2 start: 0:02:44 9.0.1 start: 0:02:43 Suggested fix: # These tests are done on an AWS m6id.xlarge instance (4 vcpus, 2 cores, 16 GB RAM and 220 GB SSD). # Same kernel and Debian version as in How to repeat. # These two global variables allow enabling the fadvise and light optimizations in the patched binary: # - innodb_tablespace_startup_testing_fadvise; # - innodb_tablespace_startup_testing_light. # Below, I use a tgz of a data dir where I previously created 1 million tables. # This tgz can easily be taken by extrapolating from How to repeat, # or below can be modified to create tables based on How to repeat. ls -l ~/sandboxes/mysql_9.0.1.data.1000000.tgz -rw-r--r-- 1 jgagne jgagne 2464569915 Sep 1 15:43 /home/jgagne/sandboxes/mysql_9.0.1.data.1000000.tgz # In my dbdeployer binary directory, I renamed mysqld to mysqld_org, # and I put my compiled binary there as mysqld_explo. ls -l ~/opt/mysql/mysql_9.0.1/bin/mysqld{,_org,_explo} lrwxrwxrwx 1 jgagne jgagne 10 Sep 1 15:49 /home/jgagne/opt/mysql/mysql_9.0.1/bin/mysqld -> mysqld_org -rwxr-xr-x 1 jgagne jgagne 277748456 Aug 29 14:32 /home/jgagne/opt/mysql/mysql_9.0.1/bin/mysqld_explo -rwxr-xr-x 1 jgagne jgagne 76973576 Aug 29 14:31 /home/jgagne/opt/mysql/mysql_9.0.1/bin/mysqld_org # Needs the start and stop functions from How to repeat. # A few additional utility functions: # - status_string (new version); # - set_bin; # - stop_wipe_restore. function status_string() { printf "%6s %13s %9s" $mv $str $1 } # Below function is to swap between binaries. function set_bin() { ( # In a sub-shell to not have to undo cd. cd ~/opt/mysql/mysql_$mv/bin rm -f mysqld ln -s mysqld_$1 mysqld ) } function stop_wipe_restore() { stop rm -rf data set_bin $1 str=$2 pv -te ~/sandboxes/mysql_${mv}.data.${n}.tgz -N "$(status_string restore)" | tar -zx } # The actual test: 1M tables with 9.0.1, org and explo. n=100000; mv="9.0.1"; { set_bin org cd; dbdeployer deploy single mysql_$mv > /dev/null cd ~/sandboxes/msb_mysql_${mv//./_} stop_wipe_restore org default_org; start stop_wipe_restore explo default_explo; start echo "innodb_tablespace_startup_testing_fadvise=ON" >> my.sandbox.cnf stop_wipe_restore explo fadvise; start echo "innodb_tablespace_startup_testing_light=ON" >> my.sandbox.cnf stop_wipe_restore explo light; start stop; cd; rm -rf ~/sandboxes/msb_mysql_${mv//./_} set_bin org } # Full results. 9.0.1 default_org restore: 0:13:03 9.0.1 default_org start: 0:02:45 9.0.1 default_org diskstats: 65695545 sector read. 9.0.1 default_explo restore: 0:13:01 9.0.1 default_explo start: 0:02:39 9.0.1 default_explo diskstats: 65695113 sector read. 9.0.1 fadvise restore: 0:13:02 9.0.1 fadvise start: 0:01:46 9.0.1 fadvise diskstats: 33695617 sector read. 9.0.1 light restore: 0:13:01 9.0.1 light start: 0:01:09 9.0.1 light diskstats: 9694921 sector read. # Diskstats results (without the restore and start details). # The fadvise optimization divides sector read by two, and the light by an additional four. 9.0.1 default_org diskstats: 65695545 sector read. 9.0.1 default_explo diskstats: 65695113 sector read. 9.0.1 fadvise diskstats: 33695617 sector read. 9.0.1 light diskstats: 9694921 sector read. # Start results (without the restore and diskstats details). # The explo binary has a similar "default" startup time, #. and fadvise reduces it to 1:46 (from 2:39), and light to 1:09. 9.0.1 default_org start: 0:02:45 9.0.1 default_explo start: 0:02:39 9.0.1 fadvise start: 0:01:46 9.0.1 light start: 0:01:09