Pimp my MPD

Für eine große Anzahl an DSL Kunden wurde mir für die LNS Funktionalität die Software MPD auf FreeBSD empfohlen. Ein Blick in die Suchergebnisse zeigt, dass vor allem in Russland wirklich große Installationen damit gefahren werden. Als Alternative stand OpenL2TP im Raum. Allerdings handelt OpenL2TP die PPP Sessions im Userspace ab, während MPD auf NetGraph zurück greift.

Erstkontakt

Das Setup ist einfach, wenn auch die Dokumentation von MPD spartanisch ist. Was ein Kommando tut, steht höchstens an einer Stelle. Wenn man die nicht kennt, ist man schlicht aufgeschmissen.

Darüberhinaus unterscheidet MPD strikt zwischen den Layern der Verbindung. Man kann also nur dort etwas konfigurieren (und nachlesen), wo diese Einstellung relevant ist. So findet sich VanJacobson Compression im IPCP-Teil eine (PPP-)Bundles. Protocol Field Compression als Bestandteil der PPP Session wird aber im Link-Layer ausgehandelt. Deswegen findet sich die Einstellung dort: wie auch die Authenisierungsprotokolle, die LCP verwenden soll.

Die Konfigurationsdatei ist strikt linear. Jedes Kommando kann den aktuellen Layer ändern. Damit sind die Folgekommandos in einem anderen Kontext auszuführen. Am besten ist es also, die Konfiguration manuell zu erstellen und parallel dazu die Eingaben mitzuschreiben (log +console).

Ernüchterung

Die ersten Tests waren das Grauen.

lasttest-mpd-patch1

Es ging ganz lustig los, brach aber dann mit "no more threads" und "Fatal message queue overflow!" ab. Der MPD stürzte kontrolliert ab, beendete alle Sessions und sich selbst.

Als Grund dafür fand sich schnell das Event-Handling für Radius Anfragen. Die Implementation erfolgt als threadinterne Pipe aus der gelesen und geschrieben wird. Parallel dazu gibt es einen Ringpuffer an Ereignisinformationen. Damit ein write auf die Pipe nicht blockt, dürfen nicht mehr Bytes (Dummywert für "Es gibt ein Ereignis") geschrieben werden, als in die Kernel-Puffer passen.

Der offenkundige Patch besteht darin, die Pipegröße und den Ringpuffer zu vergrößern. Aber damit hatte ich gar keinen Erfolg: Es knallte mit einem Segfault.

Da pro Layer verschiedene Ereignisse generiert werden, kann bei der Verarbeitung eines einzigen Ereignisses eine Flut von Folgeevents angefordert werden. Aber der Thread für die Eventverarbeitung darf nicht blockieren! Deswegen werden für externe Ereignisse ständig neue Threads aufgemacht, die dann für sich allein warten können. In diesem Fall hat Radius-Authenisierung und -Accounting die Grenze von 5000 Threads pro Prozess überschritten. Auf dem Radiusserver sah die Last dann ähnlich aus.

Wie schlimm diese Radiuskaskade werden kann, hat ein Zulieferer während eines solches Tests erlebt. Die kommerzielle LAC Lösung dort fragt pro Loginversuch einen Radiusserver nach dem Realm, um den passenden LNS zu ermitteln. Als spontan alles zusammenbrach, wurde er von so vielen Requests überrannt, dass der Radiusserver – genauer dessen Datenbank-Backend – in die Hände klatschte und die gesamte DSL-Zuführung den Dienst quittierte. Inzwischen läuft die Verteilung an die LNS statisch.

Schön langsam

Man soll also freundlich zu seinen Zulieferern sein, besonders zu seinen Radius-Servern. Ich habe das komplette Eventhandling des MPD entsorgt und auf Basis von mesg_queue neu geschrieben. Die verwendete Bibliothek war sowieso schon im MPD eingebunden.

Unterschieden wird nach "sequentiellen" und "parallelen" Ereignissen. Sequentielle Ereignisse laufen unter dem globalen Lock des MPD serialisiert und in der Reihenfolge, in der sie generiert wurden. So kommen die Layer nicht durcheinander und die Ereignisverarbeitung kann sich auf die vorbereitenden Aktionen verlassen. Die parallele Verarbeitung dient der externen Kommunikation. Dabei die die Anzahl der aktiv laufenden Worker-Threads limitiert.

Die Länge beider Warteschlangen wird dem MPD zur Overload Berechnung (Load = 2×queue_len(seriell) + 10×queue_len(parallel)) vorgelegt, so dass er bei Last die Annahme neuer Verbindungen verweigern kann.

So ausgerüstet war ich voller Hoffnung und wurde jäh enttäuscht. Der Kernel warf hin: Tief tief drin.

bsd-lasttest-panic-1

Und nicht nur einmal, sondern auch an ganz anderer Stelle.

bsd-lasttest-panic-2

War der NetGraph Code defekt? Kommt der Kernel nicht mit dem schnellen Anlegen und Löschen von Interfaces klar? Bin ich zu schnell?

Schon im Radius-Accounting gab es Probleme, weil des Testgeräten gelang dreimal innerhalb einer Sekunde eine Verbindung auf- und abzubauen. Das verletzte die unique-Constraints meines Datenschemas. Das Teil ist also definitiv zu schnell! Im Code habe ich also dann eine Pause von 20ms (konfigurierbar) zwischen zwei seriellen Ereignissen eingebaut.

Da der Thread für die serielle Abarbeitung nun vom Hauptthread getrennt läuft, was vorher nicht der Fall war, habe ich um die NetGraph Aufrufe ein separates Lock gelegt. Dies verhindert, dass konkurrierend auf den NetGraph-Sockets gearbeitet wird.

Damit wurde der Systemverhalten stabil.

Lasttests

Zum Testen wird von einem Linux mit OpenL2TP aus:

  • eine zufällige Anzahl von L2TP Tunneln aufgemacht und
  • auf diesen so schnell wie möglich reiherum bis zu 9000 Sessions aufgerissen.
  • Danach werden die Sessions mit einigen Dezisekunden Pause generiert, bis die Anzahl der der gewünschten Clients verbunden ist.
  • Anschließend wird ein Ausfall und Wiederherstellen von ca. 300 zufälligen Sessions mehrfach wiederholt.
  • Nach einer Weile Ruhe werden die Sessions kontrolliert abgebaut.

Ein typisches Beispiel dieses Tests sieht so aus: Die "Sessions" sind vom lastgenerierenden OpenL2TP, diese generieren auf den MPD L2TP-"Links" und darauf PPP-"Bundles". Mitgeplottet werden auch die Längen der Eventqueues.

mpd-logins-13040106

Initial ist die Länge der parallelen Queue groß, dann greift die Limitierung durch Overload. Die Kurven für Sessions, Links und Bundles liegen praktisch aufeinander, es gibt keine Differenzen zwischen den Systemen, denn nicht aufgebaute Sessions werden nicht mit aufgeschrieben.

Ebenso ist schön zu sehen, wie heftig die serielle Queue anwächst, wenn Interfaces wegfallen: Der MPD ist in seiner originalen Version definitiv anfällig gegen Queueoverrun. Tritt dieser Effekt ein, beendet sich der originale MPD. Ein inakzeptabler Zustand.

Die FreeBSD Kiste mit MPD hat dabei einen maximalen Load von 3 und benötigt 280 MB RAM. Die Linux Maschine hat einen Load von 20 beim Verbindungsaufbau und einen Load von 50 beim Verbindungsabbau. Der Speicherbedarf auf der Linux-Seite beträgt 2 GB, um die vielen PPPD Instanzen zu halten.

Knalleffekte

Und nun der Test mit einem heftigeren Abbruch: Anstatt die PPPD Instanzen auf Linux einzeln zu killen, wird der L2TP Kanal unterbrochen.

mpd-logins-13040211

Deutlich erkennbar ist, wie die "übergeordneten" Bundles zuerst abgebaut werden und dann die dazugehörigen L2TP-Links. Der Load auf BSD-Seite steigt auf 6.

Auf Linux-Seite nimmt eine Katastrophe ihren Lauf: Alle pppd-Instanzen konkurrieren um den Scheduler, um sich zu beenden. Der Load steigt auf über 700 und die Maschine braucht zu lange, um sich zu beruhigen. Ein beherztes "killall -9 pppd" erlöste die Maschine nach einer halben Stunde.

Aber wie weit kann man gehen? Mehr als 10000? Mehr als 20000? Probieren wir es aus:

mpd-logins-13040213

Die Kombination steigt rasant auf 11500 Sessions und hört spontan auf. Auf Linux-Seite meldet der OpenL2TP fehlerhafte "RPC"-Parameter: Er ist in die Limits seiner Implementation gelaufen. Die Maschine verheddert sich und muß neu gebootet werden. Dem BSD hat es nichts getan.

Und nun mehrfach hintereinander:

mpd-logins-130401

Hoch und runter, immer wieder. Ab und zu einen Absturz durch Dummheit, aber es tut!

Ab in die Produktion

In der produktiven Umgebung ist es erstaunlich ruhig angelaufen. Trotzdem stürzt das System weiter ab:

bsd-lasttest-panic-3

Alle Abstürze sind nun nur noch im NAT-Code. Obwohl der Code dem Stand von HEAD entspricht ... Dazu anderweitig mehr.

Änderungen am MPD haben sich noch weitere ergeben:

  • Um den Code in der Produktion schnell austauschen zu können, beendet sich der MPD selbst, wenn er keine Sessions mehr offen hat. Die Konfiguationsoption heißt "delayed-one-shot". Der Aufruf des MPD erfolgt nun in einem Loop (/usr/local/etc/rc.d/mpd5 enthält command="/usr/local/sbin/${name}.loop")
$ cat /usr/local/sbin/mpd5.loop
#! /usr/local/bin/bash

nohup /usr/local/bin/bash -c "
cd /
while true; do
  /usr/local/sbin/mpd5 -k -p /var/run/mpd5.pid -O
  sleep 5
done
"  >/dev/null 2>/dev/null </dev/null &
  • Das Radius-Accounting für die Abbruchgründe ist mangelhaft, die komplette Fehlermeldung wird nun ebenfalls reportet:
ATTRIBUTE      mpd-term-cause  23      string
  • Systemrelevantes Logging (Interfaces kommen und gehen, Nutzer melden sich an und ab) ist nun dauerhaft an und nicht mehr nur für Debugging. Ebenso sind relevante Fehler immer zu loggen. Andernfalls fährt der Server im Blindflug.

Achja, der Patch: Bitteschön. Wir liefern gern auch kommerziellen Support dafür.

Avatar
Lutz Donnerhacke 20.10.2014 14:03
In the meantime: Yes, absolutly. I'v made a lot of progress. See the other articales in the blog here.
Avatar
Tolga 12.03.2014 11:12
Hi, this is great article about MPD. Before read your article, i have an idea that i can use MPD in an ISP environment. But this article change this to negative. Are suggest such usage?

2 Kommentare

Post a comment

Verwandter Inhalt