Skip to content

Commit 1596d0d

Browse files
committed
C on xlog/standby
1 parent da184d7 commit 1596d0d

File tree

10 files changed

+237
-3
lines changed

10 files changed

+237
-3
lines changed

src/backend/access/transam/xact.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -772,6 +772,7 @@ AssignTransactionId(TransactionState s)
772772
XLogRegisterData((char *) unreportedXids,
773773
nUnreportedXids * sizeof(TransactionId));
774774

775+
// 为了备库判断:事务已经开始运行
775776
(void) XLogInsert(RM_XACT_ID, XLOG_XACT_ASSIGNMENT);
776777

777778
nUnreportedXids = 0;

src/backend/access/transam/xlog.c

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,20 @@ typedef struct
382382
typedef union WALInsertLockPadded
383383
{
384384
WALInsertLock l;
385+
/**
386+
避免伪共享问题:
387+
不同线程访问同一个缓存行中的不同变量。
388+
虽然这些变量是独立的,但由于它们共享同一个缓存行,
389+
*当一个线程修改其中一个变量时,会导致整个缓存行被标记为无效*。
390+
其他线程即使只访问缓存行中的未修改部分,也需要重新加载缓存行,从而引发不必要的缓存一致性开销。
391+
392+
为了避免伪共享问题,可以通过以下方法:
393+
- 缓存行对齐(Padding):
394+
在变量之间插入填充(padding)数据,使得每个变量独占一个缓存行。
395+
例如,可以在结构体中插入额外的字节,使得变量的起始地址对齐到缓存行边界。
396+
- 分离变量(Data Separation):
397+
将可能被不同线程频繁访问的变量分开存储,避免它们共享同一个缓存行。
398+
*/
385399
char pad[PG_CACHE_LINE_SIZE];
386400
} WALInsertLockPadded;
387401

@@ -396,6 +410,10 @@ static SessionBackupState sessionBackupState = SESSION_BACKUP_NONE;
396410
*/
397411
typedef struct XLogCtlInsert
398412
{
413+
/**
414+
预留空间时使用的spinlock锁,不允许并发插入 WAL 日志
415+
后边的WALInsertLocks来控制插入时的并发度,允许多个进程同时插入 WAL 日志。
416+
*/
399417
slock_t insertpos_lck; /* protects CurrBytePos and PrevBytePos */
400418

401419
/*
@@ -405,7 +423,9 @@ typedef struct XLogCtlInsert
405423
* prev-link of the next record. These are stored as "usable byte
406424
* positions" rather than XLogRecPtrs (see XLogBytePosToRecPtr()).
407425
*/
426+
// CurrBytePos 表示当前已保留的 WAL 日志的结束位置。下一条 WAL 记录将从这个位置开始插入。
408427
uint64 CurrBytePos;
428+
// PrevBytePos 表示上一个已插入(或保留)的 WAL 记录的起始位置。
409429
uint64 PrevBytePos;
410430

411431
/**
@@ -885,6 +905,7 @@ XLogInsertRecord(XLogRecData *rdata,
885905
* Reserve space for the record in the WAL. This also sets the xl_prev
886906
* pointer.
887907
*/
908+
// 预留空间,完全互斥
888909
ReserveXLogInsertLocation(rechdr->xl_tot_len, &StartPos, &EndPos,
889910
&rechdr->xl_prev);
890911

@@ -1523,6 +1544,10 @@ WALInsertLockUpdateInsertingAt(XLogRecPtr insertingAt)
15231544
* uninitialized page), and the inserter might need to evict an old WAL buffer
15241545
* to make room for a new one, which in turn requires WALWriteLock.
15251546
*/
1547+
/**
1548+
等待 WAL 插入完成: 该函数会等待所有小于 upto 的 WAL 插入操作完成。
1549+
这意味着在 upto 之前的所有 WAL 数据都已被完全复制到 WAL 缓冲区中,可以安全地写入磁盘。
1550+
*/
15261551
static XLogRecPtr
15271552
WaitXLogInsertionsToFinish(XLogRecPtr upto)
15281553
{

src/backend/access/transam/xlogrecovery.c

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2170,6 +2170,31 @@ CheckTablespaceDirectory(void)
21702170
* Checks if recovery has reached a consistent state. When consistency is
21712171
* reached and we have a valid starting standby snapshot, tell postmaster
21722172
* that it can start accepting read-only connections.
2173+
*/
2174+
/**
2175+
recovery时达到一致性状态的检查,对这个一致性的解释是:
2176+
要达到一致性状态,必须满足以下条件:
2177+
2178+
1. 达到 minRecoveryPoint(检查点或base backup中获取的)
2179+
minRecoveryPoint 是恢复过程中必须重放的最小 WAL 日志位置。
2180+
在恢复模式下,数据库必须至少重放到 minRecoveryPoint,以确保所有必要的日志记录都已被应用。
2181+
如果 minRecoveryPoint 尚未达到,恢复过程将继续,直到满足此条件。
2182+
2. 完成基础备份的恢复
2183+
如果数据库是从基础备份(Base Backup)恢复的,则必须重放所有与备份相关的 WAL 日志,直到达到备份结束点(backupEndPoint)。
2184+
backupEndPoint 是基础备份完成时的 WAL 日志位置,表示备份数据的一致性点。
2185+
3. 检查未初始化的页面
2186+
在 WAL 日志重放过程中,可能会出现对未初始化页面的引用。
2187+
在达到一致性状态之前,必须确保所有未初始化的页面都已正确处理。
2188+
4. 检查表空间目录
2189+
在恢复过程中,可能会创建临时的表空间目录(如 pg_tblspc)。
2190+
在达到一致性状态之前,必须确保这些目录已被正确清理。
2191+
5. 生成有效的备用快照(Standby Snapshot)
2192+
在备库模式下,数据库必须生成一个有效的备用快照,以支持只读查询。
2193+
备用快照包含所有活动事务的快照信息,用于确保查询的一致性。
2194+
2195+
2196+
只有达到一致性状态后:hot standby 模式可以开始接受只读连接。
2197+
21732198
*/
21742199
static void
21752200
CheckRecoveryConsistency(void)
@@ -2184,6 +2209,7 @@ CheckRecoveryConsistency(void)
21842209
if (XLogRecPtrIsInvalid(minRecoveryPoint))
21852210
return;
21862211

2212+
// 可能是standby模式下的恢复
21872213
Assert(InArchiveRecovery);
21882214

21892215
/*
@@ -2264,6 +2290,7 @@ CheckRecoveryConsistency(void)
22642290

22652291
LocalHotStandbyActive = true;
22662292

2293+
// 通知postmaster,热备库模式已经准备好,可以接受只读连接了
22672294
SendPostmasterSignal(PMSIGNAL_BEGIN_HOT_STANDBY);
22682295
}
22692296
}

src/backend/postmaster/bgwriter.c

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,19 @@ BackgroundWriterMain(char *startup_data, size_t startup_data_len)
284284
* start of a record, whereas last_snapshot_lsn points just past
285285
* the end of the record.
286286
*/
287+
/**
288+
主机上的运行事务发生变化时,并不总是需要调用 LogStandbySnapshot。只有在满足以下两个条件时才会调用:
289+
- 距离上次快照记录的时间间隔已超过预设值。
290+
- 自上次快照记录以来,WAL 日志中有新的“重要”记录被插入。
291+
292+
Note:
293+
备库在恢复过程中需要获取当前事务的快照信息,以便正确处理事务状态和锁定信息。
294+
只需要recovery时获取一次即可,因为备库随后可以通过重放 WAL 日志中的其他记录(如事务提交或中止记录)来更新事务状态。
295+
见standy_redo->ProcArrayApplyRecoveryInfo()
296+
297+
这里定期记录快照可以优化恢复过程,并确保备库能够"快速"恢复,因为我们并不知道备库什么时候被建立。
298+
*/
299+
287300
if (now >= timeout &&
288301
last_snapshot_lsn <= GetLastImportantRecPtr())
289302
{

src/backend/replication/syncrep.c

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,10 @@ static bool SyncRepQueueIsOrderedByLSN(int mode);
144144
* to be flushed if synchronous_commit is set to the higher level of
145145
* remote_apply, because only commit records provide apply feedback.
146146
*/
147+
/**
148+
主机等待备机同步复制结束(然后主机才能提交事务),调用栈一般为:
149+
commitTxn -> recordTransactionCommit -> SyncRepWaitForLSN
150+
*/
147151
void
148152
SyncRepWaitForLSN(XLogRecPtr lsn, bool commit)
149153
{
@@ -204,7 +208,7 @@ SyncRepWaitForLSN(XLogRecPtr lsn, bool commit)
204208
* Set our waitLSN so WALSender will know when to wake us, and add
205209
* ourselves to the queue.
206210
*/
207-
MyProc->waitLSN = lsn;
211+
MyProc->waitLSN = lsn; // 设置当前进程的waitLSN为传入的lsn值,表示等待该LSN的同步复制完成。
208212
MyProc->syncRepState = SYNC_REP_WAITING;
209213
SyncRepQueueInsert(mode);
210214
Assert(SyncRepQueueIsOrderedByLSN(mode));
@@ -216,6 +220,7 @@ SyncRepWaitForLSN(XLogRecPtr lsn, bool commit)
216220
char buffer[32];
217221

218222
sprintf(buffer, "waiting for %X/%X", LSN_FORMAT_ARGS(lsn));
223+
// 比较有趣:设置ps命令输出时的描述性标题,最终是通过setproctitle()函数实现的。
219224
set_ps_display_suffix(buffer);
220225
}
221226

@@ -285,6 +290,7 @@ SyncRepWaitForLSN(XLogRecPtr lsn, bool commit)
285290
* Wait on latch. Any condition that should wake us up will set the
286291
* latch, so no need for timeout.
287292
*/
293+
// 预期walsender进程会在同步复制完成后设置MyLatch,唤醒当前进程。
288294
rc = WaitLatch(MyLatch, WL_LATCH_SET | WL_POSTMASTER_DEATH, -1,
289295
WAIT_EVENT_SYNC_REP);
290296

@@ -860,6 +866,11 @@ SyncRepGetStandbyPriority(void)
860866
*
861867
* The caller must hold SyncRepLock in exclusive mode.
862868
*/
869+
/**
870+
* 一般由walsender进程调用,唤醒等待同步复制的主机进程。
871+
* 调用栈一般为:
872+
* ProcessStandbyReplyMessage -> SyncRepReleaseWaiters -> SyncRepWakeQueue
873+
*/
863874
static int
864875
SyncRepWakeQueue(bool all, int mode)
865876
{

src/backend/storage/ipc/procarray.c

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ typedef struct ProcArrayStruct
7676
/*
7777
* Known assigned XIDs handling
7878
*/
79+
// KnownAssignedXIDs 是一个数组,用于跟踪在备库(standby)模式下从主库接收到的未提交事务的事务 ID(XID
7980
int maxKnownAssignedXids; /* allocated size of array */
8081
int numKnownAssignedXids; /* current # of valid entries */
8182
int tailKnownAssignedXids; /* index of oldest valid element */
@@ -265,7 +266,10 @@ typedef enum KAXCompressReason
265266
KAX_STARTUP_PROCESS_IDLE, /* startup process is about to sleep */
266267
} KAXCompressReason;
267268

268-
269+
/**
270+
协同工作: procArray 和 allProcs 是 PostgreSQL 并发控制的两个核心组件。
271+
procArray 提供了全局视图,用于跟踪活动事务,而 allProcs 提供了每个进程的详细状态信息。
272+
*/
269273
static ProcArrayStruct *procArray;
270274

271275
static PGPROC *allProcs;
@@ -1086,6 +1090,7 @@ ProcArrayApplyRecoveryInfo(RunningTransactions running)
10861090
/*
10871091
* If our snapshot is already valid, nothing else to do...
10881092
*/
1093+
// 这段代码检查当前的备库快照状态(standbyState),并在快照已经处于有效状态时立即返回,无需执行进一步的操作
10891094
if (standbyState == STANDBY_SNAPSHOT_READY)
10901095
return;
10911096

src/backend/storage/ipc/standby.c

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1281,7 +1281,11 @@ standby_redo(XLogReaderState *record)
12811281
*
12821282
* Returns the RecPtr of the last inserted record.
12831283
*/
1284-
XLogRecPtr
1284+
/**
1285+
用于将当前快照的详细信息记录到 WAL(Write-Ahead Logging)日志中的函数。
1286+
其主要目的是支持热备(Hot Standby)和逻辑解码(Logical Decoding),通过在备库上重建快照状态,使其能够正确处理事务和锁信息
1287+
*/
1288+
XLogRecPtr
12851289
LogStandbySnapshot(void)
12861290
{
12871291
XLogRecPtr recptr;
@@ -1396,6 +1400,74 @@ LogCurrentRunningXacts(RunningTransactions CurrRunningXacts)
13961400

13971401
return recptr;
13981402
}
1403+
/**
1404+
函数作用,from GPT
1405+
---
1406+
1407+
### **1. 函数的作用**
1408+
- **记录快照到 WAL**:
1409+
该函数的主要目的是将当前快照的详细信息记录到 WAL(Write-Ahead Logging)日志中。这些快照信息包括:
1410+
- 所有正在运行的事务(Running Transactions)。
1411+
- 当前持有的 `AccessExclusiveLocks`。
1412+
- **支持备库恢复和逻辑解码**:
1413+
- 在热备(Hot Standby)模式下,备库通过这些快照信息重建事务状态,从而支持只读查询。
1414+
- 在逻辑解码(Logical Decoding)中,这些快照信息用于构建一致的事务视图。
1415+
1416+
---
1417+
1418+
### **2. 热备模式下的使用**
1419+
- **从关闭检查点启动**:
1420+
如果从关闭检查点启动恢复,由于没有活动事务,快照是空的,系统可以直接进入 `STANDBY_SNAPSHOT_READY` 状态。
1421+
- **从在线检查点启动**:
1422+
在更常见的在线检查点情况下,需要通过两到三步的过程来构建正确的恢复快照。这包括逐步收集事务和锁信息,并在备库上重新组装这些信息。
1423+
1424+
---
1425+
1426+
### **3. 快照记录的竞争条件**
1427+
- **时间窗口问题**:
1428+
在主库上派生快照和将其写入 WAL 之间存在时间窗口。在此期间,事务或锁可能会进入或离开快照范围。
1429+
- **解决方案**:
1430+
- 在派生快照之前开始累积变化。
1431+
- 在备库应用快照时忽略重复的事务或锁信息。
1432+
- 这种机制在 `CreateCheckPoint()` 中实现,确保在写入主检查点记录之前,运行事务记录已经写入 WAL。
1433+
1434+
---
1435+
1436+
### **4. 恢复过程中的任务**
1437+
在恢复过程中,备库需要完成以下任务:
1438+
1. **推进 `nextXid`**:
1439+
随着新事务的出现,更新共享的 `nextXid`。
1440+
2. **扩展事务日志(clog)和子事务表(subtrans)**:
1441+
为每个新事务分配必要的存储。
1442+
3. **跟踪未提交的事务**:
1443+
记录已知的未提交事务(Known Assigned XIDs)。
1444+
4. **跟踪未提交的锁**:
1445+
记录未提交事务持有的 `AccessExclusiveLocks`。
1446+
1447+
---
1448+
1449+
### **5. 事务提交和锁清理**
1450+
- **移除已完成事务的记录**:
1451+
在事务提交或中止时,移除相关的事务和锁信息。
1452+
- **处理零事务 ID**:
1453+
在某些情况下,锁可能与零事务 ID 相关联(例如,WAL 重放旧日志时)。系统需要清理这些无效的锁。
1454+
1455+
---
1456+
1457+
### **6. 对逻辑解码的支持**
1458+
- **仅需要事务信息**:
1459+
对于逻辑解码,只需要运行事务的信息,而不需要锁信息。然而,由于没有单独的开关来控制逻辑解码,锁信息也会被记录。
1460+
1461+
---
1462+
1463+
### **7. 返回值**
1464+
- 函数返回最后插入的 WAL 记录的指针(`RecPtr`)。这可以用于跟踪 WAL 日志的写入进度。
1465+
1466+
---
1467+
1468+
### **总结**
1469+
`LogStandbySnapshot` 是 PostgreSQL 中支持热备和逻辑解码的重要函数。它通过记录当前快照的事务和锁信息,确保备库能够正确地重建快照状态,从而支持只读查询和逻辑解码的需求。这种设计在性能和数据一致性之间取得了良好的平衡,是 PostgreSQL 高可用性和扩展能力的重要组成部分。
1470+
*/
13991471

14001472
/*
14011473
* Wholesale logging of AccessExclusiveLocks. Other lock types need not be

src/backend/tcop/postgres.c

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2566,6 +2566,81 @@ errdetail_recovery_conflict(ProcSignalReason reason)
25662566

25672567
return 0;
25682568
}
2569+
/**
2570+
`errdetail_recovery_conflict` 函数的主要作用是为恢复冲突(recovery conflict)添加详细的错误信息(`errdetail()`),以帮助用户理解冲突的来源。恢复冲突通常发生在备库(standby)模式下,当主库的某些操作(如清理、锁定或表空间删除)与备库的查询或事务冲突时,备库需要中止冲突的操作以继续重放 WAL 日志。以下是函数中各类错误的出现场景举例:
2571+
2572+
---
2573+
2574+
### **1. `PROCSIG_RECOVERY_CONFLICT_BUFFERPIN`**
2575+
- **场景**:
2576+
- 主库需要清理某些数据页,但备库上的查询正在访问这些数据页并持有共享缓冲区的 pin。
2577+
- **错误信息**:
2578+
- `"User was holding shared buffer pin for too long."`
2579+
- **示例**:
2580+
- 在备库上运行一个长时间的查询,该查询扫描了一个大表的某些页面,而主库试图清理这些页面。
2581+
2582+
---
2583+
2584+
### **2. `PROCSIG_RECOVERY_CONFLICT_LOCK`**
2585+
- **场景**:
2586+
- 主库需要获取某个表或行的独占锁,但备库上的查询持有冲突的共享锁。
2587+
- **错误信息**:
2588+
- `"User was holding a relation lock for too long."`
2589+
- **示例**:
2590+
- 在备库上运行一个查询,该查询对某个表持有共享锁,而主库试图对该表执行 `ALTER TABLE` 操作。
2591+
2592+
---
2593+
2594+
### **3. `PROCSIG_RECOVERY_CONFLICT_TABLESPACE`**
2595+
- **场景**:
2596+
- 主库删除了一个表空间,而备库上的查询可能正在使用该表空间中的对象。
2597+
- **错误信息**:
2598+
- `"User was or might have been using tablespace that must be dropped."`
2599+
- **示例**:
2600+
- 主库执行了 `DROP TABLESPACE`,而备库上的查询正在访问该表空间中的表。
2601+
2602+
---
2603+
2604+
### **4. `PROCSIG_RECOVERY_CONFLICT_SNAPSHOT`**
2605+
- **场景**:
2606+
- 主库清理了某些旧版本的行(VACUUM 操作),而备库上的查询需要访问这些行版本。
2607+
- **错误信息**:
2608+
- `"User query might have needed to see row versions that must be removed."`
2609+
- **示例**:
2610+
- 在备库上运行一个长时间的查询,该查询依赖于一个旧快照,而主库的 `VACUUM` 操作清理了这些旧版本的行。
2611+
2612+
---
2613+
2614+
### **5. `PROCSIG_RECOVERY_CONFLICT_LOGICALSLOT`**
2615+
- **场景**:
2616+
- 主库需要无效化一个逻辑复制槽,而备库正在使用该逻辑复制槽。
2617+
- **错误信息**:
2618+
- `"User was using a logical replication slot that must be invalidated."`
2619+
- **示例**:
2620+
- 主库删除了一个逻辑复制槽,而备库上的逻辑解码进程正在使用该槽。
2621+
2622+
---
2623+
2624+
### **6. `PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK`**
2625+
- **场景**:
2626+
- 备库上的事务与恢复进程之间发生了死锁,导致恢复无法继续。
2627+
- **错误信息**:
2628+
- `"User transaction caused buffer deadlock with recovery."`
2629+
- **示例**:
2630+
- 备库上的事务持有某些资源,而恢复进程需要这些资源才能继续重放 WAL 日志。
2631+
2632+
---
2633+
2634+
### **7. `PROCSIG_RECOVERY_CONFLICT_DATABASE`**
2635+
- **场景**:
2636+
- 主库删除了一个数据库,而备库上仍有用户连接到该数据库。
2637+
- **错误信息**:
2638+
- `"User was connected to a database that must be dropped."`
2639+
- **示例**:
2640+
- 主库执行了 `DROP DATABASE`,而备库上仍有用户会话连接到该数据库。
2641+
2642+
*/
2643+
25692644

25702645
/*
25712646
* bind_param_error_callback

src/backend/utils/init/globals.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ char postgres_exec_path[MAXPGPATH]; /* full path to backend */
8484
/* note: currently this is not valid in backend processes */
8585
#endif
8686

87+
/**
88+
MyProcNumber 是 PostgreSQL 内部的逻辑编号,*用于标识当前进程在共享内存procArray中的位置*。
89+
它是一个内部变量,通常不会直接暴露给用户,因此无法通过标准的 SQL 查询直接获取 MyProcNumber。
90+
*/
8791
ProcNumber MyProcNumber = INVALID_PROC_NUMBER;
8892

8993
ProcNumber ParallelLeaderProcNumber = INVALID_PROC_NUMBER;

0 commit comments

Comments
 (0)