Apache-Workaround in Python
15. Dezember 2016 um 17:03 Uhr von Atari-Frosch
Nach mehreren Tagen in einem wirklich üblen Burnout habe ich nun wieder genug Konzentration zusammen, um an einem Programmierprojekt weiterzuarbeiten. Dabei geht es um einen Workaround für einen durchaus bekannten, acht Jahre alten Bug im Webserver apache, der nicht reproduzierbar ist und daher bislang auch nicht behoben werden kann.
Mir lief dieser Bug zum ersten Mal im Jahr 2009 über den Weg, als apache2 nach dem Upgrade von Debian Etch auf Debian Lenny plötzlich zum Speicherfresser wurde. Auch ein Wechsel des Hosts änderte daran nichts, und ich wechselte ja bis 2010 sogar zweimal.
Auf dem damals neuen Server (der jetzt mein „alter“ ist) mit 1 GB RAM schien das Problem erst mal nicht mehr aufzutreten. Dann jedoch, nach einem Kernel-Update in Squeeze, ging es wieder los (die zeitliche Nähe kann allerdings auch Zufall gewesen sein): Ich hatte wieder Spaß mit Swapping. In einer Ergänzung dazu – Spaß mit Swapping, Teil 2 – stellte ich allerdings fest, daß eine Einstellung in der Apache-Config ungünstig war und außerdem meine damals laufende StatusNet-Instanz auch gelegentlich ganz schön Last macht.
Wie auch immer; im November 2013 knallte es wieder. Die einzige Lösung schien mir ein Serverwechsel zu sein, vor allem, um mehr RAM zur Verfügung zu haben. Oder eben ein Wechsel auf nginx als Webserver. Oder beides. Zunächst kam ich aber nicht dazu, woran das ARGE nicht ganz so unschuldig ist, um das mal freundlich auszudrücken.
Irgendwann während dieser Zeit hatte mir Thomas 'Nurbs' Luzat netterweise ein kleines Bash-Script geschrieben, das, über den Cron gesteuert, regelmäßig überprüfte, ob zu viele Apache-Instanzen liefen. Wenn dem so war, wurde der Apache gestoppt und wieder gestartet. Nach einiger Zeit baute ich da noch eine Wartezeit zwischen Stop und Start ein, weil die alten Prozesse offenbar nicht so schnell weggeputzt werden konnten. Das Script schien das Problem zunächst zu lösen, aber nach einiger Zeit hatte ich trotzdem wieder Überläufe.
Ich erweiterte das Script um einige Zeilen, die mir den aktuellen Systemstatus per Mail mitteilten, wenn es „knallte“ – im Prinzip das gleiche, was auch mein in den alten Blogeinträgen erwähntes Status-Script macht. Die Informationen, die ich aus den Meldungen herauslesen konnte, waren jedoch mager. Im Prinzip kam da nicht mehr als das, was ich bei einem Shell-Login während eines Überlaufs erfuhr: Ja, Apache macht wieder Überlast und frißt RAM und Swap, ja, die Load ist gut zweistellig, ja, mysql swappt lustig mit, nein, erreichbar ist nix mehr. Danke auch.
Zwischendurch experimentierte ich noch damit, mit dem Script auch mysql zu stoppen und neu zu starten, aber das änderte auch nichts. mysql geriet offenbar nur dadurch ins Swappen, weil Apache eben so viel RAM für sich beanspruchte und für andere Prozesse nichts mehr übrigblieb.
Mittlerweile habe ich für mich selbst das Problem ja gelöst, indem ich beim Wechsel auf den jetzigen Server auf apache2 verzichtete und stattdessen auf nginx umstellte. Seitdem habe ich bei mir keine Überlast mehr gesehen. Interessantes Detail ist allerdings, daß auch der alte Server, auf dem noch zwei Websites liegen – beide sind WordPress-Installationen –, jetzt keine Überlast mehr hat. Mit dem Umzug dieses Blogs hier war das Problem verschwunden. Die beiden übrigen Blogs werden allerdings kaum genutzt; eins davon ist nicht einmal für Suchmaschinen zugelassen.
Allerdings nutze ich ja die GnuSocial-Instanz auf StopWatchingUs Heidelberg, und auf demselben Server liegt auch die Plattform Besorgte Bürger, deren Blog ich wöchentlich mit Links zum Thema Rechtsextremismus etc. versorge. Auf diesem Server, der Alex Schestag gehört, läuft, zumindest derzeit noch – genau, ein apache2. Der Host ist ein Xeon mit vier Kernen und hat 32 GB RAM. Also keine Maschine, bei der man erwarten würde, daß eine Software mal eben ihre CPU-Load zum Anschlag treiben und das RAM vollaufen lassen kann.
Was soll ich sagen … Apache kann.
Alex hat dann das Bash-Script von Nurbs eingesetzt, um den Apachen auch dort zur Ordnung rufen zu können. Das endete jedoch oft damit, daß plötzlich gar kein Apache mehr lief. Das ist ja nun auch doof. Offenbar lief hier was Neues schief.
Inzwischen fand ja Anfang November die OpenRheinRuhr statt, und ich hatte es trotz gesundheitlicher Probleme geschafft, wenigstens einen Tag dort zu verbringen. Dort hatte ich dann ein längeres Gespräch mit Mechtilde Stehmann, unter anderem auch zum Thema „der alte Apache-Bug“. Status: Der Fehler tritt vermutlich nur in der Kombination apache2/php auf, wenn PHP als Modul eingebunden ist und eine gewisse Menge an Zugriffen verzeichnet werden kann. Wird PHP als CGI eingebunden, tritt er wohl nicht auf. Ein Zusammenhang mit mysql besteht nicht. Weiter ist man bislang aber auch nicht gekommen.
Mehr noch: Die Apache-Foundation selbst ist bereits auf ihren eigenen Servern über das Problem gestolpert. Dem Wiki der Foundation mit 20.000+ Einträgen wurde deshalb – weil man den Fehler ja nicht beheben konnte – ein anderer Webserver aus deren Projekten untergeschoben …
Was die Installation bei Alex anging, hatte ich derweil die Vermutung entwickelt, daß die Zeit zum Stoppen des Apachen möglicherweise nicht mit dem Intervall zusammengeht, in welchem das Bash-Script gestartet wird, nämlich alle drei Minuten. So schnell, nämlich binnen drei Minuten, konnte es nach meiner Beobachtung zumindest auf meinem alten Server gehen, um die Kiste von quasi idle auf Überlastung bis kurz vorm Stillstand zu treiben. Wenn nun das Stoppen der Apache-Instanzen länger als drei Minuten dauert, tritt die nächste gestartete Version des Scripts dem vorherigen auf die Füße, und die Start- und Stop-Befehle beider Scripte führen schließlich dazu, daß am Ende sozusagen das Stop gewinnt. Dann gibt es zwar keine Überlastung des Servers, aber eben auch keinen Webserver mehr.
Als Lösung kam ich darauf, das Script um ein Lockfile zu erweitern: Wenn Apache gestoppt wird, soll das Lockfile erzeugt werden, und wenn das Script dann ein weiteres Mal startet, während das vorherige noch aktiv ist, „sieht“ es das Lockfile und beendet sich sofort wieder, ohne einzugreifen. Wenn das laufende Script mit dem Neustarten fertig ist, wird das Lockfile wieder gelöscht.
Der kleine Haken besteht darin, daß ich nicht wirklich in bash programmieren kann. Die Bash-Scripte, die ich selbst schreibe, haben etwa die Qualität von sehr schlichten Batch-Dateien unter DOS, ohne jegliche Schleifen oder Abfragen. Ich habe es zwar noch versucht, scheiterte aber schließlich an der Syntax. Tatsächlich wäre es mit bash – so man es beherrscht – vermutlich wesentlich einfacher gewesen, meine Idee umzusetzen, aber das hilft mir ja nun nichts, wenn ich es nicht beherrsche.
In Python, das ich seit einiger Zeit lerne, habe ich das Syntax-Problem zumindest nicht. OK, da mache ich auch Syntax-Fehler, aber die haut mir der Interpreter dann bei einem Probelauf direkt um die Ohren, teils sogar mit einem Hinweis darauf, was ich ändern müßte. Also beschloß ich letzten Monat, das Bash-Script nach Python zu portieren und meine Erweiterungen dann erst einzubauen.
Das Ergebnis läuft derzeit in der Version 0.2.1 auf Alex' Server im Testbetrieb. Bislang warten wir da auf den nächsten Überlauf. Ich habe aber noch den Verdacht, daß ich eventuell auch die CPU-Last des Servers mit überprüfen sollte, denn auch wenn der Apache läuft, läuft er oft relativ träge. Das sollte in Anbetracht der darunterliegenden Hardware ja eigentlich nicht passieren. Aber da warten wir erstmal ab.
Derweil habe ich letzte Nacht eine nochmals erweiterte Version 0.3 mit, sagen wir, etwas eleganterem Code produziert. Insbesondere habe ich die Scriptkonfiguration aus dem Script ausgelagert und in eine eigene Konfigurationsdatei geschrieben. Außerdem ist das Logging jetzt abschaltbar. Des weiteren wird überprüft, ob ein Pfad für das Lockfile gesetzt ist; wenn nicht, ist das ein Fatal Error. Und schließlich sind die Logeinträge noch etwas genauer. Bereits vorgesehen, aber noch nicht eingebaut ist die Möglichkeit, im Falle eines Neustarts eine Mail zu versenden.
Das Problem ist, daß man das Script eigentlich nur im Produktivbetrieb testen kann. Auf meinem lokalen Apachen ist es beispielsweise – von den Grundfunktionen und einem Test auf korrekte Syntax mal abgesehen – zweckfrei, weil es hier weder PHP-Anwendungen noch erwähnenswerte Zugriffszahlen gibt. Mein alter Server hat das Problem mangels Zugriffszahlen nicht mehr, und auf dem jetzigen läuft ja kein Apache mehr. Daher wäre ich froh, wenn noch mehr Leute, bei denen der Bug auftritt, das Script testen und Feedback geben würden.
Kaputtmachen kann man damit nichts! Deshalb kann es auch bedenkenlos auf einem Produktivsystem eingesetzt werden. Benötigt werden Python 2.7 (wer auf 3.x portieren möchte – bitte gerne, ich mach das sicher auch irgendwann mal, aber vielleicht ist ja jemand anderes schneller), außerdem pip, python-dev (ja, das zieht einiges nach) und die Python-Lib psutil. Bislang läuft es nur auf Debian-Systemen; hier lokal ist das noch ein Wheezy, auf dem Server Jessie mit SysVInit, also ohne systemd. Ich würde mich sehr freuen, auch von Installationen auf anderen Distributionen zu hören!
Das Script schreibt ein eigenes Log; seit der Version 0.3 sind Pfad und Dateiname für das Logfile frei wählbar (und wenn nichts angegeben wird, wird gar nicht geloggt; aber das wäre für einen Test eher ungünstig). Die Basisfunktionen – Anlegen des Lockfiles und stoppen des Apachen bei zu hoher Anzahl von Instanzen, warten, bis alle alten Instanzen weg sind, neuen Apachen starten, Lockfile löschen – sind getestet. Dazu habe ich einfach die Anzahl der erlaubten Instanzen auf 5 runtergesetzt und das auf meinen lokalen Apachen losgelassen, der prompt gestoppt und dann wieder gestartet wurde. Auch das Logging funktioniert einwandfrei.
Außerdem habe ich bereits eine Installations-Anleitung erstellt, eine Default-Konfigurationsdatei ist vorhanden, es gibt ein ChangeLog, eine ausführlichere Beschreibung und eine ToDo (alle Textdateien in englischer Sprache).
Direkter Download: apache-watchdog.zip
Git: Das ist ein wenig komplizierter, weil ich derzeit keine Subdomains mehr anlegen kann, solange ich die Domain noch bei Netbeat liegen habe. Es gibt hier auf dem Server, wie ich im April bereits beschrieb, ein GitWeb, das ist aber nur erreichbar, wenn Ihr es in Eure Hosts-Datei eintragt:
176.9.63.237 git.atari-frosch.de
Ja, ich könnte das Projekt auch auf GitHub werfen, aber dazu wollte ich erstmal ein paar Anfangs-Probleme ausgemerzt haben. Das wird sonst vielleicht etwas peinlich. 😉 Außerdem habe ich mit git selbst noch ein paar kleine Probleme; ich muß mir da noch ein paar Sachen angewöhnen bzw. einfach öfter damit arbeiten. Zum Beispiel habe ich jetzt schon ein paarmal vergessen, vor dem commit neue Files zu adden, so'n Kleinkram halt. Aber das krieg' ich auch noch gebacken.
18. Dezember 2016 at 9:55
Wenn sich das eigentliche Problem auf mod_php zurückführen lässt, warum dann nicht PHP über (das sowieso viel modernere) PHP-FPM einbinden?
So z.B.: https://www.howtoforge.com/using-php5-fpm-with-apache2-on-ubuntu-12.04-lts#-configuring-apache
Der Vorteil wäre dass du keine komischen Workarounds machen musst du zu unsauberem Beenden von Connections führen und du nicht alles nach nginx umziehen müsstest (auch wenn das sicher die sauberste Lösung wäre)
20. Dezember 2016 at 0:57
Also wenn du Shell Scripte brauchst, ob nun produktiv oder nur zum rumspielen und lernen, solange es mehr oder weniger Kleinkram ist, kann ich dir vermutlich helfen … aber erst wieder ab etwa 6. Januar.
20. Dezember 2016 at 1:09
… mal davon abgesehen, dass man Lock-Funktionen in Shell Scripten auf diversen Wegen erzeugen kann, es gibt ein sehr einfach zu nutzendes Hilfsprogramm: flock
https://linux.die.net/man/1/flock
20. Dezember 2016 at 1:44
(sorry wollte gleich ein Beispiel dazu schreiben, dann hat erst der Rechner gepiept und dann gleich auch noch der Wecker für den Tee … 🙂
flock ist einfach zu benutzen, zerleg dein Script in zwei Teile, den eigentlichen Arbeitsteil innen (das was du hast) und ein kleines Script zum aufrufen:
# Script 1
# … hier mach was du durch Lock absichern willst
# Script 2
flock /var/lock/froschs-lock /bin/bash name_script_1
… und das war es schon:
– wartet bis /var/lock/froschs-lock angelegt und gesperrt werden kann
– dann wird dein script 1 ausgeführt
– danach wird die Lock Datei wieder frei gegeben
Ein klein bissel aufwendiger wird es wenn du beim Lock die maximale Wartezeit begrenzen willst:
if flock -w 10 /var/lock/froschs-lock /bin/bash script_1
then echo „flock hat funktioniert und script 1 ausgeführt“
else echo „nach 10s hat flock erfolglos aufgegeben“
fi
… vielleicht hilft es ja für künftige Projekte.