Fuga de memoria del lado del servidor Nuxt mediante SSR (representación del lado del servidor)

Hola Habr! Este artículo es de lectura obligatoria para cualquier persona que trabaje con Vue SSR, en particular con Nuxt . Se trata de una pérdida de memoria cuando se usan axios .

Antecedentes


Hace medio año, comencé un proyecto con una pila VueJS + Nuxt, su peculiaridad era que los servidores Nod (Nuxt) estaban muriendo constantemente en la producción y los nuevos surgían en su lugar. Según los gráficos y registros, estaba claro que el proceso operativo del nodo alcanzó el 100% y cayó con un error de falta de memoria. En este momento, uno nuevo estaba subiendo al lugar del proceso finalizado, que tardó aproximadamente 30 segundos, esto fue suficiente para que los usuarios recibieran un error 502. Obviamente, en algún lugar del código había una pérdida de memoria que necesitaba ser encontrada.

Quiero resaltar los puntos clave de inmediato, ya que leer solo una parte de este artículo puede no responder a todas sus preguntas:

  1. Relevancia del tema
  2. Interceptores Axios
  3. runInNewContext

1. Relevancia del tema.


La primera cosa, ya que muchos de nosotros haría, empecé a buscar una solución en Internet, mis preguntas parecían algo como esto: pérdidas de memoria nodejs , pérdidas de memoria nuxt , pérdidas de memoria nuxt en la producción , etc.

Por supuesto, ninguno de los veinte problemas en stackoverflow me ayudó, pero aprendí a rastrear el uso de memoria a través de chrome: // inspeccionar. Para mi decepción, descubrí que el 90% de toda la memoria que no se limpió por alguna razón era algunas funciones de Vue como renderComponent, renderElement y otras.



1. Interceptores Axios


Rápidamente pasamos por mi tormento en busca de un problema e inmediatamente procedemos al hecho de que los axios.interceptores son los culpables de todo (lo siento, Habr, por encontrar al culpable).

Inmediatamente haga una reserva de que axios se creó así:

import baseAxios from 'axios';

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


export default axios;

Y adjunto al contexto de la aplicación de esta manera:

import axios from './index';

export default function(context) {

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

  • Después de una larga búsqueda de fugas, descubrí que si deshabilita todos los axios.interceptors, la memoria comienza a limpiarse.
  • ¿Cuál es el problema?
  • interceptor es un proxy que intercepta todas las respuestas o solicitudes y le permite ejecutar cualquier código con una respuesta (por ejemplo, para manejar errores) o agregar algo antes de enviar una solicitud a nivel mundial para todas las solicitudes y en 1 lugar, conveniente, ¿no es así? Aquí hay un ejemplo de cómo se ve (archivo 'plugins / axios / interceptor.js')

export default function({ axios }) {

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

}

Y aquí comienza la diversión. Agregamos la función de agregar un interceptor a través de complementos en nuxt.config.js

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

y nuxt automáticamente para cada nueva solicitud realiza todas las funciones de complementos, luego realiza nuxtServerInit y luego todo es como de costumbre. Es decir, para el primer usuario, creamos un interceptor en el lado del servidor, en algún lugar de nuestros componentes en asyncData o en búsqueda realizamos solicitudes, y el interceptor funciona como debería, luego entra el segundo usuario y creamos el segundo interceptor y el código dentro de la función funcionará 2 veces.

Para comprender mejor mis palabras, dibujaré un contador que se incremente con cada llamada a la función y toque 5 veces el índice.



Podemos notar que se han producido 15 llamadas, y esto es 1 + 2 + 3 + 4 + 5, además me tomé el tiempo para crear el siguiente interceptor para asegurarme que hay desafíos de aquellos que fueron creados anteriormente.

Desde la escuela, todos recordamos bien la fórmula de progresión aritmética, y la suma de 1 a n se puede escribir como n * (n + 1) / 2. Resulta que cuando entra el usuario número 1000, nuestra función se llamará 1000 veces, y en total esto ya es medio millón de llamadas, por lo que si la carga es media o alta, no se sorprenda si su servidor falla.

Solución al problema


UPD Solución n. ° 0: los comentarios describen buenas soluciones a este problema.

Solución n. ° 1: no utilice axios.interceptors.

Solución n. ° 2: todo es muy simple, debe limpiar el interceptor usted mismo, guiado por la documentación de 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);
  });

}

Esto debe hacerse solo en el lado del servidor, porque de lo contrario en el lado del cliente, después de completar con éxito cualquier primera solicitud, este interceptor dejará de ejecutarse. Hay un matiz más con el hecho de que mientras todavía estamos en el servidor y estamos procesando las solicitudes del siguiente usuario, pero puede haber varias, pero varias solicitudes, luego con la expulsión de este interceptor, todas las solicitudes, excepto la primera, no pasarán por él, en este caso para pensar de forma independiente el momento en el que necesita realizar una expulsión, la forma más fácil de hacerlo es a través de setTimeout, por ejemplo, después de 10 segundos, entonces podemos suponer que en el lado del servidor lograremos completar todas las solicitudes para el usuario actual y todas ellas se ejecutarán durante este tiempo, cuando El interceptor seguirá activo.

runInNewContext


Esta es una opción muy divertida, por lo que este error no se puede jugar localmente, pero es muy fácil de jugar en la compilación. Lee sobre esto aquí . Cuando me estaba preparando para escribir este artículo, creé el proyecto nuxt de plantilla de inicio para reproducir este problema, y ​​cómo me sorprendió que para cada usuario normal, el interceptor se ejecutara 1 vez, y no n. La cuestión es que cuando escribimos npm run dev: esta opción es verdadera de forma predeterminada, y cada vez que realizamos funciones desde complementos en el lado del servidor, el contexto es nuevo cada vez (obviamente desde el nombre del indicador), y se realiza automáticamente en la compilación falso para un mejor rendimiento en el producto, por lo que tuve que deshabilitar esta opción en nuxt.config.js


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

Conclusión


En cuanto a mí, este problema es muy grave y vale la pena prestarle especial atención. Quizás este problema concierne no solo a Vue ssr, sino también a otros, y no solo a axios, sino también a cualquier otro cliente HTTP que tenga proxies similares al interceptor. Si tiene preguntas, puede escribirme en Telegram @alexander_proydenko . Todo el código utilizado en el artículo se puede ver en github aquí .

All Articles