此文章最初发布在IBM Developer。javascript
在基于容器的Node.js应用程序中管理内存的最佳实践html
在docker容器中运行Node.js应用程序时,传统的内存参数调整并不老是按预期工做。本文咱们将阐述在基于容器的Node.js应用程序内存参数调优中并不老是有效的缘由,并提供了在容器环境中使用Node.js应用程序时能够遵循的建议和最佳实践。java
当Node.js应用程序运行在设置了内存限制的容器中时(使用docker --memory
选项或者系统中的其余任意标志),请使用--max-old-space-size
选项以确保Node.js知道其内存限制而且设置其值小于容器限制。node
当Node.js应用程序在容器内运行时,将Node.js应用程序的峰值内存值设置为容器的内存容量(假如容器内存能够调整的话)。git
接下来让咱们更详细地探讨一下。github
默认状况下,容器是没有资源限制的,可使用系统(OS)容许的尽量多的可用内存资源。可是docker 运行命令能够指定选项,用于设置容器可使用的内存或CPU。docker
该docker-run
命令以下所示:docker run --memory <x><y> --interactive --tty <imagename> bash
shell
参数介绍:api
例如:docker run --memory 1000000b --interactive --tty <imagename> bash
将内存或CPU限制设置为1,000,000字节安全
要检查容器内的内存限制(以字节为单位),请使用如下命令:
cat /sys/fs/cgroup/memory/memory.limit_in_bytes
复制代码
接下来咱们一块儿来看下设置了--max_old_space_size
以后容器的各类表现。
“旧生代”是V8内存托管堆的公共堆部分(即JavaScript对象所在的位置),而且该--max-old-space-size
标志控制其最大大小。有关更多信息,请参阅关于-max-old-space-size。
一般,当应用程序使用的内存多于容器内存时,应用程序将终止。
如下示例应用程序以10毫秒的间隔插入记录到列表。这个快速的间隔使得堆无限制地增加,模拟内存泄漏。
'use strict';
const list = [];
setInterval(()=> {
const record = new MyRecord();
list.push(record);
},10);
function MyRecord() {
var x='hii';
this.name = x.repeat(10000000);
this.id = x.repeat(10000000);
this.account = x.repeat(10000000);
}
setInterval(()=> {
console.log(process.memoryUsage())
},100);
复制代码
本文全部的示例程序均可以在我推入Docker Hub的Docker映像中得到。你也能够拉取docker镜像并运行程序。使用docker pull ravali1906/dockermemory
来获取图像。
或者,你能够本身构建镜像,并使用内存限制运行镜像,以下所示:
docker run --memory 512m --interactive --tty ravali1906/dockermemory bash
复制代码
ravali1906/dockermemory
是镜像的名称
接下来,运行内存大于容器限制的应用程序:
$ node --max_old_space_size=1024 test-fatal-error.js
{ rss: 550498304,
heapTotal: 1090719744,
heapUsed: 1030627104,
external: 8272 }
Killed
复制代码
PS:
--max_old_space_size
取M为单位的值process.memoryUsage()
以字节为单位输出内存使用状况当内存使用率超过某个阈值时,应用程序终止。但这些阈值是多少?有什么限制?咱们来看一下约束。
默认状况下,Node.js(适用于11.x版本及如下)在32位和64位平台上使用最大堆大小分别为700MB和1400MB。对于当前默认值,请参阅博客末尾参考文章。
所以,理论上,当设置--max-old-space-size内存限制大于容器内存时,指望应用程序应直接被OOM(Out Of Memory)终止。
实际上,这可能不会发生。
并不是全部经过--max-old-space-size指定的内存的容量均可以提早分配给应用程序。
相反,为了响应不断增加的需求,JavaScript内存堆是逐渐增加的。
应用程序使用的实际内存(以JavaScript堆中的对象的形式)能够在process.memoryUsage()
API中的heapUsed
字段看到。
所以,如今修改后的指望是,若是实际堆大小(驻留对象大小)超过OOM-KILLER阈值(--memory容器中的标志),则容器终止应用程序。
实际上,这也可能不会发生。
当我在容器受限的环境下分析内存密集型Node.js应用程序时,我看到两种状况:
监控容器中运行应用程序的重要指标是驻留集大小(RSS-resident set size)。
它属于应用程序虚拟内存的一部分。
或者说,它表明应用程序被分配的内存的一部分。
更进一步说,它表示应用程序分配的内存中当前处于活动状态的部分。
并不是应用程序中的全部已分配内存都属于活动状态,这是由于“分配的内存”只有在进程实际开始使用它时才会真实分配。另外,为了响应其余进程的内存需求,系统可能swap out
当前进程中处于非活动或休眠状态的内存给其余进程,后续若是当前进程须要的时候经过swapped in
从新分配回来。
RSS反映了应用程序的可用和活动的内存量。
如下buffer_example.js
为往内存分配空Buffer对象的实例代码:
const buf = Buffer.alloc(+process.argv[2] * 1024 * 1024)
console.log(Math.round(buf.length / (1024 * 1024)))
console.log(Math.round(process.memoryUsage().rss / (1024 * 1024)))
复制代码
运行docker映像并限制其内存用量:
docker run --memory 1024m --interactive --tty ravali1906/dockermemory bash
复制代码
运行该应用程序。你会看到如下内容:
$ node buffer_example 2000
2000
16
复制代码
即便内存大于容器限制,应用程序也不会终止。这是由于分配的内存还未被彻底访问。rss值很是低,而且没有超过容器内存限制。
如下为往内存分配Buffer对象并填满值的实例代码:
const buf = Buffer.alloc(+process.argv[2] * 1024 * 1024,'x')
console.log(Math.round(buf.length / (1024 * 1024)))
console.log(Math.round(process.memoryUsage().rss / (1024 * 1024)))
复制代码
运行docker映像并限制其内存用量:
docker run --memory 1024m --interactive --tty ravali1906/dockermemory bash
复制代码
运行该应用程序
$ node buffer_example_fill.js 2000
2000
984
复制代码
即便在这里应用也没有被终止!为何?当活动内存达到容器设置限制时,而且swap space
还有空间时,一些旧内存片断将被推送到swap space
并可供同一进程使用。默认状况下,docker分配的交换空间量等于经过--memory标志设置的内存限制。有了这种机制,这个进程几乎可使用2GB内存 - 1GB活动内存和1GB交换空间。简而言之,因为内存的交换机制,rss仍然在容器强制限制范围内,而且应用程序可以持续运行。
const buf = Buffer.alloc(+process.argv[2] * 1024 * 1024,'x')
console.log(Math.round(buf.length / (1024 * 1024)))
console.log(Math.round(process.memoryUsage().rss / (1024 * 1024)))
复制代码
运行镜像时限制docker内存,交换空间和关闭匿名页面交换,以下所示:
docker run --memory 1024m --memory-swap=1024m --memory-swappiness=0 --interactive --tty ravali1906/dockermemory bash
复制代码
$ node buffer_example_fill.js 2000
Killed
复制代码
当--memory-swap
的值等于--memory
的值时,它表示容器不使用任何额外的交换空间。此外,默认状况下,容器的内核能够交换出必定比例的匿名页,所以将--memory-swappiness
设置为0以禁用它。所以,因为容器内没有发生交换,rss超出了容器限制,在正确的时间终止了进程。
当您运行Node.js应用程序并将其--max-old-space-size设置为大于容器限制时,看起来Node.js可能不会“尊重”容器强制限制。但正如您在上面的示例中看到的,缘由是应用程序可能没法使用标志访问JavaScript堆集的全长。
请记住,当您使用的内存多于容器中可用的内存时,没法保证应用定期望行为方式运行。为何?由于进程的活动内存(rss)受到许多因素的影响,这些因素超出了应用程序的控制范围,而且可能依赖于高负载和环境 - 例如工做负载自己,系统中的并发级别,操做系统调度程序,垃圾收集率等。此外,这些因素能够在运行之间发生变化。
top
命令和process.memoryUsage()
API获得最高值。若是在容器环境下运行,Node.js 12.x的堆内存限制根据当前可用内存进行配置,而不是使用默认值。对于设置了max_old_space_size
的场景,上面的建议仍然适用。此外,了解相关限制可让您更好地调整应用并发挥应用的性能,由于默认值是相对保守的。
有关更多信息,请参阅配置默认堆转储。