Warum beschweren sich einige Webserver über Speicherlecks, die sie verursachen?

8

Der Titel könnte ein bisschen stark sein, aber lassen Sie mich erklären, wie ich verstehe, was passiert. Ich vermute, das passierte mit Tomcat (und die angegebene Nachricht kommt von Tomcat), aber ich bin mir nicht mehr sicher.

TL; DR Am Ende gibt es eine Zusammenfassung, warum ich behaupte, dass es die Fehler der Webserver sind.

Ich könnte mich irren (aber ohne die Möglichkeit, falsch zu liegen, gäbe es keinen Grund zu fragen):

  • Eine Anwendung verwendet eine Bibliothek
  • Die Bibliothek verwendet ThreadLocal
  • Die ThreadLocal bezieht sich auf ein Objekt aus der Bibliothek
  • jedes Objekt bezieht sich auf seine ClassLoader

Der Webserver

  • bündelt seine Worker-Threads nach Effizienz
  • verleiht einer Anwendung einen beliebigen Thread
  • tut nichts besonderes (zB der Thread-Pool), wenn eine Anwendung aufhört oder erneut verteilt wird

Wenn ich es richtig verstanden habe, werden die alten "dreckigen" Threads nach einer erneuten Bereitstellung weiter verwendet. Ihre ThreadLocal s beziehen sich auf die alten Klassen, die sich auf ihre ClassLoader beziehen, die sich auf die gesamte alte Klassenhierarchie beziehen. So bleibt eine Menge Material im PermGen Raum, was im Laufe der Zeit zu einem OutOfMemoryError führt. Ist das soweit richtig?

Ich nehme zwei Dinge an:

So kostet eine vollständige Erneuerung des Threadpools bei jeder erneuten Bereitstellung einen Bruchteil einer Millisekunde einige Male pro Stunde, d. h. es gibt einen Zeitaufwand von 0.0001 * 12/3600 * 100% , d. h. 0.000033% .

Aber anstatt diesen kleinen Overhead zu akzeptieren, gibt es unzählige Probleme . Ist meine Rechnung falsch oder was übersehe ich?

Als Warnung erhalten wir die Nachricht

  

Die Webanwendung ... hat ein ThreadLocal mit dem Schlüssel vom Typ ... und einem Wert vom Typ ... erstellt, konnte es aber nicht entfernen, als die Webanwendung beendet wurde.

sollte besser als

angegeben werden
  

Der Webserver ... verwendet einen Thread-Pool, konnte ihn jedoch nach dem Beenden (oder erneuten Bereitstellen) einer Anwendung nicht erneuern.

Oder liege ich falsch? Der Zeitaufwand ist vernachlässigbar, selbst wenn alle Threads von Zeit zu Zeit neu erstellt werden. Aber ihre ThreadLocal s zu löschen, bevor sie den Anwendungen zur Verfügung gestellt werden, würde ausreichen und noch schneller sein.

Zusammenfassung

Es gibt einige echte Probleme (vor kurzem dieser ) und der Benutzer kann nichts dagegen tun. Die Bibliotheksschreiber können manchmal und manchmal nicht. IMHO die Webserver könnten es ziemlich leicht lösen. Das Ding passiert und hat eine Ursache. Also mache ich die einzige Party verantwortlich, die etwas dagegen tun könnte.

Vorschlag dafür, was der Webserver genau tun soll

Der Titel dieser Frage ist provokativer als korrekt, aber er hat seinen Zweck. Und so auch die Antwort von raphw. Diese verknüpfte Frage hat eine weitere offene Prämie.

Ich denke, die Webserver könnten es wie folgt lösen:

  • stelle sicher, dass jeder Thread irgendwann wiederverwendet (oder getötet) wird
  • speichert LastCleanupTimestamp in ThreadLocal (für neue Threads ist dies die Erstellungszeit)
  • Wenn Sie einen Thread erneut verwenden, überprüfen Sie, ob der Bereinigungszeitstempel unter einem bestimmten Schwellenwert liegt (z. B. jetzt minus einige delta , z. B. 1 Stunde)
  • Wenn ja, bereinige alle ThreadLocal s und setze eine neue LastCleanupTimestamp

Dies würde sicherstellen, dass kein solches Leck länger als delta plus der Dauer der längsten Anfrage plus der Thread-Bearbeitungszeit existiert. Die Kosten würden sich wie folgt zusammensetzen:

  • Prüfen einer einzelnen ThreadLocal (d. h. einige Nanosekunden) pro Anfrage
  • alle ThreadLocal s werden reflektiv bereinigt (d. h. einige Nanosekunden einmal pro delta pro Thread)
  • die Kosten durch das Entfernen der Daten, die möglicherweise für die Anwendung, die sie gespeichert hat, nützlich sind. Dies kann keine Anwendung unterbrechen, da keine Anwendung davon ausgehen kann, dass ein Thread die Thread-Locals enthält, die er gesetzt hat (da er nicht einmal davon ausgeht, den Thread selbst zu sehen). zB eine zwischengespeicherte DateFormat -Instanz, wenn jemand immer noch solch eine schreckliche Sache benutzt.)

Es kann ausgeschaltet werden, indem einfach die threshold-Einstellung gesetzt wird, wenn in letzter Zeit keine App-Bereitstellung aufgehoben oder neu implementiert wurde.

    
maaartinus 30.10.2013, 19:43
quelle

1 Antwort

7

TL; DR Es sind keine Webserver, die Speicherlecks verursachen. Du bist es.

Lassen Sie mich zuerst das Problem genauer angeben: ThreadLocal Variablen beziehen sich oft auf eine Instanz von Class , die von einem ClassLoader geladen wurde, das ausschließlich von der Anwendung eines Containers verwendet werden sollte. Wenn diese Anwendung nicht bereitgestellt wird, wird die ThreadLocal -Referenz verwaist. Da jede Instanz eine Referenz auf ihre Class behält und jede Class eine Referenz auf ihre ClassLoader behält und jede ClassLoader einen Verweis auf alle Klassen behält, die sie jemals geladen hat, kann der gesamte Klassenbaum der nicht-deployten Anwendung nicht Müll wird gesammelt und die JVM-Instanz leidet unter einem Speicherverlust .

Wenn Sie sich dieses Problem ansehen, können Sie Folgendes optimieren:

  • Erlaube so viele Anfragen pro Sekunde wie möglich auch während einer reploy (also halte die Antwortzeit kurz und verwende Threads aus einem Thread-Pool)
  • Stellen Sie sicher, dass die Threads sauber bleiben , indem sie die Threads verwerfen, nachdem sie verwendet waren, wenn eine erneute Bereitstellung stattgefunden hat (Patch hat also die manuelle Bereinigung vergessen)

Die meisten Entwickler von Webanwendungen würden argumentieren, dass der erste wichtiger ist, da der zweite durch das Schreiben von gutem Code erreicht werden kann. Und was passiert, wenn eine erneute Bereitstellung gleichzeitig mit lang andauernden Anfragen erfolgt? Sie können den alten Thread-Pool nicht herunterfahren, da dies laufende Anforderungen unterbrechen würde. (Es gibt kein global definiertes Maximum für die Dauer eines Request-Zyklus.) Am Ende benötigen Sie dafür ein recht komplexes Protokoll, das seine eigenen Probleme mit sich bringt.

Das ThreadLocal induzierte Leck kann jedoch vermieden werden, indem immer geschrieben wird:

%Vor%

Auf diese Weise wird Ihr Thread immer sauber . (Nebenbei bemerkt, das ist fast so, als würde man globale Variablen erstellen: Es ist fast immer eine schreckliche Idee. Es gibt einige Web-Frameworks wie zum Beispiel Wicket, die eine Menge davon nutzen. Web-Frameworks wie Dies ist schrecklich zu verwenden, wenn Sie Dinge gleichzeitig ausführen müssen und für andere Benutzer sehr unintuitiv sind.Es gibt einen Trend weg vom typischen Java Ein Thread pro Anfrage -Modell, wie es mit Play und Netty demonstriert wurde. Bleib nicht mit diesem Anti-Muster stecken, benutze ThreadLocal sparsam! Es ist fast immer ein Zeichen für schlechtes Design.)

Sie sollten außerdem beachten, dass Speicherverluste, die von ThreadLocal verursacht werden, nicht immer erkannt werden. Speicherlecks werden erkannt, indem der Worker-Thread-Pool des Webservers nach ThreadLocal Variablen durchsucht wird. Wenn eine ThreadLocal Variable gefunden wurde, zeigt die Class der Variablen ClassLoader . Wenn dieses ClassLoader oder eines seiner Eltern das der Webanwendung ist, die gerade nicht implementiert wurde, kann der Webserver einen Speicherverlust annehmen.

Stellen Sie sich jedoch vor, Sie hätten ein großes Array von String s in einer Variable ThreadLocal gespeichert. Wie kann der Webserver annehmen, dass dieses Array zu Ihrer Anwendung gehört? Das String.class wurde natürlich mit der bootstrap ClassLoader -Instanz der JVM geladen und kann keiner bestimmten Webanwendung zugeordnet werden. Durch das Entfernen des Arrays kann der Webserver einige andere Anwendungen unterbrechen, die im selben Container ausgeführt werden. Wenn der Web-Server nicht entfernt wird, kann eine große Speichermenge verloren gehen. (Diesmal ist es kein ClassLoader und seine Class es, die durchgesickert sind. Abhängig von der Größe des Arrays könnte dieses Leck jedoch sogar noch schlimmer sein.)

Und es wird schlimmer. Stellen Sie sich vor, dass Sie in Ihrer Variablen ArrayList ein ThreadLocal gespeichert haben. Das ArrayList ist Teil der Java-Standardbibliothek und wird daher mit dem System ClassLoader geladen. Auch hier kann nicht gesagt werden, dass die Instanz zu einer bestimmten Webanwendung gehört. Diesmal werden jedoch Ihre ClassLoader und all ihre Classes sowie alle Instanzen solcher Klassen, die im lokalen Thread ArrayList gespeichert sind, undicht. Diesmal kann der Webserver nicht sicher feststellen, dass ein Speicherleck aufgetreten ist, wenn festgestellt wird, dass die ClassLoader keine Speicherbereinigung war, da die Speicherbereinigung nur einer JVM empfohlen werden kann (via System#gc() ), aber nicht erzwungen wird.

Das Erneuern des Threadpools ist nicht so günstig, wie Sie vielleicht annehmen.

Eine Webanwendung kann nicht einfach alle Threads in einem Thread-Pool entfernen, wenn eine Anwendung nicht bereitgestellt wird. Was, wenn Sie einige Werte in diesen Threads gespeichert haben? Wenn eine Webanwendung einen Thread recycelt, sollte sie (ich bin mir nicht sicher, ob alle Webserver dies tun) alle nicht-leaking Thread-lokalen Variablen finden und sie im ersetzten Thread registrieren. Die Zahlen, die Sie über die Effizienz angegeben haben, würden daher nicht länger gelten.

Gleichzeitig muss der Webserver eine Logik implementieren, die das Ersetzen aller Threadpools Thread s verwaltet, was nicht für Ihre vorgeschlagene Zeitberechnung funktioniert.(Möglicherweise müssen Sie mit lang andauernden Anforderungen fertig werden - denken Sie daran, einen FTP-Server in einem Servlet-Container auszuführen - so dass diese Thread-Pool-Übergangslogik möglicherweise für eine lange Zeit aktiv ist.)

Außerdem ist ThreadLocal nicht die einzige Möglichkeit, ein Speicherleck in einem Servlet-Container zu erstellen.

Einen Abschalthaken zu setzen ist ein anderes Beispiel. (Und es ist leider eine gewöhnliche. Hier sollten Sie den heruntergefahrenen Hook manuell entfernen, wenn Ihre Anwendung nicht bereitgestellt wird. Dieses Problem würde nicht durch Verwerfen von Threads gelöst werden.) Herunterfahren-Hooks sind außerdem Instanzen von benutzerdefinierten Unterklassen von Thread das wurden immer vom Klassenlader einer Anwendung geladen.

Im Allgemeinen kann jede Anwendung, die einen Verweis auf ein Objekt behält, das von einem Kindklassenladeprogramm geladen wurde, einen Speicherverlust verursachen. (Dies ist in der Regel über Thread#getContextClassLoader() möglich.) Letztendlich ist es die Verantwortung des Entwicklers, keine Speicherverluste zu verursachen, selbst in Java-Anwendungen, in denen viele Entwickler die automatische Speicherbereinigung falsch interpretieren, da keine Speicherlecks sind . (Denken Sie an Jochua Blochs berühmtes Stack-Implementierungsbeispiel . )

Nach dieser allgemeinen Aussage möchte ich Tomcat's Speicherleckschutz kommentieren:

Tomcat verspricht nicht Ihnen, alle Speicherlecks zu erkennen , sondern deckt bestimmte Arten von solchen ab Lecks, wie sie in ihrem Wiki aufgeführt sind. Was Tomcat eigentlich macht:

  

Jeder Thread in der JVM wird untersucht und die internen Strukturen der   Thread - und ThreadLocal - Klassen werden überprüft, ob entweder die   ThreadLocal-Instanz oder der an sie gebundene Wert wurden von der geladen   WebAppClassLoader der Anwendung wird beendet.

Einige Versionen von Tomcat versuchen sogar, das Leck zu kompensieren:

  

Tomcat 6.0.24 bis 6.0.26 modifizieren interne Strukturen des JDK   (ThreadLocalMap) um den Verweis auf die ThreadLocal-Instanz zu entfernen,   aber das ist unsicher (siehe # 48895), so dass es optional wurde und   Standardmäßig deaktiviert von 6.0.27. Starten von mit Tomcat 7.0.6, dem   Die Gewinde des Pools werden erneuert, so dass das Leck sicher fixiert ist.

Sie müssen Tomcat jedoch ordnungsgemäß dafür konfigurieren. Der Wiki-Eintrag zu seinem Speicherleckschutz warnt sogar, wie man andere Anwendungen durchbrechen kann, wenn TimerThread s beteiligt sind oder wie man Speicherlecks beim Starten von eigenen Thread s oder ThreadPoolExecutor s oder bei gemeinsamen Abhängigkeiten für mehrere Web-Anwendungen.

Die von Tomcat angebotenen Aufräumarbeiten sind ein letzter Ausweg ! Es ist nichts, was Sie in Ihrem Produktionscode haben möchten.

Zusammengefasst : Es ist nicht Tomcat, das ein Speicherleck erzeugt, es ist Ihr Code. Einige Versionen von Tomcat versuchen, solche Lecks zu kompensieren, die nachweisbar sind, wenn sie dafür konfiguriert sind. Es liegt jedoch in Ihrer Verantwortung, Speicherlecks zu vermeiden. Sie sollten die Warnungen von Tomcat als Aufforderung sehen, Ihren Code zu reparieren, anstatt Tomcat neu zu konfigurieren, um Ihr Chaos zu beseitigen. Wenn Tomcat in Ihrer Anwendung Speicherlecks erkennt, sind möglicherweise sogar mehr vorhanden. Nehmen Sie also einen Heap- und Thread-Dump aus Ihrer Anwendung, und finden Sie heraus, wo sich Ihr Code befindet leckt .

    
Rafael Winterhalter 30.10.2013 21:11
quelle