<PostgreSQL数据库内核分析>之第三章:存储管理

本文详细阐述了PostgreSQL存储管理器的体系结构,包括内存管理、外存管理(如表和元组组织、磁盘管理、虚拟文件管理、TOAST与大对象存储)及关键机制,如缓冲池、VFD、FSM和VM。重点介绍了元组存储、表文件管理以及内存缓存和同步策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、存储管理器的体系结构

存储管理器是DBS与物理存取设备的接口

  • 存储管理器的体系结构如下
    (1)本地内存是每个后台进程所专有,存储属于该进程的高速缓存Cache、事务管理信息、进程信息等
    (2)内存上下文:用于统一管理内存的分配和回收
    (3)PG中每个表都用一个表文件来存储,并以表的OID命名,若超出OS文件大小限制,PG会将其切分为多个文件来存储;
    每个表除了表文件外,还拥有两个附属文件:
    可见性映射表文件VM(作用:加快VACUUM的执行速度)
    空闲空间映射表文件FSM作用:(管理表文件的空闲空间)。
    在这里插入图片描述
  • 存储管理器使用了虚拟文件描述符机制VFD,使得后台进程可以打开“无限多个”文件。
  • 缓冲池:进程间共享缓冲池
    (1)为了防止多个进程并发访问共享内存中的数据时产生的冲突,PG提供了轻量级锁,用于支持对共享内存中同一数据的互斥访问
  • 缓冲池:进程私有的本地缓冲池
    (1)PG中的数据在内存中是以页面块的形式存在,每个表空间由多个BLCKSZ(一个可配置的常量)字节大小的文件块组成,每个文件块可以包含多个元组,但是PG不支持元组的跨块存储,每个元组最大为MaxHeadTupleSize,保证了每个文件块中存储的是多个完整的元组。
    (2)表文件以文件块为单位读入内存中,每一个文件块在内存中形成一个页面块。
    (3)PG在内存中开辟了缓冲区域(缓冲池):磁盘上的文件块读入内存后被存放在缓冲区中,缓冲池被划分为与文件块的尺寸相同的若干个固定大小的缓冲区,一个标准缓冲块的默认大小是8K。

一个PG进程(postmaster、postgres)从数据库读写一个元组的流程:

  • 进程的本地内存有2种Cache:存储系统表元组、存储系统表的基本信息

  • PG的共享缓冲池与本地缓冲池:本地缓冲池用于缓冲临时表数据以及其他进程不可见的数据

  • 若缓冲池中没有包含所需元组的缓冲块,则需通过存储介质管理器SMGR来从存储介质中读取,并写入到缓冲区中。

  • 磁盘管理器负责管理所有存储在磁盘上的文件的操作

  • 虚拟文件管理:虚拟文件描述符VFD机制。VFD通过合理使用有限个实际文件描述符来满足无限的VFD访问需求,VFD通过维持一个LRU池来管理FD
    在这里插入图片描述

  • 在PG中,为每个表增加了一个附属文件,即空闲空间映射表FSM,用于记录每个表文件中块的空闲空间大小,通过一定的查找机制和数据组织实现了文件块的快速选择

  • PG使用标记删除的方式来处理元组的删除,即:对元组打上删除标记,而非从物理上删除元组。
    元组的物理删除是由VACUUM机制来完成的,由于VACUUM操作时会去遍历所有文件块,去查找被删除的元组效率低,所以也为表设计了可见性映射表VM,用以加快查找的速度。

二、外存管理

每个表文件在磁盘中都以一定的结构进行存储

  • 外存管理体系结构
    在这里插入图片描述

1.表和元组的组织方式

PG中同一个表中的元组是顺序依次插入到表文件中的。

  • 元组之间不关联的表文件,称之为堆文件。
    PG包括四种堆文件:普通堆ordinary cataloged heap、临时堆temporary heap、序列sequence relation(特殊的单行表)、TOAST表(toast table)。
    (1)临时堆仅在会话过程中创建、在会话结束时自动删除;
    (2)序列是一种元组值自动增长的特殊堆
    (3)TOAST表其实也是一种普通堆,但是它被专门用于存储变长数据。
  • 堆文件的物理结构如下,
    (1)每个堆文件由多个文件块组成
    (2)Linp是ItemIdData类型的数组,每一个ItemIdData结构用来指向文件块中的一个元组
    (3)Freespace是空闲空间,新插入页面的元组都从这部分空间来分配
    (4)Special space是特殊空间,用于存放与索引方法相关的特定数据
    (5)元组信息:存放元组的实际数据和元组的头部信息(事务ID+命令ID等信息)
    在这里插入图片描述
  • 从堆中删除一个元组:采用标记删除的方法,为每个元组使用额外的数据位作为删除标记。
    当删除元组时,只需设置相应的删除标记(在元组头部,记录了删除这个元组的事务ID和命令ID),即可设置相应的删除标记。
    若上述两个ID有效,则表明该元组应该被删除。

2.磁盘管理器

磁盘管理器并非对磁盘上的文件直接进行操作,而是通过VFD机制进行文件操作。

  • 因为一个进程打开的fd数量有限(受限于OS规定的最大值),所以设计了VFD机制
  • VFD、真实文件描述符、文件之间的关系
    (1)虚拟文件描述符是指:一个叫做VFD的数据结构,其中记录了操作系统为文件分配的真实fd;
    (2)多个进程打开同一个文件,那么每个进程将获得一个真实fd,每个真实fd对应一个VFD;
    在这里插入图片描述

LRU池

  • 每一个PG后台进程都使用一个LRU(Last Recently Used,最近最少使用)池来管理所有已经打开的VFD,池中每一个VFD都对应物理上已经打开的文件;
  • 每个进程都拥有其私有的LRU池和一系列VFD,进程需要打开文件时都是从自己私有的LRU池中申请VFD
    (1)在LRU池中,使用替换最长时间未使用的VFD策略
    (2)进程在VfdCache上保持了两个链表,一个是LRU池(双向链表,通过Vfd数据结构的IruMoreRecently属性和IruLessRecently属性链接),另一个是FreeList(空闲链表,记录了所有可被分配的VFD,通过Vfd数据结构中的nextFree属性来连接)。
    (3)PG中将一个进程当前正打开的所有文件的VFD都连成一个环,即LRU池;
    每个方框代表一个VFD(一个VfdCache数组元素),方框里的符号表示该VFD在数组中的下标。
    在LRU池中,每一个VFD都通过指针链接两个VFD,通过指针IruMoreRecently链接最近最常用的VFD,通过指针IruLessRecently链接最近不长使用的VFD;
    (4)LRU池的大小与操作系统对于进程打开文件数的限制保持一致,在postmaster进程的启动过程中会调用max_safe_fds函数来检测操作系统限制;
    (5)插入:新的VFD在VFD[0]后插入;
    删除:在LRU池中删除VFD;若LRU池已满,而又要打开新的fd,则将池中末尾的VFD删除(最少使用的VFD)
    在这里插入图片描述

空闲空间映射表FSM

  • 对于每个表文件(包括系统表在内),同时创建一个名为“关系表OID_fsm”的文件,用以记录该表的空闲空间大小,称之为空闲空间映射表文件FSM。
  • FSM物理块与逻辑地址对照
    (1)为了实现快速查找,FSM文件不太大且使用了树结构;
    FSM中存储的不是实际的文件块空间的大小,而是仅用一个字节来记录,该字节的值用于描述对应文件块中空闲空间的范围:
    所以对于任意一个表块,根据该字节的值就可以知道这个表块中空闲空间的范围;
    对于任意一个表块,根据其空闲空间的大小可以计算出它对应的FSM字节的取值,eg,N字节空闲空间的表块,FSM中记录的值为(31+N)/32;
    在这里插入图片描述
    (2)FSM块之间使用了一个三层树结构:
    第0层和第1层是辅助层:快速定位满足需要表块的FSM块
    第2层FSM块:实际存放各表块的空闲空间值;
    每一个FSM快内构成一个局部的最大堆二叉树,每个叶子节点表示一个表块的空闲空间值,按照从左至右的顺序,所有第2层FSM块中的叶子节点排列起来就一一对应了表文件中的每一个表块。
    第1层FSM块中的叶子节点从左至右顺序对应第2层FSM块的跟节点,第0层与第一层关系类似。
    (3)每个FSM块大小默认是8KB,出去头部等信息,每个叶子节点用一个字节记录,假设一个FSM块内可以保存4000个叶子节点,所以,一个FSM文件(有三个FSM块:0层FSM块、1层FSM块、2层FSM块)可以记录4000^3个叶子节点(表块);
    这是远远大于2^32,单个表的最大块数(PG的块号长度为32bit,所以单个表最多只能有2^32个块)
    (4)FSM文件中第一个文件块中二叉树(0层的0号FAM块)根节点存储的是所有表块空闲空间的最大值。
    在这里插入图片描述

可见性映射表vm

  • PG为了实现多版本的并发控制,当事务删除或者更新元组时,并非从物理上删除,而是通过将其标记为无效的方式进行标记删除,最终对这些无效的清理操作则需调用VACUUM(VACUUM查找包含无效元组的文件块)
  • VACUUM有2种情况方式:快速清理Lazy VACUUM和完全清理Full VACUUM。
    vm仅在Lazy VACUUM中使用到,Full VACUUM由于要执行跨块清理等操作,需要对整个表文件进行扫描,此时vm文件作用不大。
  • vm文件也被划分为若干个文件块(vm块),vm块中除了必要的标记信息外,其他的每一位都对应一个表块,当表块中所有的元组对当前的事务都是可见的时,表块对应的位才设为1
    当对某个表块中的元组进行更新或者删除后,那么该表块在vm文件中的对应位置的标志位将被置0。
  • vm文件仅仅是作为一个提示hint,加快vacuum的速度
    在这里插入图片描述

大数据存储

  • toast机制(the oversi-attribute storage technique,数据压缩和线外存储)和大对象机制(使用一个专门的系统表来存储大对象数据)
  • toast机制
    (1)这类数据类型必须有变长(varlena类型),eg:text类型,只有在准备向支持TOAST的属性中存储超过BLOCKSZ/4字节(2KB),TOAST机制才会被触发。
    TOAST机制会试图对将要存储的数据进行压缩或线外存储数据(即:把数据存储在其他的表中),直到数据比BLOCKSZ/4字节短,或者无法得到更好的结果的时候才停止
    (2)如果一个表中的属性是可以TOAST的,那么该表的将有一个关联的TOAST表。
    (3)TOAST机制优点:可以有效地节省查询时所占的内存空间,TOAST数据只是在向用户显示结果时才被取出来,在查询过程中并不需要检查TOAST数据的具体取值
    (4)TOAST技术保障了PG中的元组大小足以存放在一个文件块中,so,TOAST技术必须被设置为一种系统自动处理的机制。
    TOAST机制主要集中于变长的数据类型。
  • 大对象
    (1)PG的大对象存储机制可以支持三种数据类型的存储:
    二进制大对象BLOB:eg:图片、视频
    字符大对象CLOB:存储大的单字节字符集数据,eg文档
    双字节字符大对象DBCLOOB:存储大的双字节字符集数据,eg变长双字节字符图形字符串
    (2)一个大对象会被分成若干个元组存放在系统表pg_largeobject中,每一个元组也称为一个页面。
    一个元组大小为2KB,设置成2KB的原因是:可以触发TOAST的压缩机制。

TOAST与对象的区别

  • TOAST是可变长数据类型的一种大数据存储机制,属于自动触发机制;
    大对象属于用于手动调用机制
  • TOAST中的数据不能丢失,一旦丢失则报错;大对象中运行数据丢失,若丢失,则用0替代;
  • 文件不适合使用TOAST技术,原因:二进制读-》将二进制当做字符串存储到变长数据类型的属性中-》转变成文件;大对象操作是将文件作为一个对象存储到大对象表中,读取时直接读取成一个文件。
  • TOAST和大对象操作保存大数据时,都是采用了将数据切片成了片段存储到表中的方式。

3.内存管理

尽可能让使用的文件停留在内存中,这样就能有效地减少磁盘I/O代价。

  • PG内存管理
    在这里插入图片描述
  • 一个内存上下文实际上就相当于一个进程环境
    PG每个子进程都拥有多个私有的内存上下文,每个子进程的内存上下文组成一个树形结构。
    在这里插入图片描述
    高速缓存
  • 当数据库访问表时,需要表的模式信息,比如表的列属性、OID、统计信息等。
    PG将表的模式信息存放在系统表中,因此要访问表,就需要首先在系统表中取得表的模式信息。
    PG设立了高速缓存Cache来提高对系统表和普通表模式的访问效率。
    Cache中包括一个系统表元组SysCache和一个表模式信息RelCache:
    SysCache主要用于缓存系统表元组;
    RelCache存放的不是元组,而是RelationData数据结构;
  • 每一个PG进程都维护着自己的SysCache和RelCache。

Cache同步

  • PG中,每一个进程都有自己的Cache。同一个进程表在不同的进程中都有对应的Cache来缓存它的元组。桶一个系统表的元组可能被多个进程的Cache所缓存,当其中某个Cache中的一个元组被删除或更新,需要通知其他进程对其Cache进行同步。
    PG中会记录下已被删除的无效元组,并通过共享消息队列的方式在进程之间传递消息,收到无效消息的进程同步地把无效元组(RelationData结构,因为RelCache缓存的是一个RelationData数据结构)才能够自己的Cache中删除。
  • 当一个元组被删除或者更新时,在同一个SQL命令的后续执行步骤中,我们依然认为该元组是有效的,直到下一个命令开始或者事务提交时改动才生效。
    在命令的边界,旧元组变为失效,同时新元组变为有效。so,当执行heap_delete或者heap_update时,不能简单地刷新cache,正确的做法是:保持一个无效链表用于记录元组的delete/update操作。
    事务完成后,将无效链表中的信息广播该事务过程中产生的无效信息,其他进程通过消息队列读取无效信息对各自的Cache进行刷新。
    当子事务提交时,只需要将该事务产生的无效消息提交到父事务,最后由最上层的事务广播无效消息。

缓冲池管理

### PostgreSQL 数据库因信号 11(段错误)崩溃的调试与排查方法 PostgreSQL 在运行过程中如果遇到 `Segmentation fault (core dumped)` 错误,并且日志中显示 `server process (PID xxxxxx) was terminated by signal 11: Segmentation fault`,则表明某个后台进程访问了非法内存地址,导致内核强制终止该进程。这种问题通常由以下几种原因引起: - 内存访问越界或使用未初始化指针 - 使用已释放的内存区域 - 第三方扩展模块存在缺陷 - PostgreSQL 源码被修改后引入了 bug - 编译环境或依赖库版本不兼容 #### 生成并分析 core dump 文件 为了定位具体出错的位置,可以通过以下步骤生成和分析 core dump 文件: 1. **设置系统允许生成 core 文件**: ```bash ulimit -c unlimited ulimit -f unlimited ``` 这将允许生成不限大小的 core 文件[^4]。 2. **配置 core 文件路径(可选)**: 修改 `/proc/sys/kernel/core_pattern` 文件以指定 core 文件的存储路径和命名格式,例如: ```bash echo "/tmp/core.%e.%p" > /proc/sys/kernel/core_pattern ``` 3. **重新启动 PostgreSQL 并复现问题**: 如果是特定操作触发的崩溃,尝试在测试环境中复现该行为。 4. **使用 GDB 分析 core 文件**: 找到 core 文件后,使用 GDB 工具进行调试: ```bash gdb /path/to/postgres /path/to/core ``` 在 GDB 中输入 `bt` 查看堆栈跟踪信息,可以定位到发生段错误的具体代码位置。 #### 检查 PostgreSQL 日志与断言失败 在某些情况下,PostgreSQL 会因为断言失败而提前退出,例如日志中出现: ``` TRAP: FailedAssertion("strvalue != NULL", File: "snprintf.c", Line: 442) ``` 这说明程序在运行时违反了某个内部一致性检查条件。这类问题可能源于传入函数的参数异常、内存分配失败或结构体字段未正确初始化等情况[^1]。 #### 排查第三方扩展或自定义代码 如果对 PostgreSQL 进行了源码级别的修改或加载了非官方扩展,应重点审查相关代码逻辑,尤其是涉及文件操作、内存管理或线程安全的部分。例如,在调用类似 `pg_vfd_test()` 函数时,若未正确处理返回值或资源释放流程,也可能引发段错误[^1]。 #### 清理并重新编译安装 PostgreSQL 有时由于旧的构建残留文件未完全清除,可能导致链接错误或运行时异常。建议删除所有编译输出目录及中间文件后再重新编译安装: ```bash rm -rf /usr/local/pgsql rm -rf /path/to/postgresql-source-directory tar -zxvf postgresql-x.x.x.tar.gz cd postgresql-x.x.x ./configure --prefix=/usr/local/pgsql make clean && make && make install ``` 确保所有依赖库版本一致,并启用调试符号以便后续分析[^3]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

喜欢打篮球的普通人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值