postgresql , linux , oom , sql审计 , palloc , rss
数据库是比较重内存的应用软件之一,比如排序、聚合、使用较大的列、使用很长的sql(值sql本身的内容长度),或者传入很大的变长类型值时,都可能使得单个连接就会消耗很大的内存。
而另一方面,每个连接会消耗一定的内存,比如syscache , relcache,随着访问的对象变多,如果是长连接,消耗的内存也会越多。 通常来说,长连接当访问了很多元数据时,可能占用几十mb到上百mb不等。
当使用了cgroup来限制数据库实例的总内存时,随着数据库占用的rss部分的内存越来越多,如果数据库连接在申请内存时,超出了cgroup的限制,则连接可能被oom掉。
当然,即使不使用cgroup,linux也会根据内核的配置,以及用户申请内存的动作,当时的剩余内存等情况综合,发生oom。
有几篇文章可以参考一下
<a href="https://github.com/digoal/blog/blob/master/201606/20160608_01.md">《精确度量linux下进程占用多少内存的方法》</a>
<a href="https://github.com/digoal/blog/blob/master/201608/20160824_01.md">《一个笛卡尔积的update from语句引发的(内存泄露?)问题》</a>
<a href="https://github.com/digoal/blog/blob/master/201607/20160709_01.md">《postgresql relcache在长连接应用中的内存霸占"坑"》</a>
<a href="https://github.com/digoal/blog/blob/master/201612/20161221_01.md">《linux page allocation failure 的问题处理 - lowmem_reserve_ratio》</a>
当发生了oom后,如何找到引起oom的那个会话,他是罪魁祸首(指单个会话申请了过多的内存),还是压死骆驼的最后一根稻草(指连接数过多)呢?
数据库又是如何申请内存的呢?
简单讲一下rss和cache.
rss是程序申请的内存,不能被内核自动释放,由用户自己来管理它什么时候被释放。
cache,通常指缓存,比如文件系统的缓存,不由用户进程来管理,它可以被内核释放。
我们所指的内存不足,是指即使cache完全被释放,也无法分配足够的内存给用户请求。
在cgroup的memory子系统中,我们可以看到这两个部分,rss很多,cache很少时,就要注意了,可能会发生oom。
以postgresql数据库为例,为了管理方便,pg使用统一的内存分配和释放api,便于管理,详见src/backend/utils/mmgr/mcxt.c。
用得比较多的比如palloc,它申请的内存某些时候可以自动被回收,比如事务结束,会话断开,query结束时,使用palloc申请的某些内存,会自动被释放。
还有一些内存分配接口详见src/backend/utils/mmgr/mcxt.c。
处理单个值时,内存限制。单条sql的内存限制也是1gb。
用户可以根据mem context存储内存分配和释放的方法,应对不同的使用场景,这样保证了接口依旧是palloc。
我们可以在数据库的类型处理中,大量的用到了palloc,同时也可以很清晰的了解,每一种类型,在使用是需要消耗多少内存。
cd postgresql-9.6.1/src/backend/utils
比如字符串,数组,全文检索,这些属于变长类型,最长允许1gb,在申请内存时,根据被处理的值的实际的大小申请。
为了方便在一台物理机中启动多个实例,隔离资源,使用cgroup是一个很不错的方法。
把实例进程号写入tasks即可。
为了测试方便,我这里限制了100mb, 并且只将测试会话的backend process加入tasks
查看当前状态
因为限制了100mb,所以这个数据库backend process需要申请超过100mb的内存,才会被oom。
根据前面讲的,排序、聚合、使用较大的列、使用很长的sql(值sql本身的内容长度),或者传入很大的变长类型值时,都可能使得单个连接就会消耗很大的内存。
注意每次被oom后,重连,并将新的backend pid写入cgroup的tasks再测试下一轮.
1. 以使用较大的列为例,将1亿个值,聚合为一个数组,由于数组属于变长类型,最长可以放1gb,1亿已经超过1gb了,所以触发了oom。
2. 对较大的表排序,并且设置较大的work_mem
使用较小的work_mem不会被oom,因为使用了临时文件。
3. autovacuum worker进程启动后,单个worker进程可能需要申请的内存大小为maintenance_work_mem或者vacuum_work_mem。
4. 并行query
5. 带有多个hash join,多个排序操作的复杂query,可能消耗多份work_mem。
这种操作不需要很多内存:
比如查询了一张很大的表,返回了大批量(比如一亿)记录,即使不使用流式接收,也不需要很多内存。
终于说到审计了,没错,当oom发生后,我们怎么找到压死骆驼的最后一根稻草,或者是罪魁祸首呢?
由于oom发的是kill -9的信号,被kill的进程根本无法通过捕获信号来记录当时正在执行的query或者当时的状态。
那么审计就很有用了。
有这么几个参数
显然,如果我们需要在oom后,还能找到被oom进程当时执行query的蛛丝马迹,方法1:在请求时就记录下它在执行什么(开启log_statement='all'),方法2:记录detail字段,postmaster进程会收集这部分信息,不管什么方法,超过track_activity_query_size长度的query都被截断。
例如开启log_statement='all';后,我们能在日志中看到这样的信息。