From c8bfe893d75ebbe6ffcf4b4cf3c025151bce3d4c Mon Sep 17 00:00:00 2001 From: Debian Date: Wed, 5 Aug 2026 10:12:22 +0000 Subject: [PATCH v22092026 10/11] pg_basebackup: preallocate DIO writes also in case of writing tar file format The DIO from previous commit made tar-format backups slower than the buffered path it replaced (~410MB/s versus 982MB/s earlier) - the gains were only visible in the plain format. The problem is related that DIO writes should avoid extending files, but in case of tar format we do not know the target (local) tar file that we are producing (unlike like in plain format, where we know this), so we couldn't preallocate the file properly which tanked write performance (O_DIRECT writes extending file end up taking exclusive kernel inode mutex at least on ext4 fs, so apparently even with with plenty of async DIO requests in flight they all ended up being serialized with depth queue of 1). Add dio_fruncate() which - in the tar format case - performs allocation from time to time, to be ahead of the writes itself. When final output file size is known (plain format) we just preallocate after open() once. In case of O_DIRECT without posix_fallocate() working, dio_writer_start() will now fallback to buffered I/O too as this is going to be faster. Overall this brings -Ft back to expected 2.3-2.6GB/s. Author: Jakub Wartak --- src/fe_utils/astreamer_file.c | 97 ++++++++++++++++++++++++++++++----- 1 file changed, 83 insertions(+), 14 deletions(-) diff --git a/src/fe_utils/astreamer_file.c b/src/fe_utils/astreamer_file.c index d4d34e5d412..efbd9e43a97 100644 --- a/src/fe_utils/astreamer_file.c +++ b/src/fe_utils/astreamer_file.c @@ -38,6 +38,7 @@ #define DIO_NBUF 32 /* queue depth, XXX:expose it via getopt? */ #define DIO_MIN_SIZE DIO_BUFSZ /* fsize threshold for activating O_DIRECT writes */ #define DIO_SUBMIT_BATCH 8 /* how many SQEs to batch */ +#define DIO_PREALLOC_CHUNK (128 * 1024 * 1024) /* grow step, unknown size */ /* * State for the dio/io_uring writer. Used by plain(file) and tar extractors. @@ -55,10 +56,12 @@ typedef struct dio_writer const char *filename; /* for error handling */ pgoff_t offset; /* offset of the next write */ pgoff_t written; /* bytes written */ + pgoff_t allocated; /* file size space preallocated so far */ int curidx; /* buffer being filled, or -1 */ int curlen; /* bytes filled in current buffer */ int inflight; /* prepared writes not yet reaped */ int unsubmitted; /* SQEs prepared but not yet submitted */ + bool nofalloc; /* fallocate unsupported */ bool notified; /* already logged a fall-back-to-buffered? */ } dio_writer; #endif @@ -706,6 +709,59 @@ dio_get_free_buf(dio_writer *dw) } } +/* + * Ensure the file has some space ahead allocated to avoid perofrmance issues + * with O_DIRECT writes. Returns false if space cannot be allocated and in such + * scenario DIO writes should not be used as the are *slower* than buffered + * writes (outcome of many performance runs). The reason is that at least on + * Linux, async O_DIRECT writes that extend current file size and may end up + * allocating space, are queued and the depth drops to 1 due to file extension + * /space allocation happening for 1 file. + * + * This is used from dio_writer_start() and from regular dio_submit(). + * + * End-file size is known in case of plain files (tar format sends + * member->size), however in -Ft (tar) writing mode, we do knot know the + * final target tar file size, so we grow it by DIO_PREALLOC_CHUNK from time + * to time. In case file is overextended, we truncate it back to proper size + * in dio_writer_finish(). + */ +static bool +dio_fallocate(dio_writer *dw, pgoff_t end) +{ +#ifdef HAVE_POSIX_FALLOCATE + pgoff_t want; + int rc; + + if (end <= dw->allocated) + return true; + if (dw->nofalloc) + return false; + + /* Round up to a whole chunk to reduce number of fallocate calls */ + want = Max(end, dw->allocated + DIO_PREALLOC_CHUNK); + + rc = posix_fallocate(dw->fd, dw->allocated, want - dw->allocated); + if (rc != 0) + { + /* posix_fallocate() does not set errno */ + errno = rc; + + if (rc == ENOSPC) + pg_fatal("could not preallocate file \"%s\": %m", dw->filename); + + dw->nofalloc = true; + return false; + } + + dw->allocated = want; + return true; +#else + dw->nofalloc = true; + return false; +#endif +} + /* * Prepare the SQE from the buffer with full O_DIRECT write. We really * submit the SQEs to the kernel only (flush them) only once every @@ -719,6 +775,12 @@ dio_submit(dio_writer *dw, int idx, size_t len) { struct io_uring_sqe *sqe; + /* + * Keep the allocation ahead of the write cursor. This is no-op when the + * file was already preallocated to its final size at open() time (plain format). + */ + dio_fallocate(dw, dw->offset + len); + sqe = io_uring_get_sqe(&dw->ring); /* this should never happen? liburing/examples uses abort/asserts for this */ if (sqe == NULL) @@ -751,6 +813,9 @@ dio_writer_start(dio_writer *dw, const char *filename, pgoff_t prealloc_size, bo int fd; int ret; struct stat st; + /* preallocation in case of DIO also have to be aligned to DIO size */ + pgoff_t prealloc_size_aligned = TYPEALIGN64(DIO_ALIGN, prealloc_size); + pgoff_t prealloc_len = prealloc_size > 0 ? prealloc_size_aligned : DIO_PREALLOC_CHUNK; fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC | O_DIRECT | PG_BINARY, pg_file_create_mode); @@ -811,26 +876,30 @@ dio_writer_start(dio_writer *dw, const char *filename, pgoff_t prealloc_size, bo pg_log_info("using O_DIRECT with io_uring for large file writes"); } - /* If size is known (in plain mode), preallocate the space */ -#ifdef HAVE_POSIX_FALLOCATE - if (prealloc_size > 0) - { - int rc = posix_fallocate(fd, 0, prealloc_size); - - if (rc == ENOSPC) - { - errno = rc; - pg_fatal("could not preallocate file \"%s\": %m", filename); - } - } -#endif - dw->fd = fd; dw->filename = filename; dw->offset = 0; dw->written = 0; + dw->allocated = 0; dw->curidx = -1; dw->curlen = 0; + + /* Preallocate file to avoid O_DIRECT performance woes */ + if (!dio_fallocate(dw, prealloc_len)) + { + if (verbose && !dw->notified) + { + pg_log_info("could not preallocate \"%s\", using buffered I/O instead: %m", + filename); + dw->notified = true; + } + dw->fd = -1; + close(fd); + return false; + } + + Assert(dw->inflight == 0); + Assert(dw->unsubmitted == 0); return true; } -- 2.43.5