PostgreSQL数据库共享内存——概览

何为共享内存

我们知道,PostgreSQL是多进程模型,进程间通信(IPC)的方式有很多种,比如管道、消息队列、信号量、Socket和共享内存等。

共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,传来传去,大大提高了进程间通信的速度。所以共享内存可以说是最有效的进程间通信方式,也是最快的IPC形式。假设有两个不同进程A、B,那么会有同一块物理内存被映射到进程A、B各自的进程地址空间。进程A可以即时看到进程B对共享内存中数据的更新,反之亦然。
在这里插入图片描述
PostgreSQL的shared memory总共支持3种:

  • sysv
  • windows
  • mmap
    dynamic shared memory 动态共享内存支持4种:
  • posix
  • sysv
  • windows
  • mmap

基础概念

Posix 表示可移植操作系统接口(Portable Operating System Interface ,缩写为 POSIX ),POSIX标准定义了操作系统应该为应用程序提供的接口标准,是IEEE为要在各种UNIX操作系统上运行的软件而定义的一系列API标准的总称,其正式称呼为IEEE 1003,而国际标准名称为ISO/IEC 9945。
System V,曾经也被称为 AT&T System V,是Unix操作系统众多版本中的一支。它最初由 AT&T 开发,在1983年第一次发布。一共发行了4个 System V 的主要版本:版本1、2、3 和 4。

Posix提供了两种共享内存的方法:

  • 内存映射文件(mmap)
  • 共享内存区对象

Linux提供了内存映射函数mmap, 它把文件内容映射到一段内存上(准确说是虚拟内存上,运行着进程), 通过对这段内存的读取和修改, 实现对文件的读取和修改。mmap()系统调用使得进程之间可以通过映射一个普通的文件实现共享内存。普通文件映射到进程地址空间后,进程可以像访问内存的方式对文件进行访问,不需要其他内核态的系统调用(read,write)去操作

POSIX接口的共享内存,底层最终都是调用的mmap,只是不同的是共享内存打开文件调用shm_open(),而内存映射调用open()。只是说mmap并不完全是为了共享内存来设计的,它本身提供了不同于一般对普通文件的访问的方式,进程可以像读写内存一样对普通文件进行操作,IPC的共享内存是纯粹为了共享。所以,共享内存在POSIX上一定程度上就是指的内存映射了。
在这里插入图片描述
对于共享内存区使用的API shmopen(),传入的参数叫shmname,宏中声明了shmname,构造出shmdir,shm_dir的内容就是/dev/shm/,之后通过和name拼接:
在这里插入图片描述
生成了shm_name。等等,/dev/shm是个什么鬼?
在这里插入图片描述
翻译一下:tmpfs是一个文件系统,可将所有文件保留在虚拟内存中。tmpfs中的所有内容都是临时的,因为没有文件会在硬盘上创建。如果您卸载tmpfs实例,存储在其中的所有东西都会丢失。由于tmpfs都存在于page cache和swap中,因此所有tmpfs当前在内存中的页面都会显示在cache里面。它不会作为shared或类似的东西出现。可以使用df(1)和du(1)来检查tmpfs真正使用的RAM + SWAP量。
原来是一个特殊的文件系统,看一下确实贺然写着tmpfs。
在这里插入图片描述
于是总结一下,Posix的两种共享内存区的区别在于共享的数据的载体(底层支撑对象)不一样:

  1. 内存映射文件的数据载体是物理文件
  2. 共享内存区对象,共享的数据载体是物理内存
  3. 另外共享内存和文件内存映射的接口、用法不一样,一个是open(),另一个是shm_open(),POSIX的共享内存实现会默认把共享内存文件放在/dev/shm/分区下,如果没有这个分区,需要手动挂载一下。

对比

System V的方式,两个进程都通过虚拟地址空间到page table,然后通过page table映射到物理内存的同一块内存区域,这样就可以实现IPC:
在这里插入图片描述
POSIX的方式,最终通过mmap,每个进程地址空间中开辟出一块空间进行映射:
在这里插入图片描述
在这里插入图片描述
MMAP:
在这里插入图片描述
shared buffer history
Prior to PostgreSQL 9.3, only System V shared memory was used, so the amount of System V shared memory required to start the server was much larger. If you are running an older version of the server, please consult the documentation for your server version.

PostgreSQL有自己的shared_buffer,在<=9.2时,PostgreSQL是基于SYSTEM V的API,SYSTEM V的接口要求设置SHMMAX大小,所以低版本会让我们设置一大堆shmmax、shmall等,十分麻烦。而>=9.3之后,PostgreSQL大多数基于POSIX的API实现。而System V共享内存,仅用于提供互锁来保护数据目录,量很少。

If you’re on posix system without mmap, you’d use posixshm. If you’re on a unix without posixshm you’d use sysv_shm. If you only need to share memory vs a parent/child you’d use mmap if available.

Shared_buffers又来了

玩PostgreSQL的,对这玩意应该是最常打交道的了,什么推荐25% ~ 40%,什么double buffer,这些都暂且不论。今天只看shared_buffers的底层实现方式。在postgresql.conf配置文件里写的很清楚,默认使用的方式是mmap:
在这里插入图片描述
在源码里面,主要是这个CreateAnonymousSegment->mmap,里面会用到mmap的方式。ptr = mmap(NULL, allocsize, PROTREAD | PROTWRITE,PGMMAPFLAGS | mmap_flags, -1, 0);
在这里插入图片描述
用pmap看一下postmaster的,只需关注zero(deleted)这一坨即可
在这里插入图片描述
其实/zero是/dev/zero,可以看到大小为146536 kB,这个其实就是sharedbuffers,你要说这个大小也对不上啊?注意实际申请占用的共享内存比sharedbuffers稍大是正常的,可以用gdb跟一下。

至于为什么是/dev/zero呢?这和mmap的实现方式有关,如果仅仅是父子进程之间的共享内存,第一种方式就是将 mmap 映射到 /dev/zero,因为 /dev/zero 是一个特殊文件,任何写都被忽略,并且一旦映射存储区,内容都将被初始化为 0。因为PostgreSQL就是通过postmaster来fork子进程服务的,所以理解了,/dev/zero就是PostgreSQL里面的shared buffers。
在这里插入图片描述
我们可以再看一下checkpoint和bgwriter的,可以看到,大小也是 146536 kB
在这里插入图片描述
提到shared_buffers,那么也不能避免讨论另一个话题了,在PostgreSQL里面,由于进程间基于shared memory通信,假如此时一个backend process异常断开,就比如常见的kill -9了某一个backend process,那么此时PostgreSQL大概率会crash,因为不幸把shared memory弄坏了,Postmaster只能进行修复工作。对应的日志如下:
在这里插入图片描述
所以在PostgreSQL里面不要用kill -9,用自带的pg_terminate_backend和pg_cancel_backend,就算kill,也用不带参数的kill,友好地退出。之前白大师分享过一篇文章《为啥不能KILL -9杀PostgreSQL进程》,引用一下:Oracle的后台进程pmon就是负责死进程和会话的清理工作的,当有任何一个进程出现问题异常死掉后,会由pmon做善后工作,释放相关的资源,并且做一些善后修复工作,PostgreSQL因为没有这些,所以要务必注意。

动态共享内存

前面说完shared_buffers,还有一个动态共享内存也绕不开,除非你玩的是9.6以前的版本。dynamic shared memory 在并行中会用到,对于并行查询而言,执行时创建的 worker 进程与 leader 进程通过共享内存实现数据交互。但这部分内存无法像普通的共享内存那样在系统启动时预先分配,毕竟直到真正执行时才知道有多少 worker 进程,以及需要分配多少内存。动态共享内存,即在执行时动态创建,用于 leader 与 worker 间通信,执行完成后释放。基于动态共享内存的队列用于进程间传递元组和错误消息。如下图,每个 worker 在动态共享内存中都有对应的元组队列和错误队列,worker 将执行结果放入队列中,leader 会从对应队列获取元组返回给上层算子。
在这里插入图片描述
Linux上面,只有posix、system v和mmap。创建和管理动态共享内存是操作系统的任务。创建完成后,会对动态共享内存段的“引用计数”reference-counted,这个其实和以前分享的sharedbuffers里面的buffer descriptors layer类似,里面包括usage和refcount,refcount保存当前访问相应页面的PostgreSQL进程数,也被成为“钉数”,当PostgreSQL进程访问相应页面时,其计数增加1,refcount ++,访问结束后引用计数减少1,refcount --,当refcount为0,即页面当前未被访问时,页面被取消“钉”,否则会被“钉”住,而usagecount保存着相应页面加载到相应缓冲池槽后的访问次数。

这样通过“引用计数”来判断,以便在删除最后一个映射后,该段就会自动消失。
在这里插入图片描述
动态共享内存,配置文件里也写的很清楚,默认是mmap
在这里插入图片描述
启动之后,会在pg_dynshmem子目录下有所体现:
在这里插入图片描述
关了之后就没了
在这里插入图片描述
重启之后,会重新分配
在这里插入图片描述
当改为POSIX类型时,如之前所述,会在/dev/shm目录下
在这里插入图片描述
The use of the mmap option, which is not the default on any platform, is generally discouraged because the operating system may write modified pages back to disk repeatedly, increasing system I/O load;,不是很推荐MMAP,因为会加剧IO的压力。另外,POSIX的性能优于MMAP。引用一下:
在这里插入图片描述
BenckmarkSQL的测试结果

ClientsPOSIX tpsMMAP tps
2529227
4860569
623081493
83773431418
106645367267

小结

前面总结了一下PostgreSQL共享内存的实现方式,以及种类,还有底层原理,相信读完可以进一步加深我们的理解。另外知道了这些,还得大概知道Linux下面内存的去向,主要有:

  • page 管理,page table
  • slab(kmalloc、内存池)
  • 用户态内存使用(malloc、relloc 文件映射、共享内存)
  • 程序的内存 map(栈、堆、code、data)
  • 内核和用户态的数据传递(copyfromuser、copytouser)
  • 内存映射(硬件寄存器、保留内存)
  • DMA 内存
    另外,昨日请教了一下大老板,随着系统的运行,每个backend process的/dev/zero的rss会越来越多,在监控内存使用时,要将每个backend process中的/dev/zero的rss部分去掉。

参考:https://postgreshelp.com/postgresql-dynamic-shared-memory-posix-vs-mmap/
https://leopard.in.ua/2013/09/05/postgresql-sessting-shared-memory#.YG7HVugzbZQ
https://www.geeksforgeeks.org/posix-shared-memory-api/
https://www.postgresql.org/docs/12/kernel-resources.html#SYSVIPC
http://rhaas.blogspot.com/2013/10/parallelism-progress.html

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值