简介

shared buffers是数据库内存共享缓冲区。

当Postgres想要从磁盘获取数据(page)时,先搜索shared_buffers,确认该page是否在shared_buffers中,如果存在,则直接命中,返回缓存的数据以避免I/O。如果不存在,再到OS缓存查找,最后才会通过I/O访问disk获取数据。

结构

PostgreSQL 缓存管理器分成三层结构:缓冲表(buffer table)、缓冲区描述符(buffer descriptors)和缓冲池(buffer pool)

PostgreSQL shared buffers_缓冲池

buffer table - 缓冲表

是一个哈希表,它存储着页面的buffer_tag与描述符的buffer_id之间的映射关系。

例如数据项Tag_A,id=2 表示,buffer_id=2对应的缓冲区描述符中,存储着页面Tag_A的元数据。

PostgreSQL中的每个数据文件页面都可以分配到唯一的标签,即缓冲区标签(buffer tag)。 当缓冲区管理器收到请求时,PostgreSQL会用到目标页面的缓冲区标签。

缓冲区标签(buffer_tag) 由三个值组成:关系文件节点(relfilenode)关系分支编号(fork number)页面块号(block number)

例如,缓冲区标签{(16821, 16384, 37721), 0, 7}表示,在oid=16821的表空间中的oid=16384的数据库中的oid=37721的表的0号分支(关系本体)的第七号页面。再比如缓冲区标签{(16821, 16384, 37721), 1, 3}表示该表空闲空间映射文件的三号页面。


关系分支编号(fork number)

  • 这用于区分表或索引中的不同“分支”或“部分”。在PostgreSQL中,常见的分支包括:
  • 0:主分支(main fork),包含表或索引的数据页。
  • 1:空闲空间映射(FSM)分支,用于快速查找可重用空间。
  • 2:可见性映射(VM)分支,用于快速判断行是否对查询可见。
  • 其他分支可能用于特定的内部目的或扩展功能。
  • 作用:缓冲表是一个散列表,它存储着页面的buffer_tag与描述符的buffer_id之间的映射关系。这个映射关系使得系统可以快速定位到任何给定页面的缓存位置。
  • 内部机制:内置的hash函数将buffer_tags映射到特定的插槽(slot),插槽中记录了buffer_tags和描述层的buffer_id的映射关系。


buffer descriptors - 缓冲区描述符

是一个由缓冲区描述符组成的数组。

缓冲区描述符保存着页面的元数据,这些与缓冲区描述符相对应的页面保存在缓冲池槽中

  • 作用:缓冲区描述符层是一个由缓冲区描述符组成的数组,每个描述符与缓冲池槽一一对应,并保存着相应槽的元数据。这些元数据包括访问次数统计、锁信息、页面是否脏(已被修改但尚未写入磁盘)等。
  • 内容:每个描述符包含tag(数据块的标识)、buffer_id(缓冲池ID)、refcount(记录块被访问的次数)、usage_count(使用的次数)、锁信息(如countext_lock和io_in_progress_lock)、标记(如dirty bit、valid bit等)。


buffer pool - 缓冲池

缓冲池只是一个用于存储关系数据文件(例如表或索引)页面的简单数组。缓冲池数组的序号索引也就是buffer_id。

缓冲池槽的大小为8KB,等于页面大小,因而每个槽都能存储整个页面。

  • 作用:缓冲池是一个用于存储关系数据文件(如表或索引)页面的简单数组。它是实际存储数据页的地方,每个槽的大小等于页面大小(通常为8KB),因此每个槽都能存储整个页面。
  • 内部机制:缓冲池被分割成多个大小为8KB的插槽,每个插槽对应一个描述符,并存储一个数据文件页。缓冲池数组的序号索引就是buffer_id。


读数据顺序

  1. 访问请求:当数据库需要读取一个页面时,首先会检查这个页面是否已经在缓冲池中。
  2. 查询缓冲表:通过buffer_tag在缓冲表中查找对应的buffer_id。
  3. 获取描述符:如果找到,则根据buffer_id在缓冲区描述符层中获取对应的描述符。
  4. 读取数据:根据描述符中的信息,直接从缓冲池对应的插槽中读取数据。
  5. 未命中处理:如果缓冲表中未找到对应的buffer_id,则说明页面不在缓存中,需要从磁盘读取页面到缓冲池,并更新缓冲表和描述符。


写数据顺序

  1. 修改数据:当数据库需要修改一个页面时,首先会在缓冲池中找到对应的页面。
  2. 修改缓存:对页面进行修改,并设置描述符中的脏位(dirty bit)为1,表示该页面已被修改但尚未写入磁盘。
  3. 后台写入:后台写进程(如bgwriter)会定期将脏页写入磁盘,或者在达到一定条件(如脏页数量过多)时触发写操作。
  4. 更新映射:写操作完成后,会更新缓冲表和描述符中的映射关系,确保数据的一致性。


  • BufMappingLock:用于保护整个缓冲表的数据完整性。
    在缓冲表中查询条目时,后端进程会持有共享的BufMappingLock。插入或删除条目时,后端进程会持有独占的BufMappingLock。
  • content_lock - 内容锁:是一种强制限制访问的锁
  • io_in_progress_lock - IO进行锁
    io_in_progress_lock用于等待缓冲区上的I/O完成。当PostgreSQL进程加载/写入页面数据时,该进程在访问页面期间,持有对应描述符上独占的io_in_progres_lock。
  • spin lock - 自旋锁
    当检查或更改标记字段与其他字段时(例如refcount和usage_count),会用到自旋锁。
  • refcount:保存当前访问相应页面的PostgreSQL进程数,也被称为钉数(pin count)
  • usage_count:保存着相应页面加载至相应缓冲池槽后的访问次数。(时钟扫描会用到)

页面替换算法

当所有缓冲池槽位都被占用,且其中未包含所请求的页面时,缓冲区管理器必须在缓冲池中选择一个页面逐出,用于放置被请求的页面。 在计算机科学领域中,选择页面的算法通常被称为页面置换算法(page replacement algorithms),而所选择的页面被称为受害者页面(victim page)

PostgreSQL使用**时钟扫描(clock-sweep)**算法

PostgreSQL shared buffers_描述符_02

  1. nextVictimBuffer指向第一个描述符(buffer_id = 1);但因为该描述符被钉住了,所以跳过。
  2. nextVictimBuffer指向第二个描述符(buffer_id = 2)。该描述符未被钉住,但其usage_count为2;因此该描述符的usage_count将减1,而nextVictimBuffer迭代至第三个候选描述符。
  3. nextVictimBuffer指向第三个描述符(buffer_id = 3)。该描述符未被钉住,但其usage_count = 0,因而成为本轮的受害者。

当nextVictimBuffer扫过未固定的描述符时,其usage_count会减1。因此只要缓冲池中存在未固定的描述符,该算法总能在旋转若干次nextVictimBuffer后,找到一个usage_count为0的受害者。


环形缓冲区 - ring buffer

在读写大表时,PostgreSQL会使用环形缓冲区(ring buffer)。是一个临时缓冲区,在使用后被立即释放

PostgreSQL在共享内存中分配一个环形缓冲区的条件:

  • 批量读取
    当扫描关系读取数据的大小超过缓冲池的四分之一(shared_buffers/4)时,在这种情况下,环形缓冲区的大小为 256 KB 。
  • 批量写入
    当执行下列SQL命令时,这种情况下,环形缓冲区大小为 16 MB 。
  • COPY FROM命令。
  • CREATE TABLE AS命令。
  • CREATE MATERIALIZED VIEW或 REFRESH MATERIALIZED VIEW命令。
  • ALTER TABLE命令
  • 清理过程
    当自动清理守护进程执行清理过程时,这种情况环形缓冲区大小为256 KB。


共享内存泄露

日志里出现如下错误:

This error usually means that PostgreSQL's request for a shared  memory segment 
exceeded available memory or swap space,  or exceeded your kernel's SHMALL parameter.  
You can either  reduce the request size or reconfigure the kernel with larger SHMALL.

使用free命令查看内存使用情况,发现shared内存的确占用了很大一部分。

# free -g
              total        used        free      shared  buff/cache   available
Mem:             31           1           2          23         27         2
Swap:             3           3           0

使用ipcs命令进一步查看共享内存的使用情况,发现存在大量不再被进程使用但未回收的共享内存,即nattch为0的部分。

[root@pekpeuler00671 script]# ipcs -m

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status
0x00000000 65536      gnome-init 777        16384      1          dest
0x00000000 131073     gnome-init 777        16384      1          dest
0x00000000 163842     gnome-init 777        3145728    2          dest
0x00000000 393219     gnome-init 600        524288     2          dest
0x00000000 425988     gnome-init 600        4194304    2          dest
0x00000000 458757     gnome-init 777        3145728    2          dest
0x00f42401 3604486    1001       600        4455342080 0
0x00f42402 14123015   1003       600        4457177088 0
0x00f42403 23592968   1005       600        4457177088 0
0x00f42404 33062921   1007       600        4457177088 0
0x00f42405 42532874   1009       600        4457177088 0
0x00f42406 52002827   1011       600        4457177088 0
0x00f42407 61472780   1013       600        4457177088 0
0x00f42408 70942733   1015       600        4457177088 0
0x00f42409 80412686   1017       600        4457177088 0
0x00f4240a 89882639   1019       600        4457177088 0
0x00f4240b 99352592   1021       600        4457177088 0
0x00f4240c 108822545  1023       600        4457177088 0
0x00f4240d 118292498  1025       600        4457177088 0
0x00f4240e 127762451  1027       600        4457177088 0
0x00f4240f 136904724  1029       600        4455342080 0
0x00f42410 146374677  1031       600        4457177088 0
0x00f42411 155844630  1033       600        4457177088 0
0x00f42412 165314583  1035       600        4457177088 0
0x00f42413 174784536  1037       600        4457177088 0

经过定位,这部分内存是由于使用kill -9命令来退出数据库进程,导致没有调用IpcMemoryDelete函数来清理共享内存,造成了内存泄漏。

处理方法

使用ipcrm释放无属主的共享内存,例如,释放shmid为3604486的共享内存,命令如下所示。

ipcrm -m shid3604486

详情参考 https://www.interdb.jp/pg/pgsql08.html