Nuxt Server-Side Memory Leak mit SSR (Server Side Rendering)

Hallo Habr! Dieser Artikel ist ein Muss für alle, die mit Vue SSR arbeiten, insbesondere mit Nuxt . Hierbei handelt es sich um einen Speicherverlust bei Verwendung von Axios .

Hintergrund


Vor einem halben Jahr hatte ich ein Projekt mit einem VueJS + Nuxt-Stack. Die Besonderheit war, dass die Nod-Server (Nuxt) ständig im Produkt starben und neue an ihrer Stelle auftauchten. Den Grafiken und Protokollen zufolge war klar, dass der Knotenprozess 100% erreichte und mit einem Speicherfehler fehlte. Zu diesem Zeitpunkt stieg ein neuer an die Stelle des getöteten Prozesses, was ungefähr 30 Sekunden dauerte. Dies war genug, damit Benutzer einen 502-Fehler erhielten. Offensichtlich gab es irgendwo im Code einen Speicherverlust, der gefunden werden musste.

Ich möchte sofort wichtige Punkte hervorheben, da das Lesen nur eines Teils dieses Artikels möglicherweise nicht alle Ihre Fragen beantwortet:

  1. Relevanz des Themas
  2. Axios Abfangjäger
  3. runInNewContext

1. Relevanz des Themas


Als erstes suchte ich, wie viele von uns, im Internet nach einer Lösung. Meine Anfragen sahen ungefähr so aus: NodeJS-Speicherlecks , Nuxt-Speicherlecks , Nuxt-Speicherlecks in der Produktion usw.

Natürlich hat mir keines der zwanzig Probleme beim Stackoverflow geholfen, aber ich habe gelernt, wie man die Speichernutzung durch chrome: // inspect verfolgt. Zu meiner Enttäuschung stellte ich fest, dass 90% des gesamten Speichers, der aus irgendeinem Grund nicht bereinigt wurde, einige Funktionen von Vue wie renderComponent, renderElement und andere waren.



1. Axios Interceptors


Wir gehen schnell meine Qualen auf der Suche nach einem Problem durch und gehen sofort zu der Tatsache über, dass axios.interceptors für alles verantwortlich sind (es tut mir leid, Habr, dass ich die Schuld gefunden habe).

Machen Sie sofort eine Reservierung, dass Axios wie folgt erstellt wurde:

import baseAxios from 'axios';

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


export default axios;

Und wie folgt an den Anwendungskontext angehängt:

import axios from './index';

export default function(context) {

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

  • Nach einer langen Suche nach Lecks stellte ich fest, dass der Speicher bereinigt wird, wenn Sie alle axios.interceptors deaktivieren.
  • Was ist da los?
  • interceptor ist ein Proxy, der alle Antworten oder Anfragen abfängt und es Ihnen ermöglicht, jeden Code mit einer Antwort auszuführen (z. B. um Fehler zu behandeln) oder etwas hinzuzufügen, bevor Sie eine Anfrage global für alle Anfragen senden, und zwar an einer Stelle, praktisch, nicht wahr? Hier ist ein Beispiel, wie es aussieht (Datei 'plugins / axios / interceptor.js')

export default function({ axios }) {

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

}

Und hier beginnt der Spaß. Wir fügen die Funktion des Hinzufügens eines Interceptors über Plugins in nuxt.config.js hinzu

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

und nuxt führt automatisch für jede neue Anfrage alle Plugin-Funktionen aus, führt dann nuxtServerInit und dann alles wie gewohnt aus. Das heißt, für den ersten Benutzer erstellen wir einen Interceptor auf der Serverseite, irgendwo in unseren Komponenten in asyncData oder beim Abrufen stellen wir Anforderungen, und der Interceptor funktioniert wie es sollte, dann kommt der zweite Benutzer herein und wir erstellen den zweiten Interceptor und der Code innerhalb der Funktion funktioniert zweimal!

Zum besseren Verständnis meiner Wörter werde ich einen Zähler zeichnen, der mit jedem Aufruf der Funktion und 5-maligem Anklopfen des Index erhöht wird.



Wir können feststellen, dass 15 Aufrufe aufgetreten sind, und dies ist 1 + 2 + 3 + 4 + 5. Ich habe mir zusätzlich die Zeit genommen, den nächsten Interceptor zu erstellen, um sicherzugehen dass es Herausforderungen von denen gibt, die früher geschaffen wurden.

Von der Schule an erinnern wir uns alle gut an die Formel des arithmetischen Fortschritts, und die Summe von 1 bis n kann als n * (n + 1) / 2 geschrieben werden. Es stellt sich heraus, dass unsere Funktion 1000-mal und insgesamt aufgerufen wird, wenn der 1000. Benutzer hereinkommt Dies sind bereits eine halbe Million Anrufe. Wenn die Last also mittel oder hoch ist, wundern Sie sich nicht, wenn Ihr Server abstürzt.

Lösung


UPD. Lösung 0 - Die Kommentare beschreiben gute Lösungen für dieses Problem.

Lösung 1 - Verwenden Sie keine axios.interceptors.

Lösung Nr. 2 - Alles ist sehr einfach. Sie müssen den Abfangjäger anhand der Axios-Dokumentation selbst reinigen

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);
  });

}

Dies muss nur auf der Serverseite erfolgen, da andernfalls auf der Clientseite nach erfolgreichem Abschluss einer ersten Anforderung die Ausführung dieses Interceptors beendet wird. Es gibt noch eine Nuance mit der Tatsache, dass, während wir uns noch auf dem Server befinden und die Anforderungen des nächsten Benutzers verarbeiten, es jedoch möglicherweise mehrere, aber mehrere Anforderungen gibt. Mit dem Auswerfen dieses Interceptors werden in diesem Fall alle Anforderungen außer der ersten nicht durchlaufen Um unabhängig über den Moment nachzudenken, in dem Sie einen Auswurf durchführen müssen, können Sie dies am einfachsten über setTimeout tun. Nach 10 Sekunden können wir davon ausgehen, dass wir auf der Serverseite alle Anforderungen für den aktuellen Benutzer ausführen können und alle in dieser Zeit ausgeführt werden Der Abfangjäger ist weiterhin aktiv.

runInNewContext


Dies ist eine sehr lustige Option, aufgrund derer dieser Fehler nicht lokal gespielt werden kann, aber es ist sehr einfach, im Build zu spielen. Lesen Sie hier darüber . Als ich mich darauf vorbereitete, diesen Artikel zu schreiben, erstellte ich das Starter-Template-Nuxt-Projekt, um dieses Problem zu reproduzieren, und wie ich überrascht war, dass für jeden regulären Benutzer der Interceptor einmal ausgeführt wurde und nicht n. Die Sache ist, wenn wir npm run dev schreiben - diese Option ist standardmäßig wahr und jedes Mal, wenn wir Funktionen von Plugins auf der Serverseite ausführen, ist der Kontext jedes Mal neu (offensichtlich vom Flaggennamen) und wird im Build automatisch ausgeführt false für eine bessere Leistung im Produkt, daher musste ich diese Option in nuxt.config.js deaktivieren


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

Fazit


Für mich ist dieses Problem sehr ernst und es lohnt sich, ihm besondere Aufmerksamkeit zu schenken. Möglicherweise betrifft dieses Problem nicht nur Vue ssr, sondern auch andere und nicht nur Axios, sondern auch alle anderen HTTP-Clients, deren Proxys dem Interceptor ähnlich sind. Wenn Sie Fragen haben, können Sie mir unter Telegramm @alexander_proydenko schreiben . Der gesamte im Artikel verwendete Code kann hier auf github eingesehen werden .

All Articles