nginx内存占用高—内存池使用思考

问题现象

nginx top 进程 虚拟内存 200G 实际内存5G 和 CDN 平台相比要高很多

排查思路
  1. 使用pmap -p 进程号,发现从系统角度确实 有分配几百G,但是实际内存5G 说明分配的大部分内存没有实际使用,且不是内存泄漏,属于内存碎片问题。
  2. 由于存在陡增现象,怀疑是否与某些特殊请求有关,post 大文件?过滤访问日志,同时间段两日对比没有找到特别的请求。由于最近出现,怀疑是否与最近的XXXX_line_buffer_size 修改增大有关。过滤配置修改时间和zabbix 内存统计趋势,没有严格匹配到。
  3. 使用工具systemtap (https://github.com/zengxiaobai/systemtap-scripts)排查是否存在内存泄漏,没有存在泄漏,但是有时候发现下面的未释放栈内存泄漏。但是从代码角度看内存都是从nginx r->pool 申请且有在请求结束时释放,说明可能请求没有结束,r->pool 一直占用着,说明当回源较慢并发较高时可能造成内存陡增现象,且与XXXX_line_buffer_size 200K 有关。由于stap 是明确 malloc -free,r->pool 又有在请求结束时释放,说明 该现象还不是内存碎片,需要再追踪一次,再次追踪发现陡增时malloc - free == 0, 确实有释放,属于内存碎片问题
  4. 使用工具systemtap 排查分配TOP的函数栈, 发现是改写模块分配内存太多。
bytes(not free yet): 138598400
value |-------------------------------------------------- count
8192 | 0
16384 | 0
32768 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 2707
65536 | 0
131072 | 0

ngx_alloc+0xf [nginx]
ngx_palloc_large+0x13 [nginx]
ngx_palloc+0x42 [nginx]
ngx_create_temp_buf+0x27 [nginx]
ngx_http_subs_match+0xa99 [nginx]
ngx_http_subs_body_filter+0x40d [nginx]
ngx_http_pagerewrite_body_filter+0x88 [nginx]
ngx_http_sub_output+0xb0 [nginx]
ngx_http_sub_body_filter+0xb21 [nginx]
ngx_http_gunzip_body_filter+0x765 [nginx]
ngx_http_trailers_filter+0x5f [nginx]
ngx_http_lua_capture_body_filter+0x84 [nginx]
ngx_output_chain+0x8a4 [nginx]
ngx_http_copy_filter+0x191 [nginx]

分析代码,发现响应内容只要匹配到满足要求的正则,就会申请一块XXXX_line_buffer_size 大小 设置为200K(临时解决线上core时设置),并进行内容处理,处理完再copy 到 响应内容dst。由于配置200K, 无法使用nginx 默认pool 大小 4K ,也就是说 存在 一个响应内容 频繁申请 且申请都是非内存池的现象,易导致内存碎片问题。

结合 squid 的内存池设计和 nginx 的内存池设计,进行对比

squid 针对每个请求过程中 不同结构体 申请一块内存对象,并入内存池栈中,内存结构体对象 在释放后,只会把内容清零,不归还给系统,且该空间可以给任意请求,只要申请这个结构体对象就可以使用。

squid 内存池虽然可以复用请求过程中的结构体对象,但是零碎分配,从0 开始增长到n,且不同请求任意穿插 ,这中间可能会存在内存碎片,

nginx 内存池在请求进入时申请一块较大空间(4K),后续请求过程需要申请的内存都从这个4K 的大池子申请

,请求结束时 释放整块4K 大小空间,只要不超过这个4K 大小,并发请求申请的内存极有可能是连续的,有利于glibc 的内存合并机制。

nginx 机制,没有结构体对象概念,不同请求不能复用空间,但是有利于内存合并。

缺点:并发较高 pool size 较大时,占用内存可能较多。且一旦分配大小一次性超过4K. nginx 是另外分配一个空间挂到pool中,在请求结束时一起释放,所以凡是分配超过pool 大小的内存,就违背了nginx pool 设计规则。内存问题会尤为显著,增加内存占用时间,且碎片率较高。一次性分配100 K 和10次分配10K 字节所带来的效果不同,假设pool 中有50K 剩余,一次性分配100K 无法利用50K 而需要再申请100K, 而10次分配10K 只需要再申请 50K

调优
  1. 优化 XXXX_line_buffer_size 页面改写处理逻辑,避免多次拷贝分配
  2. 流处理 避免大块处理,将js 内容 从> 分隔换成 \n 换行分隔,将大块内存一次性分配换成多个小块处理
  3. pool 内避免一次性分配大块内存,按需分配
  4. 结合具体业务场景可以适当调高poolsize
  5. 使用jemalloc 等
  6. 使用malloc_trim 进行紧缩

总结

结合 squid 的内存池设计和 nginx 的内存池设计,进行对比 ,我们可以发现其设计很像 coss 缓存机制和aufs 缓存机制的区别:

coss 存储机制 在一定程度上根据请求业务的时间局部性可以使得一个页面相关元素的缓存位置访问能够空间和时间局部相邻,相同类型的元素过期时间也一一般一致。良好的局部连续性和复用是系统调优设计的关键。

后续测试

ps -o maj_flt -o min_flt -p pid
旧版本访问2000 次内存页错误:2134330,随请求增加增加
新版本访问2000 次内存页错误:567849,随请求增加增加
关闭 subs_line_buffer_size 500k 0 配置访问2000次内存页错误: 25528 后续不随请求数增长而增长,nginx 中 较大的内存分配更容易产生碎片问题