Vazamento de memória no lado do servidor Nuxt usando SSR (renderização no servidor)

Olá Habr! Este artigo é uma leitura obrigatória para quem trabalha com o Vue SSR, em particular com o Nuxt . Trata-se de um vazamento de memória ao usar axios .

fundo


Há meio ano, eu entrei em um projeto com uma pilha VueJS + Nuxt, sua peculiaridade era que os servidores Nod (Nuxt) estavam constantemente morrendo no produto e novos surgindo em seu lugar. A partir dos gráficos e logs, ficou claro que o processo operacional do nó atingiu 100% e caiu com um erro de falta de memória. Nesse momento, um novo ascendia ao local do processo morto, que demorava cerca de 30 segundos, o que era suficiente para os usuários obterem um erro 502. Obviamente, em algum lugar do código, havia um vazamento de memória que precisava ser encontrado.

Quero destacar os principais pontos imediatamente, pois a leitura de apenas parte deste artigo pode não responder a todas as suas perguntas:

  1. Relevância do tópico
  2. Axios interceptores
  3. runInNewContext

1. Relevância do tópico


A primeira coisa, como muitos de nós faria, eu comecei a procurar uma solução na Internet, minhas consultas parecia algo como isto: vazamentos de memória NodeJS , vazamentos de memória nuxt , vazamentos de memória nuxt na produção , etc.

Obviamente, nenhum dos vinte problemas no stackoverflow me ajudou, mas aprendi a controlar o uso da memória através do chrome: // inspecionar. Para minha decepção, descobri que 90% de toda a memória que não foi limpa por algum motivo foram algumas funções do Vue, como renderComponent, renderElement e outras.



1. Axios Interceptors


Rapidamente passamos por meu tormento em busca de um problema e imediatamente passamos ao fato de que axios.interceptors são os responsáveis ​​por tudo (desculpe-me, Habr, por encontrar os culpados).

Faça imediatamente uma reserva de que axios foi criado assim:

import baseAxios from 'axios';

const axios = baseAxios.create({
  timeout: 10000,
});


export default axios;

E anexado ao contexto do aplicativo como este:

import axios from './index';

export default function(context) {

  if(!context.axios) {
    context.axios = axios;
  }
}

  • Após uma longa pesquisa de vazamentos, descobri que, se você desativar todos os axios.interceptors, a memória começará a ser limpa.
  • Qual é o problema?
  • interceptor é um proxy que intercepta todas as respostas ou solicitações e permite executar qualquer código com uma resposta (por exemplo, para lidar com erros) ou adicionar algo antes de enviar uma solicitação globalmente para todas as solicitações e em um local, conveniente, não é? Aqui está um exemplo de como fica (arquivo 'plugins / axios / interceptor.js')

export default function({ axios }) {

  const interceptor = axios.interceptors.response.use( (response) => {
    return response;
  }, function (error) {
    //-   ,  
    return Promise.reject(error);
  });

}

E aqui começa a diversão. Adicionamos a função de adicionar um interceptor através de plugins no nuxt.config.js

  plugins: [
    { src: '~/plugins/axios/bindContext' },
    { src: '~/plugins/axios/interceptor' },
  ]

e o nuxt automaticamente para cada nova solicitação executa todas as funções de plug-ins, depois o nuxtServerInit e tudo é como de costume. Ou seja, para o primeiro usuário, criamos um interceptador no lado do servidor, em algum lugar em nossos componentes em asyncData ou em busca, fazemos solicitações, e o interceptador funciona como deveria, então o segundo usuário entra e criamos o segundo interceptador e o código dentro da função funcionará 2 vezes!

Para uma melhor compreensão das minhas palavras, desenharei um contador que aumenta a cada chamada para a função e bate 5 vezes no índice.Podemos



notar que ocorreram 15 chamadas, e isso é 1 + 2 + 3 + 4 + 5, além disso, dediquei um tempo para criar o próximo interceptador para garantir que existem desafios daqueles que foram criados anteriormente.

Na escola, todos nos lembramos bem da fórmula da progressão aritmética, e a soma de 1 a n pode ser escrita como n * (n + 1) / 2. Acontece que quando o 1000º usuário chegar, nossa função será chamada 1000 vezes, e no total já são meio milhão de chamadas; portanto, se a carga for média ou alta, não se surpreenda se o servidor travar.

Solução para o problema


UPD. Solução 0 - Os comentários descrevem boas soluções para esse problema.

Solução 1 - Não use axios.interceptors.

Solução No. 2 - Tudo é muito simples, você precisa limpar o interceptor, guiado pela documentação do axios

export default function({ axios }) {

  const interceptor = axios.interceptors.response.use( (response) => {
    
    if(process.server) {
      axios.interceptors.response.eject(interceptor);
    }
    
    return response;
  }, function (error) {
    if(process.server) {
      axios.interceptors.response.eject(interceptor);
    }
    
    return Promise.reject(error);
  });

}

Isso precisa ser feito apenas no lado do servidor, porque, caso contrário, no cliente, após a execução bem-sucedida de uma primeira solicitação, esse interceptador interromperá a execução. Há mais uma nuance no fato de que, enquanto ainda estamos no servidor e processamos as solicitações do próximo usuário, mas pode haver várias, mas várias solicitações, e com a ejeção desse interceptador, todas as solicitações, exceto a primeira, não serão atendidas, neste caso para considerar independentemente o momento em que você precisa executar uma ejeção, a maneira mais fácil de fazer isso é através de setTimeout, por exemplo, após 10 segundos, podemos assumir que no lado do servidor conseguiremos concluir todas as solicitações para o usuário atual e todas elas serão executadas durante esse período, quando o interceptador ainda estará ativo.

runInNewContext


Essa é uma opção muito divertida, pela qual esse bug não pode ser reproduzido localmente, mas é muito fácil de executar na compilação. Leia aqui . Quando eu estava me preparando para escrever este artigo, criei o projeto nuxt de modelo inicial para reproduzir esse problema e fiquei surpreso ao ver que, para cada usuário comum - o interceptador era executado uma vez, e não n. O problema é que, quando escrevemos npm run dev - essa opção é verdadeira por padrão, e toda vez que executamos funções de plug-ins no lado do servidor, o contexto é novo a cada vez (obviamente pelo nome do sinalizador), e isso é feito automaticamente na compilação false para melhor desempenho no prod, então tive que desativar essa opção no nuxt.config.js


render: {
    bundleRenderer: {
      runInNewContext: false,
    },
  },

Conclusão


Quanto a mim, esse problema é muito grave e vale a pena prestar atenção especial a ele. Talvez esse problema diga respeito não apenas ao Vue ssr, mas também a outros, e não apenas axios, mas também a outros clientes HTTP que possuam proxies semelhantes ao interceptor. Se você tiver alguma dúvida, pode me escrever no Telegram @alexander_proydenko . Todo o código usado no artigo pode ser visto no github aqui .

All Articles