在Docker容器中运行Node.js应用程序时,传统内存设置并不总是能按预期工作。 该材料(我们今天出版的翻译)专用于寻找为什么会这样的问题的答案。 它还将提供实用建议,以管理容器中运行的Node.js应用程序可用的内存。

审查建议
假设Node.js应用程序在具有设置的内存限制的容器中运行。 如果我们谈论的是Docker,则可以使用
--memory
选项设置此限制。 使用容器编排系统时,可能会有类似的事情。 在这种情况下,建议在启动Node.js应用程序时使用
--max-old-space-size
选项。 这使您可以通知平台可用的内存量,并考虑到该数量应小于容器级别设置的限制。
当Node.js应用程序在容器内运行时,请根据应用程序使用的活动内存的峰值来设置其可用的内存容量。 如果可以配置容器内存限制,则可以这样做。
现在让我们更详细地讨论在容器中使用内存的问题。
Docker内存限制
默认情况下,容器没有资源限制,并且可以使用操作系统允许的最大内存量。
docker run
命令具有命令行选项,可用于设置有关内存使用或处理器资源的限制。
容器启动命令可能如下所示:
docker run --memory <x><y> --interactive --tty <imagename> bash
请注意以下几点:
x
是容器可用内存量的限制,以y
为单位表示。y
可以取值b
(字节), k
(千字节), m
(兆字节), g
(千兆字节)。
这是容器启动命令的示例:
docker run --memory 1000000b --interactive --tty <imagename> bash
此处,内存限制设置为
1000000
字节。
要检查在容器级别设置的内存限制,可以在容器中运行以下命令:
cat /sys/fs/cgroup/memory/memory.limit_in_bytes
让我们谈谈使用
--max-old-space-size
键指定Node.js应用程序内存限制时的系统行为。 在这种情况下,此内存限制将对应于容器级别设置的限制。
密钥名称中所谓的“旧空间”是由V8(放置“旧” JavaScript对象的位置)控制的堆的片段之一。 如果您不进入下面的详细信息,此键将控制最大堆大小。 可在
此处找到有关Node.js命令行开关的详细信息。
通常,当应用程序尝试使用的内存超过容器中可用的内存时,其操作将终止。
在下面的示例中(应用程序文件称为
test-fatal-error.js
),将
MyRecord
对象放置在
list
数组中,间隔为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映像中,可以从Docker Hub下载:
docker pull ravali1906/dockermemory
您可以将此图像用于独立实验。
此外,您可以将应用程序打包在Docker容器中,收集映像并以内存限制运行它:
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
在此,--
--max_old_space_size
表示以MB为单位的内存限制。
process.memoryUsage()
方法提供有关内存使用情况的信息。 值以字节表示。
该应用程序在某个时间点被强制终止。 当它使用的内存量超过某个边界时,就会发生这种情况。 这是什么边界? 我们可以谈谈对内存量的哪些限制?
使用密钥运行的应用程序的预期行为是-max-old-space-size
默认情况下,在32位平台上,Node.js(最大版本11.x)中的最大堆大小为700 MB,在64位平台上为1400 MB。 您可以
在此处阅读有关设置这些值的
信息 。
从理论上讲,如果使用
--max-old-space-size
键
--max-old-space-size
内存限制超过了容器的内存限制,则可以预期该应用程序将被Linux OOM Killer内核内核安全性机制终止。
实际上,这可能不会发生。
使用键运行的应用程序的实际行为是max-old-space-size
启动后,应用程序不会立即分配使用
--max-old-space-size
指定限制的所有内存。 JavaScript堆的大小取决于应用程序的需求。 您可以根据
process.memoryUsage()
方法返回的对象中的
heapUsed
字段的值来判断应用程序使用了多少内存。 实际上,我们在谈论堆中为对象分配的内存。
结果,我们得出结论,如果容器启动时堆大小大于
--memory
键设置的限制,则应用程序将被强制终止。
但实际上,这也可能不会发生。
对在具有给定内存限制的容器中运行的资源密集型Node.js应用程序进行性能分析时,可以观察到以下模式:
- OOM Killer的触发时间远远晚于
heapTotal
和heapUsed
明显高于内存限制的时刻。 - OOM Killer不会对超出限制做出反应。
容器中Node.js应用程序行为的解释
容器监督着在其上运行的应用程序的一个重要指示器。 这是
RSS (驻留集大小)。 该指示符表示应用程序虚拟内存的特定部分。
而且,它是分配给应用程序的一块内存。
但这还不是全部。 RSS是分配给应用程序的活动内存的一部分。
并非所有分配给应用程序的内存都处于活动状态。 事实是,“已分配内存”在过程真正开始使用之前并不一定要物理分配。 另外,响应于来自其他进程的内存分配请求,操作系统可以将应用程序内存的非活动部分转储到页面文件中,并将释放的空间转移到其他进程。 当应用程序再次需要这些内存时,它们将从交换文件中取出并返回到物理内存。
RSS指标指示应用程序在其地址空间中的活动和可用内存量。 是由他来决定是否强制关闭应用程序。
证据
▍示例1。 为缓冲区分配内存的应用程序
以下示例
buffer_example.js
展示了一个为缓冲区分配内存的程序:
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 run --memory 1024m --interactive --tty ravali1906/dockermemory bash
之后,运行程序:
$ node buffer_example 2000 2000 16
如您所见,尽管程序分配的内存超出了容器限制,但系统没有完成程序。 发生这种情况的原因是程序无法使用所有分配的内存。 RSS很小,它没有超出容器的内存限制。
▍示例2 应用程序用数据填充缓冲区
在以下示例
buffer_example_fill.js
,不仅分配了内存,还填充了数据:
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 run --memory 1024m --interactive --tty ravali1906/dockermemory bash
之后,运行该应用程序:
$ node buffer_example_fill.js 2000 2000 984
显然,即使现在应用程序也没有结束! 怎么了 事实是,当活动内存量达到启动容器时设置的限制,并且页面文件中有空间时,过程内存中的某些旧页面将移至页面文件。 释放的内存可用于同一进程。 默认情况下,Docker为交换文件分配的空间等于使用
--memory
标志设置的内存限制。 鉴于此,我们可以说该进程有2 GB的内存-1 GB的活动内存和1 GB的页面文件。 也就是说,由于应用程序可以使用其自己的内存,该内存的内容被临时移动到页面文件,因此RSS索引的大小在容器限制之内。 结果,该应用程序继续运行。
▍示例3 在不使用页面文件的容器中运行的数据填充缓冲区的应用程序
这是我们将在此处尝试的代码(这是相同的
buffer_example_fill.js
文件):
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 run --memory 1024m --memory-swap=1024m --memory-swappiness=0 --interactive --tty ravali1906/dockermemory bash
启动应用程序:
$ node buffer_example_fill.js 2000 Killed
看到消息已
Killed
? 当
--memory-swap
密钥值等于
--memory
密钥
--memory
,这将告诉容器不应使用
--memory
文件。 另外,默认情况下,容器本身运行所在的操作系统的内核可以将容器使用的一定数量的匿名内存页面转储到页面文件中。
--memory-swappiness
标志
--memory-swappiness
为
0
,我们将禁用此功能。 结果,事实证明在容器内部未使用分页文件。 当RSS指标超出容器的内存限制时,该过程结束。
一般建议
当使用
--max-old-space-size
键启动Node.js应用程序时,其值超过了启动容器时设置的内存限制,看来Node.js并未“注意”容器限制。 但是,从前面的示例中可以看出,此行为的明显原因是以下事实:应用程序完全不使用通过
--max-old-space-size
标志指定的整个堆卷。
请记住,如果应用程序使用的内存多于容器中可用的内存,它将不会总是表现出相同的行为。 怎么了 事实是,进程的活动内存(RSS)受应用程序本身无法影响的许多外部因素影响。 它们取决于系统上的负载以及环境的特征。 例如,这些是应用程序本身的功能,系统中的并行度,操作系统调度程序的功能,垃圾收集器的功能等等。 此外,这些因素在发射之间可能会发生变化。
在您可以控制此选项但没有容器级内存限制的情况下,建议设置Node.js堆的大小
- 在容器中运行最小的Node.js应用程序,并测量静态RSS大小(在我的情况下,对于Node.js 10.x,这约为20 Mb)。
- Node.js堆不仅包含old_space,还包含其他堆(例如new_space,code_space等)。 因此,如果考虑到平台的标准配置,则应依靠该程序将需要大约20 MB的内存这一事实。 如果标准设置已更改,则还必须考虑这些更改。
- 现在,我们需要从容器中可用的内存量中减去获得的值(假设它将为40 MB)。 剩下的就是一个值,它可以指定为键值
--max-old-space-size
,而不必担心程序会因内存不足而--max-old-space-size
。
在可以控制此参数但不控制Node.js应用程序参数的情况下,设置容器内存限制的建议
- 以允许您找出应用程序消耗的内存峰值的模式运行该应用程序。
- 分析RSS分数。 特别是,在这里,与
process.memoryUsage()
方法一起,Linux top
命令可能会派上用场。 - 假设在计划运行该应用程序的容器中,什么也不会执行,那么将获得的值用作容器内存限制。 为了安全起见,建议至少增加10%。
总结
在Node.js 12.x中,此处讨论的一些问题是通过自适应调整堆大小来解决的,堆大小是根据可用RAM的数量来执行的。 在容器中运行Node.js应用程序时,此机制也适用。 但是设置可能与默认设置不同。 例如,在启动应用程序时使用
--max_old_space_size
键时,就会发生这种情况。 对于此类情况,上述所有内容仍然适用。 这表明在容器中运行Node.js应用程序的任何人都应对内存设置谨慎并负责。 另外,对内存使用的标准限制的了解是相当保守的,可以通过有意更改这些限制来提高应用程序性能。
亲爱的读者们! 在Docker容器中运行Node.js应用程序时是否存在内存不足的问题?

