Hi, This patch implements async fsyncs for crash recovery and checkpoints by using the AIO system. I got the idea from Andres' older work [1].
----------
High level design and implementation details:
- Since there is no interface like read-stream for the fsyncs, each
user has its own AIO functions like register_one(), drain_one() and
drain_all().
- Number of in-flight I/Os are determined by
GetFsyncConcurrencyLimit(). It uses both io_max_concurrency and the
file descriptor budget to determine max number of available in-flight
fsync I/Os.
- I/O workers need re-open files to do async I/O. Some targets open
files by path (for example, SyncDataDirectory()). This path needs to
be copied to shared memory so that worker processes can re-open these
files. However, adding 'char path[MAXPGPATH]' to PgAioTargetData seems
overkill because of the size; so it is not implemented for now. I am
open to suggestions.
----------
Patches:
- 0001 is just a base patch for adding fsync support to the AIO
system. There is no user of this patch yet.
- 0002 makes SyncDataDirectory() use async fsyncs. Its files are
identified only by path; I/O workers cannot re-open them. So, this
path is asynchronous with io_uring, while worker behaves like sync.
pre_sync_fname() doesn't use AIO because it is already very fast, less
than 1/1000 of of datadir_fsync_fname() time in my benchmarks.
- 0003 makes ProcessSyncRequests() use async fsyncs. Its files use the
smgr target and can be re-opened by an I/O worker, so both worker and
io_uring can execute these fsyncs asynchronously.
- 0004 adds a FileTag-based target for SLRU files. This gives I/O
workers enough information to re-open SLRU segments, so they can use
async fsyncs in the worker mode.
- benchmark-repro.sh is a benchmark script that I used.
----------
Benchmark:
I used a simple benchmark script generated by an LLM. I checked the
script, and it works correctly. The numbers below use the default
settings + the following:
```
checkpoint_timeout = '1h'
max_wal_size = '32GB'
checkpoint_flush_after = 0
bgwriter_lru_maxpages = 0
autovacuum = off
```
io_min_workers, io_max_workers and io_max_concurrency are default in
[2]. They are changed to 8, 8, and 64, respectively, for the second
benchmark [3].
You can run the attached benchmark script by './benchmark-repro.sh
{--syncdatadirectory, --checkpointer, --slru}' command.
- SyncDataDirectory() synced a data directory containing 4000 small relations:
[2]
io_method | startup (ms) | speedup vs sync
-----------+----------------------+----------------
sync | 1411.8 | 1.00x
worker | 1210.6 | 1.17x
io_uring | 310.3 | 4.55
[3]
io_method | startup (ms) | speedup vs sync
-----------+----------------------+----------------
sync | 1310.6 | 1.00x
worker | 1210.0 | 1.08x
io_uring | 310.6 | 4.22x
- Checkpointer benchmark dirtied 4000 relation files before checkpoint:
[2]
io_method | checkpoint (ms) | speedup vs sync
-----------+----------------------+----------------
sync | 1860.5 | 1.00x
worker | 697.1 | 2.67x
io_uring | 478.4 | 3.89x
[3]
io_method | checkpoint (ms) | speedup vs sync
-----------+----------------------+----------------
sync | 1875.5 | 1.00x
worker | 541.1 | 3.47x
io_uring | 479.9 | 3.91x
- SLRU benchmark created 640000 transactions, producing ~30 files to sync:
[2]
io_method | SLRU checkpoint (ms) | speedup vs sync
-----------+----------------------+----------------
sync | 94.1 | 1.00x
worker | 67.1 | 1.40x
io_uring | 57.1 | 1.65x
[3]
io_method | SLRU checkpoint (ms) | speedup vs sync
-----------+----------------------+----------------
sync | 97.3 | 1.00x
worker | 48.7 | 2.00x
io_uring | 37.4 | 2.60x
Any feedback would be appreciated.
[1]
https://github.com/anarazel/postgres/commit/9829a1f18176e572759ecf8b8bb99205a71a476f
https://github.com/anarazel/postgres/commit/2e180547aef6fe2a409cc144e04cbc2f0a607bed
--
Regards,
Nazir Bilal Yavuz
Microsoft
From d3197f4e6fb6a24d5ea35448b521edeca9717aeb Mon Sep 17 00:00:00 2001 From: Nazir Bilal Yavuz <[email protected]> Date: Tue, 11 Aug 2026 10:40:14 +0300 Subject: [PATCH v1 1/4] aio: Add fsync support The AIO subsystem currently supports only reads and writes (writes are not used yet). Add PGAIO_OP_FSYNC so callers can submit fsync() and fdatasync() operations through AIO. This allows callers such as the checkpointer to keep multiple syncs in flight instead of waiting for each one in turn. Let callers select the wait event because fsync targets can represent different kinds of files. The process that performs the operation reports that event; io_uring uses the generic AIO wait events because the kernel performs the operation. (Also see [1]) No callers are converted in this commit; subsequent commits do that. [1] https://postgr.es/m/can55fz0rp+94rdq-zetx0sf9h1e4uwzxh8r1cvxzx0swfqw...@mail.gmail.com --- src/backend/storage/aio/aio_funcs.c | 4 ++++ src/backend/storage/aio/aio_io.c | 27 +++++++++++++++++++++++ src/backend/storage/aio/method_io_uring.c | 6 +++++ src/backend/storage/smgr/smgr.c | 3 +++ src/include/storage/aio.h | 14 ++++++++++-- 5 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/backend/storage/aio/aio_funcs.c b/src/backend/storage/aio/aio_funcs.c index bcdd82318f7..2719556e858 100644 --- a/src/backend/storage/aio/aio_funcs.c +++ b/src/backend/storage/aio/aio_funcs.c @@ -191,6 +191,10 @@ retry: values[6] = Int64GetDatum(iov_byte_length(iov_copy, ioh_copy.op_data.write.iov_length)); break; + case PGAIO_OP_FSYNC: + nulls[5] = true; + nulls[6] = true; + break; } /* column: IO's target */ diff --git a/src/backend/storage/aio/aio_io.c b/src/backend/storage/aio/aio_io.c index 132868130e7..324fb6911e2 100644 --- a/src/backend/storage/aio/aio_io.c +++ b/src/backend/storage/aio/aio_io.c @@ -100,6 +100,19 @@ pgaio_io_start_writev(PgAioHandle *ioh, pgaio_io_stage(ioh, PGAIO_OP_WRITEV); } +void +pgaio_io_start_fsync(PgAioHandle *ioh, + int fd, bool datasync, uint32 wait_event_info) +{ + pgaio_io_before_start(ioh); + + ioh->op_data.fsync.fd = fd; + ioh->op_data.fsync.datasync = datasync; + ioh->op_data.fsync.wait_event_info = wait_event_info; + + pgaio_io_stage(ioh, PGAIO_OP_FSYNC); +} + /* -------------------------------------------------------------------------------- @@ -137,6 +150,14 @@ pgaio_io_perform_synchronously(PgAioHandle *ioh) ioh->op_data.write.offset); pgstat_report_wait_end(); break; + case PGAIO_OP_FSYNC: + pgstat_report_wait_start(ioh->op_data.fsync.wait_event_info); + if (ioh->op_data.fsync.datasync) + result = pg_fdatasync(ioh->op_data.fsync.fd); + else + result = pg_fsync(ioh->op_data.fsync.fd); + pgstat_report_wait_end(); + break; case PGAIO_OP_INVALID: elog(ERROR, "trying to execute invalid IO operation"); } @@ -189,6 +210,8 @@ pgaio_io_get_op_name(PgAioHandle *ioh) return "readv"; case PGAIO_OP_WRITEV: return "writev"; + case PGAIO_OP_FSYNC: + return "fsync"; } return NULL; /* silence compiler */ @@ -209,6 +232,8 @@ pgaio_io_uses_fd(PgAioHandle *ioh, int fd) return ioh->op_data.read.fd == fd; case PGAIO_OP_WRITEV: return ioh->op_data.write.fd == fd; + case PGAIO_OP_FSYNC: + return ioh->op_data.fsync.fd == fd; case PGAIO_OP_INVALID: return false; } @@ -233,6 +258,8 @@ pgaio_io_get_iovec_length(PgAioHandle *ioh, struct iovec **iov) return ioh->op_data.read.iov_length; case PGAIO_OP_WRITEV: return ioh->op_data.write.iov_length; + case PGAIO_OP_FSYNC: + return 0; default: pg_unreachable(); return 0; diff --git a/src/backend/storage/aio/method_io_uring.c b/src/backend/storage/aio/method_io_uring.c index 3ffe5061a20..675a91fb5f7 100644 --- a/src/backend/storage/aio/method_io_uring.c +++ b/src/backend/storage/aio/method_io_uring.c @@ -802,6 +802,12 @@ pgaio_uring_sq_from_io(PgAioHandle *ioh, struct io_uring_sqe *sqe) break; + case PGAIO_OP_FSYNC: + io_uring_prep_fsync(sqe, + ioh->op_data.fsync.fd, + ioh->op_data.fsync.datasync ? IORING_FSYNC_DATASYNC : 0); + break; + case PGAIO_OP_INVALID: elog(ERROR, "trying to prepare invalid IO operation for execution"); } diff --git a/src/backend/storage/smgr/smgr.c b/src/backend/storage/smgr/smgr.c index 5391640d861..69e61ea1661 100644 --- a/src/backend/storage/smgr/smgr.c +++ b/src/backend/storage/smgr/smgr.c @@ -1094,6 +1094,9 @@ smgr_aio_reopen(PgAioHandle *ioh) od->write.fd = smgrfd(reln, sd->smgr.forkNum, sd->smgr.blockNum, &off); Assert(off == od->write.offset); break; + case PGAIO_OP_FSYNC: + od->fsync.fd = smgrfd(reln, sd->smgr.forkNum, sd->smgr.blockNum, &off); + break; } } diff --git a/src/include/storage/aio.h b/src/include/storage/aio.h index ec543b78409..a1d5f49e51a 100644 --- a/src/include/storage/aio.h +++ b/src/include/storage/aio.h @@ -91,10 +91,10 @@ typedef enum PgAioOp PGAIO_OP_READV, PGAIO_OP_WRITEV, + PGAIO_OP_FSYNC, /** * In the near term we'll need at least: - * - fsync / fdatasync * - flush_range * * Eventually we'll additionally want at least: @@ -104,7 +104,7 @@ typedef enum PgAioOp **/ } PgAioOp; -#define PGAIO_OP_COUNT (PGAIO_OP_WRITEV + 1) +#define PGAIO_OP_COUNT (PGAIO_OP_FSYNC + 1) /* @@ -146,6 +146,13 @@ typedef union uint16 iov_length; uint64 offset; } write; + + struct + { + int fd; + bool datasync; + uint32 wait_event_info; + } fsync; } PgAioOpData; @@ -300,6 +307,9 @@ extern void pgaio_io_start_readv(PgAioHandle *ioh, int fd, int iovcnt, uint64 offset); extern void pgaio_io_start_writev(PgAioHandle *ioh, int fd, int iovcnt, uint64 offset); +extern void pgaio_io_start_fsync(PgAioHandle *ioh, int fd, bool datasync, + uint32 wait_event_info); + /* functions in aio_target.c */ extern void pgaio_io_set_target(PgAioHandle *ioh, PgAioTargetID targetid); -- 2.47.3
From 4a3f5e41edbef815059ab4ed4c64abc85e5e227a Mon Sep 17 00:00:00 2001 From: Nazir Bilal Yavuz <[email protected]> Date: Wed, 19 Aug 2026 15:56:39 +0300 Subject: [PATCH v1 2/4] Issue SyncDataDirectory() fsyncs through AIO After a crash, the startup process fsyncs every file in the data directory serially. Submit the fsyncs through AIO and keep a bounded ring of operations in flight. Each operation holds a transient file descriptor, while the directory walk needs descriptors of its own. Limit the ring with GetFsyncConcurrencyLimit() and wait before opening the next file to avoid exhausting the descriptor reserve. The files are identified by paths held only in the startup process. PGAIO_TID_SYNC does not copy those paths into shared memory, so the worker method cannot reopen the files and executes the fsyncs synchronously in the startup process. The io_uring method can execute them concurrently. Storing MAXPGPATH bytes in every AIO target does not seem justified for this startup-only operation. Use the DATA_DIR_SYNC wait event when an fsync is executed synchronously. --- src/backend/storage/aio/aio_target.c | 24 ++ src/backend/storage/file/fd.c | 277 +++++++++++++++--- .../utils/activity/wait_event_names.txt | 1 + src/include/storage/aio.h | 3 +- src/include/storage/fd.h | 1 + src/tools/pgindent/typedefs.list | 2 + 6 files changed, 265 insertions(+), 43 deletions(-) diff --git a/src/backend/storage/aio/aio_target.c b/src/backend/storage/aio/aio_target.c index fa98f010d5a..82b24b0026d 100644 --- a/src/backend/storage/aio/aio_target.c +++ b/src/backend/storage/aio/aio_target.c @@ -18,6 +18,17 @@ #include "storage/aio_internal.h" #include "storage/smgr.h" +static char *pgaio_sync_describe_identity(const PgAioTargetData *sd); + +/* + * Target info for generic file syncs (PGAIO_TID_SYNC). The file being synced + * is identified by a path that is not stored in shared memory, therefore no + * reopen callback is provided. + */ +static const PgAioTargetInfo aio_sync_target_info = { + .name = "sync", + .describe_identity = pgaio_sync_describe_identity, +}; /* * Registry for entities that can be the target of AIO. @@ -27,9 +38,22 @@ static const PgAioTargetInfo *pgaio_target_info[] = { .name = "invalid", }, [PGAIO_TID_SMGR] = &aio_smgr_target_info, + [PGAIO_TID_SYNC] = &aio_sync_target_info, }; +/* + * describe_identity callback for PGAIO_TID_SYNC. As we do not store the path + * of the file being synced in shared memory, only a generic description can + * be provided. + */ +static char * +pgaio_sync_describe_identity(const PgAioTargetData *sd) +{ + return pstrdup("generic file sync"); +} + + /* -------------------------------------------------------------------------------- * Public target related functions operating on IO Handles diff --git a/src/backend/storage/file/fd.c b/src/backend/storage/file/fd.c index 190c9974494..0fcae14541d 100644 --- a/src/backend/storage/file/fd.c +++ b/src/backend/storage/file/fd.c @@ -268,6 +268,31 @@ typedef struct } desc; } AllocateDesc; +/* + * State for asynchronously issuing the fsync()s in SyncDataDirectory(). We + * keep a bounded number of fsync IOs in flight, using a simple ring of + * entries; when the ring is full, we wait for the oldest IO to complete + * before issuing another. + */ +typedef struct DataDirSyncEntry +{ + int fd; /* transient fd being synced */ + bool isdir; /* is this a directory? */ + char path[MAXPGPATH]; /* path to file being synced */ + PgAioReturn ioret; + PgAioWaitRef iow; + bool in_use; /* is this entry being used */ +} DataDirSyncEntry; + +typedef struct DataDirSyncState +{ + int elevel; /* level to log errors at */ + int max_inflight; /* size of the ring */ + int head; /* index of oldest in-flight entry */ + int count; /* number of in-flight entries */ + DataDirSyncEntry *entries; /* ring of max_inflight entries */ +} DataDirSyncState; + static int numAllocatedDescs = 0; static int maxAllocatedDescs = 0; static AllocateDesc *allocatedDescs = NULL; @@ -346,14 +371,20 @@ static void RemovePgTempRelationFiles(const char *tsdirname); static void RemovePgTempRelationFilesInDbspace(const char *dbspacedirname); static void walkdir(const char *path, - void (*action) (const char *fname, bool isdir, int elevel), + void (*action) (const char *fname, bool isdir, int elevel, DataDirSyncState *state), bool process_symlinks, - int elevel); + int elevel, + DataDirSyncState *state); #ifdef PG_FLUSH_DATA_WORKS -static void pre_sync_fname(const char *fname, bool isdir, int elevel); +static void pre_sync_fname(const char *fname, bool isdir, int elevel, DataDirSyncState *state); #endif -static void datadir_fsync_fname(const char *fname, bool isdir, int elevel); -static void unlink_if_exists_fname(const char *fname, bool isdir, int elevel); +static void datadir_fsync_fname(const char *fname, bool isdir, int elevel, DataDirSyncState *state); +static void unlink_if_exists_fname(const char *fname, bool isdir, int elevel, DataDirSyncState *state); +static bool fsync_fname_open(const char *fname, bool isdir, bool ignore_perm, + int elevel, int *ret); +static bool fsync_fname_close(const char *fname, bool isdir, int elevel, int fd, int returncode); +static void datadir_sync_wait_one(DataDirSyncState *state); +static void datadir_sync_drain_all(DataDirSyncState *state); static int fsync_parent_path(const char *fname, int elevel); @@ -1691,7 +1722,7 @@ PathNameDeleteTemporaryDir(const char *dirname) * used in a cleanup path, we wouldn't actually behave differently: we'll * just log failures. */ - walkdir(dirname, unlink_if_exists_fname, false, LOG); + walkdir(dirname, unlink_if_exists_fname, false, LOG, NULL); } /* @@ -3566,6 +3597,24 @@ do_syncfs(const char *path) } #endif +/* + * Return a safe upper bound for the number of fsyncs to keep in flight. + * + * Callers that fsync files opened with OpenTransientFile() hold one + * AllocateDesc for each in-flight IO. At most max_safe_fds / 3 of those can + * be allocated at a time (see reserveAllocatedDesc()), and the callers of + * interest also traverse directories (see walkdir()), which needs + * AllocateDescs of its own. So, hand out at most half of the budget. + * + * XXX: This is too safe for places that don't use OpenTransientFile() and/or + * walkdir(). + */ +int +GetFsyncConcurrencyLimit(void) +{ + return Max(1, Min(io_max_concurrency, max_safe_fds / 6)); +} + /* * Issue fsync recursively on PGDATA and all its contents, or issue syncfs for * all potential filesystem, depending on recovery_init_sync_method setting. @@ -3592,6 +3641,7 @@ void SyncDataDirectory(void) { bool xlog_is_symlink; + DataDirSyncState sync_state_data = {0}; /* We can skip this whole thing if fsync is disabled. */ if (!enableFsync) @@ -3663,15 +3713,22 @@ SyncDataDirectory(void) * directory and its contents. Errors in this step are even less * interesting than normal, so log them only at DEBUG1. */ - walkdir(".", pre_sync_fname, false, DEBUG1); + walkdir(".", pre_sync_fname, false, DEBUG1, NULL); if (xlog_is_symlink) - walkdir("pg_wal", pre_sync_fname, false, DEBUG1); - walkdir(PG_TBLSPC_DIR, pre_sync_fname, true, DEBUG1); + walkdir("pg_wal", pre_sync_fname, false, DEBUG1, NULL); + walkdir(PG_TBLSPC_DIR, pre_sync_fname, true, DEBUG1, NULL); #endif /* Prepare to report progress syncing the data directory via fsync. */ begin_startup_progress_phase(); + sync_state_data.elevel = LOG; + sync_state_data.max_inflight = GetFsyncConcurrencyLimit(); + sync_state_data.head = 0; + sync_state_data.count = 0; + sync_state_data.entries = palloc0(sizeof(DataDirSyncEntry) * + sync_state_data.max_inflight); + /* * Now we do the fsync()s in the same order. * @@ -3681,10 +3738,14 @@ SyncDataDirectory(void) * in pg_tblspc, they'll get fsync'd twice. That's not an expected case * so we don't worry about optimizing it. */ - walkdir(".", datadir_fsync_fname, false, LOG); + walkdir(".", datadir_fsync_fname, false, LOG, &sync_state_data); if (xlog_is_symlink) - walkdir("pg_wal", datadir_fsync_fname, false, LOG); - walkdir(PG_TBLSPC_DIR, datadir_fsync_fname, true, LOG); + walkdir("pg_wal", datadir_fsync_fname, false, LOG, &sync_state_data); + walkdir(PG_TBLSPC_DIR, datadir_fsync_fname, true, LOG, &sync_state_data); + + datadir_sync_drain_all(&sync_state_data); + + pfree(sync_state_data.entries); } /* @@ -3704,9 +3765,10 @@ SyncDataDirectory(void) */ static void walkdir(const char *path, - void (*action) (const char *fname, bool isdir, int elevel), + void (*action) (const char *fname, bool isdir, int elevel, DataDirSyncState *state), bool process_symlinks, - int elevel) + int elevel, + DataDirSyncState *state) { DIR *dir; struct dirent *de; @@ -3728,10 +3790,10 @@ walkdir(const char *path, switch (get_dirent_type(subpath, de, process_symlinks, elevel)) { case PGFILETYPE_REG: - (*action) (subpath, false, elevel); + (*action) (subpath, false, elevel, state); break; case PGFILETYPE_DIR: - walkdir(subpath, action, false, elevel); + walkdir(subpath, action, false, elevel, state); break; default: @@ -3753,7 +3815,7 @@ walkdir(const char *path, * might not be robust against that. */ if (dir) - (*action) (path, true, elevel); + (*action) (path, true, elevel, state); } @@ -3766,10 +3828,12 @@ walkdir(const char *path, #ifdef PG_FLUSH_DATA_WORKS static void -pre_sync_fname(const char *fname, bool isdir, int elevel) +pre_sync_fname(const char *fname, bool isdir, int elevel, DataDirSyncState *state) { int fd; + Assert(!state); + /* Don't try to flush directories, it'll likely just fail */ if (isdir) return; @@ -3804,21 +3868,98 @@ pre_sync_fname(const char *fname, bool isdir, int elevel) #endif /* PG_FLUSH_DATA_WORKS */ static void -datadir_fsync_fname(const char *fname, bool isdir, int elevel) +datadir_sync_wait_one(DataDirSyncState *state) +{ + DataDirSyncEntry *entry; + + Assert(state->count > 0); + + entry = &state->entries[state->head]; + Assert(entry->in_use); + + pgaio_wref_wait(&entry->iow); + + /* + * As we didn't register a completion callback, the IO's status is always + * PGAIO_RS_OK; the raw fsync() return (0 or -errno) is available in + * ->result.result. Use the same error handling as fsync_fname_ext(). + */ + if (entry->ioret.result.result < 0) + errno = -entry->ioret.result.result; + + fsync_fname_close(entry->path, entry->isdir, state->elevel, entry->fd, entry->ioret.result.result); + + entry->in_use = false; + state->head = (state->head + 1) % state->max_inflight; + state->count--; +} + +static void +datadir_sync_drain_all(DataDirSyncState *state) +{ + while (state->count > 0) + datadir_sync_wait_one(state); + + Assert(state->count == 0); +} + +static void +datadir_fsync_fname(const char *fname, bool isdir, int elevel, DataDirSyncState *state) { + int fd; + int slot; + DataDirSyncEntry *entry; + PgAioHandle *ioh; + ereport_startup_progress("syncing data directory (fsync), elapsed time: %ld.%02d s, current path: %s", fname); /* - * We want to silently ignoring errors about unreadable files. Pass that - * desire on to fsync_fname_ext(). + * If the ring is full, wait before opening the next file, so that the + * number of open descriptors never temporarily exceeds the ring size. */ - fsync_fname_ext(fname, isdir, true, elevel); + if (state->count == state->max_inflight) + datadir_sync_wait_one(state); + + /* + * We want to silently ignore errors about unreadable files. Pass that + * desire on to fsync_fname_open(). + */ + if (!fsync_fname_open(fname, isdir, true, elevel, &fd)) + return; + + Assert(state->count < state->max_inflight); + slot = (state->head + state->count) % state->max_inflight; + entry = &state->entries[slot]; + Assert(!entry->in_use); + + entry->fd = fd; + entry->isdir = isdir; + strlcpy(entry->path, fname, MAXPGPATH); + entry->in_use = true; + + ioh = pgaio_io_acquire(CurrentResourceOwner, &entry->ioret); + pgaio_io_set_target(ioh, PGAIO_TID_SYNC); + pgaio_io_get_wref(ioh, &entry->iow); + + /* + * Interrupts must be held across staging the IO, so that the file + * descriptor it references cannot be closed by interrupt processing + * before the IO has been submitted. + */ + HOLD_INTERRUPTS(); + pgaio_io_start_fsync(ioh, fd, false, WAIT_EVENT_DATA_DIR_SYNC); + RESUME_INTERRUPTS(); + + state->count++; } static void -unlink_if_exists_fname(const char *fname, bool isdir, int elevel) +unlink_if_exists_fname(const char *fname, bool isdir, int elevel, DataDirSyncState *state) { + + Assert(!state); + if (isdir) { if (rmdir(fname) != 0 && errno != ENOENT) @@ -3834,19 +3975,24 @@ unlink_if_exists_fname(const char *fname, bool isdir, int elevel) } /* - * fsync_fname_ext -- Try to fsync a file or directory + * Helper for opening a file as part of fsync_fname_ext() and + * datadir_fsync_fname(). Split out because the latter performs the fsync + * asynchronously via AIO. * - * If ignore_perm is true, ignore errors upon trying to open unreadable - * files. Logs other errors at a caller-specified level. + * If ignore_perm is true, ignore errors upon trying to open unreadable files. * - * Returns 0 if the operation succeeded, -1 otherwise. + * If the file could not be opened, false is returned; *fd is set to 0 if the + * failure should be ignored, or -1 otherwise. Other errors are logged at the + * caller-specified level. + * + * If the file was opened successfully, true is returned and *fd is set to + * the file descriptor. */ -int -fsync_fname_ext(const char *fname, bool isdir, bool ignore_perm, int elevel) +static bool +fsync_fname_open(const char *fname, bool isdir, bool ignore_perm, + int elevel, int *fd) { - int fd; int flags; - int returncode; /* * Some OSs require directories to be opened read-only whereas other @@ -3860,27 +4006,48 @@ fsync_fname_ext(const char *fname, bool isdir, bool ignore_perm, int elevel) else flags |= O_RDONLY; - fd = OpenTransientFile(fname, flags); + *fd = OpenTransientFile(fname, flags); /* * Some OSs don't allow us to open directories at all (Windows returns * EACCES), just ignore the error in that case. If desired also silently * ignoring errors about unreadable files. Log others. */ - if (fd < 0 && isdir && (errno == EISDIR || errno == EACCES)) - return 0; - else if (fd < 0 && ignore_perm && errno == EACCES) - return 0; - else if (fd < 0) + if (*fd >= 0) + return true; + if (isdir && (errno == EISDIR || errno == EACCES)) { + *fd = 0; + return false; + } + else if (ignore_perm && errno == EACCES) + { + *fd = 0; + return false; + } + else + { + *fd = -1; ereport(elevel, (errcode_for_file_access(), errmsg("could not open file \"%s\": %m", fname))); - return -1; + return false; } +} - returncode = pg_fsync(fd); - +/* + * Helper for closing a file, and reporting any fsync failure, as part of + * fsync_fname_ext() and datadir_fsync_fname(). Split out because the latter + * performs the fsync asynchronously via AIO. + * + * "returncode" is the result of the preceding fsync; if it is negative, errno + * must describe the failure. + * + * Returns true if the operation succeeded, false otherwise. + */ +static bool +fsync_fname_close(const char *fname, bool isdir, int elevel, int fd, int returncode) +{ /* * Some OSes don't allow us to fsync directories at all, so we can ignore * those errors. Anything else needs to be logged. @@ -3897,7 +4064,7 @@ fsync_fname_ext(const char *fname, bool isdir, bool ignore_perm, int elevel) ereport(elevel, (errcode_for_file_access(), errmsg("could not fsync file \"%s\": %m", fname))); - return -1; + return false; } if (CloseTransientFile(fd) != 0) @@ -3905,9 +4072,35 @@ fsync_fname_ext(const char *fname, bool isdir, bool ignore_perm, int elevel) ereport(elevel, (errcode_for_file_access(), errmsg("could not close file \"%s\": %m", fname))); - return -1; + return false; } + return true; +} + +/* + * fsync_fname_ext -- Try to fsync a file or directory + * + * If ignore_perm is true, ignore errors upon trying to open unreadable + * files. Logs other errors at a caller-specified level. + * + * Returns 0 if the operation succeeded, -1 otherwise. + */ +int +fsync_fname_ext(const char *fname, bool isdir, bool ignore_perm, int elevel) +{ + int fd; + int returncode; + + if (!fsync_fname_open(fname, isdir, ignore_perm, elevel, &fd)) + return fd; + + Assert(fd >= 0); + returncode = pg_fsync(fd); + + if (!fsync_fname_close(fname, isdir, elevel, fd, returncode)) + return -1; + return 0; } diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index 256b3a3c02e..aede94dcfc3 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -219,6 +219,7 @@ COPY_FILE_READ "Waiting for a read during a file copy operation." COPY_FILE_WRITE "Waiting for a write during a file copy operation." COPY_FROM_READ "Waiting to read data from a pipe, a file or a program during COPY FROM." COPY_TO_WRITE "Waiting to write data to a pipe, a file or a program during COPY TO." +DATA_DIR_SYNC "Waiting for the contents of the data directory to reach durable storage after a crash." DATA_FILE_EXTEND "Waiting for a relation data file to be extended." DATA_FILE_FLUSH "Waiting for a relation data file to reach durable storage." DATA_FILE_IMMEDIATE_SYNC "Waiting for an immediate synchronization of a relation data file to durable storage." diff --git a/src/include/storage/aio.h b/src/include/storage/aio.h index a1d5f49e51a..428504c0472 100644 --- a/src/include/storage/aio.h +++ b/src/include/storage/aio.h @@ -118,9 +118,10 @@ typedef enum PgAioTargetID /* intentionally the zero value, to help catch zeroed memory etc */ PGAIO_TID_INVALID = 0, PGAIO_TID_SMGR, + PGAIO_TID_SYNC, } PgAioTargetID; -#define PGAIO_TID_COUNT (PGAIO_TID_SMGR + 1) +#define PGAIO_TID_COUNT (PGAIO_TID_SYNC + 1) /* diff --git a/src/include/storage/fd.h b/src/include/storage/fd.h index 8ac466fd346..c79f3312544 100644 --- a/src/include/storage/fd.h +++ b/src/include/storage/fd.h @@ -219,6 +219,7 @@ extern int fsync_fname_ext(const char *fname, bool isdir, bool ignore_perm, int extern int durable_rename(const char *oldfile, const char *newfile, int elevel); extern int durable_unlink(const char *fname, int elevel); extern void SyncDataDirectory(void); +extern int GetFsyncConcurrencyLimit(void); extern int data_sync_elevel(int elevel); static inline ssize_t diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 6c366d3a523..a95b09859b5 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -639,7 +639,9 @@ DataChecksumsStateStruct DataChecksumsWorkerDatabase DataChecksumsWorkerOperation DataChecksumsWorkerResult +DataDirSyncEntry DataDirSyncMethod +DataDirSyncState DataDumperPtr DataPageDeleteStack DataTypesUsageChecks -- 2.47.3
From 1845c07e610d9651759f9ce4239c042074916e7c Mon Sep 17 00:00:00 2001 From: Nazir Bilal Yavuz <[email protected]> Date: Tue, 25 Aug 2026 11:27:22 +0300 Subject: [PATCH v1 3/4] Issue checkpointer fsyncs asynchronously ProcessSyncRequests() previously fsynced pending files one at a time. For checkpoints with many files, this serialized I/O that storage could perform concurrently. Submit the fsyncs through AIO, keeping a bounded set in flight and reaping completions in submission order. Each in-flight SLRU fsync holds a transient file descriptor, so limit the depth with GetFsyncConcurrencyLimit() rather than io_max_concurrency to avoid exhausting the descriptor reserve. Reshape the sync handler API so handlers open the file, assign an AIO target, start the fsync, and record how to close it. sync.c manages the in-flight operations, errors, retries, and pendingOps bookkeeping. Absorbing requests while fsyncs are in flight requires some care: - Recheck cancellation when an operation is reaped because a request can be canceled after its fsync starts. - Defer completion bookkeeping until the pendingOps scan ends because dynahash permits removing only the entry most recently returned. - Keep a new request for a file already in flight until the next checkpoint cycle because the running fsync might not cover its write. Relation files can be reopened through the smgr target, allowing I/O workers to perform their fsyncs. SLRU files use the generic sync target and execute synchronously with the worker I/O method, although io_uring can still overlap them. Because handlers now return before an fsync completes, md.c reports submission time to pg_stat_io, as it does for asynchronous reads. Methods that execute the fsync immediately still report its duration, and fsync counts are unchanged. The per-file checkpoint timing statistics now measure submission-to-reap time rather than fsync duration. Submission-order reaping can overstate individual times, and overlapping operations can make the aggregate exceed the wall-clock time of the sync phase. --- src/backend/access/transam/clog.c | 6 +- src/backend/access/transam/commit_ts.c | 6 +- src/backend/access/transam/multixact.c | 12 +- src/backend/access/transam/slru.c | 35 +- src/backend/storage/file/fd.c | 23 ++ src/backend/storage/smgr/md.c | 68 +++- src/backend/storage/sync/sync.c | 537 ++++++++++++++++++++----- src/include/access/clog.h | 2 +- src/include/access/commit_ts.h | 2 +- src/include/access/multixact.h | 4 +- src/include/access/slru.h | 2 +- src/include/storage/fd.h | 1 + src/include/storage/md.h | 2 +- src/include/storage/sync.h | 56 +++ src/test/modules/test_slru/test_slru.c | 45 ++- src/tools/pgindent/typedefs.list | 3 + 16 files changed, 645 insertions(+), 159 deletions(-) diff --git a/src/backend/access/transam/clog.c b/src/backend/access/transam/clog.c index 6f7f6b86eb6..89fb77ea5da 100644 --- a/src/backend/access/transam/clog.c +++ b/src/backend/access/transam/clog.c @@ -1117,8 +1117,8 @@ clog_redo(XLogReaderState *record) /* * Entrypoint for sync.c to sync clog files. */ -int -clogsyncfiletag(const FileTag *ftag, char *path) +void +clogsyncfiletag(struct PgAioHandle *ioh, InflightSyncEntry *entry) { - return SlruSyncFileTag(XactCtl, ftag, path); + SlruSyncFileTag(XactCtl, ioh, entry); } diff --git a/src/backend/access/transam/commit_ts.c b/src/backend/access/transam/commit_ts.c index 9e6fd5d4657..7cbbad383b2 100644 --- a/src/backend/access/transam/commit_ts.c +++ b/src/backend/access/transam/commit_ts.c @@ -1028,8 +1028,8 @@ commit_ts_redo(XLogReaderState *record) /* * Entrypoint for sync.c to sync commit_ts files. */ -int -committssyncfiletag(const FileTag *ftag, char *path) +void +committssyncfiletag(struct PgAioHandle *ioh, InflightSyncEntry *entry) { - return SlruSyncFileTag(CommitTsCtl, ftag, path); + SlruSyncFileTag(CommitTsCtl, ioh, entry); } diff --git a/src/backend/access/transam/multixact.c b/src/backend/access/transam/multixact.c index d688815083c..deb866cd7f0 100644 --- a/src/backend/access/transam/multixact.c +++ b/src/backend/access/transam/multixact.c @@ -2998,17 +2998,17 @@ multixact_redo(XLogReaderState *record) /* * Entrypoint for sync.c to sync offsets files. */ -int -multixactoffsetssyncfiletag(const FileTag *ftag, char *path) +void +multixactoffsetssyncfiletag(struct PgAioHandle *ioh, InflightSyncEntry *entry) { - return SlruSyncFileTag(MultiXactOffsetCtl, ftag, path); + SlruSyncFileTag(MultiXactOffsetCtl, ioh, entry); } /* * Entrypoint for sync.c to sync members files. */ -int -multixactmemberssyncfiletag(const FileTag *ftag, char *path) +void +multixactmemberssyncfiletag(struct PgAioHandle *ioh, InflightSyncEntry *entry) { - return SlruSyncFileTag(MultiXactMemberCtl, ftag, path); + SlruSyncFileTag(MultiXactMemberCtl, ioh, entry); } diff --git a/src/backend/access/transam/slru.c b/src/backend/access/transam/slru.c index 885fd068535..b1e513ac3b6 100644 --- a/src/backend/access/transam/slru.c +++ b/src/backend/access/transam/slru.c @@ -68,6 +68,7 @@ #include "access/xlogutils.h" #include "miscadmin.h" #include "pgstat.h" +#include "storage/aio.h" #include "storage/fd.h" #include "storage/shmem.h" #include "storage/shmem_internal.h" @@ -1880,26 +1881,32 @@ SlruScanDirectory(SlruDesc *ctl, SlruScanCallback callback, void *data) * build the path), but they just forward to this common implementation that * performs the fsync. */ -int -SlruSyncFileTag(SlruDesc *ctl, const FileTag *ftag, char *path) +void +SlruSyncFileTag(SlruDesc *ctl, struct PgAioHandle *ioh, InflightSyncEntry *entry) { int fd; - int save_errno; - int result; - SlruFileName(ctl, path, ftag->segno); + SlruFileName(ctl, entry->path, entry->tag.segno); - fd = OpenTransientFile(path, O_RDWR | PG_BINARY); + fd = OpenTransientFile(entry->path, O_RDWR | PG_BINARY); if (fd < 0) - return -1; + { + entry->started = false; + entry->open_errno = errno; + return; + } - pgstat_report_wait_start(WAIT_EVENT_SLRU_FLUSH_SYNC); - result = pg_fsync(fd); - pgstat_report_wait_end(); - save_errno = errno; + /* + * Use the generic sync target. SLRU segments are not smgr relations and + * cannot be reopened from a FileTag in another process, so this fsync + * will run synchronously in worker mode. + */ + pgaio_io_set_target(ioh, PGAIO_TID_SYNC); - CloseTransientFile(fd); + /* Start the asynchronous fsync; the fd is closed once it completes. */ + pgaio_io_start_fsync(ioh, fd, false, WAIT_EVENT_SLRU_FLUSH_SYNC); - errno = save_errno; - return result; + entry->started = true; + entry->close_method = SYNC_CLOSE_TRANSIENT; + entry->close_file = fd; } diff --git a/src/backend/storage/file/fd.c b/src/backend/storage/file/fd.c index 0fcae14541d..f91ee488b89 100644 --- a/src/backend/storage/file/fd.c +++ b/src/backend/storage/file/fd.c @@ -2258,6 +2258,29 @@ FileStartReadV(PgAioHandle *ioh, File file, return 0; } +int +FileStartSync(PgAioHandle *ioh, File file, bool datasync, + uint32 wait_event_info) +{ + int returnCode; + Vfd *vfdP; + + Assert(FileIsValid(file)); + + DO_DB(elog(LOG, "FileStartSync: %d (%s)", + file, VfdCache[file].fileName)); + + returnCode = FileAccess(file); + if (returnCode < 0) + return returnCode; + + vfdP = &VfdCache[file]; + + pgaio_io_start_fsync(ioh, vfdP->fd, datasync, wait_event_info); + + return 0; +} + ssize_t FileWriteV(File file, const struct iovec *iov, int iovcnt, pgoff_t offset, uint32 wait_event_info) diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.c index 780c88c0630..e1fca54d9d9 100644 --- a/src/backend/storage/smgr/md.c +++ b/src/backend/storage/smgr/md.c @@ -1896,26 +1896,27 @@ _mdnblocks(SMgrRelation reln, ForkNumber forknum, MdfdVec *seg) } /* - * Sync a file to disk, given a file tag. Write the path into an output - * buffer so the caller can use it in error messages. + * Sync a file to disk, given a file tag. * - * Return 0 on success, -1 on failure, with errno set. + * Starts an asynchronous fsync on the given AIO handle and records in "entry" + * the path (for error messages), whether an IO was started, and how the file + * is to be closed once the fsync has completed. */ -int -mdsyncfiletag(const FileTag *ftag, char *path) +void +mdsyncfiletag(PgAioHandle *ioh, InflightSyncEntry *entry) { + FileTag *ftag = &entry->tag; SMgrRelation reln = smgropen(ftag->rlocator, INVALID_PROC_NUMBER); + BlockNumber segfirstblock = ftag->segno * ((BlockNumber) RELSEG_SIZE); File file; - instr_time io_start; bool need_to_close; - int result, - save_errno; + instr_time io_start; /* See if we already have the file open, or need to open it. */ if (ftag->segno < reln->md_num_open_segs[ftag->forknum]) { file = reln->md_seg_fds[ftag->forknum][ftag->segno].mdfd_vfd; - strlcpy(path, FilePathName(file), MAXPGPATH); + strlcpy(entry->path, FilePathName(file), MAXPGPATH); need_to_close = false; } else @@ -1923,28 +1924,53 @@ mdsyncfiletag(const FileTag *ftag, char *path) MdPathStr p; p = _mdfd_segpath(reln, ftag->forknum, ftag->segno); - strlcpy(path, p.str, MD_PATH_STR_MAXLEN); + strlcpy(entry->path, p.str, MD_PATH_STR_MAXLEN); - file = PathNameOpenFile(path, _mdfd_open_flags()); + file = PathNameOpenFile(entry->path, _mdfd_open_flags()); if (file < 0) - return -1; + { + entry->started = false; + entry->open_errno = errno; + return; + } need_to_close = true; } + pgaio_io_set_target_smgr(ioh, reln, ftag->forknum, segfirstblock, + 0, false); + + /* + * As with asynchronous reads, measure the time spent starting the IO. + * Synchronous execution includes the fsync itself; otherwise this only + * measures submission. + */ io_start = pgstat_prepare_io_time(track_io_timing); - /* Sync the file. */ - result = FileSync(file, WAIT_EVENT_DATA_FILE_SYNC); - save_errno = errno; + if (FileStartSync(ioh, file, false, WAIT_EVENT_DATA_FILE_SYNC) < 0) + { + entry->started = false; + entry->open_errno = errno; + if (need_to_close) + FileClose(file); + return; + } - if (need_to_close) - FileClose(file); + pgstat_count_io_op_time(IOOBJECT_RELATION, IOCONTEXT_NORMAL, IOOP_FSYNC, + io_start, 1, 0); - pgstat_count_io_op_time(IOOBJECT_RELATION, IOCONTEXT_NORMAL, - IOOP_FSYNC, io_start, 1, 0); + entry->started = true; - errno = save_errno; - return result; + /* + * If we opened the segment ourselves it has to be closed once the fsync + * has completed; segments owned by smgr are left to smgr to manage. + */ + if (need_to_close) + { + entry->close_method = SYNC_CLOSE_VFD; + entry->close_file = (int) file; + } + else + entry->close_method = SYNC_CLOSE_NONE; } /* diff --git a/src/backend/storage/sync/sync.c b/src/backend/storage/sync/sync.c index 2c964b6f3d9..4263881ddd8 100644 --- a/src/backend/storage/sync/sync.c +++ b/src/backend/storage/sync/sync.c @@ -26,7 +26,9 @@ #include "pgstat.h" #include "portability/instr_time.h" #include "postmaster/bgwriter.h" +#include "storage/aio.h" #include "storage/fd.h" +#include "storage/ipc.h" #include "storage/latch.h" #include "storage/md.h" #include "utils/hsearch.h" @@ -54,11 +56,22 @@ */ typedef uint16 CycleCtr; /* can be any convenient integer size */ -typedef struct +typedef struct PendingFsyncEntry { FileTag tag; /* identifies handler and file */ CycleCtr cycle_ctr; /* sync_cycle_ctr of oldest request */ bool canceled; /* canceled is true if we canceled "recently" */ + + /* + * Set when a request arrives for a tag that already has an entry, and + * cleared whenever an fsync for it is started. If it is set once that + * fsync completes, the request came in while the fsync was in flight, so + * the fsync cannot be assumed to have covered it. + */ + bool re_requested; + + /* fsync is done, pending hash-table bookkeeping */ + bool sync_completed; } PendingFsyncEntry; typedef struct @@ -68,10 +81,44 @@ typedef struct bool canceled; /* true if request has been canceled */ } PendingUnlinkEntry; +/* + * Transient state used while processing a batch of fsync requests. A single + * SyncState instance lives on the stack of ProcessSyncRequests() so that no + * partial state survives across calls. + */ +typedef struct SyncState +{ + dlist_head inflight; /* InflightSyncEntry being fsync'd right now */ + dlist_head retry; /* InflightSyncEntry to be retried */ + int inflight_count; /* number of entries in "inflight" */ + int max_inflight; /* max number of concurrent fsyncs */ + int absorb_counter; + + /* stats */ + int processed; + instr_time longest; + instr_time total_elapsed; +} SyncState; + static HTAB *pendingOps = NULL; static List *pendingUnlinks = NIL; static MemoryContext pendingOpsCxt; /* context for the above */ +/* + * Context for the InflightSyncEntry structs allocated while a batch of fsync + * requests is being processed. It is kept separate from pendingOpsCxt (which + * must survive for the lifetime of the process, as it holds pendingOps + * itself), so that it can be reset between batches. + */ +static MemoryContext inflightSyncCxt; + +/* + * All InflightSyncEntry structs that have not yet been freed. Unlike the + * lists in SyncState, this survives an error so that handler-owned files can + * be closed before their entries are discarded. + */ +static dlist_head activeSyncEntries = DLIST_STATIC_INIT(activeSyncEntries); + static CycleCtr sync_cycle_ctr = 0; static CycleCtr checkpoint_cycle_ctr = 0; @@ -84,7 +131,7 @@ static CycleCtr checkpoint_cycle_ctr = 0; */ typedef struct SyncOps { - int (*sync_syncfiletag) (const FileTag *ftag, char *path); + void (*sync_syncfiletag) (PgAioHandle *ioh, InflightSyncEntry *entry); int (*sync_unlinkfiletag) (const FileTag *ftag, char *path); bool (*sync_filetagmatches) (const FileTag *ftag, const FileTag *candidate); @@ -155,6 +202,10 @@ InitSync(void) &hash_ctl, HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); pendingUnlinks = NIL; + + inflightSyncCxt = AllocSetContextCreate(TopMemoryContext, + "Inflight sync context", + ALLOCSET_DEFAULT_SIZES); } } @@ -281,25 +332,295 @@ SyncPostCheckpoint(void) } /* - * ProcessSyncRequests() -- Process queued fsync requests. + * Close the file that a sync handler opened for an in-flight fsync. */ -void -ProcessSyncRequests(void) +static void +sync_close_file(InflightSyncEntry *entry) { - static bool sync_in_progress = false; + switch (entry->close_method) + { + case SYNC_CLOSE_NONE: + break; + case SYNC_CLOSE_TRANSIENT: + CloseTransientFile(entry->close_file); + break; + case SYNC_CLOSE_VFD: + FileClose((File) entry->close_file); + break; + default: + pg_unreachable(); + } + + entry->close_method = SYNC_CLOSE_NONE; +} + +static void +sync_free_entry(InflightSyncEntry *entry) +{ + dlist_delete_from(&activeSyncEntries, &entry->cleanup_node); + pfree(entry); +} + +/* + * Error cleanup callback for ProcessSyncRequests(). + */ +static void +sync_cleanup_inflight(int code, Datum arg) +{ + while (!dlist_is_empty(&activeSyncEntries)) + { + dlist_node *node = dlist_pop_head_node(&activeSyncEntries); + InflightSyncEntry *entry; + + entry = dlist_container(InflightSyncEntry, cleanup_node, node); + + if (entry->started) + pgaio_wref_wait(&entry->iow); + + sync_close_file(entry); + pfree(entry); + } +} + +static void +sync_start_one(SyncState *sync_state, InflightSyncEntry *entry) +{ + struct PgAioHandle *ioh; + instr_time io_start; + + INSTR_TIME_SET_CURRENT(io_start); + entry->start_time = io_start; + + entry->started = false; + entry->open_errno = 0; + entry->close_method = SYNC_CLOSE_NONE; + pgaio_wref_clear(&entry->iow); + + /* + * Any request that arrives from here on may cover data that the fsync + * started below does not, so start out with a clean slate. This has to + * happen before the IO is submitted; requests absorbed in between are + * covered by the fsync, so treating them as newer is merely conservative. + */ + entry->hash_entry->re_requested = false; + + ioh = pgaio_io_acquire(CurrentResourceOwner, &entry->ioret); + pgaio_io_get_wref(ioh, &entry->iow); + + /* + * The handler opens the file, assigns the target and stages the fsync. + * Hold interrupts so that the referenced descriptor cannot be closed + * during submission. + */ + HOLD_INTERRUPTS(); + syncsw[entry->tag.handler].sync_syncfiletag(ioh, entry); + RESUME_INTERRUPTS(); + + if (!entry->started) + pgaio_io_release(ioh); + + dlist_push_tail(&sync_state->inflight, &entry->node); + sync_state->inflight_count++; +} + +static void +sync_drain_one(SyncState *sync_state) +{ + dlist_node *node; + InflightSyncEntry *entry; + int result; + + Assert(sync_state->inflight_count > 0); + + node = dlist_pop_head_node(&sync_state->inflight); + entry = dlist_container(InflightSyncEntry, node, node); + sync_state->inflight_count--; + + if (entry->started) + { + pgaio_wref_wait(&entry->iow); + + /* + * We did not register a completion callback, so the distilled status + * is always PGAIO_RS_OK and the raw fsync() return value (0 on + * success, -errno on failure) is available in ->result.result. + */ + result = -entry->ioret.result.result; + } + else + result = entry->open_errno; + + sync_close_file(entry); + + if (!result) + { + instr_time io_time; + + /* + * These values measure submission-to-reap time, not necessarily fsync + * duration. Submission-order reaping can overstate individual + * durations, and the aggregate can exceed wall-clock time because + * fsyncs overlap. + */ + INSTR_TIME_SET_CURRENT(io_time); + INSTR_TIME_SUBTRACT(io_time, entry->start_time); + + if (INSTR_TIME_GT(io_time, sync_state->longest)) + sync_state->longest = io_time; + INSTR_TIME_ADD(sync_state->total_elapsed, io_time); + sync_state->processed++; + + if (log_checkpoints) + elog(DEBUG1, "checkpoint sync: number=%d file=%s time=%.3f ms", + sync_state->processed, + entry->path, + INSTR_TIME_GET_MILLISEC(io_time)); + + entry->hash_entry->sync_completed = true; + sync_free_entry(entry); + } + else + { + /* + * The request may have been canceled after we started the fsync, e.g. + * because the relation was dropped in the meantime and an intervening + * AbsorbSyncRequests() picked up the cancel message. Since + * mdunlink() queues the "cancel" before actually unlinking, a + * cancellation means the failure is expected and the entry can simply + * be dropped. + * + * The upstream, synchronous code checked this at the top of its retry + * loop; because the fsync is now in flight while requests are being + * absorbed, we have to re-check it here. + */ + if (entry->hash_entry->canceled) + { + entry->hash_entry->sync_completed = true; + sync_free_entry(entry); + return; + } + + /* + * It is possible that the relation has been dropped or truncated + * since the fsync request was entered. Therefore, allow ENOENT, but + * only if we didn't fail already on this file. + */ + errno = result; + if (!FILE_POSSIBLY_DELETED(errno) || entry->retry_count > 0) + ereport(data_sync_elevel(ERROR), + (errcode_for_file_access(), + errmsg("could not fsync file \"%s\": %m", + entry->path))); + else + ereport(DEBUG1, + (errcode_for_file_access(), + errmsg_internal("could not fsync file \"%s\" but retrying: %m", + entry->path))); + + entry->retry_count++; + dlist_push_tail(&sync_state->retry, &entry->node); + } +} +static void +sync_drain_all(SyncState *sync_state) +{ + while (sync_state->inflight_count) + sync_drain_one(sync_state); +} + +/* + * Finish requests whose fsyncs have completed. + * + * The main hash scan may only remove the entry it most recently returned, so + * completion processing is deferred until it ends. This second scan can then + * remove each completed entry as the current entry. Recheck the hash entry + * now because requests absorbed since the fsync completed may require it to + * remain for the next checkpoint cycle. + */ +static void +sync_process_completed(void) +{ HASH_SEQ_STATUS hstat; PendingFsyncEntry *entry; - int absorb_counter; - /* Statistics on sync times */ - int processed = 0; - instr_time sync_start, - sync_end, - sync_diff; - uint64 elapsed; - uint64 longest = 0; - uint64 total_elapsed = 0; + hash_seq_init(&hstat, pendingOps); + while ((entry = (PendingFsyncEntry *) hash_seq_search(&hstat)) != NULL) + { + if (!entry->sync_completed) + continue; + + /* + * We are done with this entry, unless a request for it arrived while + * the fsync was in flight. A cancel supersedes any such request, as + * RememberSyncRequest() clears "canceled" when it records a new one. + */ + if (!entry->re_requested || entry->canceled) + { + if (hash_search(pendingOps, &entry->tag, HASH_REMOVE, NULL) == NULL) + elog(ERROR, "pendingOps corrupted"); + } + else + entry->sync_completed = false; + } +} + +/* + * Reissue any fsync requests that previously failed with an ignorable error. + * + * The fsync table could contain requests to fsync segments that have been + * deleted (unlinked) by the time we get to them. Rather than just hoping an + * ENOENT (or EACCES on Windows) error can be ignored, what we do on error is + * absorb pending requests and then retry. Since mdunlink() queues a "cancel" + * message before actually unlinking, the fsync request is guaranteed to be + * marked canceled after the absorb if it really was this case. + */ +static void +sync_process_retries(SyncState *sync_state) +{ + if (dlist_is_empty(&sync_state->retry)) + return; + + AbsorbSyncRequests(); + + while (!dlist_is_empty(&sync_state->retry)) + { + dlist_node *node = dlist_pop_head_node(&sync_state->retry); + InflightSyncEntry *entry = dlist_container(InflightSyncEntry, node, node); + + if (entry->hash_entry->canceled) + { + /* Safe to remove here, the scan has already finished. */ + if (hash_search(pendingOps, &entry->hash_entry->tag, + HASH_REMOVE, NULL) == NULL) + elog(ERROR, "pendingOps corrupted"); + sync_free_entry(entry); + continue; + } + + Assert(sync_state->inflight_count <= sync_state->max_inflight); + if (sync_state->inflight_count == sync_state->max_inflight) + sync_drain_one(sync_state); + + sync_start_one(sync_state, entry); + } + + sync_drain_all(sync_state); + sync_process_completed(); +} + +/* + * Process queued fsync requests. The public wrapper ensures that any error + * closes files owned by in-flight entries. + */ +static void +ProcessSyncRequestsInternal(void) +{ + static bool sync_in_progress = false; + + HASH_SEQ_STATUS hstat; + PendingFsyncEntry *entry; + SyncState sync_state; /* * This is only called during checkpoints, and checkpoints should only @@ -350,6 +671,7 @@ ProcessSyncRequests(void) while ((entry = (PendingFsyncEntry *) hash_seq_search(&hstat)) != NULL) { entry->cycle_ctr = sync_cycle_ctr; + entry->sync_completed = false; } } @@ -359,13 +681,26 @@ ProcessSyncRequests(void) /* Set flag to detect failure if we don't reach the end of the loop */ sync_in_progress = true; + /* + * Bound concurrent fsyncs by both the AIO handle and transient descriptor + * budgets. + */ + dlist_init(&sync_state.inflight); + dlist_init(&sync_state.retry); + sync_state.inflight_count = 0; + sync_state.max_inflight = GetFsyncConcurrencyLimit(); + sync_state.processed = 0; + INSTR_TIME_SET_ZERO(sync_state.longest); + INSTR_TIME_SET_ZERO(sync_state.total_elapsed); + + Assert(dlist_is_empty(&activeSyncEntries)); + MemoryContextReset(inflightSyncCxt); + /* Now scan the hashtable for fsync requests to process */ - absorb_counter = FSYNCS_PER_ABSORB; + sync_state.absorb_counter = FSYNCS_PER_ABSORB; hash_seq_init(&hstat, pendingOps); while ((entry = (PendingFsyncEntry *) hash_seq_search(&hstat)) != NULL) { - int failures; - /* * If the entry is new then don't process it this time; it is new. * Note "continue" bypasses the hash-remove call at the bottom of the @@ -378,103 +713,94 @@ ProcessSyncRequests(void) Assert((CycleCtr) (entry->cycle_ctr + 1) == sync_cycle_ctr); /* - * If fsync is off then we don't have to bother opening the file at - * all. (We delay checking until this point so that changing fsync on - * the fly behaves sensibly.) + * If in checkpointer, we want to absorb pending requests every so + * often to prevent overflow of the fsync request queue. It is + * unspecified whether newly-added entries will be visited by + * hash_seq_search, but we don't care since we don't need to process + * them anyway. */ - if (enableFsync) + if (enableFsync && --sync_state.absorb_counter <= 0) { - /* - * If in checkpointer, we want to absorb pending requests every so - * often to prevent overflow of the fsync request queue. It is - * unspecified whether newly-added entries will be visited by - * hash_seq_search, but we don't care since we don't need to - * process them anyway. - */ - if (--absorb_counter <= 0) - { - AbsorbSyncRequests(); - absorb_counter = FSYNCS_PER_ABSORB; - } + AbsorbSyncRequests(); + sync_state.absorb_counter = FSYNCS_PER_ABSORB; + } + + if (!enableFsync || entry->canceled) + { + /* We are done with this entry, remove it */ + if (hash_search(pendingOps, &entry->tag, HASH_REMOVE, NULL) == NULL) + elog(ERROR, "pendingOps corrupted"); + } + else + { + InflightSyncEntry *inflight_entry; + + Assert(sync_state.inflight_count <= sync_state.max_inflight); + if (sync_state.inflight_count == sync_state.max_inflight) + sync_drain_one(&sync_state); /* - * The fsync table could contain requests to fsync segments that - * have been deleted (unlinked) by the time we get to them. Rather - * than just hoping an ENOENT (or EACCES on Windows) error can be - * ignored, what we do on error is absorb pending requests and - * then retry. Since mdunlink() queues a "cancel" message before - * actually unlinking, the fsync request is guaranteed to be - * marked canceled after the absorb if it really was this case. - * DROP DATABASE likewise has to tell us to forget fsync requests - * before it starts deletions. + * Mark the entry as already dealt with in this cycle. It must + * remain in the hash table until its fsync completes and the scan + * ends. If a new request arrives meanwhile, this cycle counter + * leaves the entry to be processed by the next checkpoint. */ - for (failures = 0; !entry->canceled; failures++) - { - char path[MAXPGPATH]; - - INSTR_TIME_SET_CURRENT(sync_start); - if (syncsw[entry->tag.handler].sync_syncfiletag(&entry->tag, - path) == 0) - { - /* Success; update statistics about sync timing */ - INSTR_TIME_SET_CURRENT(sync_end); - sync_diff = sync_end; - INSTR_TIME_SUBTRACT(sync_diff, sync_start); - elapsed = INSTR_TIME_GET_MICROSEC(sync_diff); - if (elapsed > longest) - longest = elapsed; - total_elapsed += elapsed; - processed++; - - if (log_checkpoints) - elog(DEBUG1, "checkpoint sync: number=%d file=%s time=%.3f ms", - processed, - path, - (double) elapsed / 1000); - - break; /* out of retry loop */ - } - - /* - * It is possible that the relation has been dropped or - * truncated since the fsync request was entered. Therefore, - * allow ENOENT, but only if we didn't fail already on this - * file. - */ - if (!FILE_POSSIBLY_DELETED(errno) || failures > 0) - ereport(data_sync_elevel(ERROR), - (errcode_for_file_access(), - errmsg("could not fsync file \"%s\": %m", - path))); - else - ereport(DEBUG1, - (errcode_for_file_access(), - errmsg_internal("could not fsync file \"%s\" but retrying: %m", - path))); - - /* - * Absorb incoming requests and check to see if a cancel - * arrived for this relation fork. - */ - AbsorbSyncRequests(); - absorb_counter = FSYNCS_PER_ABSORB; /* might as well... */ - } /* end retry loop */ + entry->cycle_ctr = sync_cycle_ctr; + + inflight_entry = MemoryContextAllocZero(inflightSyncCxt, + sizeof(InflightSyncEntry)); + inflight_entry->tag = entry->tag; + inflight_entry->hash_entry = entry; + dlist_push_tail(&activeSyncEntries, + &inflight_entry->cleanup_node); + + sync_start_one(&sync_state, inflight_entry); } + } - /* We are done with this entry, remove it */ - if (hash_search(pendingOps, &entry->tag, HASH_REMOVE, NULL) == NULL) - elog(ERROR, "pendingOps corrupted"); - } /* end loop over hashtable entries */ + sync_drain_all(&sync_state); + sync_process_completed(); + + /* + * A second failure raises an error, so normally one retry pass is enough. + * Keep an explicit bound in case that changes. + */ + for (int failures = 0; failures < 5; failures++) + { + if (dlist_is_empty(&sync_state.retry)) + break; + + sync_process_retries(&sync_state); + } + + if (!dlist_is_empty(&sync_state.inflight) || + !dlist_is_empty(&sync_state.retry)) + elog(PANIC, "in-flight sync requests remain after ProcessSyncRequests"); /* Return sync performance metrics for report at checkpoint end */ - CheckpointStats.ckpt_sync_rels = processed; - CheckpointStats.ckpt_longest_sync = longest; - CheckpointStats.ckpt_agg_sync_time = total_elapsed; + CheckpointStats.ckpt_sync_rels = sync_state.processed; + CheckpointStats.ckpt_longest_sync = INSTR_TIME_GET_MICROSEC(sync_state.longest); + CheckpointStats.ckpt_agg_sync_time = INSTR_TIME_GET_MICROSEC(sync_state.total_elapsed); /* Flag successful completion of ProcessSyncRequests */ sync_in_progress = false; } +/* + * ProcessSyncRequests() -- Process queued fsync requests. + */ +void +ProcessSyncRequests(void) +{ + PG_ENSURE_ERROR_CLEANUP(sync_cleanup_inflight, (Datum) 0); + { + ProcessSyncRequestsInternal(); + } + PG_END_ENSURE_ERROR_CLEANUP(sync_cleanup_inflight, (Datum) 0); + + Assert(dlist_is_empty(&activeSyncEntries)); +} + /* * RememberSyncRequest() -- callback from checkpointer side of sync request * @@ -554,11 +880,20 @@ RememberSyncRequest(const FileTag *ftag, SyncRequestType type) ftag, HASH_ENTER, &found); + + /* + * If an entry already existed, an fsync for it may be in flight right + * now, in which case it cannot be assumed to cover this request; see + * sync_drain_one(). + */ + entry->re_requested = found; + /* if new entry, or was previously canceled, initialize it */ if (!found || entry->canceled) { entry->cycle_ctr = sync_cycle_ctr; entry->canceled = false; + entry->sync_completed = false; } /* diff --git a/src/include/access/clog.h b/src/include/access/clog.h index 7894998c763..e089106f7fe 100644 --- a/src/include/access/clog.h +++ b/src/include/access/clog.h @@ -47,7 +47,7 @@ extern void CheckPointCLOG(void); extern void ExtendCLOG(TransactionId newestXact); extern void TruncateCLOG(TransactionId oldestXact, Oid oldestxid_datoid); -extern int clogsyncfiletag(const FileTag *ftag, char *path); +extern void clogsyncfiletag(PgAioHandle *ioh, InflightSyncEntry *entry); /* XLOG stuff */ #define CLOG_ZEROPAGE 0x00 diff --git a/src/include/access/commit_ts.h b/src/include/access/commit_ts.h index 825ccda90ed..fa4880e0d03 100644 --- a/src/include/access/commit_ts.h +++ b/src/include/access/commit_ts.h @@ -38,7 +38,7 @@ extern void SetCommitTsLimit(TransactionId oldestXact, TransactionId newestXact); extern void AdvanceOldestCommitTsXid(TransactionId oldestXact); -extern int committssyncfiletag(const FileTag *ftag, char *path); +extern void committssyncfiletag(PgAioHandle *ioh, InflightSyncEntry *entry); /* XLOG stuff */ #define COMMIT_TS_ZEROPAGE 0x00 diff --git a/src/include/access/multixact.h b/src/include/access/multixact.h index 6be5299ab68..3f980b4120d 100644 --- a/src/include/access/multixact.h +++ b/src/include/access/multixact.h @@ -114,8 +114,8 @@ extern bool MultiXactIdPrecedes(MultiXactId multi1, MultiXactId multi2); extern bool MultiXactIdPrecedesOrEquals(MultiXactId multi1, MultiXactId multi2); -extern int multixactoffsetssyncfiletag(const FileTag *ftag, char *path); -extern int multixactmemberssyncfiletag(const FileTag *ftag, char *path); +extern void multixactoffsetssyncfiletag(PgAioHandle *ioh, InflightSyncEntry *entry); +extern void multixactmemberssyncfiletag(PgAioHandle *ioh, InflightSyncEntry *entry); extern void AtEOXact_MultiXact(void); extern void AtPrepare_MultiXact(void); diff --git a/src/include/access/slru.h b/src/include/access/slru.h index b4adb1789c7..0e91df5609c 100644 --- a/src/include/access/slru.h +++ b/src/include/access/slru.h @@ -240,7 +240,7 @@ typedef bool (*SlruScanCallback) (SlruDesc *ctl, char *filename, int64 segpage, extern bool SlruScanDirectory(SlruDesc *ctl, SlruScanCallback callback, void *data); extern void SlruDeleteSegment(SlruDesc *ctl, int64 segno); -extern int SlruSyncFileTag(SlruDesc *ctl, const FileTag *ftag, char *path); +extern void SlruSyncFileTag(SlruDesc *ctl, struct PgAioHandle *ioh, struct InflightSyncEntry *entry); /* SlruScanDirectory public callbacks */ extern bool SlruScanDirCbReportPresence(SlruDesc *ctl, char *filename, diff --git a/src/include/storage/fd.h b/src/include/storage/fd.h index c79f3312544..f39469058b7 100644 --- a/src/include/storage/fd.h +++ b/src/include/storage/fd.h @@ -138,6 +138,7 @@ extern int FilePrefetch(File file, pgoff_t offset, pgoff_t amount, uint32 wait_e extern ssize_t FileReadV(File file, const struct iovec *iov, int iovcnt, pgoff_t offset, uint32 wait_event_info); extern ssize_t FileWriteV(File file, const struct iovec *iov, int iovcnt, pgoff_t offset, uint32 wait_event_info); extern int FileStartReadV(struct PgAioHandle *ioh, File file, int iovcnt, pgoff_t offset, uint32 wait_event_info); +extern int FileStartSync(struct PgAioHandle *ioh, File file, bool datasync, uint32 wait_event_info); extern int FileSync(File file, uint32 wait_event_info); extern int FileZero(File file, pgoff_t offset, pgoff_t amount, uint32 wait_event_info); extern int FileFallocate(File file, pgoff_t offset, pgoff_t amount, uint32 wait_event_info); diff --git a/src/include/storage/md.h b/src/include/storage/md.h index b8d10329eb8..53f75802ac0 100644 --- a/src/include/storage/md.h +++ b/src/include/storage/md.h @@ -58,7 +58,7 @@ extern void ForgetDatabaseSyncRequests(Oid dbid); extern void DropRelationFiles(RelFileLocator *delrels, int ndelrels, bool isRedo); /* md sync callbacks */ -extern int mdsyncfiletag(const FileTag *ftag, char *path); +extern void mdsyncfiletag(PgAioHandle *ioh, InflightSyncEntry *entry); extern int mdunlinkfiletag(const FileTag *ftag, char *path); extern bool mdfiletagmatches(const FileTag *ftag, const FileTag *candidate); diff --git a/src/include/storage/sync.h b/src/include/storage/sync.h index 88290500bc9..a72ce4c3fd3 100644 --- a/src/include/storage/sync.h +++ b/src/include/storage/sync.h @@ -13,6 +13,9 @@ #ifndef SYNC_H #define SYNC_H +#include "lib/ilist.h" +#include "portability/instr_time.h" +#include "storage/aio_types.h" #include "storage/relfilelocator.h" /* @@ -55,6 +58,59 @@ typedef struct FileTag uint64 segno; } FileTag; +struct PendingFsyncEntry; +struct PgAioHandle; + +/* + * How the file opened by a sync handler must be closed once its asynchronous + * fsync has completed. + */ +typedef enum SyncFileCloseMethod +{ + SYNC_CLOSE_NONE = 0, /* nothing to close */ + SYNC_CLOSE_TRANSIENT, /* CloseTransientFile(close_file) */ + SYNC_CLOSE_VFD, /* FileClose((File) close_file) */ +} SyncFileCloseMethod; + +/* + * State for a single in-flight asynchronous fsync request. A sync handler + * opens the file to be synced, fills in the fields it is responsible for, and + * starts an asynchronous fsync on the AIO handle it is given. + */ +typedef struct InflightSyncEntry +{ + FileTag tag; /* identifies handler and file */ + + char path[MAXPGPATH]; + + /* + * Set by the handler: whether it started an asynchronous fsync on the + * passed-in AIO handle. If the file could not be opened, the handler + * sets started = false and open_errno to the errno of the failed open. + */ + bool started; + int open_errno; + + /* set by the handler: how to close the opened file after completion */ + SyncFileCloseMethod close_method; + int close_file; /* fd, or File, depending on close_method */ + + struct PendingFsyncEntry *hash_entry; + + int retry_count; + + instr_time start_time; + + PgAioReturn ioret; + PgAioWaitRef iow; + + /* membership in the inflight / retry lists */ + dlist_node node; + + /* membership in the error-cleanup list */ + dlist_node cleanup_node; +} InflightSyncEntry; + extern void InitSync(void); extern void SyncPreCheckpoint(void); extern void SyncPostCheckpoint(void); diff --git a/src/test/modules/test_slru/test_slru.c b/src/test/modules/test_slru/test_slru.c index 40efffdbf62..ccac09a9285 100644 --- a/src/test/modules/test_slru/test_slru.c +++ b/src/test/modules/test_slru/test_slru.c @@ -17,10 +17,13 @@ #include "access/slru.h" #include "access/transam.h" #include "miscadmin.h" +#include "storage/aio.h" #include "storage/fd.h" #include "storage/ipc.h" #include "storage/shmem.h" +#include "storage/sync.h" #include "utils/builtins.h" +#include "utils/resowner.h" PG_MODULE_MAGIC; @@ -152,15 +155,47 @@ Datum test_slru_page_sync(PG_FUNCTION_ARGS) { int64 pageno = PG_GETARG_INT64(0); - FileTag ftag; - char path[MAXPGPATH]; + InflightSyncEntry entry = {0}; + PgAioHandle *ioh; + int result; /* note that this flushes the full file a segment is located in */ - ftag.segno = pageno / SLRU_PAGES_PER_SEGMENT; - SlruSyncFileTag(TestSlruCtl, &ftag, path); + entry.tag.segno = pageno / SLRU_PAGES_PER_SEGMENT; + + /* + * SlruSyncFileTag() now performs the fsync asynchronously. Drive it the + * same way sync.c does: acquire an AIO handle, let the handler start the + * fsync, wait for its completion and close the file it opened. + */ + ioh = pgaio_io_acquire(CurrentResourceOwner, &entry.ioret); + pgaio_io_get_wref(ioh, &entry.iow); + + HOLD_INTERRUPTS(); + SlruSyncFileTag(TestSlruCtl, ioh, &entry); + RESUME_INTERRUPTS(); + + if (entry.started) + { + pgaio_wref_wait(&entry.iow); + result = -entry.ioret.result.result; + CloseTransientFile(entry.close_file); + } + else + { + pgaio_io_release(ioh); + result = entry.open_errno; + } + + if (result != 0) + { + errno = result; + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not fsync file \"%s\": %m", entry.path))); + } elog(NOTICE, "Called SlruSyncFileTag() for segment %" PRIu64 " on path %s", - ftag.segno, path); + entry.tag.segno, entry.path); PG_RETURN_VOID(); } diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index a95b09859b5..4fd08012e1b 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1361,6 +1361,7 @@ IndexVacuumInfo IndxInfo InferClause InferenceElem +InflightSyncEntry InfoItem InhInfo InheritableSocket @@ -3078,12 +3079,14 @@ SupportRequestSimplify SupportRequestSimplifyAggref SupportRequestWFuncMonotonic Syn +SyncFileCloseMethod SyncOps SyncRepConfigData SyncRepStandbyData SyncRequestHandler SyncRequestType SyncStandbySlotsConfigData +SyncState SyncingRelationsState SysCacheIdentifier SysFKRelationship -- 2.47.3
From 203cf306618d1440e74e7b192edc3b13b3c679a7 Mon Sep 17 00:00:00 2001 From: Nazir Bilal Yavuz <[email protected]> Date: Tue, 25 Aug 2026 15:00:40 +0300 Subject: [PATCH v1 4/4] Allow IO workers to execute SLRU fsyncs The SLRU fsyncs introduced by the preceding commit use the generic PGAIO_TID_SYNC target. It does not store paths in shared memory, so I/O workers cannot reopen the files and the checkpointer must perform the operations itself. Add PGAIO_TID_SYNC_FILETAG, which identifies a file by the FileTag registered with sync.c. A FileTag contains enough information to reopen the file in another process. Dispatch reopening through an optional SyncOps callback, implemented for SLRUs using the new SlruOpenFileTag(). SLRUs that are not registered with sync.c continue to use PGAIO_TID_SYNC. Define the shared FileTag representation in storage/aio_types.h so PgAioTargetData can store it directly without duplicating its layout. Describe the target and worker-side open errors using the SLRU name and segment number. Unlike the smgr target, the FileTag target opens a new descriptor for each operation instead of using a cache. Add an optional PgAioTargetInfo close callback so I/O workers release these descriptors after executing the operation. SLRU_FLUSH_SYNC is now reported by the I/O worker rather than the checkpointer, as for relation fsyncs. SyncDataDirectory() continues to use PGAIO_TID_SYNC because its files have no FileTag. --- src/backend/access/transam/clog.c | 8 +- src/backend/access/transam/commit_ts.c | 8 +- src/backend/access/transam/multixact.c | 16 +++- src/backend/access/transam/slru.c | 32 ++++++- src/backend/storage/aio/aio_io.c | 8 ++ src/backend/storage/aio/aio_target.c | 36 +++++++ src/backend/storage/sync/sync.c | 125 ++++++++++++++++++++++++- src/include/access/clog.h | 1 + src/include/access/commit_ts.h | 1 + src/include/access/multixact.h | 2 + src/include/access/slru.h | 1 + src/include/storage/aio.h | 13 ++- src/include/storage/aio_internal.h | 1 + src/include/storage/aio_types.h | 14 +++ src/include/storage/sync.h | 19 ++-- src/tools/pgindent/typedefs.list | 1 + 16 files changed, 263 insertions(+), 23 deletions(-) diff --git a/src/backend/access/transam/clog.c b/src/backend/access/transam/clog.c index 89fb77ea5da..0fe0491d877 100644 --- a/src/backend/access/transam/clog.c +++ b/src/backend/access/transam/clog.c @@ -1115,10 +1115,16 @@ clog_redo(XLogReaderState *record) } /* - * Entrypoint for sync.c to sync clog files. + * Entrypoints for sync.c to sync and reopen clog files. */ void clogsyncfiletag(struct PgAioHandle *ioh, InflightSyncEntry *entry) { SlruSyncFileTag(XactCtl, ioh, entry); } + +int +clogopenfiletag(const FileTag *ftag) +{ + return SlruOpenFileTag(XactCtl, ftag); +} diff --git a/src/backend/access/transam/commit_ts.c b/src/backend/access/transam/commit_ts.c index 7cbbad383b2..4bb95e415ac 100644 --- a/src/backend/access/transam/commit_ts.c +++ b/src/backend/access/transam/commit_ts.c @@ -1026,10 +1026,16 @@ commit_ts_redo(XLogReaderState *record) } /* - * Entrypoint for sync.c to sync commit_ts files. + * Entrypoints for sync.c to sync and reopen commit_ts files. */ void committssyncfiletag(struct PgAioHandle *ioh, InflightSyncEntry *entry) { SlruSyncFileTag(CommitTsCtl, ioh, entry); } + +int +committsopenfiletag(const FileTag *ftag) +{ + return SlruOpenFileTag(CommitTsCtl, ftag); +} diff --git a/src/backend/access/transam/multixact.c b/src/backend/access/transam/multixact.c index deb866cd7f0..11f5f266075 100644 --- a/src/backend/access/transam/multixact.c +++ b/src/backend/access/transam/multixact.c @@ -2996,7 +2996,7 @@ multixact_redo(XLogReaderState *record) } /* - * Entrypoint for sync.c to sync offsets files. + * Entrypoints for sync.c to sync and reopen offsets files. */ void multixactoffsetssyncfiletag(struct PgAioHandle *ioh, InflightSyncEntry *entry) @@ -3004,11 +3004,23 @@ multixactoffsetssyncfiletag(struct PgAioHandle *ioh, InflightSyncEntry *entry) SlruSyncFileTag(MultiXactOffsetCtl, ioh, entry); } +int +multixactoffsetsopenfiletag(const FileTag *ftag) +{ + return SlruOpenFileTag(MultiXactOffsetCtl, ftag); +} + /* - * Entrypoint for sync.c to sync members files. + * Entrypoints for sync.c to sync and reopen members files. */ void multixactmemberssyncfiletag(struct PgAioHandle *ioh, InflightSyncEntry *entry) { SlruSyncFileTag(MultiXactMemberCtl, ioh, entry); } + +int +multixactmembersopenfiletag(const FileTag *ftag) +{ + return SlruOpenFileTag(MultiXactMemberCtl, ftag); +} diff --git a/src/backend/access/transam/slru.c b/src/backend/access/transam/slru.c index b1e513ac3b6..8e0d9af16df 100644 --- a/src/backend/access/transam/slru.c +++ b/src/backend/access/transam/slru.c @@ -1897,11 +1897,16 @@ SlruSyncFileTag(SlruDesc *ctl, struct PgAioHandle *ioh, InflightSyncEntry *entry } /* - * Use the generic sync target. SLRU segments are not smgr relations and - * cannot be reopened from a FileTag in another process, so this fsync - * will run synchronously in worker mode. + * If this SLRU is registered with sync.c, identify the file by its + * FileTag, so that the fsync can be executed by an IO worker, which will + * reopen the file with SlruOpenFileTag(). Otherwise there is no handler + * to reopen the file through, so use the generic sync target, whose IOs + * cannot be handed off to a worker. */ - pgaio_io_set_target(ioh, PGAIO_TID_SYNC); + if (ctl->options.sync_handler != SYNC_HANDLER_NONE) + pgaio_io_set_target_sync_filetag(ioh, &entry->tag); + else + pgaio_io_set_target(ioh, PGAIO_TID_SYNC); /* Start the asynchronous fsync; the fd is closed once it completes. */ pgaio_io_start_fsync(ioh, fd, false, WAIT_EVENT_SLRU_FLUSH_SYNC); @@ -1910,3 +1915,22 @@ SlruSyncFileTag(SlruDesc *ctl, struct PgAioHandle *ioh, InflightSyncEntry *entry entry->close_method = SYNC_CLOSE_TRANSIENT; entry->close_file = fd; } + +/* + * Counterpart to SlruSyncFileTag(), opening the segment identified by ftag in + * a process that did not stage the IO. As with SlruSyncFileTag(), individual + * SLRUs have to provide the handler function, so that the correct "SlruCtl" + * is used. + * + * Returns a file descriptor opened with OpenTransientFile(), or -1 with errno + * set. + */ +int +SlruOpenFileTag(SlruDesc *ctl, const FileTag *ftag) +{ + char path[MAXPGPATH]; + + SlruFileName(ctl, path, ftag->segno); + + return OpenTransientFile(path, O_RDWR | PG_BINARY); +} diff --git a/src/backend/storage/aio/aio_io.c b/src/backend/storage/aio/aio_io.c index 324fb6911e2..070a5ff70b3 100644 --- a/src/backend/storage/aio/aio_io.c +++ b/src/backend/storage/aio/aio_io.c @@ -169,6 +169,14 @@ pgaio_io_perform_synchronously(PgAioHandle *ioh) Assert(result <= INT_MAX); ioh->result = result < 0 ? -errno : result; + /* + * If we, rather than the process that staged the IO, opened the file, + * close it again. Has to happen after the result has been determined, as + * closing may clobber errno, and before the completion is processed, as + * that can recycle the handle. + */ + pgaio_io_close_reopened(ioh); + pgaio_io_process_completion(ioh, ioh->result); END_CRIT_SECTION(); diff --git a/src/backend/storage/aio/aio_target.c b/src/backend/storage/aio/aio_target.c index 82b24b0026d..795e8ac7046 100644 --- a/src/backend/storage/aio/aio_target.c +++ b/src/backend/storage/aio/aio_target.c @@ -17,6 +17,7 @@ #include "storage/aio.h" #include "storage/aio_internal.h" #include "storage/smgr.h" +#include "storage/sync.h" static char *pgaio_sync_describe_identity(const PgAioTargetData *sd); @@ -39,8 +40,16 @@ static const PgAioTargetInfo *pgaio_target_info[] = { }, [PGAIO_TID_SMGR] = &aio_smgr_target_info, [PGAIO_TID_SYNC] = &aio_sync_target_info, + [PGAIO_TID_SYNC_FILETAG] = &aio_sync_filetag_target_info, }; +/* + * The IO whose descriptor this process reopened and whose target requires the + * descriptor to be released after execution. Only set between + * pgaio_io_reopen() and pgaio_io_close_reopened(). + */ +static PgAioHandle *pgaio_reopened_ioh = NULL; + /* * describe_identity callback for PGAIO_TID_SYNC. As we do not store the path @@ -141,6 +150,33 @@ pgaio_io_reopen(PgAioHandle *ioh) { Assert(ioh->target > PGAIO_TID_INVALID && ioh->target < PGAIO_TID_COUNT); Assert(ioh->op > PGAIO_OP_INVALID && ioh->op < PGAIO_OP_COUNT); + Assert(pgaio_reopened_ioh == NULL); pgaio_target_info[ioh->target]->reopen(ioh); + + /* + * Remember that this process, rather than the one that staged the IO, + * owns the file descriptor now, so that pgaio_io_close_reopened() can + * release it once the IO has been executed. + */ + if (pgaio_target_info[ioh->target]->close != NULL) + pgaio_reopened_ioh = ioh; +} + +/* + * Internal: Counterpart to pgaio_io_reopen(), releasing the file descriptor it + * acquired. Does nothing unless this process reopened this very IO and its + * target needs the descriptor to be released. + * + * This has to be called before the IO's completion is processed, as that can + * make the handle be reused for an unrelated IO. + */ +void +pgaio_io_close_reopened(PgAioHandle *ioh) +{ + if (pgaio_reopened_ioh != ioh) + return; + + pgaio_reopened_ioh = NULL; + pgaio_target_info[ioh->target]->close(ioh); } diff --git a/src/backend/storage/sync/sync.c b/src/backend/storage/sync/sync.c index 4263881ddd8..35f87793171 100644 --- a/src/backend/storage/sync/sync.c +++ b/src/backend/storage/sync/sync.c @@ -132,9 +132,19 @@ static CycleCtr checkpoint_cycle_ctr = 0; typedef struct SyncOps { void (*sync_syncfiletag) (PgAioHandle *ioh, InflightSyncEntry *entry); + + /* + * Optional. Reopen the file identified by ftag, so that an fsync started + * by sync_syncfiletag() can be executed in a different process, e.g. an + * IO worker. Returns a file descriptor opened with OpenTransientFile(), + * or -1 with errno set. Handlers that provide this must use the + * PGAIO_TID_SYNC_FILETAG target (see pgaio_io_set_target_sync_filetag()). + */ + int (*sync_openfiletag) (const FileTag *ftag); int (*sync_unlinkfiletag) (const FileTag *ftag, char *path); bool (*sync_filetagmatches) (const FileTag *ftag, const FileTag *candidate); + const char *sync_target_name; } SyncOps; /* @@ -149,22 +159,129 @@ static const SyncOps syncsw[] = { }, /* pg_xact */ [SYNC_HANDLER_CLOG] = { - .sync_syncfiletag = clogsyncfiletag + .sync_syncfiletag = clogsyncfiletag, + .sync_openfiletag = clogopenfiletag, + .sync_target_name = "pg_xact" }, /* pg_commit_ts */ [SYNC_HANDLER_COMMIT_TS] = { - .sync_syncfiletag = committssyncfiletag + .sync_syncfiletag = committssyncfiletag, + .sync_openfiletag = committsopenfiletag, + .sync_target_name = "pg_commit_ts" }, /* pg_multixact/offsets */ [SYNC_HANDLER_MULTIXACT_OFFSET] = { - .sync_syncfiletag = multixactoffsetssyncfiletag + .sync_syncfiletag = multixactoffsetssyncfiletag, + .sync_openfiletag = multixactoffsetsopenfiletag, + .sync_target_name = "pg_multixact/offsets" }, /* pg_multixact/members */ [SYNC_HANDLER_MULTIXACT_MEMBER] = { - .sync_syncfiletag = multixactmemberssyncfiletag + .sync_syncfiletag = multixactmemberssyncfiletag, + .sync_openfiletag = multixactmembersopenfiletag, + .sync_target_name = "pg_multixact/members" } }; +static void sync_aio_reopen(PgAioHandle *ioh); +static void sync_aio_close(PgAioHandle *ioh); +static char *sync_aio_describe_identity(const PgAioTargetData *sd); + +/* + * Target info for files identified by a FileTag (see PGAIO_TID_SYNC_FILETAG). + * Unlike PGAIO_TID_SYNC, a FileTag contains everything needed to find the + * file again in another process, so such IOs can be executed by IO workers. + */ +const PgAioTargetInfo aio_sync_filetag_target_info = { + .name = "sync_filetag", + .reopen = sync_aio_reopen, + .close = sync_aio_close, + .describe_identity = sync_aio_describe_identity, +}; + +/* + * Set up ioh to operate on the file identified by ftag. + */ +void +pgaio_io_set_target_sync_filetag(PgAioHandle *ioh, const FileTag *ftag) +{ + PgAioTargetData *sd = pgaio_io_get_target_data(ioh); + + Assert(syncsw[ftag->handler].sync_openfiletag != NULL); + Assert(syncsw[ftag->handler].sync_target_name != NULL); + + pgaio_io_set_target(ioh, PGAIO_TID_SYNC_FILETAG); + + sd->sync_filetag = *ftag; +} + +static FileTag +sync_aio_filetag(const PgAioTargetData *sd) +{ + return sd->sync_filetag; +} + +/* + * reopen callback for PGAIO_TID_SYNC_FILETAG, to open the file in the process + * executing the IO. + */ +static void +sync_aio_reopen(PgAioHandle *ioh) +{ + PgAioTargetData *sd = pgaio_io_get_target_data(ioh); + PgAioOpData *od = pgaio_io_get_op_data(ioh); + FileTag ftag = sync_aio_filetag(sd); + int fd; + + /* + * The caller needs to prevent interrupts from being processed, otherwise + * the FD could be closed again before we get to executing the IO. + */ + Assert(!INTERRUPTS_CAN_BE_PROCESSED()); + + Assert(pgaio_io_get_op(ioh) == PGAIO_OP_FSYNC); + + fd = syncsw[ftag.handler].sync_openfiletag(&ftag); + if (fd < 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not open segment " UINT64_FORMAT " of SLRU \"%s\": %m", + ftag.segno, + syncsw[ftag.handler].sync_target_name))); + + od->fsync.fd = fd; +} + +/* + * close callback for PGAIO_TID_SYNC_FILETAG, releasing the descriptor + * acquired by sync_aio_reopen(). + * + * Called in a critical section, so a failure to close cannot be reported. + * That is not a meaningful loss: the data has already been flushed by the + * fsync, and the descriptor is not written to. + */ +static void +sync_aio_close(PgAioHandle *ioh) +{ + PgAioOpData *od = pgaio_io_get_op_data(ioh); + + (void) CloseTransientFile(od->fsync.fd); + od->fsync.fd = -1; +} + +/* + * describe_identity callback for PGAIO_TID_SYNC_FILETAG. + */ +static char * +sync_aio_describe_identity(const PgAioTargetData *sd) +{ + FileTag ftag = sync_aio_filetag(sd); + + return psprintf(_("segment " UINT64_FORMAT " of SLRU \"%s\""), + ftag.segno, + syncsw[ftag.handler].sync_target_name); +} + /* * Initialize data structures for the file sync tracking. */ diff --git a/src/include/access/clog.h b/src/include/access/clog.h index e089106f7fe..fc06794d186 100644 --- a/src/include/access/clog.h +++ b/src/include/access/clog.h @@ -48,6 +48,7 @@ extern void ExtendCLOG(TransactionId newestXact); extern void TruncateCLOG(TransactionId oldestXact, Oid oldestxid_datoid); extern void clogsyncfiletag(PgAioHandle *ioh, InflightSyncEntry *entry); +extern int clogopenfiletag(const FileTag *ftag); /* XLOG stuff */ #define CLOG_ZEROPAGE 0x00 diff --git a/src/include/access/commit_ts.h b/src/include/access/commit_ts.h index fa4880e0d03..ebb87d9534e 100644 --- a/src/include/access/commit_ts.h +++ b/src/include/access/commit_ts.h @@ -39,6 +39,7 @@ extern void SetCommitTsLimit(TransactionId oldestXact, extern void AdvanceOldestCommitTsXid(TransactionId oldestXact); extern void committssyncfiletag(PgAioHandle *ioh, InflightSyncEntry *entry); +extern int committsopenfiletag(const FileTag *ftag); /* XLOG stuff */ #define COMMIT_TS_ZEROPAGE 0x00 diff --git a/src/include/access/multixact.h b/src/include/access/multixact.h index 3f980b4120d..3f5233a3bcd 100644 --- a/src/include/access/multixact.h +++ b/src/include/access/multixact.h @@ -115,7 +115,9 @@ extern bool MultiXactIdPrecedesOrEquals(MultiXactId multi1, MultiXactId multi2); extern void multixactoffsetssyncfiletag(PgAioHandle *ioh, InflightSyncEntry *entry); +extern int multixactoffsetsopenfiletag(const FileTag *ftag); extern void multixactmemberssyncfiletag(PgAioHandle *ioh, InflightSyncEntry *entry); +extern int multixactmembersopenfiletag(const FileTag *ftag); extern void AtEOXact_MultiXact(void); extern void AtPrepare_MultiXact(void); diff --git a/src/include/access/slru.h b/src/include/access/slru.h index 0e91df5609c..43429392c7d 100644 --- a/src/include/access/slru.h +++ b/src/include/access/slru.h @@ -241,6 +241,7 @@ extern bool SlruScanDirectory(SlruDesc *ctl, SlruScanCallback callback, void *da extern void SlruDeleteSegment(SlruDesc *ctl, int64 segno); extern void SlruSyncFileTag(SlruDesc *ctl, struct PgAioHandle *ioh, struct InflightSyncEntry *entry); +extern int SlruOpenFileTag(SlruDesc *ctl, const FileTag *ftag); /* SlruScanDirectory public callbacks */ extern bool SlruScanDirCbReportPresence(SlruDesc *ctl, char *filename, diff --git a/src/include/storage/aio.h b/src/include/storage/aio.h index 428504c0472..904ab185458 100644 --- a/src/include/storage/aio.h +++ b/src/include/storage/aio.h @@ -119,9 +119,10 @@ typedef enum PgAioTargetID PGAIO_TID_INVALID = 0, PGAIO_TID_SMGR, PGAIO_TID_SYNC, + PGAIO_TID_SYNC_FILETAG, } PgAioTargetID; -#define PGAIO_TID_COUNT (PGAIO_TID_SYNC + 1) +#define PGAIO_TID_COUNT (PGAIO_TID_SYNC_FILETAG + 1) /* @@ -171,6 +172,16 @@ struct PgAioTargetInfo */ void (*reopen) (PgAioHandle *ioh); + /* + * Optional counterpart to reopen, releasing the file descriptor it + * acquired. Called in the process that reopened the IO, after the IO has + * been executed. Targets whose reopen callback hands out a descriptor + * that is cached and reused, like smgr's, do not need this. + * + * This is called in a critical section, so it must not raise errors. + */ + void (*close) (PgAioHandle *ioh); + /* describe the target of the IO, used for log messages and views */ char *(*describe_identity) (const PgAioTargetData *sd); diff --git a/src/include/storage/aio_internal.h b/src/include/storage/aio_internal.h index 9ca4087aa7f..9d2e92c6538 100644 --- a/src/include/storage/aio_internal.h +++ b/src/include/storage/aio_internal.h @@ -360,6 +360,7 @@ extern int pgaio_io_get_iovec_length(PgAioHandle *ioh, struct iovec **iov); /* aio_target.c */ extern bool pgaio_io_can_reopen(PgAioHandle *ioh); extern void pgaio_io_reopen(PgAioHandle *ioh); +extern void pgaio_io_close_reopened(PgAioHandle *ioh); extern const char *pgaio_io_get_target_name(PgAioHandle *ioh); diff --git a/src/include/storage/aio_types.h b/src/include/storage/aio_types.h index 17b59aeed7c..90bc4c73989 100644 --- a/src/include/storage/aio_types.h +++ b/src/include/storage/aio_types.h @@ -23,6 +23,18 @@ typedef struct PgAioHandle PgAioHandle; typedef struct PgAioHandleCallbacks PgAioHandleCallbacks; typedef struct PgAioTargetInfo PgAioTargetInfo; +/* + * A tag identifying a file handled by sync.c. This is defined here so that + * PgAioTargetData can store it without duplicating its representation. + */ +typedef struct PgAioSyncFileTag +{ + int16 handler; /* SyncRequestHandler value */ + int16 forknum; /* ForkNumber */ + RelFileLocator rlocator; /* physical relation identifier */ + uint64 segno; +} PgAioSyncFileTag; + /* * A reference to an IO that can be used to wait for the IO (using * pgaio_wref_wait()) to complete. @@ -69,6 +81,8 @@ typedef union PgAioTargetData bool is_temp:1; /* proc can be inferred by owning AIO */ bool skip_fsync:1; } smgr; + + PgAioSyncFileTag sync_filetag; } PgAioTargetData; diff --git a/src/include/storage/sync.h b/src/include/storage/sync.h index a72ce4c3fd3..4a2cca50703 100644 --- a/src/include/storage/sync.h +++ b/src/include/storage/sync.h @@ -46,17 +46,11 @@ typedef enum SyncRequestHandler } SyncRequestHandler; /* - * A tag identifying a file. Currently it has the members required for md.c's - * usage, but sync.c has no knowledge of the internal structure, and it is - * liable to change as required by future handlers. + * A tag identifying a file. Its representation is shared with AIO target + * data, so changes are automatically visible to processes that reopen files + * on behalf of sync.c. */ -typedef struct FileTag -{ - int16 handler; /* SyncRequestHandler value, saving space */ - int16 forknum; /* ForkNumber, saving space */ - RelFileLocator rlocator; - uint64 segno; -} FileTag; +typedef PgAioSyncFileTag FileTag; struct PendingFsyncEntry; struct PgAioHandle; @@ -119,4 +113,9 @@ extern void RememberSyncRequest(const FileTag *ftag, SyncRequestType type); extern bool RegisterSyncRequest(const FileTag *ftag, SyncRequestType type, bool retryOnError); +/* AIO support */ +extern PGDLLIMPORT const PgAioTargetInfo aio_sync_filetag_target_info; +extern void pgaio_io_set_target_sync_filetag(PgAioHandle *ioh, + const FileTag *ftag); + #endif /* SYNC_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 4fd08012e1b..0812aae6793 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2285,6 +2285,7 @@ PgAioOpData PgAioResult PgAioResultStatus PgAioReturn +PgAioSyncFileTag PgAioTargetData PgAioTargetID PgAioTargetInfo -- 2.47.3
benchmark-repro.sh
Description: application/shellscript
