Hi,

While investigating pg_basebackup performance, I found that a significant
amount of CPU is being wasted on both the server and client sides, and that
there are substantial opportunities to improve throughput without
fundamentally changing the design (so without abandoning the simple
single-threaded, single-connection design). Keeping that design intact also
lets us answer some of the questions raised in the old parallel-backup thread
[1]. The attached patches implement a couple of changes to make benchmarking
easier (0001-0003), some small optimizations that make a significant
difference in my production-like testing (0004-0008), and then implement
io_uring (Direct I/O + Async I/O) support for pg_basebackup (0009-0010).

First, a quick performance demonstration, from the AWS benchmarks described in
full later (2x c6in.8xlarge, specs in [4]), when taking a backup without
checksum verification/SSL, while still using single socket/fd:

master + 0001-0003, writing to disk: ~1610MB/s.
master + all patches, with MPTCP: 2750-2975MB/s.

That's like ~1.85x speedup on real cloud hardware. What I found is that the
gains come from several independent sources: reducing syscall numbers,
avoiding redundant memory copies in libpq, and using Direct I/O with
asynchronous submission on the client side. Above such rates, I believe we
genuinely need independent connections and a parallel-backup design, but
everything up to that point is achievable with relatively low-invasive
changes.

OK, now let's go through the patches:

0001 and 0002 extend the existing server-side --target blackhole concept into
separate server and client (pg_basebackup) blackholes, so that each stage of
the data transfer pipeline can be measured reliably. Together with 0003, this
gives DBAs a simple way to locate a bottleneck using nothing but the tool
itself:

-t server-blackhole measures the server's raw read speed
-t client-blackhole additionally sends the data over the network and but
discard writes; and a plain(classic) -Fp -D /path run exposes all three
potential bottlenecks at once.

0003 adds a simple timing message with the average transfer rate. The idea is
that if we're going to discuss numbers on this thread, it's better if the tool
measures them the same way for everyone, rather than each of us relying on
atop or other tooling.

0004 increases SINK_BUFFER_LENGTH from its current 32 kB. Profiling the
server side shows a huge number of small pread() calls coming from
basebackup_read_file(), which is surprising given that elsewhere in the tree
(backend/storage/buffer/README) we already mention that a 256 kB ring for seq.
scans scans is used because it fits comfortably in L2 cache. On my laptop,
with no SSL, no checksum generation or verification, a hot filesystem cache,
and a client blackhole to eliminate client I/O, a 10 GB backup over loopback
runs at:
  3.1 GB/s with the 32 kB buffer,
  5.1 GB/s at 128 kB,
  5.5 GB/s at 256 kB,
  6.0 GB/s at 1 MB
(those are average of five runs). The win comes from letting pread()
swallow much larger chunks of each segment in one go: per-core L2 caches are
1-2 MB even on laptops these days, syscalls have become more expensive since
the Spectre/Meltdown mitigations, and the kernel's default readahead is
already in the 128-512 kB range, so there's little reason to trickle the data
through 32 kB at a time. I also experimented with raising PQ_SEND_BUFFER_SIZE
from 8 kB to 128 kB, but saw no improvement and sometimes a regression --
apparently it defeats libpq's direct-send optimization for large messages.

0005 issues posix_fadvise(POSIX_FADV_SEQUENTIAL) on the assumption that
segments are usually cold at backup time. Even with 0004 applied, cold-cache
throughput on my laptop's NVMe goes from 1.5 GB/s to 2 GB/s with this patch,
because the synchronous pread() calls see lower latencies (visible with eBPF
funclatency). One question I tried to answer was whether it makes sense to
issue one fadvise call per file or many smaller ones (in the spirit of
maintenance_io_concurrency), but on Linux FADV_SEQUENTIAL does exactly one
thing -- it just widens the maximum readahead horizon -- so repeated calls buy
nothing; it's simply handled with read-as-you-go by the kernel's own readahead
heuristics.

0006 avoids buffering client-side writes with glibc stdio (FILE *). I
initially thought this is going to be as easy as a setvbuf(3) call, since the
extractor path was issuing a mismatched 4 kB + 258 kB write pair per 256 kB
received. A 1MB setvbuf buffer produced the tidy syscall pattern I wanted, but
it actually got slower when measured carefully on tmpfs: glibc's fwrite()
internally stats the file for its block size and introduces an extra memcpy()
for large writes. Direct I/O would bypass this entirely, but that felt
premature at this stage, so the patch takes a more conservative approach.
Throughput is roughly unchanged (3.1 vs 3.2 GB/s to a ramdisk), but the
syscall pattern becomes a clean one-to-one recvfrom()/write() pairing, which
matters for the later patches.

0007 preallocates output files via posix_fallocate(). This gave about 7%
essentially for free, though you need to already be writing fast (probably
more than of 1 GB/s) before it becomes visible.

0008 eliminates a redundant memory copy in the receive path. After the fixes
above, perf showed PQgetCopyData()'s internal memcpy() consuming 60-70% of
pg_basebackup's single CPU: libpq allocates a buffer and copies the incoming
data into it, only for the callback (ReceiveArchiveStreamChunk and friends) to
immediately consume that same buffer at an offset of one byte, skipping the
protocol message byte. Since pg_basebackup is the only consumer here, the copy
can simply be avoided. On the receive path the kernel already copies from the
NIC via DMA to a kernel buffer and again into userspace, so above roughly 10
Gbps this extra PostgreSQL-side copy becomes very visible. With the copy
removed, a CPU-saturated pg_basebackup over localhost goes from 2.4 GB/s to
~5.5 GB/s when not writing to disk; when writing even to a ramdisk, the
bottleneck shifts to fundamental kernel-side vfs_write() costs such as cgroups
v2 memory accounting.

0009: Even with all of the above, there was still idle sequential write
bandwidth left on the table, and buffered writes couldn't reach it (without
some parallelism). Direct I/O alone (O_DIRECT) carried an unavoidable latency
hit and wouldn't be usable here alone either, so it had be combined with
asynchronous submission, which means liburing (client-side only). So this
patch adds io_uring (Direct I/O) for larger files on the client side. The
patch adds PG_BASEBACKUP_NODIO too for experimenting with this (set/unset it
to see the difference).

0010 fixes a serious regression that 0009 introduced for -Ft (tar) output.
Because the final archive length isn't known in advance -- unlike -Fp (plain),
where each file's target size is known -- every Direct I/O write via io_uring
also extended the file, triggering synchronous space allocation and
serializing on ext4's per-inode i_rwsem, which defeated the entire point of a
deep async queue. The fix is proper posix_fallocate() for the tar path, with a
fallback to buffered writes when preallocation isn't possible. It's a separate
commit because it needed its own explanation: without it, tar output ran at
410MB/s; with it, ~2400MB/s.

Now, the real AWS benchmarks. These are averages of 3-5 runs with the
lowest outliers discarded, on 2x c6in.8xlarge with ENA Express (full specs in
[4]), using --no-verify-checksums and --manifest-checksums=NONE, and no SSL
unless stated otherwise:

stageI: hw baseline
===================
a1. master + 000[123] -t server-blackhole # 11573MB/s (disk I/O possible
    on serverside from hot pagecache)
a2. master + 000[123] -t client-blackhole # 1820MB/s (single-thread TCP limit,
    no writing client side)
a3. master + 000[123] -D /db/backup # ~1610MB/s (when writing, proper backup)

stageII: basic optimizations
============================

+0004 SINK_BUFFER_LENGTH increase:
b. master + 000[1234] -t client-blackhole # 2158MB/s

cold-cache scenarios (how efficient we are when data is not in the VFS cache
server-side; echo 3 > drop_caches):
c1. master + 000[1234] -t client-blackhole # 1476MB/s
+ 0005 posix_fadvise whole file:
c2. master + 000[12345] -t client-blackhole # 1610MB/s

+0006, hot-cache scenarios again, real I/O by pg_basebackup, 100% CPU
throughout for the tests below:
d1. master + 000[12345] -D /db/backup # ~1639MB/s
d2. master + 000[123456] -D /db/backup # around the same, but we now issue
    matched recvfrom()+write() pairs

+0007 preallocate file
e. master + 000[1234567] -D /db/backup # ~1687MB/s

+0008 avoid the second memcpy in libpq
f. master + 000[12345678] -D /db/backup # 1818MB/s (we cannot get more;
   this saturates the AWS's ENA Express link on a single connection/
   technically maxes out AWS network fabric/SDN)

crosscheck against baseline, just in case:
g. master + 000[123] -D /db/backup # 1613MB/s

stageIII: MPTCP
===============
I had to fall back to MPTCP [2] because the maximum single-stream TCP
bandwidth even with ENA Express fluctuated far too much to isolate. That patch
is going to posted independently in its own thread [1], but attached here too.

h0. iperf3 max with MPTCP, single connection: 43Gbit/s (but fluctuating down
    to just 27Gbit/s sometimes; just 2-3 subflows)
h1. master + 000[123] + PGMPTCP=0 -t client-blackhole # 2079MB/s
h2. master + 000[123] + PGMPTCP=1 -t client-blackhole # 2770MB/s
h3. master + 000[123] + PGMPTCP=0 -D /db/backup # 1870MB/s, occasionally
    slightly lower (ENA Express)
h4. master + 000[12345678] + PGMPTCP=1 -t client-blackhole # 3840MB/s, a
    real indication that the earlier ceiling was just AWS network --
    though note we are not actually writing here

stageIV: DIO+AIO
================
Because pg_basebackup was pinned at 100% CPU while the storage could still
take more, we need io_uring with DIO+AIO (+0009):

i1. master + 000[123456789] + PGMPTCP=1 -D /db/backup # 2750-2975MB/s; we
    max out MPTCP bandwidth with the core saturated by writing
i2. master + 000[123456789] + PGMPTCP=0 -D /db/backup # 1695MB/s, a sample
    of the fluctuations (down from 1870MB/s)
i3. master + 000[123456789] + PGMPTCP=1 without --no-verify-checksums
    # still 2772MB/s (server-side checksum calculation is fast enough!)
i4. master + 000[123456789] + PGMPTCP=1 without --no-verify-checksums and
    with default CRC32C for manifest checksumming # still 2673MB/s

So "i4." divided by "a3." gives 2975MB/ 1610 = ~1.85x , with some of the 0009
benefits greatly take from ideas in 0007 and 0008.

There were several surprises in the above, at least to me -- I hadn't
anticipated hardware-assisted checksums being that fast.

Tar (-Ft) output initially suffered badly from the synchronous space
allocation (because that was not done), as per details described under 0010.
With proper posix_fallocate() it recovers performance. Without checksums and
without SSL:
j1. master + 000[123456789] + PGMPTCP=1 -D /db/backup.tar -Ft # 410MB/s,
    regression due to the lack of effective posix_fallocate()
j2. master + 000[123456789]+0010 + PGMPTCP=1 -D /db/backup.tar -Ft
    # ~2400MB/s

TLS
===
Some measurements of TLS itself in this setup (TLSv1.3, cipher
TLS_AES_256_GCM_SHA384):
k1. ssl=on + master + 000[123] + PGMPTCP=1 -D /db/backup
    #baseline of ~1247MB/s
k2. ssl=on + master + 000[123456789] + PGMPTCP=1 -D /db/backup
    #1668-1791MB/s

On SSL specifically: the cipher alone can push close to 8-10 GB/s (64-80
Gbps) on a single CPU with openssl -bench, and reducing the number of
encryption rounds doesn't appear to yield any real benefit. But since SSL/TLS
is additional work performed on the __same__ thread, it imposes a clear
CPU overhead here: 1791 / 2975 = 0.60x.

A few conclusions:

- pg_basebackup today tops out at roughly 10-15 Gbps, because in the end we
  are constrained by what a single core can do.

- pg_basebackup is very vulnerable to single-TCP-flow performance on real
  networks. These tests were intentionally run on real hardware and networks
  within a single zone -- the kind people actually use -- rather than on an
  isolated network. See table [3] for apparent real-world limitations;
  single-flow TCP is IMHO the top constraint today, often well below 10-25
  Gbps, and without MPTCP there is no simple way to get past it...

- Some tuning is possible on our side without invasive protocol changes.

- MPTCP is easy to set up and enabled almost everywhere today, but it has
  its own ceiling: apparently ~40 Gbps in iperf3 and realistically ~25 Gbps
  when we are also writing single-threaded. Potentially in far future we
  could put recvfrom() (where MPTCP reassembly happens) on one thread and
  issue SQs/io_uring_submit() from a second thread, but that would require
  Thomas's pg_thr_*() APIs to be in place.

- I researched SSL/kTLS/sendfile/zcrx a bit as well, but this email is
  already too long, so that's not covered here.

Based on the measurements and conclusions above, I think we can improve
pg_basebackup's single-threaded performance for now with these
relatively low-invasive patches.

Thanks for reading that far.

-J.

[1] - 
https://www.postgresql.org/message-id/20200420201922.55ab7ovg6535suyz%40alap3.anarazel.de
[2] - 
https://www.postgresql.org/message-id/flat/CAKZiRmy6j9PBzDHZwdgwHavwKDzv5GWtRSWOTj6-jv6SCOZ%3DYA%40mail.gmail.com
[3] - a small audit of the single-TCP-connection speeds one can expect
      depending on where one is running:

Hardware type             | Max aggr bw | 1x TCP LAN | 1x TCP MAN| 1x TCP WAN
Physical server           | <= 200 GbE  | 1 CPU      | 1-10Gbps  | 1-10Gbps?
VM (Xen, VMware, etc)     | <= 100 GbE? | 1 CPU      | 1-10Gbps  | 1-10Gbps?
lowend AWS VM             | 5-40 Gbps   | 5 Gbps     | 5 Gbps    | <= 5 Gbps
lowend AWS VM Cluster Pl. | <= 200 Gbps | 10 Gbps    | 5 Gbps    | <= 5 Gbps
highend AWS VM            | <= 200 Gbps | 5 Gbps     | 5 Gbps    | <= 5 Gbps
highend AWS VM+ENA Express| <= 200 Gbps | 25 Gbps    | 25 Gbps   | <= 5 Gbps
lowend Azure VM           | 3(!)-40 Gbps| 3(!) Gbps  | 1.5-3 Gbps| 1.5-3 Gbps
highend Azure VM+acc. net.| <= 200 Gbps | 10-12.5Gbps| 10 Gbps   | 1.5-3 Gbps

[4] - HW: 2x c6in.8xlarge EC2 (each with 32 vCPUs, 64 GB RAM), scale
4000 / 50 GB cluster size, 3x NVMe of 50 GB / 8k IOPS each in LVM/RAID0
(lvcreate -i 3), ext4, tuned TCP stack (BBR), ENA Express enabled. In theory
and sometimes in practice this gives a real ~23 Gbps, sometimes more like
15 Gbps -- highly nondeterministic and dependent on zone, region, and time of
day. Aggregate ~50 Gbps bandwidth was always available without
problems; only the single-flow TCP performance fluctuated, typically between
15 and 19 Gbps during these measurements, and I rather lost hope of it being
deterministic. It seems to be a function of the env rather than the
configuration, which was identical throughout the excercies. I suspect that on
raw hardware with >= 40 GbE interfaces higher speeds are possible, but the
intent here was to simulate what real customers actually see.

sysctls:
net.core.rmem_max = 33554432
net.core.wmem_max = 33554432
net.core.rmem_default = 2097152
net.core.wmem_default = 2097152
net.ipv4.tcp_rmem = 4096 87380 33554432
net.ipv4.tcp_wmem = 4096 65536 33554432
net.ipv4.tcp_window_scaling = 1
net.ipv4.tcp_sack = 1
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
mptcp enabled by default so no change
From 677630ea56f8bd4b3a96e72d5d6cdeb0170d8c50 Mon Sep 17 00:00:00 2001
From: Jakub Wartak <[email protected]>
Date: Fri, 24 Jul 2026 08:30:20 +0200
Subject: [PATCH v06082026 03/10] pg_basebackup: report average data transfer
 throughput in verbose mode

Add the average data transfer rate to the "base backup completed" message.
The transfer is timed with instr_time from just before the archive data is
received until all data has arrived (but before fsync is called)

Author: Jakub Wartak <[email protected]>
Discussion:
---
 src/bin/pg_basebackup/pg_basebackup.c | 31 ++++++++++++++++++++++++++-
 1 file changed, 30 insertions(+), 1 deletion(-)

diff --git a/src/bin/pg_basebackup/pg_basebackup.c b/src/bin/pg_basebackup/pg_basebackup.c
index 41cb3f26855..af579a967a0 100644
--- a/src/bin/pg_basebackup/pg_basebackup.c
+++ b/src/bin/pg_basebackup/pg_basebackup.c
@@ -36,6 +36,7 @@
 #include "fe_utils/recovery_gen.h"
 #include "getopt_long.h"
 #include "libpq/protocol.h"
+#include "portability/instr_time.h"
 #include "receivelog.h"
 #include "streamutil.h"
 
@@ -1795,6 +1796,8 @@ BaseBackup(char *compression_algorithm, char *compression_detail,
 	int			writing_to_stdout;
 	bool		use_new_option_syntax = false;
 	PQExpBufferData buf;
+	instr_time	transfer_start;
+	instr_time	transfer_elapsed;
 
 	Assert(conn != NULL);
 	initPQExpBuffer(&buf);
@@ -2150,6 +2153,9 @@ BaseBackup(char *compression_algorithm, char *compression_detail,
 						 wal_compress_level);
 	}
 
+	/* Start timing the data transfer, for the average rate report. */
+	INSTR_TIME_SET_CURRENT(transfer_start);
+
 	if (serverMajor >= 1500)
 	{
 		/* Receive a single tar stream with everything. */
@@ -2202,6 +2208,13 @@ BaseBackup(char *compression_algorithm, char *compression_detail,
 			ReceiveBackupManifest(conn);
 	}
 
+	/*
+	 * All backup data has been received, so measure how long the transfer
+	 * took for the average rate report shown at completion.
+	 */
+	INSTR_TIME_SET_CURRENT(transfer_elapsed);
+	INSTR_TIME_SUBTRACT(transfer_elapsed, transfer_start);
+
 	if (showprogress)
 	{
 		progress_update_filename(NULL);
@@ -2373,7 +2386,22 @@ BaseBackup(char *compression_algorithm, char *compression_detail,
 	}
 
 	if (verbose)
-		pg_log_info("base backup completed");
+	{
+		double		elapsed_sec = INSTR_TIME_GET_DOUBLE(transfer_elapsed);
+
+		/*
+		 * Avoids potential division by zero.
+		 *
+		 * Timing does not include potential fsync()/syncfs(), so data might be
+		 * still in-flight from pagecache when total_done was calculated, therefore
+		 * we make it clear to the user what we are measuring.
+		 */
+		if (elapsed_sec > 0.0)
+			pg_log_info("base backup completed (avg %.1f MB/s)",
+						(double) totaldone / (1024 * 1024) / elapsed_sec);
+		else
+			pg_log_info("base backup completed");
+	}
 }
 
 
@@ -2430,6 +2458,7 @@ main(int argc, char **argv)
 	pg_logging_init(argv[0]);
 	progname = get_progname(argv[0]);
 	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_basebackup"));
+	pg_initialize_timing();
 
 	if (argc > 1)
 	{
-- 
2.43.0

From ab81384954c756134eb417fda656be0f23ebc498 Mon Sep 17 00:00:00 2001
From: Jakub Wartak <[email protected]>
Date: Thu, 30 Jul 2026 11:02:42 +0200
Subject: [PATCH v06082026 02/10] pg_basebackup: add new "client-blackhole"
 (benchmarking) backup target

Accept a new "client-blackhole" target value for pg_basebackup's --target
option. Unlike the server-side "server-blackhole" target, the whole backup
is sent to pg_basebackup as usual, but instead of being written to disk it is
discarded: regular file contents go to the /dev/null device and
directories/symbolic links/backup manifest are not created.  In tar format
the archive itself is sent to the /dev/null device too.

This is a testing and development feature. It allows measuring how fast a
backup can be produced by the server and network without being limited by
the speed of the local storage (but subject to pg_basebackup
single-threaded limitations like TLS decryption and/or checksum
validation).

The client-side discard is implemented in the astreamer extractors, which
is controlled by a new backup_target_clientblackhole flag (backup_target
stays NULL).

Author: Jakub Wartak <[email protected]>
---
 doc/src/sgml/ref/pg_basebackup.sgml          |   7 +-
 src/bin/pg_basebackup/pg_basebackup.c        | 115 +++++++++++++++----
 src/bin/pg_basebackup/t/010_pg_basebackup.pl |  18 +++
 src/fe_utils/astreamer_file.c                |  36 ++++--
 src/include/fe_utils/astreamer.h             |   3 +-
 5 files changed, 150 insertions(+), 29 deletions(-)

diff --git a/doc/src/sgml/ref/pg_basebackup.sgml b/doc/src/sgml/ref/pg_basebackup.sgml
index 2ed643f005a..9ac679a1133 100644
--- a/doc/src/sgml/ref/pg_basebackup.sgml
+++ b/doc/src/sgml/ref/pg_basebackup.sgml
@@ -269,7 +269,12 @@ PostgreSQL documentation
         server requires superuser privileges or having privileges of the
         <literal>pg_write_server_files</literal> role. If the target is set to
         <literal>server-blackhole</literal>, the contents are discarded by the
-        server and not stored anywhere. This should only be used for testing
+        server and not stored anywhere. If the target is set to
+        <literal>client-blackhole</literal>, the backup is sent to
+        <application>pg_basebackup</application> as usual, but discarded instead
+        of being written to disk; this can be used to measure how fast a backup
+        can be produced without being limited by the speed of the local
+        storage.  Both blackhole targets should only be used for testing
         purposes, as you will not end up with an actual backup.
        </para>
 
diff --git a/src/bin/pg_basebackup/pg_basebackup.c b/src/bin/pg_basebackup/pg_basebackup.c
index 8a599fc9869..41cb3f26855 100644
--- a/src/bin/pg_basebackup/pg_basebackup.c
+++ b/src/bin/pg_basebackup/pg_basebackup.c
@@ -155,6 +155,7 @@ static bool manifest = true;
 static bool manifest_force_encode = false;
 static char *manifest_checksums = NULL;
 static DataDirSyncMethod sync_method = DATA_DIR_SYNC_METHOD_FSYNC;
+static bool backup_target_clientblackhole = false;
 
 static bool success = false;
 static bool made_new_pgdata = false;
@@ -1151,7 +1152,8 @@ CreateBackupStreamer(char *archive_name, char *spclocation,
 			directory = get_tablespace_mapping(spclocation);
 		streamer = astreamer_extractor_new(directory,
 										   get_tablespace_mapping,
-										   progress_update_filename);
+										   progress_update_filename,
+										   backup_target_clientblackhole);
 	}
 	else
 	{
@@ -1163,8 +1165,15 @@ CreateBackupStreamer(char *archive_name, char *spclocation,
 		 * Normally, we write it to the archive name provided by the caller,
 		 * but when the base directory is "-" that means we need to write to
 		 * standard output.
+		 *
+		 * When discarding writes, we send the archive to the null device.
 		 */
-		if (strcmp(basedir, "-") == 0)
+		if (backup_target_clientblackhole)
+		{
+			snprintf(archive_filename, sizeof(archive_filename), "%s", DEVNULL);
+			archive_file = NULL;
+		}
+		else if (strcmp(basedir, "-") == 0)
 		{
 			snprintf(archive_filename, sizeof(archive_filename), "-");
 			archive_file = stdout;
@@ -1176,25 +1185,32 @@ CreateBackupStreamer(char *archive_name, char *spclocation,
 			archive_file = NULL;
 		}
 
+		/*
+		 * Setup streamer. In case of using client blackhole, it does not make sense
+		 * to append compression suffixes.
+		 */
 		if (compress->algorithm == PG_COMPRESSION_NONE)
 			streamer = astreamer_plain_writer_new(archive_filename,
 												  archive_file);
 		else if (compress->algorithm == PG_COMPRESSION_GZIP)
 		{
-			strlcat(archive_filename, ".gz", sizeof(archive_filename));
+			if (!backup_target_clientblackhole)
+				strlcat(archive_filename, ".gz", sizeof(archive_filename));
 			streamer = astreamer_gzip_writer_new(archive_filename,
 												 archive_file, compress);
 		}
 		else if (compress->algorithm == PG_COMPRESSION_LZ4)
 		{
-			strlcat(archive_filename, ".lz4", sizeof(archive_filename));
+			if (!backup_target_clientblackhole)
+				strlcat(archive_filename, ".lz4", sizeof(archive_filename));
 			streamer = astreamer_plain_writer_new(archive_filename,
 												  archive_file);
 			streamer = astreamer_lz4_compressor_new(streamer, compress);
 		}
 		else if (compress->algorithm == PG_COMPRESSION_ZSTD)
 		{
-			strlcat(archive_filename, ".zst", sizeof(archive_filename));
+			if (!backup_target_clientblackhole)
+				strlcat(archive_filename, ".zst", sizeof(archive_filename));
 			streamer = astreamer_plain_writer_new(archive_filename,
 												  archive_file);
 			streamer = astreamer_zstd_compressor_new(streamer, compress);
@@ -1475,6 +1491,15 @@ ReceiveArchiveStreamChunk(size_t r, char *copybuf, void *callback_data)
 					 */
 					if (state->manifest_inject_streamer != NULL)
 						state->manifest_buffer = createPQExpBuffer();
+					else if (backup_target_clientblackhole)
+					{
+						/* Throw away the manifest too */
+						snprintf(state->manifest_filename,
+								 sizeof(state->manifest_filename), "%s", DEVNULL);
+						state->manifest_file = fopen(DEVNULL, "wb");
+						if (state->manifest_file == NULL)
+							pg_fatal("could not open file \"%s\": %m", DEVNULL);
+					}
 					else
 					{
 						snprintf(state->manifest_filename,
@@ -1688,11 +1713,22 @@ ReceiveBackupManifest(PGconn *conn)
 {
 	WriteManifestState state;
 
-	snprintf(state.filename, sizeof(state.filename),
-			 "%s/backup_manifest.tmp", basedir);
-	state.file = fopen(state.filename, "wb");
-	if (state.file == NULL)
-		pg_fatal("could not create file \"%s\": %m", state.filename);
+	if (backup_target_clientblackhole)
+	{
+		/* Throw away the manifest */
+		snprintf(state.filename, sizeof(state.filename), "%s", DEVNULL);
+		state.file = fopen(DEVNULL, "wb");
+		if (state.file == NULL)
+			pg_fatal("could not open file \"%s\": %m", DEVNULL);
+	}
+	else
+	{
+		snprintf(state.filename, sizeof(state.filename),
+				 "%s/backup_manifest.tmp", basedir);
+		state.file = fopen(state.filename, "wb");
+		if (state.file == NULL)
+			pg_fatal("could not create file \"%s\": %m", state.filename);
+	}
 
 	ReceiveCopyData(conn, ReceiveBackupManifestChunk, &state);
 
@@ -1975,6 +2011,9 @@ BaseBackup(char *compression_algorithm, char *compression_detail,
 									  compression_detail);
 	}
 
+	if (verbose && backup_target_clientblackhole)
+		pg_log_info("the backup is being discarded and cannot be used for recovery");
+
 	if (verbose)
 		pg_log_info("initiating base backup, waiting for checkpoint to complete");
 
@@ -2057,7 +2096,8 @@ BaseBackup(char *compression_algorithm, char *compression_detail,
 		 * won't be storing anything into these directories and thus should
 		 * not create them.
 		 */
-		if (backup_target == NULL && format == 'p' && !PQgetisnull(res, i, 1))
+		if (backup_target == NULL && !backup_target_clientblackhole && format == 'p' &&
+			!PQgetisnull(res, i, 1))
 		{
 			char	   *path = PQgetvalue(res, i, 1);
 
@@ -2284,11 +2324,8 @@ BaseBackup(char *compression_algorithm, char *compression_detail,
 	 * synced after being completed.  In plain format, all the data of the
 	 * base directory is synced, taking into account all the tablespaces.
 	 * Errors are not considered fatal.
-	 *
-	 * If, however, there's a backup target, we're not writing anything
-	 * locally, so in that case we skip this step.
 	 */
-	if (do_sync && backup_target == NULL)
+	if (do_sync && backup_target == NULL && !backup_target_clientblackhole)
 	{
 		if (verbose)
 			pg_log_info("syncing data to disk ...");
@@ -2310,7 +2347,7 @@ BaseBackup(char *compression_algorithm, char *compression_detail,
 	 * without a backup_manifest file, decreasing the chances that a directory
 	 * we leave behind will be mistaken for a valid backup.
 	 */
-	if (!writing_to_stdout && manifest && backup_target == NULL)
+	if (!writing_to_stdout && manifest && backup_target == NULL && !backup_target_clientblackhole)
 	{
 		char		tmp_filename[MAXPGPATH];
 		char		filename[MAXPGPATH];
@@ -2487,7 +2524,15 @@ main(int argc, char **argv)
 				temp_replication_slot = false;
 				break;
 			case 't':
-				backup_target = pg_strdup(optarg);
+
+				/*
+				 * Target: everything else than "client-blackhole" is passed
+				 * through to the server as the backup target.
+				 */
+				if (strcmp(optarg, "client-blackhole") == 0)
+					backup_target_clientblackhole = true;
+				else
+					backup_target = pg_strdup(optarg);
 				break;
 			case 'T':
 				tablespace_list_append(optarg);
@@ -2587,6 +2632,17 @@ main(int argc, char **argv)
 		backup_target = NULL;
 	}
 
+	if (backup_target_clientblackhole)
+	{
+		if (basedir != NULL || backup_target != NULL)
+		{
+			pg_log_error("cannot specify both output directory and backup target");
+			pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+			exit(1);
+		}
+		basedir = pg_strdup(DEVNULL);
+	}
+
 	/*
 	 * Can't use --format with --target. Without --target, default format is
 	 * tar.
@@ -2679,6 +2735,23 @@ main(int argc, char **argv)
 		exit(1);
 	}
 
+	/*
+	 * The "client-blackhole" target throws the backup away as it is received.
+	 * As we do not have directories prepared, we cannot stream or write recovery
+	 * configuration.
+	 */
+	if (backup_target_clientblackhole)
+	{
+		if (includewal == STREAM_WAL)
+		{
+			pg_log_error("WAL cannot be streamed with the \"client-blackhole\" backup target");
+			pg_log_error_hint("Use \"%s\" or \"%s\".", "-X none", "-X fetch");
+			exit(1);
+		}
+		if (writerecoveryconf)
+			pg_fatal("recovery configuration cannot be written with the \"client-blackhole\" backup target");
+	}
+
 	/*
 	 * Sanity checks for WAL method.
 	 */
@@ -2832,9 +2905,11 @@ main(int argc, char **argv)
 	/*
 	 * If an output directory was specified, verify that it exists, or create
 	 * it. Note that for a tar backup, an output directory of "-" means we are
-	 * writing to stdout, so do nothing in that case.
+	 * writing to stdout, so do nothing in that case.  When discarding writes,
+	 * we don't create anything on disk at all.
 	 */
-	if (basedir != NULL && (format == 'p' || strcmp(basedir, "-") != 0))
+	if (basedir != NULL && !backup_target_clientblackhole &&
+		(format == 'p' || strcmp(basedir, "-") != 0))
 		verify_dir_is_empty_or_create(basedir, &made_new_pgdata, &found_existing_pgdata);
 
 	/* determine remote server's xlog segment size */
@@ -2842,7 +2917,7 @@ main(int argc, char **argv)
 		exit(1);
 
 	/* Create pg_wal symlink, if required */
-	if (xlog_dir)
+	if (xlog_dir && !backup_target_clientblackhole)
 	{
 		char	   *linkloc;
 
diff --git a/src/bin/pg_basebackup/t/010_pg_basebackup.pl b/src/bin/pg_basebackup/t/010_pg_basebackup.pl
index 1b593ea73fe..03b05df85ad 100644
--- a/src/bin/pg_basebackup/t/010_pg_basebackup.pl
+++ b/src/bin/pg_basebackup/t/010_pg_basebackup.pl
@@ -324,6 +324,24 @@ $node->command_ok(
 ok(-f "$tempdir/tarbackup/base.tar", 'backup tar was created');
 rmtree("$tempdir/tarbackup");
 
+# The "client-blackhole" target receives the whole backup but throws it away.
+$node->command_ok(
+	[
+		@pg_basebackup_defs,
+		'--target' => 'client-blackhole',
+		'--format' => 'plain',
+		'--wal-method' => 'none'
+	],
+	'client-blackhole target in plain format');
+$node->command_ok(
+	[
+		@pg_basebackup_defs,
+		'--target' => 'client-blackhole',
+		'--format' => 'tar',
+		'--wal-method' => 'fetch'
+	],
+	'client-blackhole target in tar format');
+
 $node->command_fails_like(
 	[
 		@pg_basebackup_defs,
diff --git a/src/fe_utils/astreamer_file.c b/src/fe_utils/astreamer_file.c
index fb36cecc22a..e4fa2596b08 100644
--- a/src/fe_utils/astreamer_file.c
+++ b/src/fe_utils/astreamer_file.c
@@ -37,6 +37,7 @@ typedef struct astreamer_extractor
 	void		(*report_output_file) (const char *);
 	char		filename[MAXPGPATH];
 	FILE	   *file;
+	bool		discard_backup;
 } astreamer_extractor;
 
 static void astreamer_plain_writer_content(astreamer *streamer,
@@ -60,7 +61,8 @@ static void astreamer_extractor_finalize(astreamer *streamer);
 static void astreamer_extractor_free(astreamer *streamer);
 static void extract_directory(const char *filename, mode_t mode);
 static void extract_link(const char *filename, const char *linktarget);
-static FILE *create_file_for_extract(const char *filename, mode_t mode);
+static FILE *create_file_for_extract(const char *filename, mode_t mode,
+									 bool discard_backup);
 
 static const astreamer_ops astreamer_extractor_ops = {
 	.content = astreamer_extractor_content,
@@ -181,11 +183,15 @@ astreamer_plain_writer_free(astreamer *streamer)
  * 'report_output_file' is a function that will be called each time we open a
  * new output file. The pathname to that file is passed as an argument. If
  * NULL, the call is skipped.
+ *
+ * If 'discard_backup' is true, the extracted archive is thrown away rather
+ * than written to the filesystem.
  */
 astreamer *
 astreamer_extractor_new(const char *basepath,
 						const char *(*link_map) (const char *),
-						void (*report_output_file) (const char *))
+						void (*report_output_file) (const char *),
+						bool discard_backup)
 {
 	astreamer_extractor *streamer;
 
@@ -195,6 +201,7 @@ astreamer_extractor_new(const char *basepath,
 	streamer->basepath = pstrdup(basepath);
 	streamer->link_map = link_map;
 	streamer->report_output_file = report_output_file;
+	streamer->discard_backup = discard_backup;
 
 	return &streamer->base;
 }
@@ -231,13 +238,19 @@ astreamer_extractor_content(astreamer *streamer, astreamer_member *member,
 			if (mystreamer->filename[fnamelen - 1] == '/')
 				mystreamer->filename[fnamelen - 1] = '\0';
 
-			/* Dispatch based on file type. */
+			/*
+			 * Dispatch based on file type.
+			 */
 			if (member->is_regular)
 				mystreamer->file =
 					create_file_for_extract(mystreamer->filename,
-											member->mode);
+											member->mode,
+											mystreamer->discard_backup);
 			else if (member->is_directory)
-				extract_directory(mystreamer->filename, member->mode);
+			{
+				if (!mystreamer->discard_backup)
+					extract_directory(mystreamer->filename, member->mode);
+			}
 			else if (member->is_symlink)
 			{
 				const char *linktarget = member->linktarget;
@@ -252,7 +265,8 @@ astreamer_extractor_content(astreamer *streamer, astreamer_member *member,
 							 member->linktarget);
 				}
 
-				extract_link(mystreamer->filename, linktarget);
+				if (!mystreamer->discard_backup)
+					extract_link(mystreamer->filename, linktarget);
 			}
 
 			/* Report output file change. */
@@ -369,10 +383,18 @@ extract_link(const char *filename, const char *linktarget)
  * Return the resulting handle so we can write the content to the file.
  */
 static FILE *
-create_file_for_extract(const char *filename, mode_t mode)
+create_file_for_extract(const char *filename, mode_t mode, bool discard_backup)
 {
 	FILE	   *file;
 
+	if (discard_backup)
+	{
+		file = fopen(DEVNULL, "wb");
+		if (file == NULL)
+			pg_fatal("could not open file \"%s\": %m", DEVNULL);
+		return file;
+	}
+
 	file = fopen(filename, "wb");
 	if (file == NULL)
 		pg_fatal("could not create file \"%s\": %m", filename);
diff --git a/src/include/fe_utils/astreamer.h b/src/include/fe_utils/astreamer.h
index 8329e4efbc5..7206dc0c48b 100644
--- a/src/include/fe_utils/astreamer.h
+++ b/src/include/fe_utils/astreamer.h
@@ -215,7 +215,8 @@ extern astreamer *astreamer_gzip_writer_new(char *pathname, FILE *file,
 											pg_compress_specification *compress);
 extern astreamer *astreamer_extractor_new(const char *basepath,
 										  const char *(*link_map) (const char *),
-										  void (*report_output_file) (const char *));
+										  void (*report_output_file) (const char *),
+										  bool discard_backup);
 
 extern astreamer *astreamer_gzip_decompressor_new(astreamer *next);
 extern astreamer *astreamer_lz4_compressor_new(astreamer *next,
-- 
2.43.0

From 0619925d04488f9b04b72eeaa7dea64eeda5e37f Mon Sep 17 00:00:00 2001
From: Jakub Wartak <[email protected]>
Date: Fri, 31 Jul 2026 13:51:03 +0200
Subject: [PATCH v06082026 05/10] basebackup: issue posix_fadvise() for more
 efficient cold-cache handling

By informing the OS that we are going need whole file being backed up quite
soon, it can perform more efficient read-ahead. This in turns allows our
synchronous basebackup_read_file()->pread64() calls to get lower latencies
(as they fetch faster just from page-cache more often) and therefore this
increases the efficiency of the whole data transfer.

Author: Jakub Wartak <[email protected]>
---
 src/backend/backup/basebackup.c | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/src/backend/backup/basebackup.c b/src/backend/backup/basebackup.c
index d609355f429..dedfde74f90 100644
--- a/src/backend/backup/basebackup.c
+++ b/src/backend/backup/basebackup.c
@@ -1600,6 +1600,14 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename,
 				 errmsg("could not open file \"%s\": %m", readfilename)));
 	}
 
+	/*
+	 * Let the OS know that we are going to read the whole file. It's just an
+	 * hint, but it helps avoid longer synchronous read stalls.
+	 */
+#if defined(USE_POSIX_FADVISE) && defined(POSIX_FADV_SEQUENTIAL)
+	(void) posix_fadvise(fd, 0, 0, POSIX_FADV_SEQUENTIAL);
+#endif
+
 	_tarWriteHeader(sink, tarfilename, NULL, statbuf, false);
 
 	/*
-- 
2.43.0

From e88c53b124ff61881914443f712d1d8c8b56d0ae Mon Sep 17 00:00:00 2001
From: Jakub Wartak <[email protected]>
Date: Thu, 30 Jul 2026 11:02:13 +0200
Subject: [PATCH v06082026 01/10] pg_basebackup: rename the "blackhole" backup
 target to "server-blackhole"

The built-in "blackhole" backup target discards the backup on the server
side.  Rename it to "server-blackhole" to make the location of the discard
explicit and to leave room for a "client-blackhole" in the follow-up
commit.

Author: Jakub Wartak <[email protected]>
---
 doc/src/sgml/protocol.sgml                   |  2 +-
 doc/src/sgml/ref/pg_basebackup.sgml          |  6 +++---
 src/backend/backup/basebackup_target.c       |  4 ++--
 src/bin/pg_basebackup/t/010_pg_basebackup.pl | 12 ++++++------
 4 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 49f81676712..f5d4cf4440a 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -3174,7 +3174,7 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
            sent to the client. If it is <literal>server</literal>, the backup
            data is written to the server at the pathname specified by the
            <literal>TARGET_DETAIL</literal> option. If it is
-           <literal>blackhole</literal>, the backup data is not sent
+           <literal>server-blackhole</literal>, the backup data is not sent
            anywhere; it is simply discarded.
           </para>
 
diff --git a/doc/src/sgml/ref/pg_basebackup.sgml b/doc/src/sgml/ref/pg_basebackup.sgml
index fecee08b0a5..2ed643f005a 100644
--- a/doc/src/sgml/ref/pg_basebackup.sgml
+++ b/doc/src/sgml/ref/pg_basebackup.sgml
@@ -268,9 +268,9 @@ PostgreSQL documentation
         <literal>/some/path</literal> directory. Storing a backup on the
         server requires superuser privileges or having privileges of the
         <literal>pg_write_server_files</literal> role. If the target is set to
-        <literal>blackhole</literal>, the contents are discarded and not
-        stored anywhere. This should only be used for testing purposes, as you
-        will not end up with an actual backup.
+        <literal>server-blackhole</literal>, the contents are discarded by the
+        server and not stored anywhere. This should only be used for testing
+        purposes, as you will not end up with an actual backup.
        </para>
 
        <para>
diff --git a/src/backend/backup/basebackup_target.c b/src/backend/backup/basebackup_target.c
index 1c250d2895c..61465458638 100644
--- a/src/backend/backup/basebackup_target.c
+++ b/src/backend/backup/basebackup_target.c
@@ -40,7 +40,7 @@ static void *server_check_detail(char *target, char *target_detail);
 static BaseBackupTargetType builtin_backup_targets[] =
 {
 	{
-		"blackhole", reject_target_detail, blackhole_get_sink
+		"server-blackhole", reject_target_detail, blackhole_get_sink
 	},
 	{
 		"server", server_check_detail, server_get_sink
@@ -185,7 +185,7 @@ initialize_target_list(void)
 
 /*
  * Normally, a get_sink function should construct and return a new bbsink that
- * implements the backup target, but the 'blackhole' target just throws the
+ * implements the backup target, but the 'server-blackhole' target just throws the
  * data away. We could implement that by adding a bbsink that does nothing
  * but forward, but it's even cheaper to implement that by not adding a bbsink
  * at all.
diff --git a/src/bin/pg_basebackup/t/010_pg_basebackup.pl b/src/bin/pg_basebackup/t/010_pg_basebackup.pl
index cfcfdb8b580..1b593ea73fe 100644
--- a/src/bin/pg_basebackup/t/010_pg_basebackup.pl
+++ b/src/bin/pg_basebackup/t/010_pg_basebackup.pl
@@ -683,13 +683,13 @@ $node->command_ok(
 	'pg_basebackup --wal-method fetch runs');
 
 $node->command_fails_like(
-	[ @pg_basebackup_defs, '--target' => 'blackhole' ],
+	[ @pg_basebackup_defs, '--target' => 'server-blackhole' ],
 	qr/WAL cannot be streamed when a backup target is specified/,
 	'backup target requires --wal-method');
 $node->command_fails_like(
 	[
 		@pg_basebackup_defs,
-		'--target' => 'blackhole',
+		'--target' => 'server-blackhole',
 		'--wal-method' => 'stream'
 	],
 	qr/WAL cannot be streamed when a backup target is specified/,
@@ -701,7 +701,7 @@ $node->command_fails_like(
 $node->command_fails_like(
 	[
 		@pg_basebackup_defs,
-		'--target' => 'blackhole',
+		'--target' => 'server-blackhole',
 		'--wal-method' => 'none',
 		'--pgdata' => "$tempdir/blackhole"
 	],
@@ -710,7 +710,7 @@ $node->command_fails_like(
 $node->command_fails_like(
 	[
 		@pg_basebackup_defs,
-		'--target' => 'blackhole',
+		'--target' => 'server-blackhole',
 		'--wal-method' => 'none',
 		'--format' => 'tar'
 	],
@@ -719,7 +719,7 @@ $node->command_fails_like(
 $node->command_ok(
 	[
 		@pg_basebackup_defs,
-		'--target' => 'blackhole',
+		'--target' => 'server-blackhole',
 		'--wal-method' => 'none'
 	],
 	'backup target blackhole');
@@ -780,7 +780,7 @@ $node->command_fails_like(
 $node->command_fails_like(
 	[
 		@pg_basebackup_defs,
-		'--target' => 'blackhole',
+		'--target' => 'server-blackhole',
 		'--pgdata' => "$tempdir/blackhole"
 	],
 	qr/cannot specify both output directory and backup target/,
-- 
2.43.0

From 2a5945b244b7e103202356bb4d5e86f132dc6fe2 Mon Sep 17 00:00:00 2001
From: Jakub Wartak <[email protected]>
Date: Fri, 31 Jul 2026 10:13:57 +0200
Subject: [PATCH v06082026 04/10] basebackup: bump SINK_BUFFER_LENGTH to 256kB

By increasing SINK_BUFFER_LENGTH size to be closer to modern L2 CPU cache
sizes, we are also reducing frequency of the pread() system call which
also helps avoids performance penalites (and is visible as one of the
top functions when sending backups). In isolated performance runs where
pg_basebackup client did not have write it's backup to disk, this gives
~180% of the baseline (3.1GB/s -> 5.5GB/s).

Author: Jakub Wartak <[email protected]>
---
 src/backend/backup/basebackup.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/backend/backup/basebackup.c b/src/backend/backup/basebackup.c
index fe5ce23aaba..d609355f429 100644
--- a/src/backend/backup/basebackup.c
+++ b/src/backend/backup/basebackup.c
@@ -58,7 +58,7 @@
  * NB: The buffer size is required to be a multiple of the system block
  * size, so use that value instead if it's bigger than our preference.
  */
-#define SINK_BUFFER_LENGTH			Max(32768, BLCKSZ)
+#define SINK_BUFFER_LENGTH			Max(256 * 1024, BLCKSZ)
 
 typedef struct
 {
-- 
2.43.0

From 074fe5f93d4cf6c148bd2f7617319e3ba86c408c Mon Sep 17 00:00:00 2001
From: Jakub Wartak <[email protected]>
Date: Fri, 31 Jul 2026 13:51:18 +0200
Subject: [PATCH v06082026 06/10] pg_basebackup: elimiate usage of libc to
 coalesce writes() syscalls

glibc is not that efficient when writing lots of data using fwrite(3), which
happens because data received by libpq (recv()) most of time time causes
to generate 2 write calls (4kB one/default BUFSIZE = stat.stblksize due to fs
and the remainder of it using internal glibc's bypass).

We get archive content already in large chunks, so there is not reason to split
it into multiple write(2)s syscalls. Using setvbuf() is not helpful as it often
causes extra memory copy to be created which makes things even more slow.

astreamer_extractor now tracks an int fd instead of a FILE.

TODO: re-test how much it gains alone? maybe it's all just noise

Author: Jakub Wartak <[email protected]>
---
 src/fe_utils/astreamer_file.c | 96 +++++++++++++++++++++--------------
 1 file changed, 58 insertions(+), 38 deletions(-)

diff --git a/src/fe_utils/astreamer_file.c b/src/fe_utils/astreamer_file.c
index e4fa2596b08..b867e9489cd 100644
--- a/src/fe_utils/astreamer_file.c
+++ b/src/fe_utils/astreamer_file.c
@@ -15,6 +15,7 @@
 
 #include "postgres_fe.h"
 
+#include <fcntl.h>
 #include <unistd.h>
 
 #include "common/file_perm.h"
@@ -36,7 +37,7 @@ typedef struct astreamer_extractor
 	const char *(*link_map) (const char *);
 	void		(*report_output_file) (const char *);
 	char		filename[MAXPGPATH];
-	FILE	   *file;
+	int			fd;
 	bool		discard_backup;
 } astreamer_extractor;
 
@@ -61,8 +62,10 @@ static void astreamer_extractor_finalize(astreamer *streamer);
 static void astreamer_extractor_free(astreamer *streamer);
 static void extract_directory(const char *filename, mode_t mode);
 static void extract_link(const char *filename, const char *linktarget);
-static FILE *create_file_for_extract(const char *filename, mode_t mode,
-									 bool discard_backup);
+static int	create_file_for_extract(const char *filename, mode_t mode,
+									bool discard_backup);
+static void write_file_range(int fd, const char *filename,
+							 const char *data, int len);
 
 static const astreamer_ops astreamer_extractor_ops = {
 	.content = astreamer_extractor_content,
@@ -117,15 +120,7 @@ astreamer_plain_writer_content(astreamer *streamer,
 	if (len == 0)
 		return;
 
-	errno = 0;
-	if (fwrite(data, len, 1, mystreamer->file) != 1)
-	{
-		/* if write didn't set errno, assume problem is no disk space */
-		if (errno == 0)
-			errno = ENOSPC;
-		pg_fatal("could not write to file \"%s\": %m",
-				 mystreamer->pathname);
-	}
+	write_file_range(fileno(mystreamer->file), mystreamer->pathname, data, len);
 }
 
 /*
@@ -202,6 +197,7 @@ astreamer_extractor_new(const char *basepath,
 	streamer->link_map = link_map;
 	streamer->report_output_file = report_output_file;
 	streamer->discard_backup = discard_backup;
+	streamer->fd = -1;
 
 	return &streamer->base;
 }
@@ -223,7 +219,7 @@ astreamer_extractor_content(astreamer *streamer, astreamer_member *member,
 	switch (context)
 	{
 		case ASTREAMER_MEMBER_HEADER:
-			Assert(mystreamer->file == NULL);
+			Assert(mystreamer->fd == -1);
 
 			if (!path_is_safe_for_extraction(member->pathname))
 				pg_fatal("tar member has unsafe path name: \"%s\"",
@@ -242,7 +238,7 @@ astreamer_extractor_content(astreamer *streamer, astreamer_member *member,
 			 * Dispatch based on file type.
 			 */
 			if (member->is_regular)
-				mystreamer->file =
+				mystreamer->fd =
 					create_file_for_extract(mystreamer->filename,
 											member->mode,
 											mystreamer->discard_backup);
@@ -275,27 +271,21 @@ astreamer_extractor_content(astreamer *streamer, astreamer_member *member,
 			break;
 
 		case ASTREAMER_MEMBER_CONTENTS:
-			if (mystreamer->file == NULL)
+			if (mystreamer->fd == -1)
 				break;
 
-			errno = 0;
-			if (len > 0 && fwrite(data, len, 1, mystreamer->file) != 1)
-			{
-				/* if write didn't set errno, assume problem is no disk space */
-				if (errno == 0)
-					errno = ENOSPC;
-				pg_fatal("could not write to file \"%s\": %m",
-						 mystreamer->filename);
-			}
+			if (len > 0)
+				write_file_range(mystreamer->fd, mystreamer->filename,
+								 data, len);
 			break;
 
 		case ASTREAMER_MEMBER_TRAILER:
-			if (mystreamer->file == NULL)
+			if (mystreamer->fd == -1)
 				break;
-			if (fclose(mystreamer->file) != 0)
+			if (close(mystreamer->fd) != 0)
 				pg_fatal("could not close file \"%s\": %m",
 						 mystreamer->filename);
-			mystreamer->file = NULL;
+			mystreamer->fd = -1;
 			break;
 
 		case ASTREAMER_ARCHIVE_TRAILER:
@@ -380,23 +370,28 @@ extract_link(const char *filename, const char *linktarget)
 /*
  * Create a regular file.
  *
- * Return the resulting handle so we can write the content to the file.
+ * Return an open file descriptor so we can write the content to the file.
+ *
+ * We intentionally use a raw file descriptor to get unbuffered write()
+ * and avoid potentiall libc interference.
  */
-static FILE *
-create_file_for_extract(const char *filename, mode_t mode, bool discard_backup)
+static int
+create_file_for_extract(const char *filename, mode_t mode,
+						bool discard_backup)
 {
-	FILE	   *file;
+	int			fd;
 
 	if (discard_backup)
 	{
-		file = fopen(DEVNULL, "wb");
-		if (file == NULL)
+		fd = open(DEVNULL, O_WRONLY | PG_BINARY, 0);
+		if (fd < 0)
 			pg_fatal("could not open file \"%s\": %m", DEVNULL);
-		return file;
+		return fd;
 	}
 
-	file = fopen(filename, "wb");
-	if (file == NULL)
+	fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC | PG_BINARY,
+			  pg_file_create_mode);
+	if (fd < 0)
 		pg_fatal("could not create file \"%s\": %m", filename);
 
 #ifndef WIN32
@@ -405,7 +400,32 @@ create_file_for_extract(const char *filename, mode_t mode, bool discard_backup)
 				 filename);
 #endif
 
-	return file;
+	return fd;
+}
+
+/*
+ * Wrapper for safely writing chunk of archive. Single write() is not
+ * guaranteed to consume the whole, so we loop until all is on the disk.
+ */
+static void
+write_file_range(int fd, const char *filename, const char *data, int len)
+{
+	while (len > 0)
+	{
+		ssize_t		written;
+
+		errno = 0;
+		written = write(fd, data, len);
+		if (written <= 0)
+		{
+			if (errno == 0)
+				errno = ENOSPC;
+			pg_fatal("could not write to file \"%s\": %m", filename);
+		}
+
+		data += written;
+		len -= written;
+	}
 }
 
 /*
@@ -419,7 +439,7 @@ astreamer_extractor_finalize(astreamer *streamer)
 	astreamer_extractor *mystreamer PG_USED_FOR_ASSERTS_ONLY
 	= (astreamer_extractor *) streamer;
 
-	Assert(mystreamer->file == NULL);
+	Assert(mystreamer->fd == -1);
 }
 
 /*
-- 
2.43.0

From 308367ba3464a3575034dd050f01b26303f4f6dd Mon Sep 17 00:00:00 2001
From: Jakub Wartak <[email protected]>
Date: Fri, 31 Jul 2026 13:51:21 +0200
Subject: [PATCH v06082026 07/10] pg_basebackup: preallocate extracted files
 with posix_fallocate()

When extracting a basebackup, each output (segment/data file) grows each
one write() at a time. On filesystems with delayed allocation (e.g. ext4/XFS)
this makes every write() to find space, which shows up as top bottleneck of
the single-threaded/CPU-bount pg_basebackups's time. The tar member header
already gives us the final file size up front, so preallocate the whole file
in one go.

Author: Jakub Wartak <[email protected]>
---
 src/fe_utils/astreamer_file.c | 29 ++++++++++++++++++++++++++---
 1 file changed, 26 insertions(+), 3 deletions(-)

diff --git a/src/fe_utils/astreamer_file.c b/src/fe_utils/astreamer_file.c
index b867e9489cd..32df6b09fb9 100644
--- a/src/fe_utils/astreamer_file.c
+++ b/src/fe_utils/astreamer_file.c
@@ -63,7 +63,7 @@ static void astreamer_extractor_free(astreamer *streamer);
 static void extract_directory(const char *filename, mode_t mode);
 static void extract_link(const char *filename, const char *linktarget);
 static int	create_file_for_extract(const char *filename, mode_t mode,
-									bool discard_backup);
+									bool discard_backup, pgoff_t size);
 static void write_file_range(int fd, const char *filename,
 							 const char *data, int len);
 
@@ -241,7 +241,8 @@ astreamer_extractor_content(astreamer *streamer, astreamer_member *member,
 				mystreamer->fd =
 					create_file_for_extract(mystreamer->filename,
 											member->mode,
-											mystreamer->discard_backup);
+											mystreamer->discard_backup,
+											member->size);
 			else if (member->is_directory)
 			{
 				if (!mystreamer->discard_backup)
@@ -377,7 +378,7 @@ extract_link(const char *filename, const char *linktarget)
  */
 static int
 create_file_for_extract(const char *filename, mode_t mode,
-						bool discard_backup)
+						bool discard_backup, pgoff_t size)
 {
 	int			fd;
 
@@ -400,6 +401,28 @@ create_file_for_extract(const char *filename, mode_t mode,
 				 filename);
 #endif
 
+	/*
+	 * Preallocate the file to its final size.  We know the size up front
+	 * from the tar member header, so this lets the filesystem allocate all
+	 * the blocks in one go rather than growing the file on every write.
+	 */
+#ifdef HAVE_POSIX_FALLOCATE
+	if (size > 0)
+	{
+		int			rc = posix_fallocate(fd, 0, size);
+
+		/*
+		 * This is just an optimization, so we ignore failures such as
+		 * EINVAL/EOPNOTSUPP, however we need to properly fail on ENOSPC.
+		 */
+		if (rc == ENOSPC)
+		{
+			errno = rc;
+			pg_fatal("could not preallocate file \"%s\": %m", filename);
+		}
+	}
+#endif
+
 	return fd;
 }
 
-- 
2.43.0

From a5d75428cdfa4b14a6526c1678dab8a7fe10a68d Mon Sep 17 00:00:00 2001
From: Jakub Wartak <[email protected]>
Date: Wed, 5 Aug 2026 08:53:31 +0200
Subject: [PATCH v06082026 09/10] pg_basebackup: add support for Direct I/O and
 Async I/O using liburing

Profiling pg_basebackup when doing high-data transfer reveals that it is
highly CPU-bound on kernel side inside write() due to kernel's side
page-cache memory allocations/cgroups memory accounting and copying the data
from userspace back to kernel side, just to be copied again to the device.

To avoid that we can do Direct Memory Transfers from our userspace buffers
directly to the storage device, however we cannot do use synchronous API
for that due to high individual device latency. On Linux however, we are
already using liburing for the backend code, so this commit teaches
pg_basebackup/astreamers (for writing plain and tar formats) to write using
AIO+DIO with deep I/O queues. It realistic benchmarks and with adequate
sequential storage bandwidth and no SSL encryption this makes possible for
pg_basebackup -Fp to go from ~1610MB/s to ~2975MB/s as long as the data is
hot in page-cache of the source server and the network and single stream TCP
socket performance is able to keep up.

This commits also exposes undocumented/debugging PG_BASEBACKUP_NODIO env
variable, that when set allows disabling this optimization.

Author: Jakub Wartak <[email protected]>
---
 src/bin/pg_basebackup/Makefile        |   2 +-
 src/bin/pg_basebackup/meson.build     |   2 +-
 src/bin/pg_basebackup/pg_basebackup.c |   9 +-
 src/fe_utils/astreamer_file.c         | 465 +++++++++++++++++++++++++-
 src/fe_utils/meson.build              |   2 +-
 src/include/fe_utils/astreamer.h      |   5 +-
 6 files changed, 463 insertions(+), 22 deletions(-)

diff --git a/src/bin/pg_basebackup/Makefile b/src/bin/pg_basebackup/Makefile
index df94fc27d02..448abb71e99 100644
--- a/src/bin/pg_basebackup/Makefile
+++ b/src/bin/pg_basebackup/Makefile
@@ -42,7 +42,7 @@ BBOBJS = \
 all: pg_basebackup pg_createsubscriber pg_receivewal pg_recvlogical
 
 pg_basebackup: $(BBOBJS) $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils
-	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
+	$(CC) $(CFLAGS) $(BBOBJS) $(OBJS) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) $(LIBURING_LIBS) -o $@$(X)
 
 pg_createsubscriber: pg_createsubscriber.o $(WIN32RES) | submake-libpq submake-libpgport submake-libpgfeutils
 	$(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
diff --git a/src/bin/pg_basebackup/meson.build b/src/bin/pg_basebackup/meson.build
index d70ce5786a2..e4cb673e933 100644
--- a/src/bin/pg_basebackup/meson.build
+++ b/src/bin/pg_basebackup/meson.build
@@ -7,7 +7,7 @@ common_sources = files(
   'walmethods.c',
 )
 
-pg_basebackup_deps = [frontend_code, libpq, lz4, zlib, zstd]
+pg_basebackup_deps = [frontend_code, libpq, lz4, zlib, zstd, liburing]
 pg_basebackup_common = static_library('libpg_basebackup_common',
   common_sources,
   dependencies: pg_basebackup_deps,
diff --git a/src/bin/pg_basebackup/pg_basebackup.c b/src/bin/pg_basebackup/pg_basebackup.c
index 708b0727941..490395510e9 100644
--- a/src/bin/pg_basebackup/pg_basebackup.c
+++ b/src/bin/pg_basebackup/pg_basebackup.c
@@ -1158,7 +1158,8 @@ CreateBackupStreamer(char *archive_name, char *spclocation,
 		streamer = astreamer_extractor_new(directory,
 										   get_tablespace_mapping,
 										   progress_update_filename,
-										   backup_target_clientblackhole);
+										   backup_target_clientblackhole,
+										   verbose);
 	}
 	else
 	{
@@ -1196,7 +1197,7 @@ CreateBackupStreamer(char *archive_name, char *spclocation,
 		 */
 		if (compress->algorithm == PG_COMPRESSION_NONE)
 			streamer = astreamer_plain_writer_new(archive_filename,
-												  archive_file);
+												  archive_file, verbose);
 		else if (compress->algorithm == PG_COMPRESSION_GZIP)
 		{
 			if (!backup_target_clientblackhole)
@@ -1209,7 +1210,7 @@ CreateBackupStreamer(char *archive_name, char *spclocation,
 			if (!backup_target_clientblackhole)
 				strlcat(archive_filename, ".lz4", sizeof(archive_filename));
 			streamer = astreamer_plain_writer_new(archive_filename,
-												  archive_file);
+												  archive_file, verbose);
 			streamer = astreamer_lz4_compressor_new(streamer, compress);
 		}
 		else if (compress->algorithm == PG_COMPRESSION_ZSTD)
@@ -1217,7 +1218,7 @@ CreateBackupStreamer(char *archive_name, char *spclocation,
 			if (!backup_target_clientblackhole)
 				strlcat(archive_filename, ".zst", sizeof(archive_filename));
 			streamer = astreamer_plain_writer_new(archive_filename,
-												  archive_file);
+												  archive_file, verbose);
 			streamer = astreamer_zstd_compressor_new(streamer, compress);
 		}
 		else
diff --git a/src/fe_utils/astreamer_file.c b/src/fe_utils/astreamer_file.c
index 32df6b09fb9..d4d34e5d412 100644
--- a/src/fe_utils/astreamer_file.c
+++ b/src/fe_utils/astreamer_file.c
@@ -17,17 +17,61 @@
 
 #include <fcntl.h>
 #include <unistd.h>
+#ifdef USE_LIBURING
+#include <liburing.h>
+#endif
 
 #include "common/file_perm.h"
 #include "common/logging.h"
 #include "fe_utils/astreamer.h"
 
+#ifdef USE_LIBURING
+/*
+ * Parameters for the io_uring + O_DIRECT write path. Aim is to keep deep
+ * queue of the I/O devices populated by throwing large number of independent
+ * synchronous/DIRECT_IO requests, but in asynchronous way. We avoid latency
+ * of single synchronous write, and utilize the device up to the max what it
+ * allows.
+ */
+#define DIO_ALIGN		4096			/* O_DIRECT aligment */
+#define DIO_BUFSZ		(1024 * 1024)	/* size of each direct I/O write */
+#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 */
+
+/*
+ * State for the dio/io_uring writer. Used by plain(file) and tar extractors.
+ * We buffer data as we recieve it, into the "pool" of buffers and submit them
+ * using DIO_BUFSZ sizes.
+ */
+typedef struct dio_writer
+{
+	struct io_uring ring;       /* see io_uring(7) */
+	bool		ring_ready;		/* was the ring and buffer initialized */
+	char	   *pool;			/* buffer memory, max size: DIO_NBUF * DIO_BUFSZ */
+	bool		busy[DIO_NBUF]; /* is buffer in flight? */
+	size_t		buflen[DIO_NBUF];	/* bytes submitted for in-flight buffer */
+	int			fd;				/* DIO fd: if fd > 0 then it's active, -1 otherwise */
+	const char *filename;		/* for error handling */
+	pgoff_t		offset;			/* offset of the next write */
+	pgoff_t		written;		/* bytes written */
+	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		notified;		/* already logged a fall-back-to-buffered? */
+} dio_writer;
+#endif
+
 typedef struct astreamer_plain_writer
 {
 	astreamer	base;
 	char	   *pathname;
-	FILE	   *file;
+	FILE	   *file;			/* if NULL, then dio.fd is used */
 	bool		should_close_file;
+#ifdef USE_LIBURING
+	dio_writer	dio;
+#endif
 } astreamer_plain_writer;
 
 typedef struct astreamer_extractor
@@ -37,8 +81,12 @@ typedef struct astreamer_extractor
 	const char *(*link_map) (const char *);
 	void		(*report_output_file) (const char *);
 	char		filename[MAXPGPATH];
-	int			fd;
+	int			fd;				/* if -1, then dio.fd is used */
 	bool		discard_backup;
+	bool		verbose;
+#ifdef USE_LIBURING
+	dio_writer	dio;
+#endif
 } astreamer_extractor;
 
 static void astreamer_plain_writer_content(astreamer *streamer,
@@ -66,6 +114,13 @@ static int	create_file_for_extract(const char *filename, mode_t mode,
 									bool discard_backup, pgoff_t size);
 static void write_file_range(int fd, const char *filename,
 							 const char *data, int len);
+#ifdef USE_LIBURING
+static bool dio_writer_start(dio_writer *dw, const char *filename,
+							 pgoff_t prealloc_size, bool verbose);
+static void dio_writer_write(dio_writer *dw, const char *data, int len);
+static void dio_writer_finish(dio_writer *dw);
+static void dio_writer_destroy(dio_writer *dw);
+#endif
 
 static const astreamer_ops astreamer_extractor_ops = {
 	.content = astreamer_extractor_content,
@@ -83,7 +138,7 @@ static const astreamer_ops astreamer_extractor_ops = {
  * there.
  */
 astreamer *
-astreamer_plain_writer_new(char *pathname, FILE *file)
+astreamer_plain_writer_new(char *pathname, FILE *file, bool verbose)
 {
 	astreamer_plain_writer *streamer;
 
@@ -93,13 +148,33 @@ astreamer_plain_writer_new(char *pathname, FILE *file)
 
 	streamer->pathname = pstrdup(pathname);
 	streamer->file = file;
+#ifdef USE_LIBURING
+	streamer->dio.fd = -1;
+#endif
 
 	if (file == NULL)
 	{
-		streamer->file = fopen(pathname, "wb");
-		if (streamer->file == NULL)
-			pg_fatal("could not create file \"%s\": %m", pathname);
-		streamer->should_close_file = true;
+#ifdef USE_LIBURING
+
+		/*
+		 * Fallback to classic buffered writes in case of:
+		 * - env variable PG_BASEBACKUP_NODIO is set (debugging)
+		 * - pipe/stdout is used
+		 * - /dev/null is being used (-t client-blackhole)
+		 * - filesystems without O_DIRECT support
+		 */
+		if (getenv("PG_BASEBACKUP_NODIO") == NULL &&
+			dio_writer_start(&streamer->dio, streamer->pathname, 0, verbose) == true) {
+			/* do not use buffered writes, as the dio.fd is going to be used */
+			streamer->file = NULL;
+		} else
+#endif
+		{
+			streamer->file = fopen(pathname, "wb");
+			if (streamer->file == NULL)
+				pg_fatal("could not create file \"%s\": %m", pathname);
+			streamer->should_close_file = true;
+		}
 	}
 
 	return &streamer->base;
@@ -120,6 +195,13 @@ astreamer_plain_writer_content(astreamer *streamer,
 	if (len == 0)
 		return;
 
+#ifdef USE_LIBURING
+	if (mystreamer->dio.fd != -1)
+	{
+		dio_writer_write(&mystreamer->dio, data, len);
+		return;
+	}
+#endif
 	write_file_range(fileno(mystreamer->file), mystreamer->pathname, data, len);
 }
 
@@ -134,6 +216,13 @@ astreamer_plain_writer_finalize(astreamer *streamer)
 
 	mystreamer = (astreamer_plain_writer *) streamer;
 
+#ifdef USE_LIBURING
+	if (mystreamer->dio.fd != -1)
+	{
+		dio_writer_finish(&mystreamer->dio);
+		return;
+	}
+#endif
 	if (mystreamer->should_close_file && fclose(mystreamer->file) != 0)
 		pg_fatal("could not close file \"%s\": %m",
 				 mystreamer->pathname);
@@ -156,6 +245,9 @@ astreamer_plain_writer_free(astreamer *streamer)
 	Assert(mystreamer->base.bbs_next == NULL);
 
 	pfree(mystreamer->pathname);
+#ifdef USE_LIBURING
+	dio_writer_destroy(&mystreamer->dio);
+#endif
 	pfree(mystreamer);
 }
 
@@ -186,7 +278,7 @@ astreamer *
 astreamer_extractor_new(const char *basepath,
 						const char *(*link_map) (const char *),
 						void (*report_output_file) (const char *),
-						bool discard_backup)
+						bool discard_backup, bool verbose)
 {
 	astreamer_extractor *streamer;
 
@@ -197,7 +289,11 @@ astreamer_extractor_new(const char *basepath,
 	streamer->link_map = link_map;
 	streamer->report_output_file = report_output_file;
 	streamer->discard_backup = discard_backup;
+	streamer->verbose = verbose;
 	streamer->fd = -1;
+#ifdef USE_LIBURING
+	streamer->dio.fd = -1;
+#endif
 
 	return &streamer->base;
 }
@@ -220,6 +316,9 @@ astreamer_extractor_content(astreamer *streamer, astreamer_member *member,
 	{
 		case ASTREAMER_MEMBER_HEADER:
 			Assert(mystreamer->fd == -1);
+#ifdef USE_LIBURING
+			Assert(mystreamer->dio.fd == -1);
+#endif
 
 			if (!path_is_safe_for_extraction(member->pathname))
 				pg_fatal("tar member has unsafe path name: \"%s\"",
@@ -238,11 +337,31 @@ astreamer_extractor_content(astreamer *streamer, astreamer_member *member,
 			 * Dispatch based on file type.
 			 */
 			if (member->is_regular)
-				mystreamer->fd =
-					create_file_for_extract(mystreamer->filename,
-											member->mode,
-											mystreamer->discard_backup,
-											member->size);
+			{
+#ifdef USE_LIBURING
+				/*
+				 * Fallback to classic buffered writes in case of:
+				 * - small files
+				 * - env variable PG_BASEBACKUP_NODIO is set (debugging)
+				 * - pipe/stdout is used
+				 * - /dev/null is being used (-t client-blackhole)
+				 * - filesystems without O_DIRECT support
+				 */
+				if (member->size >= DIO_MIN_SIZE &&
+					!mystreamer->discard_backup &&
+					getenv("PG_BASEBACKUP_NODIO") == NULL &&
+					dio_writer_start(&mystreamer->dio, mystreamer->filename,
+									 member->size, mystreamer->verbose)) {
+					/* do not use buffered writes, as the dio.fd is going to be used */
+					mystreamer->fd = -1;
+				} else
+#endif
+					mystreamer->fd =
+						create_file_for_extract(mystreamer->filename,
+												member->mode,
+												mystreamer->discard_backup,
+												member->size);
+			}
 			else if (member->is_directory)
 			{
 				if (!mystreamer->discard_backup)
@@ -272,6 +391,14 @@ astreamer_extractor_content(astreamer *streamer, astreamer_member *member,
 			break;
 
 		case ASTREAMER_MEMBER_CONTENTS:
+#ifdef USE_LIBURING
+			if (mystreamer->dio.fd != -1)
+			{
+				if (len > 0)
+					dio_writer_write(&mystreamer->dio, data, len);
+				break;
+			}
+#endif
 			if (mystreamer->fd == -1)
 				break;
 
@@ -281,6 +408,13 @@ astreamer_extractor_content(astreamer *streamer, astreamer_member *member,
 			break;
 
 		case ASTREAMER_MEMBER_TRAILER:
+#ifdef USE_LIBURING
+			if (mystreamer->dio.fd != -1)
+			{
+				dio_writer_finish(&mystreamer->dio);
+				break;
+			}
+#endif
 			if (mystreamer->fd == -1)
 				break;
 			if (close(mystreamer->fd) != 0)
@@ -463,6 +597,9 @@ astreamer_extractor_finalize(astreamer *streamer)
 	= (astreamer_extractor *) streamer;
 
 	Assert(mystreamer->fd == -1);
+#ifdef USE_LIBURING
+	Assert(mystreamer->dio.fd == -1);
+#endif
 }
 
 /*
@@ -474,5 +611,307 @@ astreamer_extractor_free(astreamer *streamer)
 	astreamer_extractor *mystreamer = (astreamer_extractor *) streamer;
 
 	pfree(mystreamer->basepath);
+#ifdef USE_LIBURING
+	dio_writer_destroy(&mystreamer->dio);
+#endif
 	pfree(mystreamer);
 }
+
+#ifdef USE_LIBURING
+/*
+ * IO_uring/liburing uses concept of two rings (submissions and completion).
+ * See io_uring_queue_init(3) and io_uring(7) for more information and
+ * especially https://github.com/axboe/liburing/blob/master/examples/io_uring-cp.c
+ * for nice example on how to use it.
+ */
+
+/* Take (consume) one completion event from the ring */
+static void
+dio_wait_one(dio_writer *dw)
+{
+	int			idx;
+	int			ret;
+	struct io_uring_cqe *cqe;
+
+	ret = io_uring_wait_cqe(&dw->ring, &cqe);
+	if (ret < 0)
+	{
+		errno = -ret;
+		pg_fatal("could not wait for io_uring_wait_cqe(): %m");
+	}
+
+	idx = (int) io_uring_cqe_get_data64(cqe);
+	if (cqe->res < 0)
+	{
+		errno = -cqe->res;
+		pg_fatal("could not write to file \"%s\": %m", dw->filename);
+	}
+
+	if ((size_t) cqe->res != dw->buflen[idx])
+	{
+		/* short write? */
+		errno = ENOSPC;
+		pg_fatal("could not write to file \"%s\": %m", dw->filename);
+	}
+
+	io_uring_cqe_seen(&dw->ring, cqe);
+	dw->busy[idx] = false;
+	dw->inflight--;
+}
+
+/*
+ * Common routine for flushing (submitting) earlier prepared, but not yet
+ * submitted SQEs.
+ */
+static void
+dio_flush_sq(dio_writer *dw)
+{
+	int			ret;
+
+	if (dw->unsubmitted == 0)
+		return;
+
+	ret = io_uring_submit(&dw->ring);
+	if (ret < 0)
+	{
+		errno = -ret;
+		pg_fatal("could not submit io_uring (io_uring_enter(2) failure?): %m");
+	}
+
+	/* Everything was submitted */
+	dw->unsubmitted = 0;
+}
+
+/* Get free buffer index. If none are available, wait for some to finish */
+static int
+dio_get_free_buf(dio_writer *dw)
+{
+	for (;;)
+	{
+		for (int i = 0; i < DIO_NBUF; i++)
+		{
+			if (dw->busy[i] == false)
+			{
+				/*
+				 * Mark it busy till we finish the write (technically we need CQE for
+				 * this buffer to arrive, and then can mark it as non-busy again).
+				 * */
+				dw->busy[i] = true;
+				return i;
+			}
+		}
+		/* All buffers are busy, so we wait for writes to finish */
+		dio_flush_sq(dw);
+		dio_wait_one(dw);
+	}
+}
+
+/*
+ * 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
+ * couple of times to avoid constant syscall tax (AKA io_uring batching).
+ *
+ * buffer's idx needs to have it's lenth aligned to DIO_ALIGN (O_DIRECT
+ * requirement).
+ */
+static void
+dio_submit(dio_writer *dw, int idx, size_t len)
+{
+	struct io_uring_sqe *sqe;
+
+	sqe = io_uring_get_sqe(&dw->ring);
+	/* this should never happen? liburing/examples uses abort/asserts for this */
+	if (sqe == NULL)
+		pg_fatal("io_uring submission queue full: %m");
+
+	io_uring_prep_write(sqe, dw->fd,
+						dw->pool + (size_t) idx * DIO_BUFSZ,
+						len, dw->offset);
+	io_uring_sqe_set_data64(sqe, idx);
+
+	dw->buflen[idx] = len;
+	dw->offset += len;
+	dw->inflight++;
+	dw->unsubmitted++;
+
+	if (dw->unsubmitted >= DIO_SUBMIT_BATCH)
+		dio_flush_sq(dw);
+}
+
+/*
+ * Start writing to a file through the DIO+AIO path. Returns true if that is
+ * supported (dw->fd is set).
+ *
+ * If prealloc_size is known it can be used to preallocate file which helps
+ * greatly to avoid slow writes with O_DIRECT on some filesystems.
+ */
+static bool
+dio_writer_start(dio_writer *dw, const char *filename, pgoff_t prealloc_size, bool verbose)
+{
+	int			fd;
+	int			ret;
+	struct stat st;
+
+	fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC | O_DIRECT | PG_BINARY,
+			  pg_file_create_mode);
+	if (fd < 0)
+	{
+		if (verbose && !dw->notified)
+		{
+			pg_log_info("could not open \"%s\" with O_DIRECT, using buffered I/O instead: %m",
+						filename);
+			dw->notified = true;
+		}
+		return false;
+	}
+
+	/*
+	 * Only regular files can be written with O_DIRECT. However, the /dev/null
+	 * device (used with -t client-blackhole mode) and other special files can
+	 * be still opened with O_DIRECT, but must go through the buffered path(?).
+	 * Ensure we opened regular file before setting up the io_uring, so we
+	 * don't allocate the buffer pool just to fall back.
+	 */
+	if (fstat(fd, &st) != 0 || !S_ISREG(st.st_mode))
+	{
+		if (verbose && !dw->notified)
+		{
+			pg_log_info("\"%s\" is not a regular file, using buffered I/O instead",
+						filename);
+			dw->notified = true;
+		}
+		close(fd);
+		return false;
+	}
+
+	/* Lazy setup of the io_uring */
+	if (!dw->ring_ready)
+	{
+		ret = io_uring_queue_init(DIO_NBUF, &dw->ring, 0);
+		if (ret != 0)
+		{
+			errno = -ret;
+			/* XXX: shouldn't we warn about it non-verbose mode? */
+			if (verbose && !dw->notified)
+			{
+				pg_log_info("could not set up io_uring, using buffered I/O instead: %m");
+				dw->notified = true;
+			}
+			close(fd);
+			return false;
+		}
+
+		/* O_DIRECT writes require memory aligment */
+		if (posix_memalign((void **) &dw->pool, DIO_ALIGN,
+						   (size_t) DIO_NBUF * DIO_BUFSZ) != 0)
+			pg_fatal("unable to allocate aligned memory for direct I/O writes");
+
+		dw->ring_ready = true;
+		if (verbose)
+			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->curidx = -1;
+	dw->curlen = 0;
+	return true;
+}
+
+/*
+ * Handle buffering on DIO side (in dio->pool buffers) before submiting
+ * full (big) writes as required by O_DIRECT.
+ */
+static void
+dio_writer_write(dio_writer *dw, const char *data, int len)
+{
+	dw->written += len;
+
+	while (len > 0)
+	{
+		char	   *buf;
+		int			space;
+		int			n;
+
+		if (dw->curidx < 0)
+		{
+			dw->curidx = dio_get_free_buf(dw);
+			dw->curlen = 0;
+		}
+
+		buf = dw->pool + (size_t) dw->curidx * DIO_BUFSZ;
+		space = DIO_BUFSZ - dw->curlen;
+		n = Min(space, len);
+		/* append to the pool buffer */
+		memcpy(buf + dw->curlen, data, n);
+		dw->curlen += n;
+		data += n;
+		len -= n;
+
+		if (dw->curlen == DIO_BUFSZ)
+		{
+			dio_submit(dw, dw->curidx, DIO_BUFSZ);
+			dw->curidx = -1;
+		}
+	}
+}
+
+/* Send/wait for remaining in-flight writes, truncate and close the DIO */
+static void
+dio_writer_finish(dio_writer *dw)
+{
+	/* Flush partial buffer */
+	if (dw->curidx >= 0 && dw->curlen > 0)
+	{
+		char	   *buf = dw->pool + (size_t) dw->curidx * DIO_BUFSZ;
+		/* We need to pad stuff */
+		size_t		padded = TYPEALIGN(DIO_ALIGN, dw->curlen);
+
+		memset(buf + dw->curlen, 0, padded - dw->curlen);
+		dio_submit(dw, dw->curidx, padded);
+		dw->curidx = -1;
+	}
+
+	/* Submit any batched writes and wait for them all to finish */
+	dio_flush_sq(dw);
+	while (dw->inflight > 0)
+		dio_wait_one(dw);
+
+	/* Truncate the file to the right size (we could pre-allocate too much) */
+	if (ftruncate(dw->fd, dw->written) != 0)
+		pg_fatal("could not truncate file \"%s\": %m", dw->filename);
+
+	if (close(dw->fd) != 0)
+		pg_fatal("could not close file \"%s\": %m", dw->filename);
+	dw->fd = -1;
+}
+
+/* Free the buffers and the io_uring */
+static void
+dio_writer_destroy(dio_writer *dw)
+{
+	if (dw->ring_ready)
+	{
+		io_uring_queue_exit(&dw->ring);
+		free(dw->pool);
+		dw->ring_ready = false;
+		dw->fd = -1;
+	}
+}
+#endif /* USE_LIBURING */
diff --git a/src/fe_utils/meson.build b/src/fe_utils/meson.build
index 86befca192e..bf508da183c 100644
--- a/src/fe_utils/meson.build
+++ b/src/fe_utils/meson.build
@@ -34,7 +34,7 @@ fe_utils = static_library('libpgfeutils',
   c_pch: pch_postgres_fe_h,
   include_directories: [postgres_inc, libpq_inc],
   c_args: host_system == 'windows' ? ['-DFD_SETSIZE=1024'] : [],
-  dependencies: frontend_common_code,
+  dependencies: [frontend_common_code, liburing],
   kwargs: default_lib_args + {
             'install': install_internal_static_lib,
           },
diff --git a/src/include/fe_utils/astreamer.h b/src/include/fe_utils/astreamer.h
index 7206dc0c48b..6eae8ebbb6d 100644
--- a/src/include/fe_utils/astreamer.h
+++ b/src/include/fe_utils/astreamer.h
@@ -210,13 +210,14 @@ astreamer_buffer_until(astreamer *streamer, const char **data, int *len,
  * Functions for creating astreamer objects of various types. See the header
  * comments for each of these functions for details.
  */
-extern astreamer *astreamer_plain_writer_new(char *pathname, FILE *file);
+extern astreamer *astreamer_plain_writer_new(char *pathname, FILE *file,
+											 bool verbose);
 extern astreamer *astreamer_gzip_writer_new(char *pathname, FILE *file,
 											pg_compress_specification *compress);
 extern astreamer *astreamer_extractor_new(const char *basepath,
 										  const char *(*link_map) (const char *),
 										  void (*report_output_file) (const char *),
-										  bool discard_backup);
+										  bool discard_backup, bool verbose);
 
 extern astreamer *astreamer_gzip_decompressor_new(astreamer *next);
 extern astreamer *astreamer_lz4_compressor_new(astreamer *next,
-- 
2.43.0

From c9acce9bc8e3f6baa940c8c91e49326ae6ad95d2 Mon Sep 17 00:00:00 2001
From: Jakub Wartak <[email protected]>
Date: Tue, 4 Aug 2026 12:00:23 +0200
Subject: [PATCH v06082026 08/10] libpq / pg_basebackup: add
 PQgetCopyDataInternalBuf() for heavy transfers

At high data transfer rates(>10Gbit/s), the primary bottlneck in pg_basebackup
becomes the 2-nd memory copy done in the userspace by libpq itself (the first
one is the kernel itself copying the data from network to the userspace
socket).

Add optimized variant of PQgetCopyData(): PQgetCopyDataInternalBuf() which
avoids repeated memory allocation and copying. The other major differences are:
- we do not null terminate the string (as that is not used anyway)
- the data is only valid untill the next call (so by that time it has to
  be consumed, but we can use internal libpq buffer until we receive
  more)

Author: Jakub Wartak <[email protected]>
---
 src/bin/pg_basebackup/pg_basebackup.c | 10 +++--
 src/interfaces/libpq/exports.txt      |  1 +
 src/interfaces/libpq/fe-exec.c        | 26 ++++++++++++
 src/interfaces/libpq/fe-protocol3.c   | 58 +++++++++++++++++++++++++++
 src/interfaces/libpq/libpq-fe.h       |  1 +
 src/interfaces/libpq/libpq-int.h      |  1 +
 6 files changed, 94 insertions(+), 3 deletions(-)

diff --git a/src/bin/pg_basebackup/pg_basebackup.c b/src/bin/pg_basebackup/pg_basebackup.c
index af579a967a0..708b0727941 100644
--- a/src/bin/pg_basebackup/pg_basebackup.c
+++ b/src/bin/pg_basebackup/pg_basebackup.c
@@ -1034,7 +1034,13 @@ ReceiveCopyData(PGconn *conn, WriteDataCallback callback,
 		int			r;
 		char	   *copybuf;
 
-		r = PQgetCopyData(conn, &copybuf, 0);
+		/*
+		 * Use the no-copy optimization: copybuf points directly into libpq's
+		 * receive buffer and stays valid until the next call, which is all the
+		 * callback needs (this avoids memory copy which hurts at high transfer
+		 * rates).
+		 */
+		r = PQgetCopyDataInternalBuf(conn, &copybuf, 0);
 		if (r == -1)
 		{
 			/* End of chunk. */
@@ -1048,8 +1054,6 @@ ReceiveCopyData(PGconn *conn, WriteDataCallback callback,
 			pg_fatal("background process terminated unexpectedly");
 
 		(*callback) (r, copybuf, callback_data);
-
-		PQfreemem(copybuf);
 	}
 }
 
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index 1e3d5bd5867..e02b92b285e 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -211,3 +211,4 @@ PQdefaultAuthDataHook     208
 PQfullProtocolVersion     209
 appendPQExpBufferVA       210
 PQgetThreadLock           211
+PQgetCopyDataInternalBuf  212
diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c
index 7b8edacbfde..9e584c222bf 100644
--- a/src/interfaces/libpq/fe-exec.c
+++ b/src/interfaces/libpq/fe-exec.c
@@ -2844,6 +2844,32 @@ PQgetCopyData(PGconn *conn, char **buffer, int async)
 	return pqGetCopyData3(conn, buffer, async);
 }
 
+/*
+ * PQgetCopyDataInternalBuf - variant of PQgetCopyData which bypasses memcpy()
+ * for high data transfer rates.
+ *
+ * Set *buffer to point directly into the internal receive buffer rather
+ * allocate and copy memory on every call. The *buffer is valid only until the
+ * next PQgetCopyDataInternalBuf/PQgetCopyData call on this connection, and must
+ * not be passed to PQfreemem().  The returned payload is NOT null-terminated.
+ *
+ * Otherwise it is pretty much the same as the original PQgetCopyData.
+ */
+int
+PQgetCopyDataInternalBuf(PGconn *conn, char **buffer, int async)
+{
+	*buffer = NULL;				/* for all failure cases */
+	if (!conn)
+		return -2;
+	if (conn->asyncStatus != PGASYNC_COPY_OUT &&
+		conn->asyncStatus != PGASYNC_COPY_BOTH)
+	{
+		libpq_append_conn_error(conn, "no COPY in progress");
+		return -2;
+	}
+	return pqGetCopyDataInternalBuf3(conn, buffer, async);
+}
+
 /*
  * PQgetline - gets a newline-terminated string from the backend.
  *
diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c
index 9d6a285fb28..18ba62b883f 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -2000,6 +2000,64 @@ pqGetCopyData3(PGconn *conn, char **buffer, int async)
 	}
 }
 
+/*
+ * pqGetCopyDataInternalBuf3 - like pqGetCopyData3, but avoids malloc and
+ * memory copying.
+ *
+ * Instead of allocating fresh buffer and copying the CopyData payload into
+ * that new memory, this sets *buffer directly into conn->inBuffer and returns
+ * its length. The message is marked consumed, and the returned pointer stays
+ * valid only until the next libpq call that reads from the socket (e.g. using
+ * pgReadData() or this call). Called must be done with the data processing
+ * before calling this again.
+ *
+ * Differences between thnis and the orginal pgGetCopyData3() are:
+ * - the caller must not free *buffer
+ * - the payload is not null-terminated
+ *
+ * The main advantage of this call is that it avoids memory copy for for
+ * high-throughput COPY.
+ */
+int
+pqGetCopyDataInternalBuf3(PGconn *conn, char **buffer, int async)
+{
+	int			msgLength;
+
+	for (;;)
+	{
+		/* Collect the next input message; see pqGetCopyData3 for details. */
+		msgLength = getCopyDataMessage(conn);
+		if (msgLength < 0)
+			return msgLength;	/* end-of-copy or error */
+		if (msgLength == 0)
+		{
+			/* Don't block if async read requested */
+			if (async)
+				return 0;
+			/* Need to load more data */
+			if (pqWait(true, false, conn) ||
+				pqReadData(conn) < 0)
+				return -2;
+			continue;
+		}
+
+		msgLength -= 4;
+		if (msgLength > 0)
+		{
+			/* Just use libpq internal input buffer */
+			*buffer = &conn->inBuffer[conn->inCursor];
+
+			/* Mark message consumed */
+			pqParseDone(conn, conn->inCursor + msgLength);
+
+			return msgLength;
+		}
+
+		/* Empty, so drop it and loop around for another */
+		pqParseDone(conn, conn->inCursor);
+	}
+}
+
 /*
  * PQgetline - gets a newline-terminated string from the backend.
  *
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index 8ecb9b4a4c7..350019864a0 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -564,6 +564,7 @@ extern PGnotify *PQnotifies(PGconn *conn);
 extern int	PQputCopyData(PGconn *conn, const char *buffer, int nbytes);
 extern int	PQputCopyEnd(PGconn *conn, const char *errormsg);
 extern int	PQgetCopyData(PGconn *conn, char **buffer, int async);
+extern int	PQgetCopyDataInternalBuf(PGconn *conn, char **buffer, int async);
 
 /* Deprecated routines for copy in/out */
 extern int	PQgetline(PGconn *conn, char *buffer, int length);
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 3f921207a14..234524d947e 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -779,6 +779,7 @@ extern void pqBuildErrorMessage3(PQExpBuffer msg, const PGresult *res,
 								 PGVerbosity verbosity, PGContextVisibility show_context);
 extern int	pqGetNegotiateProtocolVersion3(PGconn *conn);
 extern int	pqGetCopyData3(PGconn *conn, char **buffer, int async);
+extern int	pqGetCopyDataInternalBuf3(PGconn *conn, char **buffer, int async);
 extern int	pqGetline3(PGconn *conn, char *s, int maxlen);
 extern int	pqGetlineAsync3(PGconn *conn, char *buffer, int bufsize);
 extern int	pqEndcopy3(PGconn *conn);
-- 
2.43.0

From 63c3ead6f0e4c7fad85c54c9e11166441989f817 Mon Sep 17 00:00:00 2001
From: Debian <admin@ip-10-249-249-30>
Date: Wed, 5 Aug 2026 10:12:22 +0000
Subject: [PATCH v06082026 10/10] 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 <[email protected]>
---
 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.0

From 2e6b3d00fa99445409be73f43ff3b0ca0f8f803c Mon Sep 17 00:00:00 2001
From: Jakub Wartak <[email protected]>
Date: Thu, 4 Sep 2025 11:39:54 +0200
Subject: [PATCH v2] Add MPTCP protocol support to server and libpq on Linux.

This adds new listen_mptcp configuration option and also exposes new
environment variable PGMPTCP, which can be enabled to request
MultiPathed TCP connections.

Author: Jakub Wartak <[email protected]>
Discussion: https://postgr.es/m/CAKZiRmy6j9PBzDHZwdgwHavwKDzv5GWtRSWOTj6-jv6SCOZ%3DYA%40mail.gmail.com
---
 doc/src/sgml/libpq.sgml                       | 26 +++++++++++++++++++
 src/backend/libpq/pqcomm.c                    | 20 +++++++++++++-
 src/backend/postmaster/postmaster.c           |  3 +++
 src/backend/utils/misc/guc_parameters.dat     |  6 +++++
 src/backend/utils/misc/postgresql.conf.sample |  2 ++
 src/include/postmaster/postmaster.h           |  1 +
 src/interfaces/libpq/fe-connect.c             | 17 +++++++++++-
 src/interfaces/libpq/libpq-int.h              |  1 +
 8 files changed, 74 insertions(+), 2 deletions(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 7d3c3bb66d8..ef7655ae5cb 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2602,6 +2602,22 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-mptcp" xreflabel="mptcp">
+      <term><literal>MPTCP</literal><indexterm><primary>MultiPath TCP</primary></indexterm></term>
+      <listitem>
+       <para>
+        Controls whether client-side MPTCP protocol is used. The default
+        value is 0, meaning off, but you can change this to 1, meaning on.
+        This parameter is ignored for connections made via a Unix-domain socket.
+       </para>
+
+       <para>
+        MPTCP protocol is only supported on Linux and allows connection aggregation
+        (multiplexing) over mulitple network paths, provided that remote also
+        supports MPTCP.
+       </para>
+      </listitem>
+     </varlistentry>
     </variablelist>
    </para>
   </sect2>
@@ -9212,6 +9228,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGMPTCP</envar></primary>
+      </indexterm>
+      <envar>PGMPTCP</envar> behaves the same as the <xref
+      linkend="libpq-connect-mptcp"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/src/backend/libpq/pqcomm.c b/src/backend/libpq/pqcomm.c
index aaae7214f13..dc4f5c7f4ac 100644
--- a/src/backend/libpq/pqcomm.c
+++ b/src/backend/libpq/pqcomm.c
@@ -439,6 +439,15 @@ ListenServerPort(int family, const char *hostName, unsigned short portNumber,
 	int			one = 1;
 #endif
 
+#ifndef IPPROTO_MPTCP
+	if (ListenMPTCP)
+	{
+		ereport(WARNING,
+				(errmsg("setting the MPTCP listening socket is not supported on this platform")));
+		return STATUS_ERROR;
+	}
+#endif
+
 	/* Initialize hint structure */
 	MemSet(&hint, 0, sizeof(hint));
 	hint.ai_family = family;
@@ -488,6 +497,8 @@ ListenServerPort(int family, const char *hostName, unsigned short portNumber,
 
 	for (addr = addrs; addr; addr = addr->ai_next)
 	{
+		int			ipprotocol = 0;
+
 		if (family != AF_UNIX && addr->ai_family == AF_UNIX)
 		{
 			/*
@@ -539,7 +550,14 @@ ListenServerPort(int family, const char *hostName, unsigned short portNumber,
 			addrDesc = addrBuf;
 		}
 
-		if ((fd = socket(addr->ai_family, SOCK_STREAM, 0)) == PGINVALID_SOCKET)
+		/*
+		 * enable MPTCP only on IP and IPv6 sockets and not for UNIX domain
+		 * sockets
+		 */
+		if (addr->ai_family != AF_UNIX)
+			ipprotocol = ListenMPTCP ? IPPROTO_MPTCP : 0;
+
+		if ((fd = socket(addr->ai_family, SOCK_STREAM, ipprotocol)) == PGINVALID_SOCKET)
 		{
 			ereport(LOG,
 					(errcode_for_socket_access(),
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 90c7c4528e8..f97419e0325 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -209,6 +209,9 @@ char	   *Unix_socket_directories;
 /* The TCP listen address(es) */
 char	   *ListenAddresses;
 
+/* Whether to use MPTCP */
+bool		ListenMPTCP;
+
 /*
  * SuperuserReservedConnections is the number of backends reserved for
  * superuser use, and ReservedConnections is the number of backends reserved
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index adb72361ce0..b8bb6da0a76 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -1599,6 +1599,12 @@
   boot_val => '"localhost"',
 },
 
+{ name => 'listen_mptcp', type => 'bool', context => 'PGC_POSTMASTER', group => 'CONN_AUTH_SETTINGS',
+  short_desc => 'Whether to enable MPTCP on the listening socket',
+  variable => 'ListenMPTCP',
+  boot_val => 'false',
+},
+
 { name => 'lo_compat_privileges', type => 'bool', context => 'PGC_SUSET', group => 'COMPAT_OPTIONS_PREVIOUS',
   short_desc => 'Enables backward compatibility mode for privilege checks on large objects.',
   long_desc => 'Skips privilege checks when reading or modifying large objects, for compatibility with PostgreSQL releases prior to 9.0.',
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 7958653077b..d52e71d700a 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -63,6 +63,8 @@
                                         # comma-separated list of addresses;
                                         # defaults to 'localhost'; use '*' for all
                                         # (change requires restart)
+#listen_mptcp = off                     # whether to enable Multipathing TCP or not
+                                        # (change requires restart)
 #port = 5432                            # (change requires restart)
 #max_connections = 100                  # (change requires restart)
 #reserved_connections = 0               # (change requires restart)
diff --git a/src/include/postmaster/postmaster.h b/src/include/postmaster/postmaster.h
index 716b4c912b3..a183fefd2d8 100644
--- a/src/include/postmaster/postmaster.h
+++ b/src/include/postmaster/postmaster.h
@@ -60,6 +60,7 @@ extern PGDLLIMPORT int Unix_socket_permissions;
 extern PGDLLIMPORT char *Unix_socket_group;
 extern PGDLLIMPORT char *Unix_socket_directories;
 extern PGDLLIMPORT char *ListenAddresses;
+extern PGDLLIMPORT bool ListenMPTCP;
 extern PGDLLIMPORT bool ClientAuthInProgress;
 extern PGDLLIMPORT int PreAuthDelay;
 extern PGDLLIMPORT int AuthenticationTimeout;
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 17c2288e9bc..63d9f49f89b 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -421,6 +421,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"SSL-Key-Log-File", "D", 64,
 	offsetof(struct pg_conn, sslkeylogfile)},
 
+	{"mptcp", "PGMPTCP", "0", NULL,
+		"MPTCP-Protocol", "", 1,
+	offsetof(struct pg_conn, mptcp)},
+
 	/* Terminating entry --- MUST BE LAST */
 	{NULL, NULL, NULL, NULL,
 	NULL, NULL, 0}
@@ -3255,6 +3259,7 @@ keep_going:						/* We will come back to here until there is
 					char		host_addr[NI_MAXHOST];
 					int			sock_type;
 					AddrInfo   *addr_cur;
+					int			ip_protocol = 0;
 
 					/*
 					 * Advance to next possible host, if we've tried all of
@@ -3340,7 +3345,17 @@ keep_going:						/* We will come back to here until there is
 					 */
 					sock_type |= SOCK_NONBLOCK;
 #endif
-					conn->sock = socket(addr_cur->family, sock_type, 0);
+
+					/*
+					 * enable MPTCP only on IP and IPv6 sockets and not for
+					 * UNIX domain sockets
+					 */
+					if (addr_cur->family != AF_UNIX && conn->mptcp && conn->mptcp[0] == '1')
+					{
+						fprintf(stderr, "enabling MPTCP client\n");
+						ip_protocol = IPPROTO_MPTCP;
+					}
+					conn->sock = socket(addr_cur->family, sock_type, ip_protocol);
 					if (conn->sock == PGINVALID_SOCKET)
 					{
 						int			errorno = SOCK_ERRNO;
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 3f921207a14..c9eac1c9290 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -432,6 +432,7 @@ struct pg_conn
 	char	   *scram_client_key;	/* base64-encoded SCRAM client key */
 	char	   *scram_server_key;	/* base64-encoded SCRAM server key */
 	char	   *sslkeylogfile;	/* where should the client write ssl keylogs */
+	char	   *mptcp;			/* use MPTCP ? */
 
 	bool		cancelRequest;	/* true if this connection is used to send a
 								 * cancel request, instead of being a normal
-- 
2.43.0

Reply via email to