static void
XLogWrite(XLogwrtRqst WriteRqst, TimeLineID tli, bool flexible)
- XLogwrtRqst:请求写入位置信息
- TimeLineID:时间线,表示一个从创建点到当前点的完整数据库历史
- bool flexible:指示写入操作是否可以灵活处理,即不必严格按照WriteRqst指定的位置进行写入,可以在方便的边界(如缓存或日志文件的边界)停止
Assert(CritSectionCount > 0);
- 保护共享资源,避免数据竞争
- CritSectionCount 变量见XLogFlush中开启临界区函数,会使该变量++
LogwrtResult = XLogCtl->LogwrtResult;
npages = 0;
startidx = 0;
startoffset = 0;
curridx = XLogRecPtrToBufIdx(LogwrtResult.Write);
- npages:表示可以连续写入磁盘的 WAL 页面数量(待转储的页面数)。由于 WAL 页面在内存中通常是连续分配的,因此可以优化写入操作,通过一次磁盘 I/O 操作写入多个页面,从而减少磁盘操作的次数,提高性能
被初始化为 0,表示还没有找到任何可以连续写入的页面
- startidx:表示可以连续写入的 WAL 页面序列中第一个页面的缓存块索引。这个索引用于在 WAL 缓存中定位起始页面,以便从那里开始收集可以连续写入的页面。
被初始化为 0 或某个无效值,表示还没有确定起始页面
- startoffset:表示在 WAL 日志文件中,第一个可以写入的页面应该被写入的位置(即文件偏移量)。这个偏移量允许系统知道从哪里开始写入这些页面,以确保 WAL 日志的完整性和顺序性。
索引startidx和偏移量startoffset就决定了WAL日志写入的位置
- curridx:代表当前正在考虑的 WAL 缓存页面的索引,该索引由
XLogRecPtrToBufIdx(LogwrtResult.Write);
返回该函数将传入的已经写入的末位置转换为缓存页面的索引
while (LogwrtResult.Write < WriteRqst.Write)
{
}
该循环只能在请求写入的位置大于已经写入位置才能进行
XLogRecPtr EndPtr = XLogCtl->xlblocks[curridx];
if (LogwrtResult.Write >= EndPtr)
elog(PANIC, "xlog write request %X/%X is past end of log %X/%X",
LSN_FORMAT_ARGS(LogwrtResult.Write),
LSN_FORMAT_ARGS(EndPtr));
LogwrtResult.Write = EndPtr;
ispartialpage = WriteRqst.Write < LogwrtResult.Write;
if (!XLByteInPrevSeg(LogwrtResult.Write, openLogSegNo, wal_segment_size))
{
Assert(npages == 0);
if (openLogFile >= 0)
XLogFileClose();
XLByteToPrevSeg(LogwrtResult.Write, openLogSegNo, wal_segment_size);
openLogTLI = tli;
/* create/use new log file */
openLogFile = XLogFileInit(openLogSegNo, tli);
ReserveExternalFD();
}
XLByteInPrevSeg
函数:判断当前已经写入的位置(LogwrtResult.Write
)是否在当前打开的日志分段(openLogSegNo
)内,并且考虑日志分段的大小(wal_segment_size
)如果该函数返回false,表示当前的写入操作已经超出了当前日志分段的范围,因此需要切换到新的日志分段。
Assert(npages == 0);
若条件为false,表示有连续写入的页数,会触发错误。即要切换新分段之前,要确保清空所有待处理的数据才能进行if (openLogFile >= 0)
如果之前已经打开日志文件了,调用XLogFileClose();
关闭XLByteToPrevSeg(LogwrtResult.Write, openLogSegNo, wal_segment_size);
更新日志分段(logSegNo
即openLogSegNo
)的位置注意:
XLByteInPrevSeg
函数和XLByteToPrevSeg
函数的区别
#define XLByteInPrevSeg(xlrp, logSegNo, wal_segsz_bytes) \
((((xlrp) - 1) / (wal_segsz_bytes)) == (logSegNo))
#define XLByteToPrevSeg(xlrp, logSegNo, wal_segsz_bytes) \
logSegNo = ((xlrp) - 1) / (wal_segsz_bytes)
openLogTLI = tli;
更新时间线,为了让新的日志分段能正确反应当前时间线openLogFile = XLogFileInit(openLogSegNo, tli);
创建并初始化日志文件ReserveExternalFD();
在系统中为外部使用预留一个文件描述符(FD)。文件描述符:它是一个索引值,指向内核中每个进程打开文件的记录表。当打开一个文件(或设备、管道等)时,内核会向进程返回一个文件描述符。这个文件描述符随后被用于后续的读、写或其他文件操作。if (openLogFile < 0)
{
XLByteToPrevSeg(LogwrtResult.Write, openLogSegNo, wal_segment_size);
openLogTLI = tli;
openLogFile = XLogFileOpen(openLogSegNo, tli);
ReserveExternalFD();
}
if (npages == 0)
{
startidx = curridx;
startoffset = XLogSegmentOffset(LogwrtResult.Write - XLOG_BLCKSZ, wal_segment_size);
}
npages++;
last_iteration = WriteRqst.Write <= LogwrtResult.Write;
finishing_seg = !ispartialpage &&
(startoffset + npages * XLOG_BLCKSZ) >= wal_segment_size;
last_iteration
if (last_iteration ||
curridx == XLogCtl->XLogCacheBlck ||
finishing_seg)
{
}
如果满足条件,则进行下一步:
定义变量,并赋值
from = XLogCtl->pages + startidx * (Size) XLOG_BLCKSZ;
nbytes = npages * (Size) XLOG_BLCKSZ;
nleft = nbytes;
from
计算要写入的起始位置nbytes
总字节数nleft
剩余未写入字节数,初始化剩余字节数为总字节数开始循环,目的是将nleft
减到0,即完成所有数据的写入
do
{
...
} while (nleft > 0);
errno = 0;
if (track_wal_io_timing)
INSTR_TIME_SET_CURRENT(start);
pgstat_report_wait_start(WAIT_EVENT_WAL_WRITE);
written = pg_pwrite(openLogFile, from, nleft, startoffset);
pgstat_report_wait_end();
pg_pwrite
写入函数,用于将nleft
字节从from
指向的位置写入到openLogFile
指定的文件中,从startoffset
偏移量开始if (track_wal_io_timing)
{
instr_time duration;
INSTR_TIME_SET_CURRENT(duration);
INSTR_TIME_SUBTRACT(duration, start);
PendingWalStats.wal_write_time += INSTR_TIME_GET_MICROSEC(duration);
}
start
到当前时间的差值,并将结果存储在duration
中PendingWalStats.wal_write_time
上if (written <= 0)
{
char xlogfname[MAXFNAMELEN];
int save_errno;
if (errno == EINTR)
continue;
save_errno = errno;
XLogFileName(xlogfname, tli, openLogSegNo,
wal_segment_size);
errno = save_errno;
ereport(PANIC,
(errcode_for_file_access(),
errmsg("could not write to log file %s "
"at offset %u, length %zu: %m",
xlogfname, startoffset, nleft)));
}
如果写入错误written <= 0
:
errno
是否为EINTR
,EINTR
是一个特殊的错误代码,表示系统调用被信号中断errno
至save_errno
XLogFileName
函数根据给定的时间线ID(tli
)、打开的日志段号(openLogSegNo
)和WAL段大小(wal_segment_size
),生成WAL文件的名称,并存储在xlogfname
数组中。这个文件名将用于错误报告中,以帮助诊断问题。errno
nleft -= written;
from += written;
startoffset += written;
如果没有写入错误,则更新对应的剩余写入字符、起始位置指针以及偏移量
npages = 0;
剩余字节数都写完后,即该do
while
循环结束,则将待转储页面置为0
if (finishing_seg)
{
issue_xlog_fsync(openLogFile, openLogSegNo, tli);
WalSndWakeupRequest();
LogwrtResult.Flush = LogwrtResult.Write; /* end of page */
if (XLogArchivingActive())
XLogArchiveNotifySeg(openLogSegNo, tli);
XLogCtl->lastSegSwitchTime = (pg_time_t) time(NULL);
XLogCtl->lastSegSwitchLSN = LogwrtResult.Flush;
if (IsUnderPostmaster && XLogCheckpointNeeded(openLogSegNo))
{
(void) GetRedoRecPtr();
if (XLogCheckpointNeeded(openLogSegNo))
RequestCheckpoint(CHECKPOINT_CAUSE_XLOG);
}
}
如果完成对一个段的写入:
issue_xlog_fsync(openLogFile, openLogSegNo, tli);
来立即同步这个文件段到磁盘flush
的位置。将日志写入结果中的Flush
更新为Write
的值,表示当前页面已经被完全写入并准备同步XLogArchivingActive()
是激活的,代码会调用XLogArchiveNotifySeg(openLogSegNo, tli);
来通知归档器这个新的日志文件段已经准备好被复制到归档存储中。
- 更新重做记录指针
- 再次检查是否需要检查点
请求检查点
检查点是数据库用来减少恢复时间的一种机制,它确保了数据库可以从一系列固定的点快速恢复
if (ispartialpage)
{
LogwrtResult.Write = WriteRqst.Write;
break;
}
curridx = NextBufIdx(curridx);
if (flexible && npages == 0)
break;
ispartialpage
为真,表示当前请求只要求写入一个部分页面,不是整页。则设置已经写入的位置值为请求写入的位置值,并跳出循环if (LogwrtResult.Flush < WriteRqst.Flush && LogwrtResult.Flush < LogwrtResult.Write)
{
}
如果已经刷新的日志位置同时小于请求刷新的位置以及已经完成写入的位置(这表明有部分数据已经写入但未刷新到磁盘,且这部分是请求要求刷新的部分)
if (sync_method != SYNC_METHOD_OPEN &&
sync_method != SYNC_METHOD_OPEN_DSYNC)
{
if (openLogFile >= 0 &&
!XLByteInPrevSeg(LogwrtResult.Write, openLogSegNo,
wal_segment_size))
XLogFileClose();
if (openLogFile < 0)
{
XLByteToPrevSeg(LogwrtResult.Write, openLogSegNo,
wal_segment_size);
openLogTLI = tli;
openLogFile = XLogFileOpen(openLogSegNo, tli);
ReserveExternalFD();
}
issue_xlog_fsync(openLogFile, openLogSegNo, tli);
}
作用是用于判断是否需要进行文件操作
openLogFile
有效,并且当前写入位置LogwrtResult.Write
不在当前打开的日志文件段内(通过XLByteInPrevSeg
函数检查),则关闭当前文件openLogFile
无效(即小于0),则根据LogwrtResult.Write
计算应该打开哪个日志文件段(XLByteToPrevSeg
),然后打开该文件(XLogFileOpen
),并保留一个外部文件描述符(ReserveExternalFD
)。无论是否进行了文件关闭和重新打开操作,都会调用
issue_xlog_fsync
函数来刷新当前打开的日志文件段到磁盘。这个函数负责将文件描述符openLogFile
指定的文件段同步到磁盘。
{
SpinLockAcquire(&XLogCtl->info_lck);
XLogCtl->LogwrtResult = LogwrtResult;
if (XLogCtl->LogwrtRqst.Write < LogwrtResult.Write)
XLogCtl->LogwrtRqst.Write = LogwrtResult.Write;
if (XLogCtl->LogwrtRqst.Flush < LogwrtResult.Flush)
XLogCtl->LogwrtRqst.Flush = LogwrtResult.Flush;
SpinLockRelease(&XLogCtl->info_lck);
}
LogwrtResult
中最新的日志写入和刷新状态更新到XLogCtl->LogwrtResult
中XLogCtl
中请求写入和刷新的位置