前段时间,在服务器的监控后台发现,在线人数上去后 skynet 进程内存成倍升高,占系统 40% ~ 50 %,但人数回落到 1/3 后,内存使用并没有明显减少,仍有 40 % 左右。
服务器、skynet相关版本信息如下:
linux version 4.18.0
skynet v1.1.0
lua 5.3.4
jemalloc 5.0.1-0
我在本地压测时,却没有发现这个情况。在线人数上去,内存也是一样增加,但人数下来后,内存也逐渐释放了。项目代码是一样的,唯一不同的是,本地压测使用的系统是 CentOS 7 (Linux version 3.10.0), 我换了 CentOS 8 (Linux version 4.18.0) 压测后,发现跟线上的问题一样。
我开始思考:
难道这两个系统内存的申请、释放策略不同,我对比了两个系统的内核参数,内存相关的参数几乎都是一样的,或者说,都改一样了,结果还是不变;
难道新版本的 linux 做了改进,在系统内存够用的情况下,避免立即回收内存,减少进程缺页中断。但是,不用 skynet,我另外写的测试例子不会这样;
难道 jemalloc (skynet 内存管理模块)做了这件事情,但 CentOS 7 测试没有这个情况,应该不是 jemalloc 单方面优化的结果。
...
我带着各种疑问,接着研究 jemalloc ,发现他确实做了相关的优化,避免频繁系统调用申请释放内存,内存也会延迟释放。
我对比了两个系统 jemalloc 的编译日志,有一个参数引起了我的注意:
Linux version 3.10.0下:
je_cv_madv_free=no
Linux version 4.18.0下:
je_cv_madv_free=yes
以上参数,当 je_cv_madv_free=yes 时,JEMALLOC_PURGE_MADVISE_FREE 会被定义
// pages.h #if defined(_WIN32) || defined(JEMALLOC_PURGE_MADVISE_FREE) # define PAGES_CAN_PURGE_LAZY #endif static const bool pages_can_purge_lazy = #ifdef PAGES_CAN_PURGE_LAZY true #else false #endif
当 JEMALLOC_PURGE_MADVISE_FREE 有定义时, pages_can_purge_lazy 值为 true
// pages.c bool pages_purge_lazy(void *addr, size_t size) { if (!pages_can_purge_lazy) { return true; } #ifdef _WIN32 VirtualAlloc(addr, size, MEM_RESET, PAGE_READWRITE); return false; #elif defined(JEMALLOC_PURGE_MADVISE_FREE) return (madvise(addr, size, MADV_FREE) != 0); #elif defined(JEMALLOC_PURGE_MADVISE_DONTNEED) && \ !defined(JEMALLOC_PURGE_MADVISE_DONTNEED_ZEROS) return (madvise(addr, size, MADV_DONTNEED) != 0); #endif }
就是说,linux 下, je_cv_madv_free 为 yes 时,pages_purge_lazy 会调用 madvise 函数,优先尝试 MADV_FREE 参数
madvise 函数,进程可以告诉内核将如何使用自己的内存,内核根据这些提示优化内存管理,以实现更好的整体性能。 madvise 文档点这里
通过 madvise(addr, length, advise) 的 advise 参数,可以告诉内核如何处理从 addr 开始的 length 字节。
MADV_DONTNEED:不再需要的页面,建议 linux 立即回收
MADV_FREE:不再需要的页面,建议 linux 在需要时回收(linux 4.5 新增的)
所以,调用 madvise(addr, length, MADV_FREE) 后,在系统内存足够的情况下,进程内存(RSS)不会马上释放。带来的好处是,如果在内存被释放前有写入,那么内核将取消释放操作,减少内存频繁申请、释放,有效减少缺页中断、页面分配、页面清零等。
注:RSS 是常驻内存集(Resident Set Size)的缩写,是进程实际占用的物理内存大小, ps 命令显示 RSS,top 命令显示 RES
我在 CentOS 8 (Linux version 4.18.0) 下,如下重新编译 jemalloc
echo "je_cv_madv_free=no" > config.cache ./autogen.sh -C --with-jemalloc-prefix=je_ --disable-valgrind make
编译后测试,内存很快就被回收了。
结束语
我觉得 MADV_FREE 是一把双刃剑,MADV_FREE 可以有效避免内存反复申请释放,减少缺页中断,但也造成了内存不及时释放,看起来高占用。
存在以下两个问题:
1. 假设系统进程A MADV_FREE 占了大量内存,这时候有进程B 需要使用大量内存,进程B 缺页中断过程还要先等进程A MADV_FREE 的大内存释放后,才能分配给进程B
2. MADV_FREE 操作是由内核态完成的,什么时候操作用户态很难感知,且不能准确控制,所以很难估算进程实际使用的内存,特别进程如果有内存泄漏,会变得更难发现。
参考:
https://man7.org/linux/man-pages/man2/madvise.2.html
https://kernelnewbies.org/Linux_4.5
老大又更新了,赞