Um guia prático para lidar com vazamentos de memória no Node.js

Vazamentos de memória são semelhantes às entidades parasitárias em um aplicativo. Eles penetram discretamente no sistema, a princípio, sem causar nenhum dano. Mas se o vazamento for forte o suficiente, poderá levar o aplicativo ao desastre. Por exemplo - para desacelerá-lo com força ou simplesmente para "matá-lo". O autor do artigo, cuja tradução estamos publicando hoje, sugere falar sobre vazamentos de memória em JavaScript. Em particular, falaremos sobre gerenciamento de memória em JavaScript, como identificar vazamentos de memória em aplicativos reais e como lidar com vazamentos de memória.





O que é um vazamento de memória?


Um vazamento de memória é, em um sentido amplo, uma parte da memória alocada para um aplicativo que ele não precisa mais, mas não pode ser devolvido ao sistema operacional para uso futuro. Em outras palavras, é um bloco de memória capturado pelo aplicativo sem a intenção de usar essa memória no futuro.

Gerenciamento de memória


O gerenciamento de memória é um mecanismo para alocar memória do sistema para um aplicativo que precisa e um mecanismo para retornar memória desnecessária ao sistema operacional. Existem muitas abordagens para gerenciamento de memória. Qual abordagem é usada depende da linguagem de programação usada. Aqui está uma visão geral de várias abordagens comuns ao gerenciamento de memória:

  • . . . , . C C++. , , malloc free, .
  • . , , , . , , , . , , , , . . — JavaScript, , JVM (Java, Scala, Kotlin), Golang, Python, Ruby .
  • Aplicação do conceito de propriedade da memória. Com essa abordagem, cada variável deve ter seu próprio proprietário. Assim que o proprietário está fora do escopo, o valor na variável é destruído, liberando memória. Essa ideia é usada no Rust.

Existem outras abordagens para gerenciamento de memória usadas em diferentes linguagens de programação. Por exemplo, o C ++ 11 usa o idioma RAII , enquanto o Swift usa o mecanismo ARC . Mas falar sobre isso está além do escopo deste artigo. Para comparar os métodos acima de gerenciamento de memória, para entender seus prós e contras, precisamos de um artigo separado.

O JavaScript, uma linguagem sem a qual os programadores da Web não conseguem imaginar seu trabalho, usa a idéia da coleta de lixo. Portanto, falaremos mais sobre como esse mecanismo funciona.

Coleta de lixo JavaScript


Como já mencionado, o JavaScript é uma linguagem que usa o conceito de coleta de lixo. Durante a operação dos programas JS, um mecanismo chamado coletor de lixo é iniciado periodicamente. Ele descobre quais partes da memória alocada podem ser acessadas a partir do código do aplicativo. Ou seja, quais variáveis ​​são referenciadas. Se o coletor de lixo descobrir que um pedaço de memória não é mais acessado a partir do código do aplicativo, ele libera essa memória. A abordagem acima pode ser implementada usando dois algoritmos principais. O primeiro é o chamado algoritmo Mark and Sweep. É usado em JavaScript. O segundo é a contagem de referência. É usado em Python e PHP.


Fases de marcação (marcação) e varredura (limpeza) do

algoritmo de marcação e varredura Ao implementar o algoritmo de marcação, uma lista de nós raiz representados por variáveis ​​de ambiente global (este é um objeto no navegadorwindow) é criada primeiro e, em seguida, a árvore resultante é rastreada dos nós raiz para folha marcados com todos encontrado no caminho objetos. A memória no heap que é ocupada por objetos não rotulados é liberada.

Vazamentos de memória nos aplicativos Node.js.


Até o momento, analisamos conceitos teóricos suficientes relacionados a vazamentos de memória e coleta de lixo. Então - estamos prontos para ver como tudo fica em aplicativos reais. Nesta seção, escreveremos um servidor Node.js. com um vazamento de memória. Vamos tentar identificar esse vazamento usando várias ferramentas e depois eliminá-lo.

▍ Familiaridade com um código com vazamento de memória


Para fins de demonstração, escrevi um servidor Express que possui uma rota de vazamento de memória. Vamos depurar este servidor.

const express = require('express')

const app = express();
const port = 3000;

const leaks = [];

app.get('/bloatMyServer', (req, res) => {
  const redundantObj = {
    memory: "leaked",
    joke: "meta"
  };

  [...Array(10000)].map(i => leaks.push(redundantObj));

  res.status(200).send({size: leaks.length})
});

app.listen(port, () => console.log(`Example app listening on port ${port}!`));

Há uma matriz leaksque está fora do escopo do código de processamento de solicitações da API. Como resultado, toda vez que o código correspondente é executado, novos elementos são simplesmente adicionados à matriz. A matriz nunca é limpa. Como o link para essa matriz não desaparece após a saída do manipulador de solicitações, o coletor de lixo nunca libera a memória usada.

LeakChame vazamento de memória


Aqui chegamos ao mais interessante. Muitos artigos foram escritos sobre como, usando node --inspect, depurar vazamentos de memória do servidor, após preencher o servidor com solicitações usando algo como artilharia . Mas essa abordagem tem uma desvantagem importante. Imagine que você tenha um servidor de API com milhares de terminais. Cada um deles usa muitos parâmetros, o código específico que será chamado depende dos recursos. Como resultado, em condições reais, se o desenvolvedor não souber onde está o vazamento de memória, ele precisará acessar cada API muitas vezes usando todas as combinações possíveis de parâmetros para preencher a memória. Quanto a mim, não é fácil fazê-lo. A solução para esse problema, no entanto, é facilitada usando algo comogoreplay - um sistema que permite gravar e "reproduzir" tráfego real.

Para lidar com o nosso problema, vamos fazer a depuração na produção. Ou seja, permitiremos que o servidor sobrecarregue a memória durante seu uso real (pois recebe uma variedade de solicitações de API). E depois que encontrarmos um aumento suspeito na quantidade de memória alocada, faremos a depuração.

▍ Despejo de pilha


Para entender o que é um dump de heap, primeiro precisamos descobrir o significado do conceito de heap. Se você descrever esse conceito da maneira mais simples possível, verifica-se que o heap é o local em que tudo o que a memória está alocada se encaixa. Tudo isso fica na pilha até o coletor de lixo remover tudo o que é considerado desnecessário. Um despejo de heap é algo como uma captura instantânea do estado atual de um heap. O dump contém todas as variáveis ​​internas e variáveis ​​declaradas pelo programador. Ele representa toda a memória alocada no heap no momento em que o dump foi recebido.

Como resultado, se pudéssemos comparar de alguma forma o despejo de pilha do servidor iniciado com o despejo de pilha do servidor, que está em execução há muito tempo e transborda memória, poderemos identificar objetos suspeitos que o aplicativo não precisa, mas que não são excluídos pelo coletor de lixo.

Antes de continuar a conversa, vamos falar sobre como criar despejos de heap. Para resolver esse problema, usaremos o pacote hpmdump do npm , que permite obter programaticamente um dump do heap do servidor.

Instale o pacote:

npm i heapdump

Faremos algumas alterações no código do servidor que nos permitirá usar este pacote:

const express = require('express');
const heapdump = require("heapdump");

const app = express();
const port = 3000;

const leaks = [];

app.get('/bloatMyServer', (req, res) => {
  const redundantObj = {
    memory: "leaked",
    joke: "meta"
  };

  [...Array(10000)].map(i => leaks.push(redundantObj));

  res.status(200).send({size: leaks.length})
});

app.get('/heapdump', (req, res) => {
  heapdump.writeSnapshot(`heapDump-${Date.now()}.heapsnapshot`, (err, filename) => {
    console.log("Heap dump of a bloated server written to", filename);

    res.status(200).send({msg: "successfully took a heap dump"})
  });
});

app.listen(port, () => {
  heapdump.writeSnapshot(`heapDumpAtServerStart.heapsnapshot`, (err, filename) => {
    console.log("Heap dump of a fresh server written to", filename);
  });
});

Aqui usamos este pacote para despejar um servidor recém-lançado. Também criamos uma API /heapdumpprojetada para criar uma pilha ao acessá-la. Voltaremos a essa API no momento em que percebermos que o servidor começou a consumir muita memória.

Se o seu servidor estiver executando em um cluster Kubernetes, você não poderá, sem esforço extra, recorrer ao mesmo pod cujo servidor está executando no qual consome muita memória. Para fazer isso, você pode usar o encaminhamento de porta . Além disso, como você não terá acesso ao sistema de arquivos para baixar arquivos de despejo, seria melhor fazer o upload desses arquivos para o armazenamento em nuvem externo (como o S3).

Detection Detecção de vazamento de memória


E agora, o servidor está implantado. Ele trabalha há vários dias. Ele recebe muitas solicitações (no nosso caso, apenas solicitações de um tipo) e prestamos atenção ao aumento da quantidade de memória consumida pelo servidor. Um vazamento de memória pode ser detectado usando ferramentas de monitoramento como o Express Status Monitor , Clinic , Prometheus . Depois disso, chamamos a API para despejar o heap. Este despejo conterá todos os objetos que o coletor de lixo não pôde excluir.

Aqui está a aparência da consulta para criar um dump:

curl --location --request GET 'http://localhost:3000/heapdump'

Quando um dump de heap é criado, o coletor de lixo é forçado a executar. Como resultado, não precisamos nos preocupar com os objetos que podem ser removidos pelo coletor de lixo no futuro, mas ainda estão na pilha. Ou seja, sobre os objetos ao trabalhar com os quais não ocorrem vazamentos de memória.

Depois que tivermos ambos os dumps à nossa disposição (um dump de um servidor recém-lançado e um dump de um servidor que funcionou por algum tempo), podemos começar a compará-los.

Obter um despejo de memória é uma operação de bloqueio que requer muita memória para ser concluída. Portanto, deve ser realizado com cautela. Você pode ler mais sobre os possíveis problemas encontrados durante esta operação aqui .

Inicie o Chrome e pressione a teclaF12. Isso levará à descoberta de ferramentas de desenvolvedor. Aqui você precisa ir para a guia Memorye carregar os dois instantâneos de memória.


Baixando despejos de memória na guia Memória das ferramentas para desenvolvedores Chrome

Depois de baixar os dois instantâneos, você precisa mudarperspectiveaComparisone clique no instantâneo da memória do servidor que trabalhou por algum tempo.


Comece a comparar instantâneos

Aqui, podemos analisar a colunaConstructore procurar objetos que o coletor de lixo não pode remover. A maioria desses objetos será representada por links internos que os nós usam. Aqui é útil usar um truque, que consiste em classificar a lista por campoAlloc. Size. Isso encontrará rapidamente os objetos que usam mais memória. Se você expandir o bloco(array)e depois -(object elements), poderá ver uma matrizleakscontendo um grande número de objetos que não podem ser excluídos usando o coletor de lixo.


Análise de uma matriz suspeita

Essa técnica nos permitirá entrar na matrizleakse entender que é a operação incorreta que causa um vazamento de memória.

Corrigir vazamento de memória


Agora que sabemos que o "culpado" é uma matriz leaks, podemos analisar o código e descobrir que o problema é que a matriz é declarada fora do manipulador de solicitações. Como resultado, acontece que o link para ele nunca é excluído. Para corrigir este problema é bastante simples - basta transferir a declaração da matriz para o manipulador:

app.get('/bloatMyServer', (req, res) => {
  const redundantObj = {
    memory: "leaked",
    joke: "meta"
  };

  const leaks = [];

  [...Array(10000)].map(i => leaks.push(redundantObj));

  res.status(200).send({size: leaks.length})
});

Para verificar a eficácia das medidas tomadas, basta repetir as etapas acima e comparar novamente as imagens da pilha.

Sumário


Vazamentos de memória acontecem em diferentes idiomas. Em particular, naqueles que usam mecanismos de coleta de lixo. Por exemplo, em JavaScript. Geralmente não é difícil consertar um vazamento - as dificuldades reais surgem apenas quando você o procura.

Neste artigo, você se familiarizou com os conceitos básicos de gerenciamento de memória e como o gerenciamento de memória é organizado em diferentes idiomas. Aqui, reproduzimos um cenário real de vazamento de memória e descrevemos um método para solução de problemas.

Queridos leitores! Você encontrou vazamentos de memória em seus projetos da web?


All Articles