XLogWrite函数
- 函数定义
static void
XLogWrite(XLogwrtRqst WriteRqst, TimeLineID tli, bool flexible)
- XLogwrtRqst:请求写入位置信息
- TimeLineID:时间线,表示一个从创建点到当前点的完整数据库历史
- bool flexible:指示写入操作是否可以灵活处理,即不必严格按照WriteRqst指定的位置进行写入,可以在方便的边界(如缓存或日志文件的边界)停止
- 该函数调用必须处于临界区
Assert(CritSectionCount > 0);
- 保护共享资源,避免数据竞争
- CritSectionCount 变量见XLogFlush中开启临界区函数,会使该变量++
- 更新写入和刷新的位置result
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));
- 赋值末尾位置给EndPtr,可以理解为从EndPtr开始,要开始写入了,EndPtr是上一个的结尾,即当前缓冲区的结束位置
- 如果已经写入的位置比将要开始写的位置(或者说上一个结束的位置)要大,则报PANIC日志错误,并输出二者位置信息
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++;
- 如果待转储的页面数为0,这意味着当前页面是这一组待转储页面中的第一个。则更新索引和偏移量
- 不管是不是第一页,待转储的页面数都+1。这表示当前页面已经被添加到待转储的页面集合中。
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)
{
}
- 条件判断:如果是最后一次迭代;或者当前索引是缓存块索引、或正在完成一个WAL段的写入
如果满足条件,则进行下一步:
定义变量,并赋值
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);
- 在循环内部,首先设置errno为0
- 如果启用了WAL I/O时间跟踪,则测量I/O操作的开始时间
pgstat_report_wait_start(WAIT_EVENT_WAL_WRITE);
written = pg_pwrite(openLogFile, from, nleft, startoffset);
pgstat_report_wait_end();
- 报告WAL写入的等待开始
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);
}
- 如果启用了WAL I/O时间跟踪
- 获取当前时间
- 计算从
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
- 生成WAL文件名:
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);
来立即同步这个文件段到磁盘 - 唤醒WAL发送者。因为上面已经同步到磁盘了,可以通过流复制,发送到从节点
- 更新已完成
flush
的位置。将日志写入结果中的Flush
更新为Write
的值,表示当前页面已经被完全写入并准备同步 - 通知归档器:如果WAL归档功能
XLogArchivingActive()
是激活的,代码会调用XLogArchiveNotifySeg(openLogSegNo, tli);
来通知归档器这个新的日志文件段已经准备好被复制到归档存储中。 - 更新最后切换时间和LSN
- 如果处于Postmaster下且需要检查点
- 更新重做记录指针
- 再次检查是否需要检查点
请求检查点
检查点是数据库用来减少恢复时间的一种机制,它确保了数据库可以从一系列固定的点快速恢复
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
指定的文件段同步到磁盘。
- 唤醒发送者。因为刷新操作可能意味着有新的WAL数据可供流复制
- 更新已经刷新的日志位置
{
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
中请求写入和刷新的位置 - 释放自旋锁