在OOM现场 谈一谈数据库内存分配, 以及审计的重要性

标签

PostgreSQL , Linux , OOM , SQL审计 , palloc , rss


背景

数据库是比较重内存的应用软件之一,比如排序、聚合、使用较大的列、使用很长的SQL(值SQL本身的内容长度),或者传入很大的变长类型值时,都可能使得单个连接就会消耗很大的内存。

而另一方面,每个连接会消耗一定的内存,比如SYSCACHE , RELCACHE,随着访问的对象变多,如果是长连接,消耗的内存也会越多。 通常来说,长连接当访问了很多元数据时,可能占用几十MB到上百MB不等。

当使用了cgroup来限制数据库实例的总内存时,随着数据库占用的RSS部分的内存越来越多,如果数据库连接在申请内存时,超出了cgroup的限制,则连接可能被OOM掉。

当然,即使不使用cgroup,Linux也会根据内核的配置,以及用户申请内存的动作,当时的剩余内存等情况综合,发生OOM。

有几篇文章可以参考一下

《精确度量Linux下进程占用多少内存的方法》

《一个笛卡尔积的update from语句引发的(内存泄露?)问题》

《PostgreSQL relcache在长连接应用中的内存霸占"坑"》

《Linux page allocation failure 的问题处理 - lowmem_reserve_ratio》

当发生了OOM后,如何找到引起OOM的那个会话,他是罪魁祸首(指单个会话申请了过多的内存),还是压死骆驼的最后一根稻草(指连接数过多)呢?

数据库又是如何申请内存的呢?

rss和cache

简单讲一下rss和cache.

rss是程序申请的内存,不能被内核自动释放,由用户自己来管理它什么时候被释放。

cache,通常指缓存,比如文件系统的缓存,不由用户进程来管理,它可以被内核释放。

我们所指的内存不足,是指即使cache完全被释放,也无法分配足够的内存给用户请求。

在cgroup的memory子系统中,我们可以看到这两个部分,rss很多,cache很少时,就要注意了,可能会发生OOM。

# cat memory.stat

数据库申请内存举例

以PostgreSQL数据库为例,为了管理方便,pg使用统一的内存分配和释放API,便于管理,详见src/backend/utils/mmgr/mcxt.c。

用得比较多的比如palloc,它申请的内存某些时候可以自动被回收,比如事务结束,会话断开,QUERY结束时,使用palloc申请的某些内存,会自动被释放。

还有一些内存分配接口详见src/backend/utils/mmgr/mcxt.c。

* mcxt.c

处理单个值时,内存限制。单条SQL的内存限制也是1GB。

* memutils.h

用户可以根据mem context存储内存分配和释放的方法,应对不同的使用场景,这样保证了接口依旧是palloc。

* memnodes.h

我们可以在数据库的类型处理中,大量的用到了palloc,同时也可以很清晰的了解,每一种类型,在使用是需要消耗多少内存。

cd postgresql-9.6.1/src/backend/utils

$grep -r palloc *|less

比如字符串,数组,全文检索,这些属于变长类型,最长允许1GB,在申请内存时,根据被处理的值的实际的大小申请。

cgroup例子

为了方便在一台物理机中启动多个实例,隔离资源,使用cgroup是一个很不错的方法。

# mount -t cgroup -o cpu,memory cpu_and_mem /cgroup/memory

把实例进程号写入tasks即可。

为了测试方便,我这里限制了100MB, 并且只将测试会话的backend process加入tasks

postgres=# select pg_backend_pid();
# echo 85938 > tasks

查看当前状态

# cat memory.stat

模拟数据库进程被OOM

因为限制了100MB,所以这个数据库backend process需要申请超过100MB的内存,才会被OOM。

根据前面讲的,排序、聚合、使用较大的列、使用很长的SQL(值SQL本身的内容长度),或者传入很大的变长类型值时,都可能使得单个连接就会消耗很大的内存。

注意每次被OOM后,重连,并将新的BACKEND PID写入CGROUP的tasks再测试下一轮.

1. 以使用较大的列为例,将1亿个值,聚合为一个数组,由于数组属于变长类型,最长可以放1GB,1亿已经超过1GB了,所以触发了OOM。

postgres=# explain (analyze,verbose,timing,costs,buffers) select array_agg(id::text) from generate_series(1,10000000) t(id);

2. 对较大的表排序,并且设置较大的work_mem

postgres=# set work_mem ='101MB';

使用较小的work_mem不会被OOM,因为使用了临时文件。

postgres=# set work_mem ='10MB';

3. autovacuum worker进程启动后,单个WORKER进程可能需要申请的内存大小为maintenance_work_mem或者vacuum_work_mem。

4. 并行QUERY

5. 带有多个hash join,多个排序操作的复杂QUERY,可能消耗多份WORK_MEM。

这种操作不需要很多内存:

比如查询了一张很大的表,返回了大批量(比如一亿)记录,即使不使用流式接收,也不需要很多内存。

审计

终于说到审计了,没错,当OOM发生后,我们怎么找到压死骆驼的最后一根稻草,或者是罪魁祸首呢?

由于OOM发的是KILL -9的信号,被KILL的进程根本无法通过捕获信号来记录当时正在执行的QUERY或者当时的状态。

那么审计就很有用了。

有这么几个参数

postgres=# set log_statement='all'; // 在SQL请求时就写日志

显然,如果我们需要在OOM后,还能找到被OOM进程当时执行QUERY的蛛丝马迹,方法1:在请求时就记录下它在执行什么(开启log_statement='all'),方法2:记录detail字段,postmaster进程会收集这部分信息,不管什么方法,超过track_activity_query_size长度的QUERY都被截断。

例如开启log_statement='all';后,我们能在日志中看到这样的信息。

开启了log_statement='all';后,在客户端发起QUERY请求时的日志。

参考

《精确度量Linux下进程占用多少内存的方法》

《一个笛卡尔积的update from语句引发的(内存泄露?)问题》

《PostgreSQL relcache在长连接应用中的内存霸占"坑"》

《Linux page allocation failure 的问题处理 - lowmem_reserve_ratio》

相关推荐