skynet 高内存占用问题

前段时间,在服务器的监控后台发现,在线人数上去后 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

《skynet 高内存占用问题》上有1条评论

发表评论

邮箱地址不会被公开。 必填项已用*标注