Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Node.js memory management in container environments #4

Open
manooog opened this issue Aug 20, 2019 · 0 comments
Open

Node.js memory management in container environments #4

manooog opened this issue Aug 20, 2019 · 0 comments
Labels
finished 完成

Comments

@manooog
Copy link
Owner

manooog commented Aug 20, 2019

原文地址:https://medium.com/the-node-js-collection/node-js-memory-management-in-container-environments-7eb8409a74e8
翻译理由:最近个人vps爆出一个内存不够的问题(上传文件的时候一次性将文件load到buffer中了,直接造成进程被杀死),看了这篇文章之后,通过增加vps的swap的大小,解决了内存不够的问题。


本文首发于 IBM Developer.

在基于容器的 Node.js 应用中管理内存的最佳实践

在容器中运行 Node.js 程序的时候,传统的内存限制参数可能不会如预期般生效。本文中,我们将探讨造成这个现象的原因并且提供一些你在容器化环境中运行 Node.js 程序时候可以参考的建议和最佳实践。

总结

当 Node.js 程序运行在有内存限制的容器化环境中(例如,docker 下的--memory参数或者其他系统下的某些参数),同时使用--max-old-space-size参数来确保 Node 知道它能够使用的内存限制,并且这个值要小于容器的限制值。

当 Node.js 程序运行在容器环境中,并且容器内存是可调的,此时可以根据程序使用的活动内存的峰值来设置容器的内存。

让我们更进一步进行探索。

Docker 内存限制

通常,容器没有内存限制并且可以使用宿主机允许使用的全部内存资源。docker run 命令提供了命令行参数来设置容器可以使用的内存和 CPU 资源。

形如:

docker run --memory <x><y> --interactive --tty <imagename> bash

解析:

  • x 是以 y 为单位的内存值
  • y 可以是 b(bytes), k(kilobytes), m(megabytes), g(gigabytes)

更多例子:

docker run --memory 1000000b --interactive --tty <imagename> bash

设置了内存限制为 1,000,000 bytes。

可以通过以下命令来检查容器内以 bytes 为单位的内存限制:

cat /sys/fs/cgroup/memory/memory.limit_in_bytes

如果以这个值为--max-old-space-size的值,让我们来看看容器的行为。

“Old space” 是 V8 引擎管理的堆内存中的公共部分(例如,js 中的对象存放的位置),而--max-old-space-size则是用来控制这块空间的最大值的。更多内容,请参考About –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 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 的单位是 Mbytes
  • process.memoryUsage() 的单位则是 bytes

程序会在内存使用达到某一个阈值的时候被终止。这个阈值是什么呢?限制又是什么?让我们一起来探讨一下。

在具有容器内存限制下的--max-old-space-size预期行为

默认情况下,Node.js(直至 11.x 版本)的最大堆内存空间为 32 位操作系统下的 700MB 和 64 位操作系统下的 1400MB。可以通过查看文末提到的博客来了解当前的默认值。

因此,当设置--max-old-space-size的值超过了容器的限制的时候,程序将会被 OOM 终止掉。

实际上,这个情况可能不会出现。

在具有容器内存限制下的--max-old-space-size的实际行为

通过--max-old-space-size声明的内存空间不是在程序最开始就可用的。

相反的,JavaScript 堆内存是随着增长的需求而增长的。

程序实际使用的内存(以对象形式存在于 JavaScript 堆内存中的)可以通过process.memoryUsage()接口的heapUsed字段表示。

因此,此时的预期行为是如果实际使用的堆内存大小(常驻对象的大小)超过了 OOM 的阈值(容器--memory的大小),程序则会被终止。

实际上,这个情况也不会出现。

当我在具有内存限制下的容器环境中执行一个内存敏感的 Node.js 应用的时候,我发现了两种模式:

  1. OOM 会在heapTotalheapUsed的值超过了容器内存限制之后很久才会触发终止程序的行为
  2. OOM 根本没有发挥作用

Node.js 在容器环境中的行为:解释

容器会持续追踪内部运行的程序的“常驻居民大小”(RSS)。

译者注:RSS 到底咋翻译啊,哈哈;感觉很别扭

它表示程序使用的虚拟内存的一部分,进一步解释就是,它表示的是程序已分配的内存中的一部分。

更进一步解释就是它表示的是程序已分配的内存中的活动的那部分。

译者注:这个解释非常拗口,=。=

并不是所有程序中被分配的内存都是活动的。这是因为“被分配的内存”在进程开始使用它之前不需要被分配。此外,为了应对其他进程的内存需求,操作系统会将程序中休眠的或者是非活动的那部分内存写到交换区(swap out),从而将内存腾出来给需要它的进程使用。此后,当这个程序需要这些信息的时候,操作系统则会将之前写到交换区的内存重新写到内存中。

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. 分配内存并且填充它

在下面的程序中,此时的内存被数据填充满了:

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设置的值同样大小的交换空间。由于这个特性,示例程序当前分配了 2GB 内存-1GB 是活动的内存,1GB 在交换空间。简而言之,由于交换空间中分但了一部分内存压力,rss的大小依然处于容器的限制之内,程序能够继续执行。

例 3. 分配内存并以数据填充,同时容器不允许使用交换空间

以下设置禁止容器使用交换空间:

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)));

Issue the docker memory limit, swap limit, and swappiness while running the image as:

启动容器,分别设置内存限制、交换空间限制和、内存交换比例限制:

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-swappiness的值为 0 来禁用这种交换。此时,容器内不会再发生内存交换,rss达到了容器的限制,立刻终止进程。

总结

当你以超过容器限制的--max-old-space-size的值来运行程序的时候,似乎 Node.js“无视”了容器内存限制的存在。但是如你在上面的例子中见到的,真正的原因(程序没有被终止)是程序没有完全用完--max-old-space-size设置的值。

需要铭记于心的是,当使用超过容器限制的内存时,你不能期待程序总是按照同样的方式运行。因为进程的活动内存(也就是 rss)被一系列超过了程序的控制范围的因素所影响,并且高度依赖负载和环境,例如工作负载本身、系统并发量、操作系统调度、垃圾回收率等等。

关于 Node.js 堆内存的建议 (当你可以控制堆内存,但是不能控制容器的内存)

  • 在容器内运行一个空的 Node.js 应用,测量静态的 rss 使用量(我在 Node.js v10.x 版本中测试的结果为 20MB)
  • 由于 Node.js 在堆内存中有其他的内存区域(例如 new_space、code_space 等),额外的 20MB 空间可能是他们的默认大小。如果你修改了默认值,也需要相应的调整这个值
  • 减去这个值(也就是 40MB),余下的值就是对于 JavaScript 堆内存中的old_space_size来说的一个安全的值

关于容器内存的建议 (当你能控制容器内存,但是不能控制 Node.js 使用的内存)

  • 以超过峰值负荷的方式运行程序
  • 测量rss的增长。我同时使用了top命令和process.memoryUsage()接口来观测 rss 的增长
  • 如果容器中没有其他的活动进程,将这个值设为容器内存的限制值。为了更保险,你需要在这个基础上增加 10%或者更多的内存分配。

后记

如果运行在容器环境中,Node.js 12.x 会通过限制默认的堆内存在可用限制范围内的方式解决一些不一致的问题。然而,对于没有默认max_old_space_size设置的情况,上述的例子也是成立的,当在调整默认值的时候需要谨慎一点。此外,由于默认值是保守的,知道默认限制可以让你更好的调整(knowing the default limits will let you tune better as the defaults are conservative)。

更多内容,请移步 Configuring default heap dumps.

@manooog manooog added the working 进行中 label Aug 20, 2019
@manooog manooog added finished 完成 and removed working 进行中 labels Aug 29, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
finished 完成
Projects
None yet
Development

No branches or pull requests

1 participant