Fuite de mémoire côté serveur Nuxt à l'aide de SSR (rendu côté serveur)

Bonjour, Habr! Cet article est une lecture incontournable pour tous ceux qui travaillent avec Vue SSR, en particulier avec Nuxt . Il s'agit d'une fuite de mémoire lors de l'utilisation d' axios .

Contexte


Il y a six mois, j'ai eu un projet avec une pile VueJS + Nuxt, sa particularité était que les serveurs Nod (Nuxt) mouraient constamment dans la prod et que de nouveaux montaient à leur place. Selon les graphiques et les journaux, il était clair que le processus de traitement des nœuds a atteint 100% et il est tombé avec une erreur de mémoire insuffisante. À ce moment, un nouveau se levait à la place du processus tué, ce qui prenait environ 30 secondes, cela suffisait aux utilisateurs pour obtenir une erreur 502. De toute évidence, quelque part dans le code, il y avait une fuite de mémoire qui devait être trouvée.

Je veux souligner tout de suite les points clés, car la lecture d'une partie seulement de cet article peut ne pas répondre à toutes vos questions:

  1. Pertinence du sujet
  2. Intercepteurs Axios
  3. runInNewContext

1. Pertinence du sujet


La première chose, comme beaucoup d' entre nous faire, je commencé à chercher une solution sur Internet, mes questions avaient l' air quelque chose comme ceci: les fuites de mémoire NodeJS , les fuites de mémoire nuxt , les fuites de mémoire nuxt dans la production , etc.

Bien sûr, aucun des vingt problèmes sur stackoverflow ne m'a aidé, mais j'ai appris à suivre l'utilisation de la mémoire via chrome: // inspect. À ma grande déception, j'ai trouvé que 90% de toute la mémoire qui n'était pas nettoyée pour une raison quelconque était des fonctions de Vue comme renderComponent, renderElement et d'autres.



1. Intercepteurs Axios


Nous traversons rapidement mon tourment à la recherche d'un problème et procédons immédiatement au fait que les axios.interceptors sont à blâmer pour tout (je suis désolé, Habr, d'avoir trouvé le coupable).

Faites immédiatement une réservation que axios a été créé comme ceci:

import baseAxios from 'axios';

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


export default axios;

Et attaché au contexte d'application comme ceci:

import axios from './index';

export default function(context) {

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

  • Après une longue recherche de fuites, j'ai constaté que si vous désactivez tous les axios.interceptors, la mémoire commence à être nettoyée.
  • Que se passe-t-il?
  • interceptor est un proxy qui intercepte toutes les réponses ou demandes et vous permet d'exécuter n'importe quel code avec une réponse (par exemple, pour gérer les erreurs) ou d'ajouter quelque chose avant d'envoyer une demande globalement pour toutes les demandes et en 1 endroit, pratique, n'est-ce pas? Voici un exemple de son apparence (fichier 'plugins / axios / interceptor.js')

export default function({ axios }) {

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

}

Et ici, le plaisir commence. Nous ajoutons la fonction d'ajout d'un intercepteur via des plugins dans nuxt.config.js

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

et nuxt automatiquement pour chaque nouvelle requête exécute toutes les fonctions des plugins, puis fait nuxtServerInit et tout est comme d'habitude. Autrement dit, pour le premier utilisateur, nous créons un intercepteur côté serveur, quelque part dans nos composants dans asyncData ou dans fetch, nous faisons des demandes, et l'intercepteur fonctionne comme il se doit, puis le deuxième utilisateur entre et nous créons le deuxième intercepteur et le code à l'intérieur de la fonction fonctionnera 2 fois!

Pour une meilleure compréhension de mes mots, je vais dessiner un compteur qui s'incrémente à chaque appel à la fonction et 5 fois sur l'index.



Nous pouvons remarquer que 15 appels ont eu lieu, et c'est 1 + 2 + 3 + 4 + 5, j'ai également pris le temps de créer le prochain intercepteur pour m'assurer qu'il y a des défis de ceux qui ont été créés plus tôt.

De l'école, nous nous souvenons tous bien de la formule de progression arithmétique, et la somme de 1 à n peut être écrite comme n * (n + 1) / 2. Il s'avère que lorsque le 1000e utilisateur entre, notre fonction sera appelée 1000 fois, et au total c'est déjà un demi-million d'appels, donc si la charge est moyenne ou élevée, alors ne soyez pas surpris si votre serveur tombe en panne.

Solution au problème


UPD. Solution # 0 - Les commentaires décrivent de bonnes solutions à ce problème.

Solution # 1 - N'utilisez pas axios.interceptors.

Solution n ° 2 - Tout est très simple, vous devez nettoyer l'intercepteur par vous-même, guidé par la documentation 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);
  });

}

Cela doit être fait uniquement côté serveur, car sinon côté client, après la réussite de toute première demande, cet intercepteur cessera de s'exécuter. Il y a une nuance de plus avec le fait que pendant que nous sommes toujours sur le serveur et que nous traitons les demandes de l'utilisateur suivant, mais il peut y avoir plusieurs, mais plusieurs demandes, puis avec l'éjection de cet intercepteur, toutes les demandes sauf la première ne passeront pas par là, dans ce cas pour réfléchir indépendamment au moment où vous devez effectuer une éjection, la manière la plus simple de le faire est de définir setTimeout, par exemple, après 10 secondes, puis nous pouvons supposer que côté serveur, nous parviendrons à traiter toutes les demandes de l'utilisateur actuel et toutes seront exécutées pendant ce temps, lorsque l'intercepteur sera toujours actif.

runInNewContext


C'est une option très amusante, à cause de laquelle ce bogue ne peut pas être reproduit localement, mais il est très facilement reproduit dans la build. Lisez à ce sujet ici . Quand je me préparais à écrire cet article, j'ai créé le projet nuxt de modèle de démarrage pour reproduire ce problème, et comment j'ai été surpris que pour chaque utilisateur régulier - interceptor ait été exécuté 1 fois, et non n. Le fait est que lorsque nous écrivons npm run dev - cette option est vraie par défaut, et chaque fois que nous effectuons des fonctions à partir de plugins côté serveur, le contexte est nouveau à chaque fois (évidemment à partir du nom du drapeau), et cela se fait automatiquement dans la construction false pour de meilleures performances dans la prod, j'ai donc dû désactiver cette option dans nuxt.config.js


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

Conclusion


Pour moi, ce problème est très grave, et il vaut la peine d'y prêter une attention particulière. Peut-être que ce problème concerne non seulement Vue ssr, mais aussi d'autres, et non seulement axios, mais également tous les autres clients HTTP qui ont des proxys similaires à interceptor. Si vous avez des questions, vous pouvez m'écrire sur Telegram @alexander_proydenko . Tout le code utilisé dans l'article peut être consulté sur github ici .

All Articles