ICode9

精准搜索请尝试: 精确搜索
首页 > 数据库> 文章详细

PostgreSQL 内核源码分析——lazy Vacuum

2022-02-03 19:02:47  阅读:274  来源: 互联网

标签:lazy 调用 PostgreSQL 清理 元组 源码 vacuum 页面


本文将概要性的讲解 PostgreSQL 中 lazy vacuum 的流程和原理,代码将以 PG 10.17 版本为例。

背景知识

死元组和表空间膨胀

在 PG 中,update/delete 语句的实现通过 MVCC 机制的多版本链实现。对于一条已经被更新/删除的元组来说,当这条元组对所有事物都不可见后,它的存在就没有意义了,理应被删除,对于这种元组,我们称之为“死元组”。当一张表有大量更新/删除时,如果不做清理的话,表里面就会积攒很多这样的“死元组”,占用大量的空间,造成表空间膨胀。

事物号回卷

PG 的事物号采用 32 位无符号整数 xid 来表示,约能表示 40 亿数字。借用[1]中的图 b 来说,对于任意一个 xid ,它在环中的前 20 亿对它来说是过去,后 20 亿对它来说是未来。
在这里插入图片描述
以上图为例,对于 xid 为 100 的事物来说,本来 99 是它的过去,当事物号耗尽的时候,99 会回卷重新使用。这时候就无法判断一条 xmin 或 xmax 为 99 的元组到底是过去还是未来。

Vacuum 功能概述

在提出上两个问题之后,就可以简单的介绍一下 vacuum 的功能:

  1. 清理死元组:删除死元组和其索引,回收表空间;
  2. 冻结元组的事物号:如果一个元组的事物号已经很老了,对于当前所有的事物都可见,那么就直接将它的事物号改为 FrozenTransactionId ;(注:FrozenTransactionId 对于所有事物号都属于过去)
  3. 更新表的统计信息,方便优化器工作。

代码分析

总体结构

vacuum 的函数调用关系如下图所示。当执行 vacuum full 的时候,会调用图中左下角的 cluster_rel 函数;否则调用 lazy_vacuum_rel 函数。
在这里插入图片描述
lazy_vacuum_rel 函数中,主要完成以下几件事:

  1. 调用 vacuum_set_xid_limits 设置一些阈值,用于后续判断是否采取迫切清理的策略(aggressive);
  2. 调用 lazy_scan_heap 做 vacuum 操作:删除元组和索引,清理表空间;
  3. 调用 lazy_truncate_heap 做截断操作,将表最后的空白页面截断;
  4. 调用 FreeSpaceMapVacuum 对 fsm 做 vacuum;
  5. 更新系统表中的统计信息

lazy_scan_heap 函数

lazy_scan_heap 是做 vacuum 的主要函数。在介绍该函数之前,有几个术语/变量先要讲解一下含义:

  • 死元组列表:vacuum 会维护一个数组,将当前 page 的所有死元组的头指针存在这个数组里;

  • relfrozenxid:标记一张表中所有 <relfrozenxid 的元组均被冻结;

  • 迫切模式:这里直接引用 [2] 中的原文。当 relfrozenxid 满足特定条件时,将触发迫切模式;

    冻结的过程有两种模式,依特定条件而择其一执行。一种为惰性模式另一种为迫切模式。惰性模式下,冻结过程仅适用目标表对应的VM扫描包含死元组的页面。迫切模式会扫描所有的页面,无论其是否包含死元组,都会更新与冻结过程相关的系统视图,并在可能的情况下删除不必要的 CLOG 文件。

  • 过期元组:对所有事物均不可见的元组

该函数主要有以下几个步骤:

  1. 初始化若干变量,给死元组列表分配空间;
  2. 计算 next_unskippable_block,用于跳页逻辑判断;
  3. 循环遍历当前表的所有页面,进行 vacuum 处理。
  4. 如果有索引的话,需要先清理索引,再次访问表中的所有页面,进行清理操作;

计算 next_unskippable_block

如果出现很多页面都无需做 vacuum 的话,那么这些页面可以直接被跳过,提高 vacuum 效率。
vacuum 可以通过 vm (visbility map) 的状态来快速判断该页是否 全部可见/全部冻结,如果满足以下两个条件,那么当前页就可以跳过 vacuum。

  1. 处于迫切模式,且当前页面所有元组被冻结;
  2. 处于非迫切模式,且当前页面所有元组全可见;

如果发现超过连续 SKIP_PAGES_THRESHOLD (一般为32)个页面可跳,则后续处理到该页面时可以跳页。具体的跳页逻辑如下图所示,绿色的 page 为可以跳过的页面
在这里插入图片描述

之所以设置这个阈值有以下几个原因:

  1. OS 本身有页面预读算法,在读一个页的时候,OS 通常已经将该页后面的页面读进了操作系统的缓存,这时候只跳一个页并不会增加很高的性能;
  2. 如果跳页的话,整张表的 relfrozenxid 无法被更新,会对后续 vacuum 执行的策略造成影响;

遍历处理所有页面

循环遍历当前表的所有页面,对于每个页面,都进行如下操作:

  1. 计算 next_unskippable_block,按照上图的逻辑判断是否需要跳页;
  2. 如果当前死元组列表快满了,调用 lazy_vacuum_indexlazy_vacuum_heap 清理索引和元组;
  3. 调用 ReadBufferExtended函数读取当前页面;
  4. 获取当前页面的 clean up 锁,即排它锁。因为要修改页面,所以拿写锁。注意,这里只获取一次,如果处于非迫切模式且并不是最后一页的话,没获取到就算了,直接跳过当前页面,等下一次 vacuum 来处理。其他情况请参考代码里的注释;
  5. 调用 heap_page_prune 函数,将当前页面上的 HOT 链进行修剪,将过期元组标记为死元组,对并调用 PageRepairFragmentation 进行页面空间整理 (关于这一部分的细节可以参考 [3] 这篇文章);
  6. 遍历当前页面的所有元组,清理之前因为并发没有清理掉的过期元组和其他种类的元组,可冻结的元组进行冻结处理,并将所有死元组记录到死元组列表中;
  7. 如果当前表没有索引,那么直接调用 lazy_vacuum_page 清理该页面:将死元组标记为 LP_UNUSED ,其他事物放元组的时候可以直接重用标记为 LP_UNUSED 的元组;

清理索引

如果当前表有索引的话,那么需要先清理索引。然后再次访问当前表的所有页面,调用 lazy_vacuum_page 进行清理。
为什么在有索引的情况下不能进行上一节的第 7 步,其原因在本文中不再详述,建议阅读 [3] 这篇文章。

总结

本文以一个较粗的粒度大致介绍了 lazy vacuum 的具体流程。对于其他的部分,如 vacuum full、autovacuum 的工作原理,将在后续的文章中讲解。

参考资料

[1] https://www.interdb.jp/pg/pgsql05.html
[2] https://www.modb.pro/db/238547
[3] https://blog.csdn.net/obvious__/article/details/121318928?spm=1001.2014.3001.5502

标签:lazy,调用,PostgreSQL,清理,元组,源码,vacuum,页面
来源: https://blog.csdn.net/zxwsbg/article/details/122775015

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有