Here is an updated patch that incorporates some of the suggestions. In
particular, some of the warning messages have been rephrased to more
accurate (but also less specific), the warning message at recovery pause
repeats every 1 minute, and the documentation has been updated.
--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
From 3fb5c70b4b7e132cb75271bbd70c4a1523a72e86 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <pe...@eisentraut.org>
Date: Wed, 11 Mar 2020 19:25:44 +0100
Subject: [PATCH v2] Improve handling of parameter differences in physical
replication
When certain parameters are changed on a physical replication primary,
this is communicated to standbys using the XLOG_PARAMETER_CHANGE WAL
record. The standby then checks whether its own settings are at least
as big as the ones on the primary. If not, the standby shuts down
with a fatal error.
The correspondence of settings between primary and standby is required
because those settings influence certain shared memory sizings that
are required for processing WAL records that the primary might send.
For example, if the primary sends a prepared transaction, the standby
must have had max_prepared_transaction set appropriately or it won't
be able to process those WAL records.
However, fatally shutting down the standby immediately upon receipt of
the parameter change record might be a bit of an overreaction. The
resources related to those settings are not required immediately at
that point, and might never be required if the activity on the primary
does not exhaust all those resources. If we just let the standby roll
on with recovery, it will eventually produce an appropriate error when
those resources are used.
So this patch relaxes this a bit. Upon receipt of
XLOG_PARAMETER_CHANGE, we still check the settings but only issue a
warning and set a global flag if there is a problem. Then when we
actually hit the resource issue and the flag was set, we issue another
warning message with relevant information. Additionally, at that
point we pause recovery, so a hot standby remains usable.
---
doc/src/sgml/high-availability.sgml | 48 +++++++++++++-----
src/backend/access/transam/twophase.c | 3 ++
src/backend/access/transam/xlog.c | 72 +++++++++++++++++++++++----
src/backend/storage/ipc/procarray.c | 9 +++-
src/backend/storage/lmgr/lock.c | 10 ++++
src/include/access/xlog.h | 1 +
6 files changed, 120 insertions(+), 23 deletions(-)
diff --git a/doc/src/sgml/high-availability.sgml
b/doc/src/sgml/high-availability.sgml
index bc4d98fe03..c5a9f9d5df 100644
--- a/doc/src/sgml/high-availability.sgml
+++ b/doc/src/sgml/high-availability.sgml
@@ -2148,18 +2148,14 @@ <title>Administrator's Overview</title>
</para>
<para>
- The setting of some parameters on the standby will need reconfiguration
- if they have been changed on the primary. For these parameters,
- the value on the standby must
- be equal to or greater than the value on the primary.
- Therefore, if you want to increase these values, you should do so on all
- standby servers first, before applying the changes to the primary server.
- Conversely, if you want to decrease these values, you should do so on the
- primary server first, before applying the changes to all standby servers.
- If these parameters
- are not set high enough then the standby will refuse to start.
- Higher values can then be supplied and the server
- restarted to begin recovery again. These parameters are:
+ The settings of some parameters determine the size of shared memory for
+ tracking transaction IDs, locks, and prepared transactions. These shared
+ memory structures should be no smaller on a standby than on the primary.
+ Otherwise, it could happen that the standby runs out of shared memory
+ during recovery. For example, if the primary uses a prepared transaction
+ but the standby did not allocate any shared memory for tracking prepared
+ transactions, then recovery will abort and cannot continue until the
+ standby's configuration is changed. The parameters affected are:
<itemizedlist>
<listitem>
@@ -2188,6 +2184,34 @@ <title>Administrator's Overview</title>
</para>
</listitem>
</itemizedlist>
+
+ The easiest way to ensure this does not become a problem is to have these
+ parameters set on the standbys to values equal to or greater than on the
+ primary. Therefore, if you want to increase these values, you should do
+ so on all standby servers first, before applying the changes to the
+ primary server. Conversely, if you want to decrease these values, you
+ should do so on the primary server first, before applying the changes to
+ all standby servers. The WAL tracks changes to these parameters on the
+ primary, and if a standby processes WAL that indicates that the current
+ value on the primary is higher than its own value, it will log a warning,
for example:
+<screen>
+WARNING: insufficient setting for parameter max_connections
+DETAIL: max_connections = 80 is a lower setting than on the master server
(where its value was 100).
+HINT: Change parameters and restart the server, or there may be resource
exhaustion errors sooner or later.
+</screen>
+ Recovery will continue but could abort at any time thereafter. (It could
+ also never end up failing if the activity on the primary does not actually
+ require the full extent of the allocated shared memory resources.) If
+ recovery reaches a point where it cannot continue due to lack of shared
+ memory, recovery will pause and another warning will be logged, for
example:
+<screen>
+WARNING: recovery paused because of insufficient parameter settings
+DETAIL: See earlier in the log about which settings are insufficient.
+HINT: Recovery cannot continue unless the configuration is changed and the
server restarted.
+</screen>
+ This warning will repeated once a minute. At that point, the settings on
+ the standby need to be updated and the instance restarted before recovery
+ can continue.
</para>
<para>
diff --git a/src/backend/access/transam/twophase.c
b/src/backend/access/transam/twophase.c
index 5adf956f41..ce35f15f34 100644
--- a/src/backend/access/transam/twophase.c
+++ b/src/backend/access/transam/twophase.c
@@ -2360,11 +2360,14 @@ PrepareRedoAdd(char *buf, XLogRecPtr start_lsn,
/* Get a free gxact from the freelist */
if (TwoPhaseState->freeGXacts == NULL)
+ {
+ StandbyParamErrorPauseRecovery();
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
errmsg("maximum number of prepared
transactions reached"),
errhint("Increase max_prepared_transactions
(currently %d).",
max_prepared_xacts)));
+ }
gxact = TwoPhaseState->freeGXacts;
TwoPhaseState->freeGXacts = gxact->next;
diff --git a/src/backend/access/transam/xlog.c
b/src/backend/access/transam/xlog.c
index 614a25242b..3e825a2414 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -258,6 +258,8 @@ bool InArchiveRecovery = false;
static bool standby_signal_file_found = false;
static bool recovery_signal_file_found = false;
+static bool need_restart_for_parameter_values = false;
+
/* Was the last xlog file restored from archive, or local? */
static bool restoredFromArchive = false;
@@ -5971,6 +5973,52 @@ SetRecoveryPause(bool recoveryPause)
SpinLockRelease(&XLogCtl->info_lck);
}
+/*
+ * If in hot standby, pause recovery because of a parameter conflict.
+ *
+ * Similar to recoveryPausesHere() but with a different messaging. The user
+ * is expected to make the parameter change and restart the server. If they
+ * just unpause recovery, they will then run into whatever error change is
+ * after this function call for the non-hot-standby case.
+ *
+ * We intentionally do not give advice about specific parameters or values
+ * here because it might be misleading. For example, if we run out of lock
+ * space, then in the single-server case we would recommend raising
+ * max_locks_per_transaction, but in recovery it could equally be the case
+ * that max_connections is out of sync with the primary. If we get here, we
+ * have already logged any parameter discrepancy in
+ * RecoveryRequiresIntParameter(), so users can go back to that and get
+ * concrete and accurate information.
+ */
+void
+StandbyParamErrorPauseRecovery(void)
+{
+ TimestampTz last_warning = 0;
+
+ if (!AmStartupProcess() || !need_restart_for_parameter_values)
+ return;
+
+ SetRecoveryPause(true);
+
+ do
+ {
+ TimestampTz now = GetCurrentTimestamp();
+
+ if (TimestampDifferenceExceeds(last_warning, now, 60000))
+ {
+ ereport(WARNING,
+ (errmsg("recovery paused because of
insufficient parameter settings"),
+ errdetail("See earlier in the log
about which settings are insufficient."),
+ errhint("Recovery cannot continue
unless the configuration is changed and the server restarted.")));
+ last_warning = now;
+ }
+
+ pg_usleep(1000000L); /* 1000 ms */
+ HandleStartupProcInterrupts();
+ }
+ while (RecoveryIsPaused());
+}
+
/*
* When recovery_min_apply_delay is set, we wait long enough to make sure
* certain record types are applied at least that interval behind the master.
@@ -6150,16 +6198,20 @@ GetXLogReceiptTime(TimestampTz *rtime, bool *fromStream)
* Note that text field supplied is a parameter name and does not require
* translation
*/
-#define RecoveryRequiresIntParameter(param_name, currValue, minValue) \
-do { \
- if ((currValue) < (minValue)) \
- ereport(ERROR, \
- (errcode(ERRCODE_INVALID_PARAMETER_VALUE), \
- errmsg("hot standby is not possible because %s
= %d is a lower setting than on the master server (its value was %d)", \
- param_name, \
- currValue, \
- minValue))); \
-} while(0)
+static void
+RecoveryRequiresIntParameter(const char *param_name, int currValue, int
minValue)
+{
+ if (currValue < minValue)
+ {
+ ereport(WARNING,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("insufficient setting for parameter
%s", param_name),
+ errdetail("%s = %d is a lower setting than on
the master server (where its value was %d).",
+ param_name, currValue,
minValue),
+ errhint("Change parameters and restart the
server, or there may be resource exhaustion errors sooner or later.")));
+ need_restart_for_parameter_values = true;
+ }
+}
/*
* Check to see if required parameters are set high enough on this server
diff --git a/src/backend/storage/ipc/procarray.c
b/src/backend/storage/ipc/procarray.c
index f45a619deb..cfb88db4a4 100644
--- a/src/backend/storage/ipc/procarray.c
+++ b/src/backend/storage/ipc/procarray.c
@@ -3654,7 +3654,14 @@ KnownAssignedXidsAdd(TransactionId from_xid,
TransactionId to_xid,
* If it still won't fit then we're out of memory
*/
if (head + nxids > pArray->maxKnownAssignedXids)
- elog(ERROR, "too many KnownAssignedXids");
+ {
+ StandbyParamErrorPauseRecovery();
+ ereport(ERROR,
+ (errcode(ERRCODE_OUT_OF_MEMORY),
+ errmsg("out of shared memory"),
+ errdetail("There are no more
KnownAssignedXids slots."),
+ errhint("You might need to increase
max_connections.")));
+ }
}
/* Now we can insert the xids into the space starting at head */
diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c
index 56dba09299..cbcc7611f4 100644
--- a/src/backend/storage/lmgr/lock.c
+++ b/src/backend/storage/lmgr/lock.c
@@ -924,10 +924,13 @@ LockAcquireExtended(const LOCKTAG *locktag,
if (locallockp)
*locallockp = NULL;
if (reportMemoryError)
+ {
+ StandbyParamErrorPauseRecovery();
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
errmsg("out of shared memory"),
errhint("You might need to
increase max_locks_per_transaction.")));
+ }
else
return LOCKACQUIRE_NOT_AVAIL;
}
@@ -962,10 +965,13 @@ LockAcquireExtended(const LOCKTAG *locktag,
if (locallockp)
*locallockp = NULL;
if (reportMemoryError)
+ {
+ StandbyParamErrorPauseRecovery();
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
errmsg("out of shared memory"),
errhint("You might need to increase
max_locks_per_transaction.")));
+ }
else
return LOCKACQUIRE_NOT_AVAIL;
}
@@ -2747,6 +2753,7 @@ FastPathGetRelationLockEntry(LOCALLOCK *locallock)
{
LWLockRelease(partitionLock);
LWLockRelease(&MyProc->backendLock);
+ StandbyParamErrorPauseRecovery();
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
errmsg("out of shared memory"),
@@ -4077,6 +4084,7 @@ lock_twophase_recover(TransactionId xid, uint16 info,
if (!lock)
{
LWLockRelease(partitionLock);
+ StandbyParamErrorPauseRecovery();
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
errmsg("out of shared memory"),
@@ -4142,6 +4150,7 @@ lock_twophase_recover(TransactionId xid, uint16 info,
elog(PANIC, "lock table corrupted");
}
LWLockRelease(partitionLock);
+ StandbyParamErrorPauseRecovery();
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
errmsg("out of shared memory"),
@@ -4434,6 +4443,7 @@ VirtualXactLock(VirtualTransactionId vxid, bool wait)
{
LWLockRelease(partitionLock);
LWLockRelease(&proc->backendLock);
+ StandbyParamErrorPauseRecovery();
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
errmsg("out of shared memory"),
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index 98b033fc20..fae19a9d26 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -286,6 +286,7 @@ extern XLogRecPtr GetXLogInsertRecPtr(void);
extern XLogRecPtr GetXLogWriteRecPtr(void);
extern bool RecoveryIsPaused(void);
extern void SetRecoveryPause(bool recoveryPause);
+extern void StandbyParamErrorPauseRecovery(void);
extern TimestampTz GetLatestXTime(void);
extern TimestampTz GetCurrentChunkReplayStartTime(void);
--
2.25.0