Optimization of vacuum for logical replication

Started by Konstantin Knizhnikover 6 years ago9 messages
#1Konstantin Knizhnik
k.knizhnik@postgrespro.ru
1 attachment(s)

Hi, hackers.

Right now if replication level is rgeater or equal than "replica",
vacuum  of relation copies all its data to WAL:

    /*
     * We need to log the copied data in WAL iff WAL archiving/streaming is
     * enabled AND it's a WAL-logged rel.
     */
    use_wal = XLogIsNeeded() && RelationNeedsWAL(NewHeap);

Obviously we have to do it for physical replication and WAL archiving.
But why do we need to do so expensive operation (actually copy all table
data three times) if we use logical replication?
Logically vacuum doesn't change relation so there is no need to write
any data to the log and process it by WAL sender.

I wonder if we can check that

1. wal_revel is "logical"
2. There are no physical replication slots
3. WAL archiving is disables

and in this cases do not write cloned relation to the WAL?
Small patch implementing such behavior is attached to this mail.
It allows to significantly reduce WAL size when performing vacuum at
multimaster, which uses logical replication between cluster nodes.

What can be wrong with such optimization?

--

Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

Attachments:

logical_vacuum.patchtext/x-patch; name=logical_vacuum.patchDownload
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index f1ff01e..1d0a957 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -36,6 +36,7 @@
 #include "commands/progress.h"
 #include "executor/executor.h"
 #include "pgstat.h"
+#include "replication/slot.h"
 #include "storage/bufmgr.h"
 #include "storage/bufpage.h"
 #include "storage/bufmgr.h"
@@ -719,7 +720,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
 	 * We need to log the copied data in WAL iff WAL archiving/streaming is
 	 * enabled AND it's a WAL-logged rel.
 	 */
-	use_wal = XLogIsNeeded() && RelationNeedsWAL(NewHeap);
+	use_wal = XLogIsNeeded() && RelationNeedsWAL(NewHeap)
+		&& (wal_level != WAL_LEVEL_LOGICAL || ReplicationSlotsCountPhysicalSlots() != 0 || XLogArchiveMode != ARCHIVE_MODE_OFF);
 
 	/* use_wal off requires smgr_targblock be initially invalid */
 	Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber);
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index 62342a6..7961056 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -829,6 +829,27 @@ ReplicationSlotsComputeLogicalRestartLSN(void)
 }
 
 /*
+ * Count physical replication slots
+ */
+int
+ReplicationSlotsCountPhysicalSlots(void)
+{
+	int			i;
+	int			n_slots = 0;
+
+	LWLockAcquire(ReplicationSlotControlLock, LW_SHARED);
+	for (i = 0; i < max_replication_slots; i++)
+	{
+		ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i];
+		n_slots += !SlotIsLogical(s);
+	}
+	LWLockRelease(ReplicationSlotControlLock);
+
+	return n_slots;
+}
+
+
+/*
  * ReplicationSlotsCountDBSlots -- count the number of slots that refer to the
  * passed database oid.
  *
diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h
index 8fbddea..50ca192 100644
--- a/src/include/replication/slot.h
+++ b/src/include/replication/slot.h
@@ -199,6 +199,7 @@ extern void ReplicationSlotsComputeRequiredLSN(void);
 extern XLogRecPtr ReplicationSlotsComputeLogicalRestartLSN(void);
 extern bool ReplicationSlotsCountDBSlots(Oid dboid, int *nslots, int *nactive);
 extern void ReplicationSlotsDropDBSlots(Oid dboid);
+extern int  ReplicationSlotsCountPhysicalSlots(void);
 
 extern void StartupReplicationSlots(void);
 extern void CheckPointReplicationSlots(void);
#2Bernd Helmle
mailings@oopsware.de
In reply to: Konstantin Knizhnik (#1)
Re: Optimization of vacuum for logical replication

Am Mittwoch, den 21.08.2019, 12:20 +0300 schrieb Konstantin Knizhnik:

I wonder if we can check that

1. wal_revel is "logical"
2. There are no physical replication slots
3. WAL archiving is disables

Not sure i get that correctly, i can still have a physical standby
without replication slots connected to such an instance. How would your
idea handle this situation?

Bernd

#3Konstantin Knizhnik
k.knizhnik@postgrespro.ru
In reply to: Bernd Helmle (#2)
Re: Optimization of vacuum for logical replication

On 21.08.2019 12:34, Bernd Helmle wrote:

Am Mittwoch, den 21.08.2019, 12:20 +0300 schrieb Konstantin Knizhnik:

I wonder if we can check that

1. wal_revel is "logical"
2. There are no physical replication slots
3. WAL archiving is disables

Not sure i get that correctly, i can still have a physical standby
without replication slots connected to such an instance. How would your
idea handle this situation?

Yes, it is possible to have physical replica withotu replication slot.
But it is not safe, because there is always a risk that lag between
master and replica becomes larger than size of WAL kept at master.
Also I can't believe that  DBA which explicitly sets wal_level is set to
logical will use streaming replication without associated replication slot.

And certainly it is possible to add GUC which controls such optimization.

--

Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

In reply to: Konstantin Knizhnik (#3)
Re: Optimization of vacuum for logical replication

Hello

Also I can't believe that  DBA which explicitly sets wal_level is set to
logical will use streaming replication without associated replication slot.

I am.

Yes, it is possible to have physical replica withotu replication slot.
But it is not safe, because there is always a risk that lag between
master and replica becomes larger than size of WAL kept at master.

Just an example: replica for manual queries, QA purposes or for something else that is not an important part of the system.
If I use replication slots - my risk is out-of-space on primary and therefore shutdown of primary. With downtime for application.
If I use wal_keep_segments instead - I have some limited (and usually stable) amount of WAL but risk to have outdated replica.

I prefer to have an outdated replica but primary is more safe. Its OK for me to just take fresh pg_basebackup from another replica.
And application want to use logical replication so wal_level = logical.

If we not want support such usecase - we need explicitly forbid replication without replication slots.

regards, Sergei

#5Bernd Helmle
mailings@oopsware.de
In reply to: Konstantin Knizhnik (#3)
Re: Optimization of vacuum for logical replication

Am Mittwoch, den 21.08.2019, 13:26 +0300 schrieb Konstantin Knizhnik:

Yes, it is possible to have physical replica withotu replication
slot.
But it is not safe, because there is always a risk that lag between
master and replica becomes larger than size of WAL kept at master.

Sure, but that doesn't mean use cases for this aren't real.

Also I can't believe that DBA which explicitly sets wal_level is set
to
logical will use streaming replication without associated replication
slot.

Well, i know people doing exactly this, for various reasons (short
living replicas, logical replicated table sets for reports, ...). The
fact that they can have loosely coupled replicas with either physical
or logical replication is a feature they'd really miss....

Bernd

#6Konstantin Knizhnik
k.knizhnik@postgrespro.ru
In reply to: Bernd Helmle (#5)
Re: Optimization of vacuum for logical replication

On 21.08.2019 14:45, Bernd Helmle wrote:

Am Mittwoch, den 21.08.2019, 13:26 +0300 schrieb Konstantin Knizhnik:

Yes, it is possible to have physical replica withotu replication
slot.
But it is not safe, because there is always a risk that lag between
master and replica becomes larger than size of WAL kept at master.

Sure, but that doesn't mean use cases for this aren't real.

Also I can't believe that DBA which explicitly sets wal_level is set
to
logical will use streaming replication without associated replication
slot.

Well, i know people doing exactly this, for various reasons (short
living replicas, logical replicated table sets for reports, ...). The
fact that they can have loosely coupled replicas with either physical
or logical replication is a feature they'd really miss....

Bernd

Ok, you convinced me that there are cases when people want to combine
logical replication with streaming replication without slot.
But is it acceptable to have GUC variable (disabled by default) which
allows to use this optimizations?

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#7Kyotaro Horiguchi
horikyota.ntt@gmail.com
In reply to: Konstantin Knizhnik (#6)
Re: Optimization of vacuum for logical replication

Hello.

At Wed, 21 Aug 2019 18:06:52 +0300, Konstantin Knizhnik <k.knizhnik@postgrespro.ru> wrote in <968fc591-51d3-fd74-8a55-40aa770baa3a@postgrespro.ru>

Ok, you convinced me that there are cases when people want to combine
logical replication with streaming replication without slot.
But is it acceptable to have GUC variable (disabled by default) which
allows to use this optimizations?

The odds are quite high. Couldn't we introduce a new wal_level
value instead?

wal_level = logical_only

I think this thread shows that logical replication no longer is a
superset(?) of physical replication. I thougt that we might be
able to change wal_level from scalar to bitmap but it breaks
backward compatibility..

regards.

--
Kyotaro Horiguchi
NTT Open Source Software Center

#8Konstantin Knizhnik
k.knizhnik@postgrespro.ru
In reply to: Kyotaro Horiguchi (#7)
Re: Optimization of vacuum for logical replication

On 22.08.2019 6:13, Kyotaro Horiguchi wrote:

Hello.

At Wed, 21 Aug 2019 18:06:52 +0300, Konstantin Knizhnik <k.knizhnik@postgrespro.ru> wrote in <968fc591-51d3-fd74-8a55-40aa770baa3a@postgrespro.ru>

Ok, you convinced me that there are cases when people want to combine
logical replication with streaming replication without slot.
But is it acceptable to have GUC variable (disabled by default) which
allows to use this optimizations?

The odds are quite high. Couldn't we introduce a new wal_level
value instead?

wal_level = logical_only

I think this thread shows that logical replication no longer is a
superset(?) of physical replication. I thougt that we might be
able to change wal_level from scalar to bitmap but it breaks
backward compatibility..

regards.

I think that introducing new wal_level is good idea.
There are a lot of other places (except vacuum) where we insert in the
log information which is not needed for logical decoding.
Instead of changing all places in code where this information is
inserted, we can filter it at xlog level (xlog.c).
My only concern is how much incompatibilities will be caused by
introducing new wal level.

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#9Konstantin Knizhnik
k.knizhnik@postgrespro.ru
In reply to: Kyotaro Horiguchi (#7)
1 attachment(s)
Re: Optimization of vacuum for logical replication

On 22.08.2019 6:13, Kyotaro Horiguchi wrote:

Hello.

At Wed, 21 Aug 2019 18:06:52 +0300, Konstantin Knizhnik <k.knizhnik@postgrespro.ru> wrote in <968fc591-51d3-fd74-8a55-40aa770baa3a@postgrespro.ru>

Ok, you convinced me that there are cases when people want to combine
logical replication with streaming replication without slot.
But is it acceptable to have GUC variable (disabled by default) which
allows to use this optimizations?

The odds are quite high. Couldn't we introduce a new wal_level
value instead?

wal_level = logical_only

I think this thread shows that logical replication no longer is a
superset(?) of physical replication. I thougt that we might be
able to change wal_level from scalar to bitmap but it breaks
backward compatibility..

regards.

I can propose the following patch introducing new level logical_only.
I will be please to receive comments concerning adding new wal_level and
possible problems caused by it.

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

Attachments:

logical_only.patchtext/x-patch; name=logical_only.patchDownload
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index f1ff01e..1009341 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -719,7 +719,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
 	 * We need to log the copied data in WAL iff WAL archiving/streaming is
 	 * enabled AND it's a WAL-logged rel.
 	 */
-	use_wal = XLogIsNeeded() && RelationNeedsWAL(NewHeap);
+	use_wal = XLogPhysicalIsNeeded() && RelationNeedsWAL(NewHeap);
 
 	/* use_wal off requires smgr_targblock be initially invalid */
 	Assert(RelationGetTargetBlock(NewHeap) == InvalidBlockNumber);
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index a3c4a1d..831d87b 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -466,7 +466,7 @@ vacuum_log_cleanup_info(Relation rel, LVRelStats *vacrelstats)
 	 * Skip this for relations for which no WAL is to be written, or if we're
 	 * not trying to support archive recovery.
 	 */
-	if (!RelationNeedsWAL(rel) || !XLogIsNeeded())
+	if (!RelationNeedsWAL(rel) || !XLogPhysicalIsNeeded())
 		return;
 
 	/*
diff --git a/src/backend/access/nbtree/nbtsort.c b/src/backend/access/nbtree/nbtsort.c
index ab19692..4ce1632 100644
--- a/src/backend/access/nbtree/nbtsort.c
+++ b/src/backend/access/nbtree/nbtsort.c
@@ -577,7 +577,7 @@ _bt_leafbuild(BTSpool *btspool, BTSpool *btspool2)
 	 * We need to log index creation in WAL iff WAL archiving/streaming is
 	 * enabled UNLESS the index isn't WAL-logged anyway.
 	 */
-	wstate.btws_use_wal = XLogIsNeeded() && RelationNeedsWAL(wstate.index);
+	wstate.btws_use_wal = XLogPhysicalIsNeeded() && RelationNeedsWAL(wstate.index);
 
 	/* reserve the metapage */
 	wstate.btws_pages_alloced = BTREE_METAPAGE + 1;
diff --git a/src/backend/access/rmgrdesc/xlogdesc.c b/src/backend/access/rmgrdesc/xlogdesc.c
index 33060f3..83c4427 100644
--- a/src/backend/access/rmgrdesc/xlogdesc.c
+++ b/src/backend/access/rmgrdesc/xlogdesc.c
@@ -30,6 +30,7 @@ const struct config_enum_entry wal_level_options[] = {
 	{"archive", WAL_LEVEL_REPLICA, true},	/* deprecated */
 	{"hot_standby", WAL_LEVEL_REPLICA, true},	/* deprecated */
 	{"logical", WAL_LEVEL_LOGICAL, false},
+	{"logical_only", WAL_LEVEL_LOGICAL_ONLY, false},
 	{NULL, 0, false}
 };
 
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index e651a84..f881ea8 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -6155,7 +6155,13 @@ CheckRequiredParameterValues(void)
 	{
 		ereport(WARNING,
 				(errmsg("WAL was generated with wal_level=minimal, data may be missing"),
-				 errhint("This happens if you temporarily set wal_level=minimal without taking a new base backup.")));
+				 errhint("This happens if you temporarily set wal_level=minimal or logical_only without taking a new base backup.")));
+	}
+	if (ArchiveRecoveryRequested && ControlFile->wal_level == WAL_LEVEL_LOGICAL_ONLY)
+	{
+		ereport(WARNING,
+				(errmsg("WAL was generated with wal_level=logical_only, data may be missing"),
+				 errhint("This happens if you temporarily set wal_level=logical_only or logical_only without taking a new base backup.")));
 	}
 
 	/*
@@ -6164,7 +6170,7 @@ CheckRequiredParameterValues(void)
 	 */
 	if (ArchiveRecoveryRequested && EnableHotStandby)
 	{
-		if (ControlFile->wal_level < WAL_LEVEL_REPLICA)
+		if (ControlFile->wal_level < WAL_LEVEL_REPLICA || ControlFile->wal_level == WAL_LEVEL_LOGICAL_ONLY)
 			ereport(ERROR,
 					(errmsg("hot standby is not possible because wal_level was not set to \"replica\" or higher on the master server"),
 					 errhint("Either set wal_level to \"replica\" on the master, or turn off hot_standby here.")));
@@ -10207,7 +10213,7 @@ do_pg_start_backup(const char *backupidstr, bool fast, TimeLineID *starttli_p,
 	 * During recovery, we don't need to check WAL level. Because, if WAL
 	 * level is not sufficient, it's impossible to get here during recovery.
 	 */
-	if (!backup_started_in_recovery && !XLogIsNeeded())
+	if (!backup_started_in_recovery && !XLogPhysicalIsNeeded())
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("WAL level not sufficient for making an online backup"),
@@ -10736,7 +10742,7 @@ do_pg_stop_backup(char *labelfile, bool waitforarchive, TimeLineID *stoptli_p)
 	 * During recovery, we don't need to check WAL level. Because, if WAL
 	 * level is not sufficient, it's impossible to get here during recovery.
 	 */
-	if (!backup_started_in_recovery && !XLogIsNeeded())
+	if (!backup_started_in_recovery && !XLogPhysicalIsNeeded())
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("WAL level not sufficient for making an online backup"),
diff --git a/src/backend/access/transam/xlogfuncs.c b/src/backend/access/transam/xlogfuncs.c
index 4795c6f..31ab26a 100644
--- a/src/backend/access/transam/xlogfuncs.c
+++ b/src/backend/access/transam/xlogfuncs.c
@@ -316,7 +316,7 @@ pg_create_restore_point(PG_FUNCTION_ARGS)
 				 (errmsg("recovery is in progress"),
 				  errhint("WAL control functions cannot be executed during recovery."))));
 
-	if (!XLogIsNeeded())
+	if (!XLogPhysicalIsNeeded())
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 				 errmsg("WAL level not sufficient for creating a restore point"),
diff --git a/src/backend/catalog/storage.c b/src/backend/catalog/storage.c
index 3cc886f..d8708e9 100644
--- a/src/backend/catalog/storage.c
+++ b/src/backend/catalog/storage.c
@@ -326,7 +326,7 @@ RelationCopyStorage(SMgrRelation src, SMgrRelation dst,
 	 * We need to log the copied data in WAL iff WAL archiving/streaming is
 	 * enabled AND it's a permanent relation.
 	 */
-	use_wal = XLogIsNeeded() &&
+	use_wal = XLogPhysicalIsNeeded() &&
 		(relpersistence == RELPERSISTENCE_PERMANENT || copying_initfork);
 
 	nblocks = smgrnblocks(src, forkNum);
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index f115d4b..7fc0ca9 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -232,7 +232,7 @@ CreatePublication(CreatePublicationStmt *stmt)
 
 	InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0);
 
-	if (wal_level != WAL_LEVEL_LOGICAL)
+	if (wal_level < WAL_LEVEL_LOGICAL)
 	{
 		ereport(WARNING,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cceefbd..32f75e3 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -4772,7 +4772,7 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap, LOCKMODE lockmode)
 		bistate = GetBulkInsertState();
 
 		ti_options = TABLE_INSERT_SKIP_FSM;
-		if (!XLogIsNeeded())
+		if (!XLogPhysicalIsNeeded())
 			ti_options |= TABLE_INSERT_SKIP_WAL;
 	}
 	else
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 3339804..ca01fd7 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -896,6 +896,9 @@ PostmasterMain(int argc, char *argv[])
 	if (XLogArchiveMode > ARCHIVE_MODE_OFF && wal_level == WAL_LEVEL_MINIMAL)
 		ereport(ERROR,
 				(errmsg("WAL archival cannot be enabled when wal_level is \"minimal\"")));
+	if (XLogArchiveMode > ARCHIVE_MODE_OFF && wal_level == WAL_LEVEL_LOGICAL_ONLY)
+		ereport(ERROR,
+				(errmsg("WAL archival cannot be enabled when wal_level is \"logical_only\"")));
 	if (max_wal_senders > 0 && wal_level == WAL_LEVEL_MINIMAL)
 		ereport(ERROR,
 				(errmsg("WAL streaming (max_wal_senders > 0) requires wal_level \"replica\" or \"logical\"")));
diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c
index b1bcd93..7ea26bb 100644
--- a/src/backend/replication/slot.c
+++ b/src/backend/replication/slot.c
@@ -1523,6 +1523,12 @@ RestoreSlotFromDisk(const char *name)
 				 errmsg("logical replication slot \"%s\" exists, but wal_level < logical",
 						NameStr(cp.slotdata.name)),
 				 errhint("Change wal_level to be logical or higher.")));
+	else if (wal_level == WAL_LEVEL_LOGICAL_ONLY)
+		ereport(FATAL,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("physical replication slot \"%s\" exists, but wal_level = logical_only",
+						NameStr(cp.slotdata.name)),
+				 errhint("Change wal_level to be replica or logical.")));
 	else if (wal_level < WAL_LEVEL_REPLICA)
 		ereport(FATAL,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c
index 23870a2..9dd5e2f 100644
--- a/src/backend/replication/walsender.c
+++ b/src/backend/replication/walsender.c
@@ -542,6 +542,11 @@ StartReplication(StartReplicationCmd *cmd)
 	StringInfoData buf;
 	XLogRecPtr	FlushPtr;
 
+	if (wal_level == WAL_LEVEL_LOGICAL_ONLY)
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 (errmsg("cannot start physical replication in logical_only WAl mode"))));
+
 	if (ThisTimeLineID == 0)
 		ereport(ERROR,
 				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
diff --git a/src/bin/pg_controldata/pg_controldata.c b/src/bin/pg_controldata/pg_controldata.c
index ff17804..d584dcf 100644
--- a/src/bin/pg_controldata/pg_controldata.c
+++ b/src/bin/pg_controldata/pg_controldata.c
@@ -80,6 +80,8 @@ wal_level_str(WalLevel wal_level)
 			return "replica";
 		case WAL_LEVEL_LOGICAL:
 			return "logical";
+		case WAL_LEVEL_LOGICAL_ONLY:
+			return "logical_only";
 	}
 	return _("unrecognized wal_level");
 }
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index d519252..a44c5b3 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -161,17 +161,18 @@ typedef enum WalLevel
 {
 	WAL_LEVEL_MINIMAL = 0,
 	WAL_LEVEL_REPLICA,
-	WAL_LEVEL_LOGICAL
+	WAL_LEVEL_LOGICAL,
+	WAL_LEVEL_LOGICAL_ONLY
 } WalLevel;
 
 extern PGDLLIMPORT int wal_level;
 
 /* Is WAL archiving enabled (always or only while server is running normally)? */
 #define XLogArchivingActive() \
-	(AssertMacro(XLogArchiveMode == ARCHIVE_MODE_OFF || wal_level >= WAL_LEVEL_REPLICA), XLogArchiveMode > ARCHIVE_MODE_OFF)
+	(AssertMacro(XLogArchiveMode == ARCHIVE_MODE_OFF || XLogPhysicalIsNeeded()), XLogArchiveMode > ARCHIVE_MODE_OFF)
 /* Is WAL archiving enabled always (even during recovery)? */
 #define XLogArchivingAlways() \
-	(AssertMacro(XLogArchiveMode == ARCHIVE_MODE_OFF || wal_level >= WAL_LEVEL_REPLICA), XLogArchiveMode == ARCHIVE_MODE_ALWAYS)
+	(AssertMacro(XLogArchiveMode == ARCHIVE_MODE_OFF || XLogPhysicalIsNeeded()), XLogArchiveMode == ARCHIVE_MODE_ALWAYS)
 #define XLogArchiveCommandSet() (XLogArchiveCommand[0] != '\0')
 
 /*
@@ -181,6 +182,11 @@ extern PGDLLIMPORT int wal_level;
 #define XLogIsNeeded() (wal_level >= WAL_LEVEL_REPLICA)
 
 /*
+ * Is WAL-logging necessary for archival or physical replication?
+ */
+#define XLogPhysicalIsNeeded() (XLogIsNeeded() && wal_level != WAL_LEVEL_LOGICAL_ONLY)
+
+/*
  * Is a full-page image needed for hint bit updates?
  *
  * Normally, we don't WAL-log hint bit updates, but if checksums are enabled,