在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》