[MDEV-32268] GNU libc posix_fallocate() may be extremely slow Created: 2023-09-27 Updated: 2024-01-18 Resolved: 2024-01-18 |
|
| Status: | Closed |
| Project: | MariaDB Server |
| Component/s: | Storage Engine - InnoDB |
| Affects Version/s: | None |
| Fix Version/s: | 10.4.33, 10.5.24, 10.6.17, 10.11.7, 11.0.5, 11.1.4, 11.2.3, 11.3.2, 11.4.1 |
| Type: | Bug | Priority: | Critical |
| Reporter: | Valerii Kravchuk | Assignee: | Marko Mäkelä |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Issue Links: |
|
||||||||||||
| Description |
|
After the "fix" for
while in some cases (like ALTER TABLE ... ALGORITHM=COPY for COMPRESSED table and datadir on a NAS/NFS mount) using alternative allocation method provides performance benefits. Consider the following test case on 10.1 that proves the point to some extent on a local NFS mount on current Ubuntu 22.04:
Here we have 378 seconds to ALTER the table that is twice as large as the buffer pool with innodb_use_fallocate = OFF vs 385 where it is ON. I expect bigger difference when table size increases, but wanted to keep the test fast enough to be pracical on my slow HDD. Users reported much more serious impact (that prevents the use of 10.4+ for table terabytes in size) and upgrade from pre-10.2.4 versions. |
| Comments |
| Comment by Marko Mäkelä [ 2023-09-28 ] | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
I think that the option name innodb_use_fallocate would be inappropriate and misleading here. There are 3 ways of setting the file size:
Basically, starting with MariaDB Server 10.2, we always behave as if innodb_use_fallocate=ON, that is, we use fallocate() whenever we think it makes sense. I think that new parameter name should be something like innodb_sparse_allocate or innodb_logical_allocate. In that way, it would also be OS agnostic; the system call interface names are something completely different on Microsoft Windows. I am not against adding a new parameter. This should be rather straightforward to implement: just add something like
to the start of os_file_set_size(), and add a MYSQL_SYSVAR_BOOL binding to ha_innodb.cc. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Comment by Marko Mäkelä [ 2023-12-05 ] | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
I took a closer look at the code. In a 10.1 after-merge fix of
It appears to me that only files that are associated with page_compressed tables could be logically extended by ftruncate(). The setting innodb_use_fallocate=FALSE would disallow the use of physical allocation via posix_fallocate(), and always cause the original pwrite() code path to be used. The code was later refactored to invoke the function os_file_set_size(), but the logic remained the same, as far as I can tell. I do not see how reintroducing the parameter innodb_use_fallocate would help here. We would be allocating pages to the file anyway. If fallocate() returns EOPNOTSUPP on some file system ( The support ticket specifically mentions page_compressed tables and not ROW_FORMAT=COMPRESSED, which is what the test case in the Description is using. As far as I can tell, files of anything else than page_compressed tables were always extended by physical writes or allocation of pages, while files of page_compressed tables should be extended logically by ftruncate(), because those files will be sparse by design. valerii, can you provide some strace or similar output as a proof what is actually going on? | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Comment by Valerii Kravchuk [ 2023-12-05 ] | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
It's clear from my test case we speak about COMPRESSED tables here:
and this is what customer cares about. We discussed page_compressed maybe in the issue or related issues, but it does not provide enough compression benefit for the customer. So, what statement(s) and in what version (10.1?) do you want me to strace, in a hope to make what exact point? | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Comment by Marko Mäkelä [ 2023-12-19 ] | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
I had to jump through some hoops to compile MariaDB Server 10.1 on my system. With GCC 13 it was doomed, because there is a class byte defined somewhere in the libstdc++ headers. With GCC 10 I succeeded, after making some small modifications to the latest 10.1 branch. I used the following test:
I invoked this as
Between MariaDB Server 5.5 and 10.1, there were 2 implementations of InnoDB included: a dynamically linked innodb_plugin (storage/innobase) and a statically linked xtradb. The former is much faster on my NVMe on Linux 6.5.13 and ext4 file system:
I can confirm that innodb_use_fallocate=1 leads to a slightly faster result on the 10.1 XtraDB. I was surprised to see XtraDB being so much slower, but that’s not the point of this investigation. MariaDB Server 10.6 is faster than both. (Note: There is some variance in the numbers, which I noticed when running the 10.6 test another time, with the To better highlight the difference between innodb_use_fallocate=0 and innodb_use_fallocate=1 I revised the test so that it would create and populate an uncompressed table first:
I also specified innodb_compression_level=0 so that less time would be spent on compressing data and more on extending the file. The time consumed by ALTER TABLE was about 1 second longer with innodb_use_fallocate=1. I also tried applying the following patch, and it did not change this:
Note: This patch is incorrect; fallocate() on Linux returns 0 or -1, and errno would be set to what the surrounding code expects. I went on to run the faster test variant on 10.6, with and without a patch to replace posix_fallocate() with fallocate(). The ALTER TABLE time would be cut to about 10% (1.3 seconds instead of 13 seconds). Setting innodb_compression_level=0 had little effect. I do not see any difference between applying or not applying the patch to 10.6:
I think that this needs to be tested in a NFS or NAS environment. I have not set up one. valerii, can you help? In the above 10.6 patch, you could also try replacing the first #if with #if 0 so that fallocate() will not be invoked in any form. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Comment by Marko Mäkelä [ 2023-12-19 ] | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Because the performance regression was reported for 10.4, I tested the current 10.4 branch as well, using similar build options as 10.1 (only GCC 13 instead of GCC 10). The ALTER TABLE statement in the original test took 6.09 seconds, which is not that much worse from 10.6. With the smaller test and innodb_compression_level=0, it was 1.8 to 2.2 seconds, again, not much worse than the 1.3 or 1.4 seconds on 10.6. As noted already, results from a fast local NVMe drive cannot be generalized to an environment that uses NFS. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Comment by Valerii Kravchuk [ 2023-12-19 ] | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Baseline test (using Marko's recent rest case, that is IMHO oversimplified, the problem was to ALTER already COMPRESSED table originally) on 10.1 with NFS-mounted datadir:
Now the results with --innodb_use_fallocate=1 (rest of settings are the same as for my original test, as I've got no suggestions to the contrary):
With --innodb_use_fallocate=0 we have SLOWER execution for this test:
Now my ORIGINAL test case:
still shows some positive impact of NOT using posix_fallocate when ALTER is performed for COMPRESSED table of a proper size vs buffer pool (more carefully scaled down based on the real life use case):
368 vs 381 is some difference. So, question remains: should I use my original or Marko's test case on 10.4, 10.6 and 10.6 patched? I see no real point to use Marko's as it does NOT show any positive imact of innodb_use_fallocate = 0 on 10.1. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Comment by Marko Mäkelä [ 2023-12-19 ] | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
valerii, I believe that my test case should be equivalent to yours if you change the number of rows from 262144 to 1048576. I thought that the interesting part is the file extension in ALTER TABLE, not the compression of data pages before that, or the allocation of index record structures in the InnoDB buffer pool for your variant of INSERT…SELECT. One parameter that I completely forgot about is the size of the buffer pool. On 10.6 my variant would be even faster (and a somewhat unfair comparison to 10.4) because it enables the Can you run any version under strace -e fallocate,pwrite64 or similar, to see if the fallocate system call is reporting EOPNOTSUPP all the time, like https://www.linux-nfs.org/wiki/index.php/Fallocate suggests it could? Can you run 10.6 with the HAVE_POSIX_FALLOCATE logic disabled? If it is faster, then that would be a strong reason to bring back the setting innodb_use_fallocate=OFF so that this ‘shortcut’ system call could be brought back. An alternative would be that we keep track of file systems that support fallocate(). We already keep track of non-rotational devices (SSD) so that innodb_flush_neighbours=ON will be ignored on them. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Comment by Marko Mäkelä [ 2023-12-21 ] | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
valerii, could you execute finish in GDB when the breakpoint on fallocate64 is hit, so that we will see if it succeeds or fails? I assume that it fails on this setup, but I would like to be sure about that. You could also have checked the backtrace to ensure that the caller is posix_fallocate() inside InnoDB. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Comment by Marko Mäkelä [ 2024-01-12 ] | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
The customer replied that with a custom build that disables the posix_fallocate() calls, their ALTER TABLE data rate would be improved by almost 4×. The patch was as follows:
Based on this, it makes sense to add back the Boolean parameter innodb_use_fallocate, and default it to ON. This customer would set that parameter to OFF in their environment. ralf.gebhardt and serg, I wonder if we could handle this rather trivial change a bug fix. After all, it is fixing a regression that had been introduced in MariaDB Server 10.2. Basically, we would introduce a Boolean parameter and add a runtime check for it in the infrequently called function os_file_set_size(). | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Comment by Marko Mäkelä [ 2024-01-12 ] | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There is an alternative that on Linux, we invoke fallocate(2) instead of posix_fallocate(). We do not know yet if the performance problem is caused by the fallocate() system call itself or by the EOPNOTSUPP fallback in GNU libc posix_fallocate(). I hope that the customer can test another build that implements this. Fewer configuration parameters would be better. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Comment by Marko Mäkelä [ 2024-01-12 ] | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
For the record, in GNU libc there is a generic implementation sysdeps/posix/posix_fallocate64.c that invokes pwrite() of 1 byte at a time, as well as a Linux specific sysdeps/unix/sysv/linux/posix_fallocate64.c that first tries fallocate() and then falls back to the generic implementation, which a #define renames to internal_fallocate64. There also exist variants without the 64 suffix. In case the second custom build (of invoking fallocate() instead of posix_fallocate()) turns out to fix the performance regression, I have a hypothesis that could explain it. Quoting sysdeps/posix/posix_fallocate64.c:
Let us assume we write 1 byte every 4096 bytes. If the file is extended to 4 megabytes, it would be done by 1024 writes of 1 byte, every 4096 bytes. In case the NFS server uses 512-byte allocation block size, the physical size of the file would be 1024*512 = 0.5 megabytes instead of 4 megabytes, and the file would consist of 1024 fragments. When the file is being written with actual data later, it would have to be "defragmented" or "unsparsed" by the file system, which can be very expensive and depending on the file system implementation, block any concurrent I/O on that file system. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Comment by Sergei Golubchik [ 2024-01-12 ] | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Just for the record, I think that preallocating space only ever makes sense for local storage, we don't know anything about physical layout of the network storage and cannot assume that preallocating there helps or even that it does anything at all. That is, I think InnoDB should do fallocate() and if that fails — do not fallback to pwrite() | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Comment by Marko Mäkelä [ 2024-01-18 ] | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
We got feedback from the customer. With the second custom build, they are back to 4× the write rate of the normal build that uses posix_fallocate(). So, my hypothesis about the sparse files seems to be right. The patch was as follows:
I think that we’d better apply the fix to all our releases. We would not need any new configuration option at this point. The fallback code at the end of os_file_set_size(), which is writing 1-megabyte blocks of NUL bytes, has been demonstrated to work on Alpine Linux (which uses musl libc) and on some version of Docker for Microsoft Windows, if you look at serg, sure, in a separate feature request we could introduce a configuration option to logically extend files (ftruncate(), which we currently use for extending files of page_compressed tables). On an update-in-place file system such as Linux ext4, that could lead to more likely breakage the a design assumption of InnoDB that writes cannot fail due to running out of space. |