一、 XLogWrite概述
XLogWrite是XLOG落盘的最底层函数,负责将XLOG真正写入磁盘。
static void XLogWrite(XLogwrtRqst WriteRqst, bool flexible)
参数1表示请求写入的起点LSN,参数2表示是否灵活写入(If flexible == true, we don't have to write as far as WriteRqst)
1. 概念回顾
这个函数的细节较多,我们来慢慢分析,首先回顾几个概念
- LSN:日志序列号,初始值为0,单调递增。
- log buffer:一个循环队列,队尾写满之后,会循环到队头继续写入。
前面提到过,GetXLogBuffer会定位每个LSN对应的XLOG应该写入哪个buffer page。在上图中,LSN的1号页面中的内容会写入log buffer的1号页面,LSN的2号页面中的内容会写入log buffer的2号页面…。实际上LSN中没有页面的概念,这里只是方便说明。
3号页面比较特殊,是一个partial page。partial page是指当前页面还未写满,由于log buffer是顺序写的,所以有且仅有一个partial page。由于落盘也是顺序落盘,所以当partial page(3号页面)落盘后,就说明相关的XLOG都已经落盘。
- disk:物理的xlog文件。
如何知道log buffer中的XLOG和物理块的对应关系呢?实际上也是通过LSN。一个segment对应一个单独的物理文件,一个page对应物理文件中的一个页,它们的大小相等、一一对应。所以通过简单的映射规则就能知道一个LSN对应的XLOG应该写入哪个物理块。
- 段号 = LSN / XLogSegSize
- 段内块偏移 = LSN % XLogSegSize
当一个物理文件写满之后,会写下一个。在实际落盘时,是以页面为单位进行落盘,所以在落盘前需要找到每个页面的起始位置,然后将整个页面进行落盘。
2. 总体流程
综上所述,XLOG的落盘流程如下:
- 以LogwrtResult(最后已落盘的LSN)为起点,依据LSN及映射规则在log buffer中获取对应的页面。
- 依据LSN及映射规则获取页面需要写入的段号及段内页号。
- 将buffer page写入对应的物理页面。
3. 对应XLogWrite代码实现流程
- part1:调用wirte,对WriteRqst.Write之前的XLOG进行写入(写入操作系统缓存,如果是异步提交,则不保证一定落盘)。这个部分是XLogWrite的核心,包含上述所有流程。
- part2:调用flush,对part1写入的XLOG进行落盘。
- part3:更新XLogCtl(全局)的LogwrtResult及LogwrtRqst。
注意:上面说的write和flush指的是下文代码中的pg_pwrite函数 和 issue_xlog_fsync函数(会再调用pg_fdatasync函数),而不是XLogWrite和XLogFlush函数,不要弄混。
二、 调用write前的准备工作(part1)
注意part 1部分所有的写入(可能也有些地方会写成落盘)都是指的写入os缓存,对于异步提交需要注意区分。
part 1是XLogWrite的核心,分为多种场景。下面我们先讲解主要流程,然后再根据情况来添加代码。
最简单的场景:log buffer没有循环,segment 1也没有写满,可以容纳所有的XLOG。
我们将XLogWrite的代码进行了简化,代码如下:
static void
XLogWrite(XLogwrtRqst WriteRqst, bool flexible)
{
bool ispartialpage;
bool last_iteration;
bool finishing_seg;
bool use_existent;
int curridx;
int npages;
int startidx;
uint32 startoffset;
/* We should always be inside a critical section here */
Assert(CritSectionCount > 0);
/*
* Update local LogwrtResult (caller probably did this already, but...),更新本地已落盘的XLOG LSN
*/
LogwrtResult = XLogCtl->LogwrtResult;
//npages用于记录需要落盘的页面数量;startidx表示第一个需要落盘的页面的下标;startoffset表示页面的起始写入偏移。这几个变量详见图3。
npages = 0;
startidx = 0;
startoffset = 0;
/* 通过XLogRecPtrToBufIdx将LogwrtResult.Write转换为buffer page的下标,LogwrtResult.Write是第一个需要落盘的page对应的LSN,* 所以curridx为第一个page的数组下标。
*/
curridx = XLogRecPtrToBufIdx(LogwrtResult.Write);
//当已落盘Write的LSN小于请求Write的LSN时,循环操作。每次循环对一个buffer page进行一系列处理,curridx表示当前正在处理的buffer page
while (LogwrtResult.Write < WriteRqst.Write)
{
/*
* 首先从xlblocks获取一个页面当前可以存放的XLOG的最大LSN
*/
XLogRecPtr EndPtr = XLogCtl->xlblocks[curridx];
// 做一个判断,已落盘Write的LSN不应该大于等于该buffer page的LSN上限,否则就说明后者已经Write完了。
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));
/* Advance LogwrtResult.Write to end of current buffer page,将本地缓存的LogwrtResult.Write修改为当前log buffer page的结束位置。 */
LogwrtResult.Write = EndPtr;
/* 判断当前写入的page是不是partial page(结束页)。如果页是被填满的,这两个值应该相等,只有在页只被写入一部分时,WriteRqst.Write < LogwrtResult.Write */
ispartialpage = WriteRqst.Write < LogwrtResult.Write;
/* Add current page to the set of pending pages-to-dump,npages为0时,(重新)获取startidx,startoffset */
if (npages == 0)
{
/* first of group */
startidx = curridx;
startoffset = XLogSegmentOffset(LogwrtResult.Write - XLOG_BLCKSZ,
wal_segment_size);
}
npages++;
/*
* 判断当前是否为最后一次循环或最后一个页
*/
last_iteration = WriteRqst.Write <= LogwrtResult.Write;
/*
* 判断当前是否为最后一个segment,后面场景会提到
*/
finishing_seg = !ispartialpage &&
(startoffset + npages * XLOG_BLCKSZ) >= wal_segment_size;
// 这三个落盘条件对应我们要讨论的3种场景,后面会提到
if (last_iteration ||
curridx == XLogCtl->XLogCacheBlck ||
finishing_seg)
{
// 落盘代码,暂时省略
}
/* 如果落盘则表示WriteRqst.Write之前的所有XLOG都已经落盘,则结束循环
* 注意在结束循环前,需要将LogwrtResult.Write改为WriteRqst.Write
* 因为我们前面将LogwrtResult.Write改为了XLogCtl->xlblocks[curridx]
* 而我们实际写入的内容仅限于WriteRqst.Write之前
*/
// ispartialpage用于表示partial page是否已经落盘
if (ispartialpage)
{
/* Only asked to write a partial page */
LogwrtResult.Write = WriteRqst.Write;
break;
}
curridx = NextBufIdx(curridx);
/* If flexible, break out of loop as soon as we wrote something */
if (flexible && npages == 0)
break;
}
Assert(npages == 0);
…
代码分析:
① 首先对三个非常重要的成员进行了初始化
npages、startidx、startoffset。这三个成员的作用如图3所示:npages用于记录需要落盘的页面数量;startidx表示第一个需要落盘的页面的下标;startoffset表示页面的起始写入偏移。
② 通过XLogRecPtrToBufIdx函数找到LogwrtResult.Write对应的buffer page。这个page其实就是第一个需要落盘的page。
③ 进入循环,每次循环对一个buffer page进行一系列处理,curridx表示当前正在处理的buffer page。
while (LogwrtResult.Write < WriteRqst.Write)
- 在循环体内部,首先从xlblocks获取一个页面当前可以存放的XLOG的最大lsn。
/*
* 从xlblocks获取一个页面当前可以存放的XLOG的最大LSN
*/
XLogRecPtr EndPtr = XLogCtl->xlblocks[curridx];
xlblocks
xlblocks是XLogCtlData的又一个成员,是一个XLogRecPtr的数组,数组元组的个数为log buffer的页面数,xlblocks与buffer page一一对应。
typedef struct XLogCtlData { ... XLogRecPtr *xlblocks; /* 1st byte ptr-s + XLOG_BLCKSZ */ ... } typedef uint64 XLogRecPtr;
前面讲过LSN到buffer page的转换,也就是说每个LSN都对应一个buffer page。反之每个buffer page都对应一个范围内的LSN。由于log buffer是循环队列,所以用xlblocks数组来表示某个buffer page当前可写入的XLOG的LSN的上限。
由于buffer page的大小固定为XLOG_BLCKSZ,所以通过xlblocks-XLOG_BLCKSZ就可以得到该page可写入XLOG的LSN的下限。所以一个buffer page当前可以写入XLOG的LSN的范围为 [xlblocks-XLOG_BLCKSZ, xlblocks]。这个范围主要用来判断当前写入的XLOG是否会覆盖页面中之前写入的XLOG。
- 在获取到EndPtr之后,首先进行校验,由于LogwrtResult.Write之后的页面都是需要落盘的,所以LogwrtResult.Write不可能>=EndPtr。
// 做一个判断,已落盘Write的LSN不应该大于等于该buffer page的LSN上限,否则就说明后者已经Write完了。
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。注意:LogwrtResult是本地缓存的LogwrtResult而不是全局LogwrtResult。接着判断当前写入的页面是不是partial page。
/* Advance LogwrtResult.Write to end of current buffer page,将本地缓存的LogwrtResult.Write修改为当前log buffer page的结束位置。 */
LogwrtResult.Write = EndPtr;
/* 判断当前写入的page是不是partial page(结束页)。如果页是被填满的,这两个值应该相等,只有在页只被写入一部分时,WriteRqst.Write < LogwrtResult.Write */
ispartialpage = WriteRqst.Write < LogwrtResult.Write;
- 接下来,在npages为0时获取startidx以及startoffset。
/* Add current page to the set of pending pages-to-dump */
if (npages == 0)
{
/* first of group */
startidx = curridx;
startoffset = XLogSegmentOffset(LogwrtResult.Write - XLOG_BLCKSZ,
wal_segment_size);
}
npages++;
这里需要特别注意startoffset的运算。如图4所示:
假设初始时LogwrtResult.Write对应一个页面的中间位置,通过前面的流程,已经将LogwrtResult.Write修改为页面结束位置对应的LSN。所以LogwrtResult.Write - XLOG_BLCKSZ对应页面起始位置的LSN。由于是以页面为单位进行落盘,所以很显然,我们需要获取log buffer对应的物理页偏移。而前面说过将LSN % XLogSegSize就可以实现LSN到物理偏移的转换。所以startoffset = (LogwrtResult.Write - XLOG_BLCKSZ) % XLogSegSize;就得到了log buffer写盘的起始物理偏移。
- 接下来需要对页面数进行累加。然后判断当前页是否为最后一个需要落盘的页面。对于图2的场景,只有在这个时候才会开始真正的落盘(Flush)操作。
/*
* 判断当前是否为最后一次循环或最后一个页
*/
last_iteration = WriteRqst.Write <= LogwrtResult.Write;
// 实际上在最简单的场景只要判断 if (last_iteration) 即可,另外两个条件在后面的场景会提到
if (last_iteration ||
curridx == XLogCtl->XLogCacheBlck ||
finishing_seg)
{
// 正式调用Write写入os缓存,暂时省略
}
- 最后,判断当前页面是否为partial page,如果是则表示WriteRqst.Write之前的所有XLOG都已经落盘,循环结束。否则继续循环。
/* 如果落盘则表示WriteRqst.Write之前的所有XLOG都已经落盘,则结束循环
* 注意在结束循环前,需要将LogwrtResult.Write改为WriteRqst.Write
* 因为我们前面将LogwrtResult.Write改为了XLogCtl->xlblocks[curridx]
* 而我们实际写入的内容仅限于WriteRqst.Write之前
*/
// ispartialpage用于表示partial page是否已经落盘
if (ispartialpage)
{
/* Only asked to write a partial page */
LogwrtResult.Write = WriteRqst.Write;
break;
}
//获取下一个buffer page
curridx = NextBufIdx(curridx);
/* If flexible, break out of loop as soon as we wrote something */
if (flexible && npages == 0)
break;
OK,我们现在已经讲完了while (LogwrtResult.Write < WriteRqst.Write)循环的主要框架。在这个循环中主要做以下事情:
- 每次获取一个buffer page
- 校验每个page的xlblocks与LogwrtResult.Write是否合法
- 第一次循环时获取startidx、startoffset
- 累加npages
- 最后一次循环时执行真正的落盘(FLush)操作
三、 调用write,写入os缓存(part1)
来看之前省略的正式调用Write写入os缓存的代码。
if (last_iteration ||
curridx == XLogCtl->XLogCacheBlck ||
finishing_seg)
{
char *from;
Size nbytes;
Size nleft;
int written;
instr_time start;
/* OK to write the page(s),写入的起点 */
from = XLogCtl->pages + startidx * (Size) XLOG_BLCKSZ;
// 总共要写入的bytes
nbytes = npages * (Size) XLOG_BLCKSZ;
// 剩余要写入的bytes
nleft = nbytes;
do
{
errno = 0;
/* Measure I/O timing to write WAL data */
if (track_wal_io_timing)
INSTR_TIME_SET_CURRENT(start);
pgstat_report_wait_start(WAIT_EVENT_WAL_WRITE);
/* 打开LogFile,写入数据到os缓存。written为已写入的量 */
written = pg_pwrite(openLogFile, from, nleft, startoffset);
pgstat_report_wait_end();
/*
* Increment the I/O timing and the number of times WAL data
* were written out to disk.
*/
if (track_wal_io_timing)
{
instr_time duration;
INSTR_TIME_SET_CURRENT(duration);
INSTR_TIME_SUBTRACT(duration, start);
WalStats.m_wal_write_time += INSTR_TIME_GET_MICROSEC(duration);
}
WalStats.m_wal_write++;
/* written<=0,说明写入报错了 */
if (written <= 0)
{
char xlogfname[MAXFNAMELEN];
int save_errno;
if (errno == EINTR)
continue;
save_errno = errno;
XLogFileName(xlogfname, ThisTimeLineID, 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)));
}
/* 下一次写入的起点 */
nleft -= written;
from += written;
startoffset += written;
// 当剩余要写入的量>0(还有要处理的),继续循环写入
} while (nleft > 0);
npages = 0;
}
四、 两个特殊场景(part 1)
第二部分我们介绍了最简单的一种场景,实际上还可能出现一些特殊场景需要处理。注意一下这部分还是属于part 1的。
1. 场景2
当前segment也可以容纳所有的XLOG,但是log buffer已经写满一轮然后循环到了队头。此时page1和page2、page3不再连续,所以需要分两次进行落盘。也就是说当while (LogwrtResult.Write < WriteRqst.Write) 循环到log buffer的最后一个页面时,需要先将LogwrtResult.Write到该页面之间的所有页面都写盘,然后再继续循环。
因此除了last_iteration时需要写盘,在curridx == XLogCtl->XLogCacheBlck时也需要写盘。XLogCacheBlck存放最大的log buffer页面下标,当curridx等于它时,说明已经到log buffer最后一个page了。
参考 https://blog.csdn.net/Hehuyi_In/article/details/125447500?spm=1001.2014.3001.5501
还记得之前这个写入的判断,场景2对应的就是 curridx == XLogCtl->XLogCacheBlck 这个条件。
if (last_iteration ||
curridx == XLogCtl->XLogCacheBlck ||
finishing_seg)
{
// 调用Write写入os缓存的代码
…
nleft -= written;
from += written;
startoffset += written;
} while (nleft > 0);
npages = 0;
…
}
这里重要的是将npages重置为0。针对上图的场景,page1落盘之后会获取下一个buffer page:
- 显然,此时的curridex值为0指向队头的页面page2,然后继续循环。
- 再次循环时,由于npages被重置为0,所以会重新获取startidx和startoffset,而startidx就指向了page2。
- 如此当遍历到page3这个partial page时就会再次将page2和page3写盘。
2. 场景3
还记得文章最开始这张图,这就是最后一种场景 —— 当前segment中空闲空间不足,需要写入到两个segment中。这种情况下需要先将buffer page中的一部分写入当前segment,并将这部分内容强制落盘(调用fsync),防止后面还需要将重新打开这个segment再进行落盘。
所以,我们需要添加finishing_seg相关的代码:
last_iteration = WriteRqst.Write <= LogwrtResult.Write;
finishing_seg = !ispartialpage &&
(startoffset + npages * XLOG_BLCKSZ) >= wal_segment_size;
if (last_iteration ||
curridx == XLogCtl->XLogCacheBlck ||
finishing_seg)
{
// 调用Write写入os缓存的代码
…
nleft -= written;
from += written;
startoffset += written;
} while (nleft > 0);
npages = 0;
/*
* If we just wrote the whole last page of a logfile segment,fsync the segment immediately. This avoids having to go back and re-open prior segments when an fsync request comes along later.
* Doing it here ensures that one and only one backend will perform this fsync.
* This is also the right place to notify the Archiver that the segment is ready to copy to archival storage, and to update the timer for archive_timeout, and to signal for a checkpoint if too many logfile segments have been used since the last checkpoint.
*/
if (finishing_seg)
{
// 强制执行fsync
issue_xlog_fsync(openLogFile, openLogSegNo);
/* signal that we need to wakeup walsenders later,发送请求唤醒walsenders进程 */
WalSndWakeupRequest();
LogwrtResult.Flush = LogwrtResult.Write; /* end of page,更新已完成落盘Flush的位置 */
// XLog归档
if (XLogArchivingActive())
XLogArchiveNotifySeg(openLogSegNo);
// 更新XLog日志的切换时间和LSN
XLogCtl->lastSegSwitchTime = (pg_time_t) time(NULL);
XLogCtl->lastSegSwitchLSN = LogwrtResult.Flush;
/*
* Request a checkpoint if we've consumed too much xlog since
* the last one. For speed, we first check using the local
* copy of RedoRecPtr, which might be out of date; if it looks
* like a checkpoint is needed, forcibly update RedoRecPtr and
* recheck. 判断是否需要执行检查点,如果需要则请求
*/
if (IsUnderPostmaster && XLogCheckpointNeeded(openLogSegNo))
{
(void) GetRedoRecPtr();
if (XLogCheckpointNeeded(openLogSegNo))
RequestCheckpoint(CHECKPOINT_CAUSE_XLOG);
}
}
至此,XLogWrite第一部分的核心功能终于走完。
五、 调用flush,对part1写入的数据强制落盘(part2)
在第一部分,我们只是调用write将log buffer中的内容写入日志文件,但如果日志文件不是以O_SYNC或者O_DSYNC的方式打开的话,write函数无法保证成功即落盘(日志可能只是写入了操作系统缓存),所以还需要调用fsync来将系统缓存中的数据强制落盘。
/*
* If asked to flush, do so
*/
if (LogwrtResult.Flush < WriteRqst.Flush &&
LogwrtResult.Flush < LogwrtResult.Write)
{
/*
* Could get here without iterating above loop, in which case we might
* have no open file or the wrong one. However, we do not need to
* fsync more than one file.
*/
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);
openLogFile = XLogFileOpen(openLogSegNo);
ReserveExternalFD();
}
issue_xlog_fsync(openLogFile, openLogSegNo);
}
/* signal that we need to wakeup walsenders later */
WalSndWakeupRequest();
LogwrtResult.Flush = LogwrtResult.Write;
}
六、 修改全局LogwrtResult和LogwrtRqst(part3)
完成了落盘操作之后,最后就是修改全局LogwrtResult和LogwrtRqst。
/*
* Update shared-memory status
*
* We make sure that the shared 'request' values do not fall behind the
* 'result' values. This is not absolutely essential, but it saves some
* code in a couple of places.
*/
{
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和LogwrtRqst的修改需要同时持有WALWriteLock锁和info_lck锁,在XLogFlush调用XLogWrite之前就已经持有了WALWriteLock,所以这里只需要持有info_lck锁即可。
至此,XLogWrite函数终于结束了,感天动地!