PostgreSQL使用的内存去向

1.前言

近期遇到了好几例与PostgreSQL有关的内存溢出、内存使用过大等问题,所以特意找了一些资料,整理以备后用。


2.内存都去哪了

这个问题会在不同地方出现:PostgreSQL使用了太多的内存,为什么?该如何缓解?
在我们进行"优化"之前,我们应该了解问题所在,包括ps和top在内的两个标准工具都在说谎。
我的本地有一个非常简单的PostgreSQL实例,配置了4 GB的shared_buffers和100MB的work_mem。我做了一些工作,现在ps显示如下:

top显示的数据基本相同,这是可疑的,因为"free"显示

即仅使用了1.5GB的内存(并且有大约10GB的内存用作磁盘缓存,如果有任何应用需要更多的内存,可以随时释放它们)。
那么,为什么会在ps中看到如此庞大的数字呢?
首先,我们需要忽略VSZ列。重要的是RSS。但它也不是很有用:

可以看到此刻计算出来的总内存是15GB,比总内存更大。那么,真实的内存使用情况是什么呢?如果我杀了PostgreSQL的进程,我能获得多少内存?

幸运的是,我们可以从/proc目录下的相关文件查看内存的确切用途。

Linux上的每个进程在/ proc下都有一个目录。在此目录中,有许多文件和目录。不要被文件大小所迷惑,它们都是"0"字节,但是它们确实包含信息,这是不可思议的。

我们感兴趣的一个文件是"smaps"。

它的内容看起来像这样:

该进程的smaps展示了内存消耗,里有超过2000多行,所以没有完全展示它。然而无论怎么样,根据ps显示,进程27713使用了4285716 KB的内存。那么,它有多大?快速grep,我们看到:

只有一个区域的大小超过100MB,它的大小非常接近进程的总大小。
完整信息如下:

这些信息大部分比较神秘,但我们看到了如下几点:

1)它是共享内存(第一行包含rw-s,其中"s"代表共享)

2)通过(/SYSV… deleted),看起来共享内存是使用mmaping删除文件来完成的,因此在free的输出中,这些内存将在Cached中,而不是Used列。

3)共享块的大小为4317224,而共享块中的4280924实际上驻留在内存中

没关系,那是shared_buffers。但事实是大多数后端进程都会使用shared_buffers。而且,更糟的是,并不总是相同的程度。例如,进程27722相同的shared_buffers的数据:

在这里,我们看到该进程只请求/使用了388MB的内存。

因此计算将很复杂。例如,我们可能有两个进程,每个进程使用了400MB的shared buffers,但是它没有告诉我们它实际使用了多少内存,可能他们正在使用100MB的相同缓冲区,以及300MB的不同缓冲区,因此总的内存使用量将为700MB。

我们知道此shared_buffers块的总大小为4317224KB。这太好了。但是其他事情呢?例如库——它们可以由内核在多个进程之间共享。

幸运的是,在2007年,Fengguang Wu发送了(以前写过)一个非常酷的内核补丁——在smaps中添加了"Pss"信息。

基本上,Pss最多为Rss,但是如果多个进程使用相同的内存页,则Pss也会减少。这就是为什么上面的Pss远远低于Rss / Size的原因。例如在最后一个例子中。Rss是388652,而Pss只有95756,这意味着这个后端进程使用的大多数页面也被其他3个后端进程使用。

因此,现在知道了Pss之后,我们终于可以了解正在运行的PostgreSQL集群的实际内存使用情况:

如果你只是说"天啊,他跑了什么?",让我解释一下。第一个命令:

只返回pgdba用户的pid(通常是postgres,但我不同,是以pgdba的身份运行PostgreSQL)
第二,使用sed将pids更换为smaps文件的路径:

然后对sed给出的文件中包含 ^Pss的行执行简单的grep。它返回很多行,如下:

然后使用awk统计第二列(也就是size),最后得到了4329040KB。

因此理论上,如果我停止PostgreSQL,将回收这么多内存。让我们看看这是不是真的:

使用的内存从12145424下降到7781960,这意味着我得到了4363464 kB的内存。这甚至比预期的4329040KB略高,但已经足够接近了。正如预期的那样,它大部分来自磁盘缓存,因为它用于shared buffers。
这很不错,但是这个方法可以用来估算杀死单个后端进程可以回收的内存吗?可以说是也可以说不是。关闭整个PostgreSQL实例意味着正在使用的共享内存可以被释放。在正常环境中,当您终止后端进程时,最终只能释放该后端的私有内存。这个数字通常很低。
例如,在另一台机器上,有更令人印象深刻的硬件环境:

也就是说,该进程有1.7GB的RSS(在ps输出中可见),但是只有52MB是私有内存,如果它被杀死,私有内存将被释放。
所以在这儿你不能使用Pss,但你可以使用来自smaps的Private_*数据来获取私有内存。总而言之,PostgreSQL使用的内存比乍一看要少得多,虽然可以得到非常准确的数字,但需要执行一些shell脚本来获得它们。

3.小结

Linux的内存管理确实十分复杂,PostgreSQL本身基于共享内存,同时还有double buffer,再加上local memory,要得到精确的数字比较困难。

这张图可以大概知道涉及到了哪些内存,包括pagetables、slab(内核为了提高性能每个需要重复使用的对象都会有个池,这个slab池会cache大量常用的对象,所以会消耗大量的内存)以及进程本身的消耗:

3.1.何为smaps

/proc/PID/smaps文件是基于/proc/PID/maps的扩展,他展示了一个进程的内存消耗,比同一目录下的maps文件更为详细。


上面看到的就是VMA,每一个VMA(虚拟内存区域,即一个vm_area_struct结构指向的内存区域)都有如下的一系列数据:

3.2.混淆的内存指标

另外,对于ps和top等命令看到的RSS、PSS、USS等相关概念容易混淆,特意整理了一下。


  • SIZE

Size:虚拟内存空间大小。但是这个内存值不一定是物理内存实际分配的大小,因为在用户态上,虚拟内存总是延迟分配的。这个值计算也非常简单,就是该VMA的开始位置减结束位置。
延迟分配就是当进程申请内存的时候,Linux会给他先分配页,但是并不会区建立页与页框的映射关系,意思就是说并不会分配物理内存,而当真正使用的时候,就会产生一个缺页异常,硬件跳转page fault处理程序执行,在其中分配物理内存,然后修改页表(创建页表项)。异常处理完毕,返回程序用户态,继续执行。

  • VSS

VSS : Virtual Set Size 虚拟耗用内存(包含共享库占用的内存),即单个进程全部可访问的地址空间,其大小可能包括还尚未在内存中驻留的部分。对于确定单个进程实际内存使用大小,VSS用处不大。


  • RSS

RSS(Resident set size),使用top命令可以查询到,是最常用的内存指标,即单个进程实际占用的内存大小,表示进程占用的物理内存大小。但是,将各进程的RSS值相加,通常会超出整个系统的内存消耗,这是因为RSS中包含了各进程间共享的内存。RSS不太准确的地方在于它包括该进程所使用共享库全部内存大小。对于一个共享库,可能被多个进程使用,实际该共享库只会被装入内存一次。
RSS的计算方式如下:Rss=Shared_Clean+Shared_Dirty+Private_Clean+Private_Dirty
share/private:该页面是共享还是私有。
dirty/clean:该页面是否被修改过,如果修改过(dirty),在页面被淘汰的时候,就会把该脏页面回写到交换分区(换出,swap out)。有一个标志位用于表示页面是否dirty。
share/private_dirty/clean计算逻辑:查看该page的引用数,如果引用>1,则归为shared,如果是1,则归为private,同时也查看该page的flag,是否标记为_PAGE_DIRTY,如果不是,则认为干净的。


  • PSS

PSS(Proportional set size)所有使用某共享库的程序均分该共享库占用的内存时,每个进程占用的内存。显然所有进程的PSS之和就是系统的内存使用量。它会更准确一些,它将共享内存的大小进行平均后,再分摊到各进程上去。
实际上包含下面private_clean+private_dirty,和按比例均分的shared_clean、shared_dirty。
举个计算Pss的例子:
如果进程A有x个private_clean页面,有y个private_dirty页面,有z个shared_clean仅和进程B共享,有h个shared_dirty页面和进程B、C共享。那么进程A的Pss为:
x + y + z/2 + h/3


  • USS

USS(Unique set size )进程独自占用的物理内存(不包含共享库占用的内存)即单个进程私有的内存大小,即该进程独占的内存部分。USS揭示了运行一个特定进程在的真实内存增量大小。如果进程终止,USS就是实际被返还给系统的内存大小。


3.3.常用的统计命令

1)使用ps命令找出占用内存资源最多的20个进程:
ps aux | head -1;ps aux |grep -v PID |sort -rn -k +4 | head -20
ps -aux --sort -pmem | head -n 20

2)查看进程占用的实际物理内存,这个也包含了共享库等:

ps -eo size,pid,user,command --sort -size | awk '{ hr=$1/1024 ; printf("%13.2f Mb ",hr) } { for ( x=4 ; x<=NF ; x++ ) { printf("%s ",$x) } print "" }' |cut -d "" -f2 | cut -d "-" -f1

3)通过python脚本观察私有内存和共享内存:
python ps_mem.py -p 999
脚本位置在https://raw.githubusercontent.com/pixelb/ps_mem/master/ps_mem.py,直接cp即可

4)通过smaps观察私有内存,这个和python脚本得出的数字差不多,大约为5.7MB
ps uxf -p 999 | sort -nk6 | tail -n 1 | tee >( cat - >&2) | awk '{system("cat /proc/"$2"/smaps")}' | grep ^Private | awk '{A+=$2} END{print A}'

5)通过smem命令观察,这个得出来的USS和python脚本以及smaps差不多,此例都约为6.1MB
smem -k | grep -w '/usr/pgsql-13/bin/postgres'

6)追查内存使用情况,用到脚本:

注:前文译自How much RAM is PostgreSQL using?

参考:linux中top命令 VSS,RSS,PSS,USS 四个内存字段的解读。

参考:Linux内存管理 -- /proc/{pid}/smaps讲解



请使用浏览器的分享功能分享到微信等