Java GC und Native Socket-Ressourcen

Hallo zusammen,

ich kämpfe gerade mit folgendem Problem und finde keine Antwort:

Wir haben hier eine Java Consolenanwendung (läuft unter SuSE Linux Enterpreise Server 11), welche einen proprietären Dienst über einen Socketserver zur Verfügung stellt.

Die Anwendung belegt auch nach Tagen nur rund 7MB von einem maximal erlaubten Heap von rund 200MB.
Das Problem das wir nun haben:

Nach unbestimmter Zeit gehen dem Prozess die Filedescriptoren (FD) aus. Wenn man in /proc//fd schaut, dann gibt’s da nach 24 Stunden locker mal über 1000 Socket-FDs, während netstat nur ~40 Socketverbindungen für den Prozess anzeigt.

Wenn ich über JConsole von außen manuell den GC triggere, so bricht die Anzahl der offenen Socket FDs wieder auf rund 110, während die Anzahl der Socketverbindungen noch bei rund 40 liegt.

Mit einem Profiler hab ich schon die Sache untersucht: Sieht, mal abgesehen davon dass sich immer viele Socket-Objekte anhäufen, die dann vom GC wieder abgerüumt werden können, nicht besonders wild aus. Alles im grünen Bereich.

Meine Vermutung ist jetzt:

Der Dienst macht in kurzer Zeit sehr viele Verbindungen auf und wieder zu. Aber der Heap ist damit quasi gar nicht belastet, weshalb sich verdammt viele, unbenutzte Socketobjekte (und daran auch FDs) anhäufen. Der Java Seite macht das nix. Aber auf nativer Seite gammeln mit der Zeit zu viele FD rum, so dass das Limit von 4k FD nach 1-2 Tagen erreicht ist und nix mehr geht.

Triggere ich den GC manuell, so wird abgeräumt. Aber von alleine scheint das, aufgrund des wirklich niedrigen Heap-Bedarfs zu selten zu passieren.

Was sagt ihr zu dieser These? Wie verhindert man das, ohne selbst den GC hin und wieder aufzurufen?!

Gruß
Alex

P.S. Verwenden aktuell noch Java 1.7.0 Update 10. EIn Update auf Update 45 steht noch aus, dauert aber noch, da noch viel zu testen ist.

Sieht für mich danach aus, dass du die Sockets nach Verwendung einfach vergammeln lässt und vergessen hast, sie ordentlich zu closen. So leben sie im RAM weiter und blocken Resourcen. Das ist ein Klassiker. Resourcen müssen freigegeben werden, wenn sie nicht mehr benötigt werden und nicht erst, wenn der GC irgendwann mal finalize aufruft und die Objekte abräumt.

Theoretisch ja. Praktisch find’ ich da nix.

Den Code hab ich shcon durchforstet. Wenn ich nix übersehen hab, dann wird immer brav geclosed + im catch/finally-Block mit einem close() aufgeräumt.

Der Yourkit Java Profiler hat sogenannte “Probes” mit denen er sich an verschiedene Stellen im Code hängen kann. So auch an Sockets. Damit sehe ich

a) wo an welcher Stelle mit welchem Stack ein Socket geöffnet wird
b) wo an welcher Stelle mit welchem Stack ein Socket geschlossen wird

Und ich seh da dass Sockets auf gehen und wieder geschlossen werden. Deine Theorie zufolge, müssten sich hier Sockets die geöffnet, aber nicht geschlossen werden häufen (solange bis ich den GC manuell auslöse). Aber das ist irgendwie nicht der Fall.

Ich steh’ also etwas vor einem Rätsel:

  • Eine Stelle wo ein close() fehlt hab ich nicht gefunden
  • Der Profiler sagt dass alle geöffneten Sockets brav wieder beschlossen werden

… Ich dreh noch am Rad. Suche seit 2 Tagen ohne Ergebnis.

  • Alex

Du hast ja bereits sehr tief reingeschaut. Deswegen mögen Dir die folgenden Fragen trivial erscheinen. Nur der Vollständigkeit halber:

  • Gilt die Aussage “Sockets werden ordnungsgemäß geclosed” auch für die Input-/Outputstreams, die du über die Sockets verwendest?

Und für eine etwas breitere Betrachtung der Situation:

  • Läuft das komplette Konsolenprogramm in einer JVM? Oder werden von diesem aus andere Prozesse gestartet?
  • Wie hast du netstat aufgerufen? Werden dort wirklich alle Sockets angezeigt (ich denke hier an halb offene Verbindungen und Time Waits)?

Neben den Fragen von nillehammer: Dein Erklaerungsversuch klingt ueberzeugend. Aehnliches Verhalten (nicht mit Sockets) hab ich auch schon beobachtet. Du koenntest der Anwendung schlicht weniger Speicher geben, also nur 15MB oder so. Und dann schauen, ob der GC haeufiger taetig wird und es somit nicht zu den vielen offenen Sockets kommt. Dann haettest du einen Beweis, ob deine Theorie stimmt :wink:

Ansonsten kannst du mal an den GC Optionen spielen. Noch was zum Lesen

Gilt die Aussage “Sockets werden ordnungsgemäß geclosed” auch für die Input-/Outputstreams, die du über die Sockets verwendest?

Wenn ich den JRE Source richtig interpretiert habe, dann wird für die In/Out Streams ein Zähler auf dem FileDescriptor inkrementiert/dekrementiert. Von daher wäre es schon wichtig die Streams korrekt zu schließen (werden sie, soweit ich den Code korrekt gelesen habe auch). Aber wenn der Socket geschlossen wird, wird auch der FD vollständig frei gegeben. Denn ein Stream ohne Socket macht ja keinen Sinn. Von daher:

Soweit ich das gesehen habe wird alles korrekt geschlossen. Kann’s aber nicht zu 100% sagen. Dennoch sollte es keine Probleme geben wenn der Socket korrekt geschlossen wird. Was eigentlich der Fall sein sollte.

  • Läuft das komplette Konsolenprogramm in einer JVM? Oder werden von diesem aus andere Prozesse gestartet?

Jepp. In einer VM. Es geht auch ausschließlich um Socket-FDs, nicht um File- oder Pipe-FDs oder sonstige FDs.

  • Wie hast du netstat aufgerufen? Werden dort wirklich alle Sockets angezeigt (ich denke hier an halb offene Verbindungen und Time Waits)?
netstat -a -p -n | grep <pid>

Das sollte alle Socket-Zustände zeigen.

Dein Erklaerungsversuch klingt ueberzeugend. Aehnliches Verhalten (nicht mit Sockets) hab ich auch schon beobachtet.

Kannst du das etwas näher erläutern? Was sind so deine Erfahrungen dazu? Mein Chef will meine Argumentation noch nicht ganz glauben und vermutet ein fehlendes close(). Kann aber wie gesagt keins finden und der Profiler (dem er auch nicht 100% über den Weg traut) findet auch nix.

Die Idee mit dem weniger Speicher: Da muss ich klären ob das eine allgemeingültige Option für uns ist. Die Anwendung kommt in vielerlei Szenarien zum Einsatz. Und da könnte es durchaus sein, dass in einer anderen Installation weniger Speicher nicht wirklich sinnvoll ist. AKtuell ist das nur auf einem Kundensystem zu sehen.

An GC feintuning hab ich auch schon gedacht. Spiele gerade in einem anderen Fall mit dem G1 GC um zu schauen ob der dort des Rätsels Lösung ist. Vielleicht würde der auch hier helfen.

[update]
Hab nochmal den JRE Source durchforstet… PlainSocket ist die zentrale Basis-Implementierung eines Sockets. Dort findet man im JavaDoc der finalize() Methode: “Cleans up if the user forgets to close it.”
Da wird dann close() aufgerufen. Das heisst im Umkehrschluss: Wenn man selbst überall close() aufruft, dann sollte im finalize() nix mehr großartig geclosed werden können… Also vllt. doch ein close() vergessen? Aber warum taucht das im Profiler nirgends auf? --> Rätsel!

Hab nochmal den JRE Source durchforstet… PlainSocket ist die zentrale Basis-Implementierung eines Sockets. Dort findet man im JavaDoc der finalize() Methode: “Cleans up if the user forgets to close it.”

Setz doch an die entsprechende Stelle in finalize einen Breakpoint, bzw. hake Deinen Profiler da ein. Sollte das was ergeben, weißt Du zumindest, welches Socket nicht ordnungsgemäß geclosed wurde. Vielleicht reicht Dir das, um rauszufinden, wo ein close vergessen wurde. Wenn es keine Ergebnisse gibt, ist das vielleicht wenigstens ein Beweis, dem Dein Chef glaubt…

Das ist eine gute Idee. Hätte ich auch drauf kommen können. Nur weiß ich noch nicht ob ich damit drauf komme wo das close() fehlt. Die Sockets sind alle gleicher Art und durchlaufen nur unterschiedliche Wege im Programm. Dem Socket-Objekt kann ich den bisherigen Verlauf nicht wirklich ansehen :frowning:

Gerade darauf gestoßen:

Von der Materia an sich hab ich jetzt nicht viel Ahnung, aber mal noch ein Ansatz: wann werden denn die FDs wieder frei gegeben? Häng dich doch mal an den Code der das tut und finde heraus, warum er beim Aufruf des GCs ausgeführt wird, nicht aber beim manuellen closen.
Außerdem kannst du dem GC vielleicht die Arbeit etwas erleichtern, in dem du soft references von Hand nullst oder weak references verwendest. (solltest du welche verwenden).

Ich glaube @TheDarkRose hat die Ursache für das Problem bereits gefunden. Der GC wird nicht aufgerufen weil noch so viel Heap übrig ist und dementsprechend werden die FDs nicht entsorgt.

Wenn die Vermutung von TheDarkRose stimmt, kannst du mit entsprechenden GC-Settings das Problem lösen. Auf Stackoverflow gibt’s eine dazu passende Fragemit dieser Antwort:

-XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=N

where N is roughly what percentage occupied will trigger a full GC. Default is ~92, which is why I am seeing full GC’s at 9% free. Switching it to 65 worked for my use case. A full GC happens ~ 35% free now.

Am besten mal ausprobieren ob das bei dir auch hilft.

Ich hab die Ursache mittlerweile gefunden…

Hab einfach einen Client gebastelt der eine Socketverbindung nach der anderen auf den Server aufgemacht hat, irgend einen Müll geschickt hat, und dann, ohne die Verbindung zu schließen zum nächsten Socket übergegangen ist.

Dabei sind mir dann auf dem Server Exceptions um die Ohren geflogen die Rückschlüsse auf die entsprechende Codestelle gegeben haben: Die Verbindungen die noch halboffen in der Luft hingen wurden irgendwann von einem “AvailabilityCheck” überprüft. Das ging bei vielen Verbindungen schief. Es wurde eine Exception ausgelöst, gefangen und geloggt. Aber das war’s dann auch schon. Die Verbindung war dann tot, wurde aber von der Anwendung nicht abgeräumt und geclosed.

Daneben ist bei bei einer weiteren Code-Analyse (hatte da mal ein paar Tage Abstand gebraucht…) noch eine Stelle aufgefallen wo man im Fehlerfall sauber hätte aufräumen müssen.

Merke: Auch wenn eine IOException auftritt, diese gefangen und von der Anwendung weitgehend behandelt wird: Ein close() ist nach wie vor von Nöten um die Nativen Resourcen sauber abzuräumen. Geht auch ohne. Aber dann muss der GC rechtzeitig anspringen. Und besser man behandelt es selbst statt es dem GC zu überlassen.

Gruß
Alex

Welche Java-Version wurde verwendet für euer Konsolen-Tool? Sollten solche Socket-Geschichten nicht mittlerweile AutoCloseable implementieren?

Die Software muss aktuell noch auf Java 1.6 ausgelegt sein. Deshalb können wir AutoCloseable noch nicht einsetzen :frowning: