当前位置:   article > 正文

postgresql源码学习(27)—— 事务日志⑦-日志落盘上层函数 XLogFlush

xlogflush

一、 预备知识

1. XLOG什么时候需要落盘

  • 事务commit之前
  • log buffer被覆盖之前
  • 后台进程定期落盘

2. 两个核心结构体

这两个结构体定义代码在xlog.c,它们在日志落盘过程中非常重要,会反反复复出现。

  • XLogwrtRqst表示请求落盘的XLOG LSN
  • XLogwrtResult表示已经落盘的XLOG LSN
  1. typedef struct XLogwrtRqst
  2. {
  3. XLogRecPtr Write; /* last byte + 1 to write out */
  4. XLogRecPtr Flush; /* last byte + 1 to flush */
  5. } XLogwrtRqst;
  6. typedef struct XLogwrtResult
  7. {
  8. XLogRecPtr Write; /* last byte + 1 written out */
  9. XLogRecPtr Flush; /* last byte + 1 flushed */
  10. } XLogwrtResult;

Write与Flush的区别

  • Write表示在此位置之前的XLOG已经写入操作系统缓存(不确定是否落盘)。
  • Flush表示在此位置之前的XLOG已经落盘。
  • 一般的环境使用的都是同步提交,Write和Flush是相等的

源码中关于这两个结构体的注释很长,值得关注的是:

  • XLogwrtRqst与XLogwrtResult都是存放于共享内存中被所有进程共享的,所以在读写时需要加锁:读时需要info_lck或者WALWriteLock任意一个锁,写时两个锁都需要。
  • 每一个处理进程都有一个本地的LogwrtResult,这是共享LogwrtResult的一个copy,而XLogwrtRqst并没有本地copy。

 二、 调试XLogFlush函数

        XLogFlush函数用于将record之前的所有XLog全部落盘,它是XLogWrite的上层函数,XLogWrite是真正的落盘函数,下一篇我们会讨论。

       在上面三种情况中,最简单也是最可控的就是commit之前的XLOG落盘,为了方便调试排除其他干扰因素,我们需要首先将后台落盘的进程walwriter挂起,以免在commit之前后台进程将XLOG进行落盘。后台进程挂起后,在commit之前,事务中的相关XLOG一定没有落盘,就可以很好地观察了。

1. 挂起walwriter进程

2. 设置断点

断点函数为XLogFlush

3. 执行insert操作(含commit)

insert into t_insert values(1,'11','12','13');

4. debug函数

函数入参,待刷入的LSN  24119528

        比较record与本地缓存的XLogwrtResult.Flush值。LogwrtResult.Flush表示已经刷入的LSN 24118560,小于24119528,说明入参的LSN 还未被刷入磁盘。如果已经刷入,则直接结束函数。

       LogwrtRqst由info_lck保护,读取需要先获取锁,如果没能获取,则循环等待。参考postgresql源码学习(21)—— 事务日志②-日志初始化_Hehuyi_In的博客-CSDN博客

       代码注释有提到,fsync是一个昂贵的操作,因此我们尽量每次多刷一些。获取锁后if语句对比请求写入的LSN和入参LSN,并将请求写入的WriteRqstPtr 设为其中较大的值,可以多写一点。这里WriteRqstPtr的值更大,因此直接用这个值即可。  

       这里遇到一个问题是调试过程中pg挂掉了,怀疑跟挂起了walwriter进程有关。下面重新debug了一次,所以LSN号变了。

加锁后获取全局XLogwrtResult,以更新本地XLogwrtResult。

       更新本地XLogwrtResult后再次判断record之前的XLOG是否已经落盘。这是一个典型的乐观锁方式,以此提高并发性。

       这里如果没有挂起WAL后台进程,很有可能在调试过程中它已经把record对应LSN给落盘了,就会出现record<=LogwrtResult.Flush 退出循环的情况。因为我们挂起了WAL后台进程,目前record还是大于LogwrtResult.Flush,即还未落盘。

       再往下走,遇到一个老朋友WaitXLogInsertionsToFinish,这就是前篇提到的,日志落盘之前必须先确认相关进程XLOG都已写入WAL Buffer。

https://blog.csdn.net/Hehuyi_In/article/details/125481617?spm=1001.2014.3001.5502

       然后,我们需要获取WALWriteLock锁,前面提到过必须拿到两个锁才能更新。如果还没获取到,则进入下一次循环,继续等待。

       获取锁之后,需要再次读取全局XLogwrtResult,然后判断record之前的XLOG是否已经落盘。如果已经刷入,则释放锁,退出循环。

       判断有没有设置CommitDelay(组提交)、enableFsync是否为true,最小活跃事务数量是否大于CommitSiblings(值为5)。 这里CommitDelay=0 ,所以不会进这个if。如果进的话需要sleep CommitDelay的时间,另外再次调用WaitXLogInsertionsToFinish函数。

      insertpos表示最旧的、仍在进行WAL写入的LSN,在这点之前的所有WAL数据均已写入WAL buffer,可以被刷入磁盘。

      因此将insertpos赋值给WriteRqst.Write和WriteRqst.Flush,表示希望将该LSN之前的数据全部刷入磁盘。

      下面调用XLogWrite函数,真正将日志刷入磁盘(我们下篇来看)。

最后判断,确保已落盘的LSN大于入参LSN,否则说明落盘失败了。

三、 函数源码

  1. /*
  2. * Ensure that all XLOG data through the given position is flushed to disk.
  3. *
  4. * NOTE: this differs from XLogWrite mainly in that the WALWriteLock is not
  5. * already held, and we try to avoid acquiring it if possible.
  6. */
  7. void
  8. XLogFlush(XLogRecPtr record)
  9. {
  10. XLogRecPtr WriteRqstPtr;
  11. XLogwrtRqst WriteRqst;
  12. /*
  13. * During REDO, we are reading not writing WAL. Therefore, instead of
  14. * trying to flush the WAL, we should update minRecoveryPoint instead. We
  15. * test XLogInsertAllowed(), not InRecovery, because we need checkpointer
  16. * to act this way too, and because when it tries to write the
  17. * end-of-recovery checkpoint, it should indeed flush.
  18. */
  19. if (!XLogInsertAllowed())
  20. {
  21. UpdateMinRecoveryPoint(record, false);
  22. return;
  23. }
  24. /* Quick exit if already known flushed,比较record LSN与本地缓存的XLogwrtResult.Flush LSN值。如果已经刷入,则直接结束函数。 */
  25. if (record <= LogwrtResult.Flush)
  26. return;
  27. #ifdef WAL_DEBUG
  28. if (XLOG_DEBUG)
  29. elog(LOG, "xlog flush request %X/%X; write %X/%X; flush %X/%X",
  30. LSN_FORMAT_ARGS(record),
  31. LSN_FORMAT_ARGS(LogwrtResult.Write),
  32. LSN_FORMAT_ARGS(LogwrtResult.Flush));
  33. #endif
  34. START_CRIT_SECTION();
  35. /*
  36. * fsync是一个昂贵的操作,因此我们尽量每次多刷一些。获取锁后if语句对比请求写入的LSN和入参LSN,并将请求写入的WriteRqstPtr 设为其中较大的值,可以多写一点。
  37. */
  38. /* initialize to given target; may increase below,初始化请求写入的位置为传入的参数record */
  39. WriteRqstPtr = record;
  40. /*
  41. * Now wait until we get the write lock, or someone else does the flush for us. 读取需要先获取锁,如果没能获取,则循环等待。或者等到有别人(例如walwriter)替我们完成日志落盘。
  42. */
  43. for (;;)
  44. {
  45. XLogRecPtr insertpos;
  46. /* read LogwrtResult and update local state,获取info_lck锁,读取全局变量XLogwrtRqst和XLogwrtResult,以更新本地XLogwrtResult */
  47. SpinLockAcquire(&XLogCtl->info_lck);
  48. if (WriteRqstPtr < XLogCtl->LogwrtRqst.Write)
  49. WriteRqstPtr = XLogCtl->LogwrtRqst.Write;
  50. LogwrtResult = XLogCtl->LogwrtResult;
  51. SpinLockRelease(&XLogCtl->info_lck);
  52. /* done already? 当前记录是否已经落盘?这就是所谓的等到有别人(例如walwriter)替我们完成日志落盘。*/
  53. if (record <= LogwrtResult.Flush)
  54. break;
  55. /*
  56. * Before actually performing the write, wait for all in-flight
  57. * insertions to the pages we're about to write to finish. 老朋友WaitXLogInsertionsToFinish,这就是前篇提到的,日志落盘之前必须先确认相关进程XLOG都已写入WAL Buffer。
  58. */
  59. insertpos = WaitXLogInsertionsToFinish(WriteRqstPtr);
  60. /*
  61. * 然后,我们需要获取WALWriteLock锁,前面提到过必须拿到两个锁才能更新。如果还没获取到,则进入下一次循环,继续等待。
  62. */
  63. if (!LWLockAcquireOrWait(WALWriteLock, LW_EXCLUSIVE))
  64. {
  65. /*
  66. * The lock is now free, but we didn't acquire it yet. Before we
  67. * do, loop back to check if someone else flushed the record for
  68. * us already.
  69. */
  70. continue;
  71. }
  72. /* Got the lock; recheck whether request is satisfied. 获取锁之后,需要再次读取全局XLogwrtResult,然后判断record之前的XLOG是否已经落盘。如果已经刷入,则释放锁,退出循环。 */
  73. LogwrtResult = XLogCtl->LogwrtResult;
  74. if (record <= LogwrtResult.Flush)
  75. {
  76. LWLockRelease(WALWriteLock);
  77. break;
  78. }
  79. /*
  80. * 判断有没有设置CommitDelay(组提交)、enableFsync是否为true,最小活跃事务数量是否大于CommitSiblings
  81. */
  82. if (CommitDelay > 0 && enableFsync &&
  83. MinimumActiveBackends(CommitSiblings))
  84. {
  85. pg_usleep(CommitDelay);
  86. /*
  87. * 如果进这个if,需要sleep CommitDelay的时间,另外再次调用WaitXLogInsertionsToFinish函数。
  88. */
  89. insertpos = WaitXLogInsertionsToFinish(insertpos);
  90. }
  91. /* try to write/flush later additions to XLOG as well,insertpos表示最旧的、仍在进行WAL写入的LSN,在这点之前的所有WAL数据均已写入WAL buffer,可以被刷入磁盘。因此将insertpos赋值给WriteRqst.Write和WriteRqst.Flush,表示希望将该LSN之前的数据全部刷入磁盘。*/
  92. WriteRqst.Write = insertpos;
  93. WriteRqst.Flush = insertpos;
  94. // 调用XLogWrite函数,真正将日志刷入磁盘(我们下篇来看)
  95. XLogWrite(WriteRqst, false);
  96. // 释放锁
  97. LWLockRelease(WALWriteLock);
  98. /* done */
  99. break;
  100. }
  101. END_CRIT_SECTION();
  102. /* wake up walsenders now that we've released heavily contended locks */
  103. WalSndWakeupProcessRequests();
  104. /*
  105. * 最后判断,确保已落盘的LSN大于入参LSN,否则说明落盘失败了。
  106. */
  107. if (LogwrtResult.Flush < record)
  108. elog(ERROR,
  109. "xlog flush request %X/%X is not satisfied --- flushed only to %X/%X",
  110. LSN_FORMAT_ARGS(record),
  111. LSN_FORMAT_ARGS(LogwrtResult.Flush));
  112. }

参考

PostgreSQL重启恢复---Log Buffer_obvious__的博客-CSDN博客

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/434184
推荐阅读
相关标签
  

闽ICP备14008679号