Ao executar aplicativos Node.js em contêineres do Docker, as configurações tradicionais de memória nem sempre funcionam conforme o esperado. O material, cuja tradução publicamos hoje, é dedicado a encontrar a resposta para a pergunta de por que isso acontece. Ele também fornecerá recomendações práticas para gerenciar a memória disponível para aplicativos Node.js. executados em contêineres.

Revisão de recomendações
Suponha que um aplicativo Node.js seja executado em um contêiner com um limite de memória definido. Se estamos falando sobre o Docker, a opção
--memory
pode ser usada para definir esse limite. Algo semelhante é possível ao trabalhar com sistemas de orquestração de contêineres. Nesse caso, é recomendável que, ao iniciar o aplicativo
--max-old-space-size
, use a opção
--max-old-space-size
. Isso permite que você informe a plataforma sobre quanta memória está disponível e também leve em consideração o fato de que essa quantidade deve ser menor que o limite definido no nível do contêiner.
Quando o aplicativo Node.js for executado dentro do contêiner, configure a capacidade da memória disponível para ele, de acordo com o valor máximo do uso de memória ativa pelo aplicativo. Isso é feito se os limites de memória do contêiner puderem ser configurados.
Agora vamos falar sobre o problema do uso de memória em contêineres em mais detalhes.
Limite de memória do Docker
Por padrão, os contêineres não têm limites de recursos e podem usar a quantidade de memória que o sistema operacional permitir. O comando
docker run
possui opções de linha de comando que permitem definir limites em relação ao uso de memória ou recursos do processador.
O comando de inicialização do contêiner pode ficar assim:
docker run --memory <x><y> --interactive --tty <imagename> bash
Observe o seguinte:
x
é o limite da quantidade de memória disponível para o contêiner, expressa em unidades de y
.y
pode levar o valor b
(bytes), k
(kilobytes), m
(megabytes), g
(gigabytes).
Aqui está um exemplo de um comando de inicialização de contêiner:
docker run --memory 1000000b --interactive --tty <imagename> bash
Aqui, o limite de memória é definido para
1000000
bytes.
Para verificar o limite de memória definido no nível do contêiner, você pode, no contêiner, executar o seguinte comando:
cat /sys/fs/cgroup/memory/memory.limit_in_bytes
Vamos falar sobre o comportamento do sistema ao especificar o limite de memória do aplicativo
--max-old-space-size
usando a chave
--max-old-space-size
. Nesse caso, esse limite de memória corresponderá ao limite definido no nível do contêiner.
O que é chamado de "espaço antigo" no nome da chave é um dos fragmentos do heap controlado pelo V8 (o local em que os objetos JavaScript "antigos" são colocados). Essa tecla, se você não entrar nos detalhes que tocamos abaixo, controla o tamanho máximo da pilha. Detalhes sobre opções de linha de comando do Node.js. podem ser encontrados
aqui .
Em geral, quando um aplicativo tenta usar mais memória do que o disponível no contêiner, sua operação é encerrada.
No exemplo a seguir (o arquivo do aplicativo é chamado
test-fatal-error.js
), os objetos
MyRecord
são colocados na matriz da
list
, com um intervalo de 10 milissegundos. Isso leva ao crescimento descontrolado de heap, simulando um vazamento de memória.
'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);
Observe que todos os exemplos de programas que discutiremos aqui são colocados na imagem do Docker, que pode ser baixada no Docker Hub:
docker pull ravali1906/dockermemory
Você pode usar esta imagem para experimentos independentes.
Além disso, você pode empacotar o aplicativo em um contêiner do Docker, coletar a imagem e executá-la com o limite de memória:
docker run --memory 512m --interactive --tty ravali1906/dockermemory bash
Aqui
ravali1906/dockermemory
é o nome da imagem.
Agora você pode iniciar o aplicativo especificando um limite de memória que exceda o limite do contêiner:
$ node --max_old_space_size=1024 test-fatal-error.js { rss: 550498304, heapTotal: 1090719744, heapUsed: 1030627104, external: 8272 } Killed
Aqui, a
--max_old_space_size
representa o limite de memória indicado em megabytes. O método
process.memoryUsage()
fornece informações sobre o uso da memória. Os valores são expressos em bytes.
O aplicativo em algum momento é forçado a terminar. Isso acontece quando a quantidade de memória usada por ele atravessa uma determinada borda. O que é essa fronteira? De que limitações podemos falar sobre a quantidade de memória?
O comportamento esperado de um aplicativo em execução com a chave é - max-old-space-size
Por padrão, o tamanho máximo de heap no Node.js (até a versão 11.x) é de 700 MB em plataformas de 32 bits e 1400 MB em plataformas de 64 bits. Você pode ler sobre como definir esses valores
aqui .
Em teoria, se você usar a chave
--max-old-space-size
para
--max-old-space-size
limite de memória que exceda o limite de memória do contêiner, poderá esperar que o aplicativo seja finalizado pelo mecanismo de segurança do kernel do Linux OOM Killer.
Na realidade, isso pode não acontecer.
O comportamento real do aplicativo em execução com a chave é max-old-space-size
O aplicativo, imediatamente após o lançamento, não aloca toda a memória cujo limite é especificado usando
--max-old-space-size
. O tamanho da pilha do JavaScript depende das necessidades do aplicativo. Você pode avaliar quanta memória o aplicativo usa com base no valor do campo
heapUsed
do objeto retornado pelo método
process.memoryUsage()
. De fato, estamos falando sobre a memória alocada no heap para objetos.
Como resultado, concluímos que o aplicativo será encerrado à força se o tamanho do heap for maior que o limite definido pela chave
--memory
quando o contêiner for iniciado.
Mas, na realidade, isso também não pode acontecer.
Ao criar um perfil de aplicativos Node.js intensivos em recursos que são executados em contêineres com um determinado limite de memória, os seguintes padrões podem ser observados:
- OOM Killer é acionado muito depois do momento em que os
heapUsed
e heapUsed
são significativamente maiores que os limites de memória. - OOM Killer não responde a limites excedentes.
Uma explicação do comportamento dos aplicativos Node.js. nos contêineres
Um contêiner supervisiona um indicador importante dos aplicativos executados nele. Este é o
RSS (tamanho do conjunto residente). Este indicador representa uma certa parte da memória virtual do aplicativo.
Além disso, é um pedaço de memória que é alocado para o aplicativo.
Mas isso não é tudo. O RSS faz parte da memória ativa alocada para o aplicativo.
Nem toda a memória alocada para um aplicativo pode estar ativa. O fato é que a “memória alocada” não é necessariamente alocada fisicamente até que o processo comece a realmente usá-la. Além disso, em resposta a solicitações de alocação de memória de outros processos, o sistema operacional pode despejar partes inativas da memória do aplicativo no arquivo de paginação e transferir o espaço livre para outros processos. E quando o aplicativo precisar novamente desses pedaços de memória, eles serão retirados do arquivo de troca e retornados à memória física.
A métrica RSS indica a quantidade de memória ativa e disponível para o aplicativo em seu espaço de endereço. É ele quem influencia a decisão sobre o encerramento forçado do aplicativo.
Evidência
▍ Exemplo nº 1. Um aplicativo que aloca memória para um buffer
O exemplo a seguir,
buffer_example.js
, mostra um programa que aloca memória para um 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)))
Para que a quantidade de memória alocada pelo programa exceda o limite definido quando o contêiner foi iniciado, primeiro execute o contêiner com o seguinte comando:
docker run --memory 1024m --interactive --tty ravali1906/dockermemory bash
Depois disso, execute o programa:
$ node buffer_example 2000 2000 16
Como você pode ver, o sistema não concluiu o programa, embora a memória alocada pelo programa exceda o limite do contêiner. Isso aconteceu devido ao fato de o programa não funcionar com toda a memória alocada. O RSS é muito pequeno, não excede o limite de memória do contêiner.
▍ Exemplo No. 2. Aplicativo preenchendo o buffer com dados
No exemplo a seguir,
buffer_example_fill.js
, a memória não é apenas alocada, mas também preenchida com dados:
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)))
Execute o contêiner:
docker run --memory 1024m --interactive --tty ravali1906/dockermemory bash
Depois disso, execute o aplicativo:
$ node buffer_example_fill.js 2000 2000 984
Aparentemente, mesmo agora o aplicativo não termina! Porque O fato é que, quando a quantidade de memória ativa atinge o limite definido quando o contêiner foi iniciado e há espaço no arquivo de paginação, algumas das páginas antigas na memória do processo são movidas para o arquivo de paginação. A memória liberada é disponibilizada para o mesmo processo. Por padrão, o Docker aloca espaço para o arquivo de troca igual ao limite de memória definido usando o sinalizador
--memory
. Diante disso, podemos dizer que o processo possui 2 GB de memória - 1 GB na memória ativa e 1 GB no arquivo de paginação. Ou seja, devido ao fato de o aplicativo poder usar sua própria memória, cujo conteúdo é movido temporariamente para o arquivo de paginação, o tamanho do índice RSS está dentro do limite do contêiner. Como resultado, o aplicativo continua funcionando.
▍ Exemplo No. 3. Um aplicativo que preenche um buffer com dados em execução em um contêiner que não usa um arquivo de paginação
Aqui está o código com o qual experimentaremos aqui (este é o mesmo arquivo
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)))
Desta vez, execute o contêiner, configurando explicitamente os recursos de trabalho com o arquivo de permuta:
docker run --memory 1024m --memory-swap=1024m --memory-swappiness=0 --interactive --tty ravali1906/dockermemory bash
Inicie o aplicativo:
$ node buffer_example_fill.js 2000 Killed
Veja a mensagem
Killed
? Quando o valor da chave
--memory-swap
é igual ao
--memory
chave
--memory
, isso informa ao contêiner que ele não deve usar o arquivo de
--memory
. Além disso, por padrão, o kernel do sistema operacional no qual o contêiner é executado pode despejar uma certa quantidade de páginas de memória anônima usadas pelo contêiner no arquivo de paginação.
--memory-swappiness
como
0
, desativamos esse recurso. Como resultado, o arquivo de paginação não é usado dentro do contêiner. O processo termina quando a métrica RSS excede o limite de memória do contêiner.
Recomendações gerais
Quando os aplicativos Node.js são iniciados com a chave
--max-old-space-size
, cujo valor excede o limite de memória definido quando o contêiner foi iniciado, pode parecer que o Node.js "não está prestando atenção" ao limite do contêiner. Mas, como pode ser visto nos exemplos anteriores, a razão óbvia para esse comportamento é o fato de o aplicativo simplesmente não usar todo o volume de heap especificado com o
--max-old-space-size
.
Lembre-se de que o aplicativo nem sempre se comportará da mesma maneira se usar mais memória do que a disponível no contêiner. Porque O fato é que a memória ativa do processo (RSS) é influenciada por muitos fatores externos que o próprio aplicativo não pode influenciar. Eles dependem da carga no sistema e nas características do ambiente. Por exemplo, esses são recursos do próprio aplicativo, o nível de paralelismo no sistema, recursos do planejador do sistema operacional, recursos do coletor de lixo e assim por diante. Além disso, esses fatores, de um lançamento para outro, podem mudar.
Recomendações sobre como definir o tamanho do heap do Node.js para os casos em que você pode controlar esta opção, mas não com restrições de memória no nível do contêiner
- Execute o aplicativo Node.js mínimo no contêiner e meça o tamanho estático do RSS (no meu caso, para o Node.js.x, isso é cerca de 20 Mb).
- O heap do Node.js contém não apenas o old_space, mas também outros (como new_space, code_space e assim por diante). Portanto, se você levar em consideração a configuração padrão da plataforma, deve confiar no fato de que o programa precisará de cerca de 20 MB a mais de memória. Se as configurações padrão foram alteradas, essas alterações também devem ser levadas em consideração.
- Agora precisamos subtrair o valor obtido (suponha que seja 40 MB) da quantidade de memória disponível no contêiner. O que resta é um valor que, sem medo de que a
--max-old-space-size
do programa fique sem memória, possa ser especificado como o valor da chave - --max-old-space-size
.
Recomendações para definir limites de memória do contêiner para casos em que esse parâmetro pode ser controlado, mas os parâmetros do aplicativo Node.js.
- Execute o aplicativo nos modos que permitem descobrir os valores de pico da memória consumida por ele.
- Analise a pontuação do RSS. Em particular, aqui, junto com o método
process.memoryUsage()
, o comando top
do Linux pode ser útil. - Desde que no contêiner no qual está planejado executar o aplicativo, nada além de não ser executado, o valor obtido possa ser usado como limite de memória do contêiner. Para ser seguro, é recomendável aumentá-lo em pelo menos 10%.
Sumário
No Node.j 12.x, alguns dos problemas discutidos aqui são resolvidos ajustando adaptativamente o tamanho do heap, que é executado de acordo com a quantidade de RAM disponível. Esse mecanismo também funciona ao executar aplicativos Node.js. em contêineres. Mas as configurações podem diferir das configurações padrão. Isso, por exemplo, acontece quando a chave
--max_old_space_size
foi usada ao iniciar o aplicativo. Para esses casos, todos os itens acima permanecem relevantes. Isso sugere que qualquer pessoa que execute aplicativos Node.js. em contêineres seja cuidadosa e responsável com as configurações de memória. Além disso, o conhecimento dos limites padrão de uso de memória, que é bastante conservador, pode melhorar o desempenho do aplicativo alterando deliberadamente esses limites.
Caros leitores! Você ficou com problemas de memória ao executar aplicativos Node.js. nos contêineres do Docker?

