Neues vom Speicherfresser

In der Vergangenheit hat mich ein speicherfressendes Monster regelmäßig um den Schlaf gebracht. Die auf der jeweilig betroffenen Plattform laufenden PPP Sitzungen wurden, wenn es zu wild wurde, zwangsgetrennt. Besonders unschön war daran, daß sich das Problem schlicht nicht eingrenzen lies. Aber das scheint nun vorbei.

Das eigentliche Problem war schon länger vermutet worden: Der FreeBSD Kernel kann es nicht ab, wenn während der Laufzeit Netzwerkinterfaces gelöscht werden. Dies ist aber bei einem LNS, der ständig Verbindungen auf- und abbaut, der Normalfall.

NetGraph seziert

Die Interfaces, um die es geht, sind NetGraph-Knoten. Die erste Frage ist, ob diese Knoten überhaupt allein lebensfähig sind.

Schließlich ist es Philosophie von NetGraph, ein rein funktionales Datenverarbeitungframework zu bilden, das Netzwerkpakete beim Durchlauf durch den Kernel leicht modifiziert und weiterreicht. Ohne eine Einbindung in einen Datenfluß ist ein NetGraph-Knoten deswegen praktisch sinnfrei. Neue Netgraph-Knoten können ausschließlich durch Erweiterung eines bestehenden Graphes angelegt werden.

Ein Blick in den Code zeigt aber, daß ein ng_iface-Knoten sich nicht automatisch löscht, wenn er die letzten Verbindungen verliert. Als Übergang vom Kernelbereich in die Welt der Programme, ist er auch alleinstehend sinnvoll.

Die nächste Frage besteht darin, ob sich der MPD darauf verläßt, daß das Interface gelöscht und bei Bedarf wieder neu erzeugt werden kann. Genau genommen ist zu prüfen, welche Standardwerte im Laufe des Lebens eines solchen Interfaces verändert und ob diese vor der Vernichtung des Knotens wieder zurückgesetzt werden.

Wird der Interface-Knoten sauber zuückgelassen, kann man ihn aufheben anstatt ihn zu löschen. Das  Löschen des Interfaces entfällt somit. Beim nächsten Bedarf nach einem Interface kann ein "altes" Interface recycelt werden.

MPD Handling

Die Kontrolle all dieser Eigenschaften im Kernel- und MPD-Code zeigt ermutigendes.

Zum einen generiert sich der MPD die Interface-Knoten schon als alleinstehende Objekte und verbindet sie erst später. Dies kommt mir jetzt natürlich sehr entgegen.

Zum anderen räumt der MPD sehr ordentlich die Konfiguration des Interfaces auf. Selbst Umbenennungen des Knotens werden vor de Löschung wieder rückgängig gemacht.

Desweiteren ist das Erzeugen und Löschen von Netgraph-Knoten bereits in eine extra Quelltextdatei absepariert. Allerdings ist dort nicht mehr feststellbar, welcher Knotentyp nun konkret gelöscht werden soll. Wollte man an dieser Stelle das Löschen von Interface-Knoten verhindern, müßte man vorab noch den Knoten inspizieren.

Andererseits werden Interface-Knoten ausschließlich im Bundle-Layer des MPD erzeugt und gelöscht. Ja, das muß man kontrollieren!

Also setze ich genau da an:

--- mpd-orig/src/bund.c   2013-01-22 17:50:19.000000000 +0100
+++ mpd-5.6-lutz/src/bund.c      2013-09-24 22:08:11.000000000 +0200
@@ -133,3 +133,11 @@
     { 0,       0,                      NULL            },
   };

+  struct unused_interface {
+     char name[IFNAMSIZ];
+     STAILQ_ENTRY(unused_interface) elem;
+  };
+  STAILQ_HEAD(unused_interfaces, unused_interface) unused_interfaces =
+    STAILQ_HEAD_INITIALIZER(unused_interfaces);
+
+

Zuerst einmal wird eine Warteschlange für unbenutzte Interfaces eingeführt. Diese ist global, wie auch die Verwendung der Interfaces global, d.h. unabhängig von den einzelenen L2TP-Tunneln ist.

Die Standard-Datenstruktur STAILQ stellt eine einfach verkettete Liste dar, die man an einem Ende einfach befüllen und leeren und am anderen Ende einfach befüllen kann. Mit dieser Datenstruktur werden neu gelöschte Interfaces hinten angehängt und bei Bedarf unbenutzte Interfaces vorne wieder herausgenommen. Die Interfaces bleiben also so lange wie möglich unbenutzt.

Die Datenstruktur ist allerdings nicht threadsicher. Deswegen wird sie bei jedem Zugriff von einem schon vorhanden Mutex geschützt.

@@ -1669,11 +1693,19 @@
     int                        newIface = 0;
     int                        newPpp = 0;

-    /* Create new iface node */
-    if (NgFuncCreateIface(b,
-       b->iface.ifname, sizeof(b->iface.ifname)) < 0) {
-      Log(LG_ERR, ("[%s] can't create netgraph interface", b->name));
-      goto fail;
+    MUTEX_LOCK(gNgMutex);
+    if(!STAILQ_EMPTY(&unused_interfaces)) {
+       struct unused_interface *o = STAILQ_FIRST(&unused_interfaces);
+       STAILQ_REMOVE_HEAD(&unused_interfaces, elem);
+       strlcpy(b->iface.ifname, o->name, sizeof(b->iface.ifname));
+       Freee(o);
+    } else {
+       /* Create new iface node */
+       if (NgFuncCreateIface(b,
+          b->iface.ifname, sizeof(b->iface.ifname)) < 0) {
+         Log(LG_ERR, ("[%s] can't create netgraph interface", b->name));
+         goto fail;
+       }
     }
     strlcpy(b->iface.ngname, b->iface.ifname, sizeof(b->iface.ngname));
     newIface = 1;

Wird ein neues Interface benötigt, schaut man zuerst nach, ob es aktuell unbenutzte Interfaces gibt. Wenn das der Fall ist, wird das älteste unbenutzte Interface genommen. Andernfalls generiert man mit dem schon vorhandenen Code ein neues Interface.

@@ -1725,13 +1759,44 @@
 {
     char       path[NG_PATHSIZ];

+    MUTEX_LOCK(gNgMutex);
     if (iface) {
-       snprintf(path, sizeof(path), "%s:", b->iface.ngname);
-       NgFuncShutdownNode(gLinksCsock, b->name, path);
+       struct unused_interface *o = Malloc(MB_BUND, sizeof(*o));
+       if(o) {
+         strlcpy(o->name, b->iface.ngname, sizeof(o->name));
+         STAILQ_INSERT_TAIL(&unused_interfaces, o, elem);
+       } else {
+         snprintf(path, sizeof(path), "%s:", b->iface.ngname);
+         NgFuncShutdownNode(gLinksCsock, b->name, path);
+       }
     }
     if (ppp) {
        snprintf(path, sizeof(path), "[%x]:", b->nodeID);
        NgFuncShutdownNode(gLinksCsock, b->name, path);
     }
     b->hook[0] = 0;
+    MUTEX_UNLOCK(gNgMutex);
+}

Ganz anders sieht es dagegen beim Löschen von Interfaces aus. Das zu löschende Interface wird als neues Element an die Warteschlage angehängt. Scheitert dieses, wird es regulär gelöscht.

Aber was ist mit den aufgehobenen Interfaces, wenn der Daemon sich beendet? Er kann sie nicht liegen lassen, weil es systemweite Interfaces sind. Aus der main.c müssen also Initializierungs- und Aufräumfunktionen aufgerufen werden. Eine weitere in die Liste der schon vorhandenen Funktionsaufrufe einzufügen, ist nicht schwer.

+int
+BundsInit(void)
+{
+   return 0;
+}
+
+void
+BundsShutdown(void)
+{
+   struct unused_interface *o;
+   char        path[NG_PATHSIZ];
+
+   MUTEX_LOCK(gNgMutex);
+   while(NULL != (o = STAILQ_FIRST(&unused_interfaces))) {
+      STAILQ_REMOVE_HEAD(&unused_interfaces, elem);
+      snprintf(path, sizeof(path), "%s:", o->name);
+      NgFuncShutdownNode(gLinksCsock, o->name, path);
+      Freee(o);
+   }
+   MUTEX_UNLOCK(gNgMutex);
 }

Der Startup ist trivial, weil schon beim Anlegen des Speicherbereiches die korrekten Initialisierungen hinterlegt wurden. Beim Herunterfahren dagegen muß all das nachgeholt werden, was bisher versäumt wurde. Interface für Interface wird gelöscht.

Stellt das nicht genau den Fall dar, bei dem bisher die Probleme auftraten?

Ja, das tut es. Aber zu diesem Zeitpunkt gibt es keine offenen PPP Verbindungen mehr. Es gibt nicht einmal mehr die L2TP Tunnel, die neue Verbindungen zuführen könnten. Sollte der Server jetzt abstürzen und booten, wären keine Kunden mehr betroffen.

Andererseits haben die Interfaces inzwischen genig Ruhe gehabt, um nicht mehr vor den ARP und IPv6 Neighbor Caches erfaßt zu sein. Sie sind auch nicht mehr Bestandteil der aktiven Routingtabellen. Über diese Interfaces laufen keine Pakte mehr, die genattet werden müßten. Diese Interfaces sind einfach schon eine ganze Weile "down".

Im Test stellte sich heraus:

  • Es gibt keine Speicherprobleme mehr. Kernelspeicher bleibt unüberschrieben.
  • Die Maschinen laufen durch.
  • Es gibt keine Speicherlecks, der RAM-Bedarf bleibt konstant.
  • Es gibt keine Probleme mit den Kunden.

Post a comment

Related content