一次ngx_slab内存泄露问题
1、问题
根据现场描述,隔几天就会出现nginx无法访问的情况,场景为正常使用状态,出现异常时,日志报大量 ngx_slab_free() : chunk is already free的错误,出现core dump,进程会不端重新拉起,不断core。此情况下,需要重启nginx才能恢复正常。
现场使用了访问控制功能,其中配置了几百条对客户端ip的访问控制规则。
2、现象分析
1、调试core,首先发现core的原因是释放slab内存块时,重复释放了一块内存,ngx_slab_free_locked(shpool,f->k);关键问题是在释放f->k时,没有保护f->k为NULL的场景。但是问题是,为什么会重复释放?其他那个地方已经释放了这块内存,为什么还会走到这块逻辑,或者说这块内存一直为NULL?
2、调用释放共享内存的函数的地方只有2处
- 启动阶段
- 更新访问控制模块的动态配置时
因此需要着重排查这2块的问题
3、复现过程
考虑到现场配置了几百条访问控制规则,于是测试环境配置了1000多条,在重复更新动态配置后,可以复现该问题,nginx出现core,进程会不断被拉起,又崩溃,循环这个过程。在这种异常发生时,nginx不能正常工作,与现场的现象一致。
可以确认是内存导致的异常,只有重启才能恢复。复现的关键要素是:
1、访问控制的规则数据量大
2、重复调用动态配置更新
4、代码排查
4.1、初步结论
nginx是多进程,实现访问控制、限流功能时,进程需要共享数据,比如一个uri的多笔请求,可能由多个进程处理,此时进程需要判断这个uri是否达到了限制次数,因此访问控制模块和限流模块使用共享内存进行进程间的通信,slab共享内存。
同时,异常时,日志中会出现ngx_slab_alloc() failed:no memory,因此怀疑是共享内存溢出了。目前访问控制模块的slab共享内存大小只有1M,当数据量过大时,1M的内存池会很快用完,后续访问控制模块在处理ip的规则时,会为每一个ip规则申请内存,内存池满时内存申请会失败,但是并不会直接阻断代码逻辑。
而每次更新配置时,会先清除访问控制模块的内存,因为上次申请内存时没有得到有效的地址,因此在释放时,释放了未知地址,导致core。当work core时,master会尝试拉起一个新的work,初始化访问控制模块时,会初始化共享内存,又会尝试释放上一次的内存,又会导致core,不断循环。
本质是共享内存用完了,导致模块申请内存时失败,后续又释放了该随机地址,导致core dump。
4.2、内存泄露
疑问是,1M的内存,为什么多调用几次就不够用了?所以我怀疑有内存泄漏,如果真的是内存泄露,那么增大多少内存都没有用,因为不重启的情况下,总有一天内存又会满。
因此需要继续排查,Tengine有一个非常好用的模块,ngx_slab_stat,这个模块的作用与火焰图的ngx-shm类似,可以展示nginx使用的每块共享内存的具体情况(模块地址:https://www.bookstack.cn/read/nginx-official-doc/29.md) 。简单编译使用后,我发现确实有内存泄露,具体测试数据如下:
调用次数/次 | 调用接口 | 访问控制规则条数/条 | 共享内存内存剩余/KB |
---|---|---|---|
1 | xxx | 823 | 896 |
2 | xxx | 823 | 884 |
3 | xxx | 823 | 872 |
4 | xxx | 823 | 860 |
5 | xxx | 823 | 844 |
1、第一次调用
2、第二次
3、第三次
4、第四次
5、第五次
823条的访问控制数据,每次会泄露12KB左右的内存,所以内存使用确实是有问题的。
4.3、代码定位
slab内存申请主要是2个函数ngx_slab_calloc与ngx_slab_calloc_locked,前者也是调用后者,只不过上了锁。
访问控制模块申请内存的地方只有3处,但是不确定是哪一处,所以这里取巧一下,这3处申请内存时,分别乘以3、6、9,然后再看泄露的内存是12KB的几倍,实际测试为6倍,因此很快就定位了代码
最后发现是ip->data申请的内存没有释放,ip是一个ngx_str_t结构体
1 |
|
ip本身和data都申请了内存
1 |
|
但是释放时,只释放了ip本身,并没有释放ip->data,因此每次都会有内存泄露。
因此修复代码缺陷,正确释放ip->data,再进行测试,发现没有了内存泄露问题,可用内存一直处于固定值。
5、总结
这个问题很有意思,借着这次的问题对slab有了一些了解,看了陶辉的《深入理解Nginx模块开发与架构解析》第16章感觉受益匪浅,然后简单看了下linux关于slab的原理,整体有一定的理解,具体总结在另外一篇文章,会持续更新。
这次还使用了火焰图辅助排查,章亦春的2个脚本,不过只有ngx-shm执行成功了,不过效果不如Tengine的ngx_slab_stat,这个确实很好很好的模块,后续我会编译到我们的产品中去,而且我发现Tengine还有一些有意思的模块,后续可以研究一下。
其实看了陶辉的《深入理解Nginx模块开发与架构解析》第16章,ngx_slab_stat的实现原理就很容易理解,后续有时间可以写一篇分析文章。
最后找到问题,其实发现是c语言指针、内存的问题,c语言确实容易出现这个问题。