Avoid streaming zero-filled WAL switch padding

Started by Andrey Borodin3 months ago5 messageshackers
Beta feature

Hackorum builds and tests every patch posted to the lists, not only commitfest submissions. This is Hackorum's own CI rather than the PostgreSQL project's, and it is still under testing - please report anything that looks wrong.

needs rebasesuccessCI history

You can run a PostgreSQL built from this patch straight from Docker, with no checkout and no build:

docker run --rm -p 5432:5432 ghcr.io/hackorum-dev/postgres-patch:t253331
psql -h localhost -U postgres

Built from patchset v4 (message #4), September 23, 2026 at 10:04 PM.

Every patchset is also pushed to a branch of our PostgreSQL fork, so you can check out the same tree CI built. Without a PostgreSQL checkout:

git clone --branch t253331_4 https://github.com/hackorum-dev/postgres.git

In a checkout you already have, add the fork once:

git remote add hackorum https://github.com/hackorum-dev/postgres.git

then, for this patchset and every later one:

git fetch hackorum t253331_4 && git checkout t253331_4

Patchset v4 (message #4) is on t253331_4

Jump to latest
#1Andrey Borodin
amborodin@acm.org

Hi hackers,

pg_switch_wal() fills the remainder of the WAL segment with zeros.
The filesystem can store this efficiently, but walsender still sends all
of those zeros to every standby.

We operate many small clusters that generate little WAL but have strict
statement_timeout requirements. On these clusters, archive_timeout causes
occasional SyncRep latency spikes while the zero-filled tail is streamed.
One user worked around this by adding a continuous dummy write workload that
fills each WAL segment before archive_timeout switches it.

The attached patch adds a compact replication message for zero-filled WAL
padding. Walreceiver reconstructs the tail using truncate-and-extend, so
it is neither transferred over the network nor written to the filesystem.
pg_receivewal and compressed output are supported too.

With a nearly empty default-size segment, this reduces the transfer from
about 16 MB to at most one 128 kB WAL chunk plus a 33-byte message, or
roughly 99.2%.

PFA. WDYT?

Best regards, Andrey Borodin.

Attachments:

t253331_1
v1-0001-Avoid-streaming-zero-filled-WAL-switch-padding.patchapplication/octet-stream; name=v1-0001-Avoid-streaming-zero-filled-WAL-switch-padding.patch; x-unix-mode=0644Download+445-9
#2Sehrope Sarkuni
sehrope@jackdb.com
In reply to: Andrey Borodin (#1)
Re: Avoid streaming zero-filled WAL switch padding

On Thu, Aug 6, 2026 at 3:49 AM Andrey Borodin <x4mmm@yandex-team.ru> wrote:

The attached patch adds a compact replication message for zero-filled WAL
padding.

This is neat. It's almost a custom compression handler for a common
situation.

I tried this atop 798bdcae89d. It applies cleanly and builds without
warnings. The suite passes and the numbers match yours for the
walsender traffic.

The reasoning behind the zero check looks sound to me. Messages start at
a record or page boundary, both of which begin with nonzero bytes, and
CopyXLogRecordToWAL zeroes the padding pages including their headers, so
an all-zero payload can only be switch padding. The sentPtr /
WalSndCaughtUp handling in XLogSendPhysical also looks right.

Also thought a bit about how this would interact with wire compression as
well. I think it'd still be useful together as even if the network byte
savings are less, you're still avoiding the disk writes.

A few things I noticed:

1. The new test is not added to src/test/recovery/meson.build, so meson
never runs it (I added it locally to test it out).

2. There is no negotiation. Every physical START_REPLICATION client gets
the 'z' message, so third-party physical stream consumers and older
pg_receivewal fail with "unrecognized streaming header". I think this
needs an opt-in from the client. Something like a START_REPLICATION
option or at least a protocol guard.

3. The sparse tail defeats wal_init_zero on the standby. Those segments
get recycled with holes, so later writes into them allocate blocks.
That would show up after a promotion. Perhaps only take the truncate
path when wal_init_zero is off, and pwrite the zeros otherwise? The
network saving is the same either way.

--- a/src/backend/replication/walreceiver.c
+++ b/src/backend/replication/walreceiver.c
@@ -59,6 +59,7 @@
   #include "access/xlogwait.h"
   #include "catalog/pg_authid.h"
+#include "common/file_utils.h"
   #include "funcapi.h"
@@ -1048,7 +1049,21 @@ XLogWalRcvWriteZeros(Size nbytes, XLogRecPtr
recptr, TimeLineID tli)
                                 errmsg_internal("zero WAL range
crosses a segment boundary")));
-     if (recvFileZeroedFrom < 0 || recvFileZeroedFrom > startoff)
+     if (wal_init_zero)
+     {
+             /*
+              * XLogFileInit allocated the whole segment so that
writes into it,
+              * including after it is recycled, never have to allocate blocks.
+              */
+             pgstat_report_wait_start(WAIT_EVENT_WAL_WRITE);
+             if (pg_pwrite_zeros(recvFile, nbytes, (pgoff_t) startoff) < 0)
+                     ereport(PANIC,
+                                     (errcode_for_file_access(),
+                                      errmsg("could not write
zero-filled WAL tail: %m")));
+             pgstat_report_wait_end();
+     }
+     else if (recvFileZeroedFrom < 0 || recvFileZeroedFrom > startoff)

4. If the wal_init_zero case is changed to write the zero range with
pg_pwrite_zeros(), it should use the same pgstat_...() accounting as
XLogWalRcvWrite(). Otherwise those writes will be missing stats.

5. A crash between the two ftruncate calls leaves a short segment. I
think replay is fine since the padding is never read, but anything
that expects to read whole segments would have a problem.

6. The walsender scan can use pg_memory_is_all_zeros() instead of the
byte loop. Early exit case would be the same and faster on long
zero runs.

Regards,
-- Sehrope Sarkuni
Founder & CEO | JackDB, Inc. | https://www.jackdb.com/

#3Andrey Borodin
amborodin@acm.org
In reply to: Sehrope Sarkuni (#2)
Re: Avoid streaming zero-filled WAL switch padding

Hi Sehrope,

Thank you for the detailed review!

On Sat, Sep 5, 2026, Sehrope Sarkuni wrote:

I think this needs an opt-in from the client. Something like a
START_REPLICATION option or at least a protocol guard.

I agree. v2 adds a SKIP_WAL_PADDING option to physical
START_REPLICATION. Existing clients continue to receive ordinary WALData
messages, and built-in clients request the new format only from PG20 servers.

The receiver no longer truncates and extends the segment. A segment newly
created by walreceiver is already zero-filled according to wal_init_zero. For
an existing or recycled segment, walreceiver writes the zeros locally with
the normal wait-event and pg_stat_io accounting. This also removes the
short-file crash window. New uncompressed pg_receivewal output is already
pre-padded. Resumed, compressed, and tar output generate the zeros locally.

I also added the missing Meson registration and used
pg_memory_is_all_zeros(). As a compatibility check, I connected a pre-patch
PG20 pg_receivewal to the patched server and compared the resulting switched
segment byte for byte.

PFA v2. Thank you!

Best regards, Andrey Borodin.

Attachments:

t253331_3
v2-0001-Avoid-streaming-zero-filled-WAL-switch-padding.patchapplication/octet-stream; name=v2-0001-Avoid-streaming-zero-filled-WAL-switch-padding.patch; x-unix-mode=0644Download+508-21
#4Sehrope Sarkuni
sehrope@jackdb.com
In reply to: Andrey Borodin (#3)
Re: Avoid streaming zero-filled WAL switch padding

On Sun, Sep 6, 2026 at 8:49 AM Andrey Borodin <x4mmm@yandex-team.ru> wrote:

On Sat, Sep 5, 2026, Sehrope Sarkuni wrote:

I think this needs an opt-in from the client. Something like a
START_REPLICATION option or at least a protocol guard.

I agree. v2 adds a SKIP_WAL_PADDING option to physical
START_REPLICATION. Existing clients continue to receive ordinary WALData
messages, and built-in clients request the new format only from PG20 servers.

The receiver no longer truncates and extends the segment. A segment newly
created by walreceiver is already zero-filled according to wal_init_zero. For
an existing or recycled segment, walreceiver writes the zeros locally with
the normal wait-event and pg_stat_io accounting. This also removes the
short-file crash window. New uncompressed pg_receivewal output is already
pre-padded. Resumed, compressed, and tar output generate the zeros locally.

I also added the missing Meson registration and used
pg_memory_is_all_zeros(). As a compatibility check, I connected a pre-patch
PG20 pg_receivewal to the patched server and compared the resulting switched
segment byte for byte.

Looks good. v2 addresses everything from my first pass and it all looks
right to me.

Tested on 798bdcae89d. Builds clean, recovery suite passes, and a
pre-patch pg_receivewal against the patched server gets plain WALData
and byte-identical segments. With the patch on a standby the wire
traffic drops from 16 MiB to ~125 kB.

I'm not particularly familiar with the replication command grammar,
so other eyes on the START_REPLICATION option parsing would be good,
but it looks fine and behaves correctly in all the tests.

A few small things:

1. The test no longer proves the 'z' path was taken. An old client
produces the same bytes, so the compare passes either way. The
attached patch resets pg_stat_io on the standby before the switch
and checks the walreceiver wrote less than wal_segment_size: 248 kB
here vs exactly 16 MB unpatched. It restarts the standby first to
flush the counters, since the walreceiver only reports them on its
status timer. Byte counts also avoid the Windows skip a sparse-file
check would need.

2. The pg_stat_activity check runs right after $standby->start, before
the walreceiver has necessarily connected. That should use
poll_query_until(...).

3. recvFileIsNew is reset in XLogWalRcvClose but not at the
end-of-timeline close in WalReceiverMain.

See attached for a patch atop your v2 to address 1-3. The 0001 is your
v2 patch rebased and the additions are in 0002. Nothing special for the
rebase either (the patch is identical).

CI run for the modified tests shows it ran on Windows and passed there
as well:

https://github.com/sehrope/postgres/actions/runs/34376945257/job/102552798366#step:14:94

Regards,
-- Sehrope Sarkuni
Founder & CEO | JackDB, Inc. | https://www.jackdb.com/

Attachments:

t253331_4
v3-0001-Avoid-streaming-zero-filled-WAL-switch-padding.patchtext/x-patch; charset=US-ASCII; name=v3-0001-Avoid-streaming-zero-filled-WAL-switch-padding.patchDownload+508-21
v3-0002-Fix-up-test-races-and-reset-recvFileIsNew-on-ever.patchtext/x-patch; charset=US-ASCII; name=v3-0002-Fix-up-test-races-and-reset-recvFileIsNew-on-ever.patchDownload+28-5
#5Andrey Borodin
amborodin@acm.org
In reply to: Sehrope Sarkuni (#4)
Re: Avoid streaming zero-filled WAL switch padding

Hi Sehrope,

On 9 Sep 2026, Sehrope Sarkuni wrote:

See attached for a patch atop your v2 to address 1-3. The 0001 is your
v2 patch rebased and the additions are in 0002.

Thank you for the review and fixes! Your changes look good to me.
The byte-count check is a useful addition: matching files alone did not
prove that we actually skipped the padding. Let's consider your v3 as the
current version.

Best regards, Andrey Borodin.