海山数据库(He3DB)源码详解:He3DB-XLogWrite函数

XLogWrite函数

  • 函数定义
c 复制代码
static void 
XLogWrite(XLogwrtRqst WriteRqst, TimeLineID tli, bool flexible)
  • XLogwrtRqst:请求写入位置信息
  • TimeLineID:时间线,表示一个从创建点当前点的完整数据库历史
  • bool flexible:指示写入操作是否可以灵活处理,即不必严格按照WriteRqst指定的位置进行写入,可以在方便的边界(如缓存或日志文件的边界)停止
  • 该函数调用必须处于临界区
c 复制代码
Assert(CritSectionCount > 0);
  • 保护共享资源,避免数据竞争
  • CritSectionCount 变量见XLogFlush中开启临界区函数,会使该变量++
  • 更新写入和刷新的位置result
c 复制代码
LogwrtResult = XLogCtl->LogwrtResult;
  • 初始化变量
c 复制代码
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);返回

该函数将传入的已经写入的末位置转换为缓存页面的索引

  • 开始循环
c 复制代码
while (LogwrtResult.Write < WriteRqst.Write)
{

}

该循环只能在请求写入的位置大于已经写入位置才能进行


c 复制代码
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日志错误,并输出二者位置信息

c 复制代码
LogwrtResult.Write = EndPtr;
ispartialpage = WriteRqst.Write < LogwrtResult.Write;
  • 将已经写入的位置更新为当前缓冲区的结束位置
  • 记录请求写入位置是否小于当前缓冲区结束位置

c 复制代码
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);更新日志分段(logSegNoopenLogSegNo)的位置

注意: XLByteInPrevSeg函数和XLByteToPrevSeg函数的区别

c 复制代码
#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)。文件描述符:它是一个索引值,指向内核中每个进程打开文件的记录表。当打开一个文件(或设备、管道等)时,内核会向进程返回一个文件描述符。这个文件描述符随后被用于后续的读、写或其他文件操作。

  • 如果没有打开文件,则打开当前日志文件
c 复制代码
if (openLogFile < 0)
{
    XLByteToPrevSeg(LogwrtResult.Write, openLogSegNo, wal_segment_size);
    openLogTLI = tli;
    openLogFile = XLogFileOpen(openLogSegNo, tli);
    ReserveExternalFD();
}

c 复制代码
if (npages == 0)
{
    startidx = curridx;
    startoffset = XLogSegmentOffset(LogwrtResult.Write - XLOG_BLCKSZ, wal_segment_size);
}
npages++;
  • 如果待转储的页面数为0,这意味着当前页面是这一组待转储页面中的第一个。则更新索引和偏移量
  • 不管是不是第一页,待转储的页面数都+1。这表示当前页面已经被添加到待转储的页面集合中。

c 复制代码
last_iteration = WriteRqst.Write <= LogwrtResult.Write;

finishing_seg = !ispartialpage &&
    (startoffset + npages * XLOG_BLCKSZ) >= wal_segment_size;
  • 如果请求写入的位置小于等于已经写入的位置,则表明这是最后一次循环迭代last_iteration
  • 检查是否正在完成一个段的写入

c 复制代码
if (last_iteration ||
    curridx == XLogCtl->XLogCacheBlck ||
    finishing_seg)
{

}
  • 条件判断:如果是最后一次迭代;或者当前索引是缓存块索引、或正在完成一个WAL段的写入

如果满足条件,则进行下一步:

定义变量,并赋值

c 复制代码
from = XLogCtl->pages + startidx * (Size) XLOG_BLCKSZ;
nbytes = npages * (Size) XLOG_BLCKSZ;
nleft = nbytes;
  • from 计算要写入的起始位置
  • nbytes 总字节数
  • nleft 剩余未写入字节数,初始化剩余字节数为总字节数

开始循环,目的是将nleft减到0,即完成所有数据的写入

c 复制代码
do  
{  
    ...  
} while (nleft > 0);
c 复制代码
errno = 0;

if (track_wal_io_timing)
    INSTR_TIME_SET_CURRENT(start);
  • 在循环内部,首先设置errno为0
  • 如果启用了WAL I/O时间跟踪,则测量I/O操作的开始时间
c 复制代码
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偏移量开始
  • 报告等待结束
c 复制代码
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
c 复制代码
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是否为EINTREINTR是一个特殊的错误代码,表示系统调用被信号中断
  • 记录并保存errnosave_errno
  • 生成WAL文件名:XLogFileName函数根据给定的时间线ID(tli)、打开的日志段号(openLogSegNo)和WAL段大小(wal_segment_size),生成WAL文件的名称,并存储在xlogfname数组中。这个文件名将用于错误报告中,以帮助诊断问题。
  • 更新errno
  • 报告错误
c 复制代码
nleft -= written;
from += written;
startoffset += written;

如果没有写入错误,则更新对应的剩余写入字符、起始位置指针以及偏移量

npages = 0;

剩余字节数都写完后,即该do while循环结束,则将待转储页面置为0

c 复制代码
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下且需要检查点
  • 更新重做记录指针
  • 再次检查是否需要检查点

请求检查点

检查点是数据库用来减少恢复时间的一种机制,它确保了数据库可以从一系列固定的点快速恢复

c 复制代码
if (ispartialpage)
{
    LogwrtResult.Write = WriteRqst.Write;
    break;
}
curridx = NextBufIdx(curridx);

if (flexible && npages == 0)
    break;
  • ispartialpage为真,表示当前请求只要求写入一个部分页面,不是整页。则设置已经写入的位置值为请求写入的位置值,并跳出循环
  • 更新当前索引
  • 如果写入操作是灵活的(即可以根据实际情况提前结束),并且到目前为止还没有写入任何页面,那么就可以停止写入过程
    循环结束

  • 条件判断
c 复制代码
if (LogwrtResult.Flush < WriteRqst.Flush && LogwrtResult.Flush < LogwrtResult.Write)
{

}

如果已经刷新的日志位置同时小于请求刷新的位置以及已经完成写入的位置(这表明有部分数据已经写入但未刷新到磁盘,且这部分是请求要求刷新的部分)

  • 再次判断
c 复制代码
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);
}

作用是用于判断是否需要进行文件操作

  1. 如果openLogFile有效,并且当前写入位置LogwrtResult.Write不在当前打开的日志文件段内(通过XLByteInPrevSeg函数检查),则关闭当前文件
  2. 如果openLogFile无效(即小于0),则根据LogwrtResult.Write计算应该打开哪个日志文件段(XLByteToPrevSeg),然后打开该文件(XLogFileOpen),并保留一个外部文件描述符(ReserveExternalFD)。

无论是否进行了文件关闭和重新打开操作,都会调用issue_xlog_fsync函数来刷新当前打开的日志文件段到磁盘。这个函数负责将文件描述符openLogFile指定的文件段同步到磁盘。

  • 唤醒发送者。因为刷新操作可能意味着有新的WAL数据可供流复制
  • 更新已经刷新的日志位置

c 复制代码
{
    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);
}
  • 更新共享内存的状态
  1. 上自旋锁
  2. LogwrtResult中最新的日志写入和刷新状态更新到XLogCtl->LogwrtResult
  3. 分别更新XLogCtl中请求写入和刷新的位置
  4. 释放自旋锁
相关推荐
得物技术17 小时前
得物 iOS 启动优化之 Building Closure
ios·性能优化
数据智能老司机18 小时前
CockroachDB权威指南——CockroachDB SQL
数据库·分布式·架构
数据智能老司机19 小时前
CockroachDB权威指南——开始使用
数据库·分布式·架构
松果猿19 小时前
空间数据库学习(二)—— PostgreSQL数据库的备份转储和导入恢复
数据库
无名之逆19 小时前
Rust 开发提效神器:lombok-macros 宏库
服务器·开发语言·前端·数据库·后端·python·rust
s91236010119 小时前
rust 同时处理多个异步任务
java·数据库·rust
数据智能老司机20 小时前
CockroachDB权威指南——CockroachDB 架构
数据库·分布式·架构
hzulwy20 小时前
Redis常用的数据结构及其使用场景
数据库·redis
程序猿熊跃晖20 小时前
解决 MyBatis-Plus 中 `update.setProcInsId(null)` 不生效的问题
数据库·tomcat·mybatis
Three~stone1 天前
MySQL学习集--DDL
数据库·sql·学习