简介
shared buffers是数据库内存共享缓冲区。
当Postgres想要从磁盘获取数据(page)时,先搜索shared_buffers,确认该page是否在shared_buffers中,如果存在,则直接命中,返回缓存的数据以避免I/O。如果不存在,再到OS缓存查找,最后才会通过I/O访问disk获取数据。
结构
PostgreSQL 缓存管理器分成三层结构:缓冲表(buffer table)、缓冲区描述符(buffer descriptors)和缓冲池(buffer pool)
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。
读数据顺序
- 访问请求:当数据库需要读取一个页面时,首先会检查这个页面是否已经在缓冲池中。
- 查询缓冲表:通过buffer_tag在缓冲表中查找对应的buffer_id。
- 获取描述符:如果找到,则根据buffer_id在缓冲区描述符层中获取对应的描述符。
- 读取数据:根据描述符中的信息,直接从缓冲池对应的插槽中读取数据。
- 未命中处理:如果缓冲表中未找到对应的buffer_id,则说明页面不在缓存中,需要从磁盘读取页面到缓冲池,并更新缓冲表和描述符。
写数据顺序
- 修改数据:当数据库需要修改一个页面时,首先会在缓冲池中找到对应的页面。
- 修改缓存:对页面进行修改,并设置描述符中的脏位(dirty bit)为1,表示该页面已被修改但尚未写入磁盘。
- 后台写入:后台写进程(如bgwriter)会定期将脏页写入磁盘,或者在达到一定条件(如脏页数量过多)时触发写操作。
- 更新映射:写操作完成后,会更新缓冲表和描述符中的映射关系,确保数据的一致性。
锁
- 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)**算法
- nextVictimBuffer指向第一个描述符(buffer_id = 1);但因为该描述符被钉住了,所以跳过。
- nextVictimBuffer指向第二个描述符(buffer_id = 2)。该描述符未被钉住,但其usage_count为2;因此该描述符的usage_count将减1,而nextVictimBuffer迭代至第三个候选描述符。
- 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