Artikel mit ‘linux’ getagged

Die Annalen der Bash-Geschichte

Dienstag, 02. Oktober 2012

Heute mal wieder eine Episode aus den „adventures in modern computing“.

Wer die Bash (oder eine andere gängige Shell, hier geht es aber spezifisch um die Bash) kennt, wird höchstwahrscheinlich ihre äußerst nützliche History-Funktion kennen. Die Bash loggt die Befehle die man auf ihr eingibt und hält sie in einem Puffer bereit, auf den man verschiedentlich zugreifen kann um Befehle erneut auszuführen, anzupassen oder sich einfach ins Gedächtnis zu rufen. In erster Linie ist dies wohl als Arbeitserleichterung gedacht, es hat aber auch Anklänge eines regelrechten Logs, allerdings mit der Einschränkung, daß in der History normalerweise nur die 500 letzten Befehlszeilen vorgehalten werden (mit ein paar Tricks drumherum, z.B. kann man das mehrfache Speichern wiederholter Befehle ganz oder teilweise vermeiden). 500 wird heute vielen als ein vergleichsweise kleiner Wert erscheinen und das sehen offenbar die meisten Betriebssystem-Schmiede genauso und liefern ihre Software mit höheren Voreinstellungen aus (manchmal nur 1000 Zeilen, manchmal aber auch 10000 oder 40000 oder noch mehr). Speicher ist schließlich billig, oder?

Nun, wer eine SSD sein Eigen nennt sieht das vermutlich etwas anders, aber vor allem gibt es noch einen anderen Speicher der nicht sooo billig ist: RAM. Normalerweise liest die Bash nämlich den gesamten Inhalt ihrer History-Datei ein wenn sie startet. Das verlangsamt den Start der Bash und erhöht ihren RAM-Bedarf. Wer also die Bash History auch als Log nutzen möchte und daher die Variable $HISTSIZE auf einen sehr hohen Wert setzt, zahlt dafür mit verringerter Performance. Das fällt auf aktuellen Computern vermutlich gar nicht mal so sehr auf, zumindest bis die Datei so allmählich über ihre ersten Megabytes hinaus gewachsen ist. Auch für diesen Fall ist in der Bash bereits vorgesorgt: es gibt neben der Variablen $HISTSIZE noch die Variable $HISTFILESIZE, erstere definiert wie viele Zeilen der Datei beim Starten der Bash in den Puffer gelesen werden, letztere ist nützlich, um die tatsächliche Größe der Datei (in Zeilen, nicht in Byte) abweichend zu definieren. Eine Möglichkeit ist also, $HISTSIZE vergleichsweise klein zu wählen und $HISTFILESIZE extrem groß zu wählen. So spart man RAM und auch etwas Zeit beim Starten der Bash, kann aber sehr viele Befehle in der History halten. Die sind dann zwar nicht direkt über Funktionen der Bash zugänglich, aber es gibt ja noch die Möglichkeit die Datei zu durchsuchen, z.B. mit dem Programm grep.

Nun würde ich aber gerne ein ewiges Log haben. Auf Kundensystemen ist das zwar kein Ersatz für eine gute Dokumentation des Setups, aber es ist eine gute Ergänzung dazu. Auf meinen eigenen Computer will ich das sogar noch viel mehr, weil ich da auch häufiger mal Dinge ausprobiere, für die ich gar keinen alltäglichen Nutzen habe, an die ich mich aber gerne „erinnern“ können möchte, selbst wenn es nur Spielereien sind wie etwa „Twitter auf der Kommandozeile nutzen“ oder „Nachgucken was für ein Wochentag ein bekanntes historisches Datum hatte“ oder dergleichen. Hier habe ich dann unweigerlich das Problem, daß die History auf mehr Zeilen anwachsen könnte als ich in $HISTFILESIZE erlaubt habe. Zwar kann ich den Wert dort sehr hoch setzen, sogar lächerlich hoch, aber trotzdem ist es eine Grenze und es ist weiterhin nötig, daß die Bash sich anschaut wie groß die Datei überhaupt ist, was mir ja völlig egal wäre. Zumal ich stark annehme, daß ich niemals mehr Zeilen in der History-Datei erlauben könnte als die Bash als größte Zahl unterstützt. Die Bash selbst mag zwar nicht typisiert sein, aber trotzdem wird sie nicht unendlich lange Zahlen verarbeiten können.

Ich hab ein bißchen rumprobiert, konnte aber keine wirklich gute Lösung finden. Laut Dokumentation bewirkt eine $HISTFILESIZE von 0, daß gar keine History-Datei unterhalten wird. Ich würde da am liebsten „unendlich“ reinschreiben, aber das geht nunmal nicht. Die Dokumentation der Bash sagt aber noch, daß das Fehlen der Variable $HISTFILESIZE dazu führen würde, daß die Datei beliebig groß sein darf. Also änderte ich meine .bashrc:

export HISTSIZE=10000
#export HISTFILESIZE=1000000000000000000
unset HISTFILESIZE

Das hatte aber nicht den gewünschten Effekt. Die Bash setzte den Wert von $HISTFILESIZE beharrlich auf 10.000, also den gleichen Wert wie $HISTSIZE. Nur warum?

Ich überlegte bereits, mir eine Logrotate-Config für die History-Datei zu erstellen (das mache ich vielleicht trotzdem noch), aber es ließ mich nicht in Ruhe, daß das nicht mit „Bordmitteln“ der Bash funktionieren wollte, denn eigentlich sollte das was ich mir wünschte durchaus gehen. Nach längerem vergeblichen Suchen (ich kann gar nicht oft genug erwähnen, wie sehr ich Foren hasse), hab ich dann auf einer Mailingliste den entscheidenden Hinweis gefunden: Die Bash liest zuerst ihre Configs ein und dann die History-Datei und wenn zu diesem Zeitpunt die Variable $HISTFILESIZE nicht gesetzt ist, wird sie auf den gleichen Wert gesetzt wie $HISTSIZE. Das ist natürlich vollkommen bescheuert, denn dann kann ich ja de facto diese Variable nicht nicht setzen, jedenfalls nicht in einer Config der Bash. Ich müßte also nach jedem Start einer Bash manuell ein „unset HISTFILESIZE“ auslösen, damit meine History nicht auf 10.000 Zeilen zusammengekürzt wird. Unpraktisch.

Wie soll man also die $HISTFILESIZE nicht setzen können, wenn ihr Fehlen gleichzeitig dazu führt, daß sie automatisch gesetzt wird? Ich erinnerte mich düster, daß die Bash durchaus einen Unterschied macht zwischen gar nicht gesetzten Variablen und gesetzten Variablen denen kein Wert zugewiesen wurde, also „NIL“ sind, wie man so schön sagt. Vielleicht meinte die Dokumentation genau das?

Sie meinte es. Dieser Teil meiner .bashrc sorgt für eine History-Datei ohne Größenbegrenzung, von der dann die letzten 10.000 Zeilen in den History-Puffer geladen werden:

# read this number of lines into history buffer on startup
# carefull with this, it will increase bash memory footprint and load time
export HISTSIZE=10000
# HISTFILESIZE is set *after* bash reads the history file
# (which is done after reading any configs like .bashrc)
# if it is unset at this point it is set to the same value as HISTSIZE
# therefore we must set it to NIL, in which case it isn't "unset",
# but doesn't have a value either, go figure
#unset HISTFILESIZE
export HISTFILESIZE=""

Wie George Takei sagen würde: Oooh my!

Enterprise, my ass

Freitag, 10. August 2012

Es war nur eine vergleichsweise unscheinbare Supportanfrage. Über das Webmail-System könnten gerade keine Mails mehr verschickt werden – es gäbe einen Authentifizierungsfehler. Merkwürdig ist das vor allem aus einem Grund: Für SMTP AUTH werden die gleichen Zugangsdaten verwendet wie für IMAP, und die stimmen – sonst hätte sich der User gar nicht erst am Webmail-System einloggen können. Die Möglichkeit, für SMTP AUTH davon abweichende Daten einzustellen, gibt es überhaupt nicht. Wie kann es also sein, dass Zugangsdaten für IMAP funktionieren, aber für SMTP AUTH nicht mehr?

Praktischerweise hat Roundcube die Option $rcmail_config['smtp_debug'], mit der man zu Debugzwecken die gesamte SMTP-Session loggen kann. Das habe ich gemacht. So sah’s aus:

[09-Aug-2012 12:16:47 +0200]: Recv: 220 helium.uberspace.de ESMTP
[09-Aug-2012 12:16:47 +0200]: Send: EHLO webmail.taurus.uberspace.de
[09-Aug-2012 12:16:47 +0200]: Recv: 250-helium.uberspace.de
[09-Aug-2012 12:16:47 +0200]: Recv: 250-STARTTLS
[09-Aug-2012 12:16:47 +0200]: Recv: 250-PIPELINING
[09-Aug-2012 12:16:47 +0200]: Recv: 250-8BITMIME
[09-Aug-2012 12:16:47 +0200]: Recv: 250-SIZE 0
[09-Aug-2012 12:16:47 +0200]: Recv: 250 AUTH LOGIN PLAIN
[09-Aug-2012 12:16:47 +0200]: Send: AUTH LOGIN
[09-Aug-2012 12:16:47 +0200]: Recv: 334 VXNlcm5hbWU6
[09-Aug-2012 12:16:47 +0200]: Send: [...]
[09-Aug-2012 12:16:47 +0200]: Recv: 334 UGFzc3dvcmQ6
[09-Aug-2012 12:16:47 +0200]: Send: [...]
[09-Aug-2012 12:16:52 +0200]: Recv: 535 authentication failed (#5.7.1)
[09-Aug-2012 12:16:52 +0200]: Send: RSET
[09-Aug-2012 12:16:52 +0200]: Recv: 250 flushed
[09-Aug-2012 12:16:52 +0200]: Send: QUIT
[09-Aug-2012 12:16:52 +0200]: Recv: 221 helium.uberspace.de

Nun, das ist interessant. Am EHLO ist nämlich gut zu erkennen, dass wir uns hier auf taurus befinden. Der SMTP-Server meldet sich allerdings mit dem Namen helium, was zwar auch einer unserer Server ist (nämlich der, auf dem Uberspace.de selbst läuft), aber eben ein anderer. Wie zur..? Schnell die config/main.inc.php kontrolliert, aber dort steht ganz klar:

$rcmail_config['smtp_server'] = 'localhost';

Wie kann also der Host taurus, wenn er localhost kontaktiert, auf einem ganz anderen Host rauskommen?! Selbstverständlich ist in der /etc/hosts durchaus fest 127.0.0.1 localhost eingetragen; es kann also eigentlich nicht mal ein DNS-Problem sein.

Ich habe mich dann mit strace an den PHP-Prozess geklemmt, erneut versucht eine Mail zu versenden und zu beobachten, was passiert. Was klar erkennbar war: Es wurde eine DNS-Auflösung von localhost.uberspace.de gemacht, die dann 82.98.87.96 zurücklieferte – die IP von helium.

Nachdem ich ein bisschen Debugging in die PHP-Klassen Net_SMTP und Net_Socket reingehackt hatte (die Roundcube für den Mailversand verwendet), war klar, dass hier nicht plötzlich etwas anderes passiert als sonst. Das wäre auch unlogisch gewesen, aber seien wir ehrlich, das ganze Problem ist schließlich unlogisch. Also habe ich erstmal versucht, das Problem zu isolieren, mit folgendem Testscript:

[webmail@taurus ~]$ cat test.php
<?php echo gethostbyname("localhost"); ?>

Ist klar, oder? Gut, dann führen wir das mal aus:

[webmail@taurus ~]$ php test.php 
82.98.87.96

What! The! Fuck! Wie kann denn bitte das sein? Es geht doch nicht von heute auf morgen in einem unangetasteten PHP die gethostbyname-Funktion plötzlich kaputtgehen …

Eine überraschend kurze Recherche nach „php gethostbyname localhost“ brachte mich auf einen Forenbeitrag mit dem vielsagenden Titel kernel upgraded, now php_network_getaddresses can’t resolve localhost, der auch schnell deutlich machte, dass es weniger der Kernel war, sondern:

I had a similar problem yesterday on Centos 5. It turns out a sudo updated corrupted the selinux context of /etc/nsswitch.conf which made all localhost lookups fail. See if it works with selinux set to permissive. If so, try restorecon /etc/nsswitch.conf and reset selinux to enforcing. Good luck.

Nun setzen wir allerdings SELinux bei uns gar nicht ein, insofern konnte es kein Problem des security contexts sein – auch wenn das durchaus bei anderen auch ein Problem war, zu dem Red Hat kurz darauf einen Patch herausgegeben hat. Und dieses bereits gepatchte sudo-RPM ist auch das, was wir hier einsetzen – also das, in dem jener Bug eigentlich schon gefixt war. Aber ein sudo-Update haben wir durchaus auch gemacht. Gerade eben nämlich. Und jetzt geht localhost nicht mehr.

Der Bugreport wies aber indirekt auf ein anderes Problem hin: Das SELinux-Problem bestand nämlich darin, dass die /etc/nsswitch.conf plötzlich auf rpm_script_tmp_t gelabelt war. Das macht man ja nun nicht einfach so zum Spaß. Vielmehr roch es danach, dass das sudo-Update irgendwie an der Datei rumfummelt, und zwar, in dem es eine temporäre Datei erstellt (die dann korrekt das Label rpm_script_tmp_t erhält) und jene dann mittels mv auf die eigentliche Datei drüberbewegt wird. Und da die /etc/nsswitch.conf kein Bestandteil des sudo-RPMs ist, kann es eigentlich nur eine Stelle geben, mit der sudo an der Datei rummachen kann:

[root@taurus ~]# rpm -q --scripts sudo
postinstall scriptlet (using /bin/sh):
/bin/chmod 0440 /etc/sudoers || :

#
# rhbz#841070: remove the old line, the new one won't be matched
#              by the old regexp and won't be deleted on upgrade
#
if grep -q '^sudoers: files ldap$' "/etc/nsswitch.conf"; then
   NSSWITCH_TMPFILE=$(mktemp)
   grep -v '^sudoers: files ldap$' "/etc/nsswitch.conf" > "$NSSWITCH_TMPFILE" && \
   mv -f "$NSSWITCH_TMPFILE" "/etc/nsswitch.conf"
   restorecon "/etc/nsswitch.conf"
fi

if ! grep -q '^[[:space:]]*sudoers:' "/etc/nsswitch.conf"; then
   # No "sudoers:" line in nsswitch.conf, add a default one
   echo "sudoers:  files ldap" >> "/etc/nsswitch.conf"
   restorecon "/etc/nsswitch.conf"
fi
[...]

Wir sehen hier also, dass dann, wenn in der /etc/nsswitch.conf die Zeile sudoers: files ldap steht, jene entfernt wird und im zweiten Codeblock durch sudoers: files ldap ersetzt wird. Ja: Die einzige Änderung beläuft sich darauf, dass hier nun zwei Leerzeichen drinstehen, und die Historie dieser Änderung ist wiederum eine ganz eigene Horrorgeschichte.

Was hier gut erkennbar ist: Es wird eine temporäre Datei mit mktemp angelegt, um den bisherigen Inhalt der /etc/nsswitch.conf abzüglich der zu entfernenden Zeile reinzuschreiben, die neue Zeile zu ergänzen und den Inhalt zurückzuschreiben. mktemp also. In dessen man page klar zu lesen ist:

If mktemp can successfully generate a unique filename, the file (or directory) is created with file permissions such that it is only readable and writable by its owner (unless the -u flag is given) and the filename is printed to standard output.

Und genau so – ohne irgendein chmod – wird die Datei dann anschließend auf die /etc/nsswitch.conf umbenannt: „only readable and writable by its owner“, und der wäre dann in diesem Fall root. Und das bringt dann diesen „lustigen“ Effekt mit sich (hier mal an einem Test-Host verifiziert und nicht mehr auf taurus):

[root@lyra ~]# telnet localhost 25
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
220 lyra.uberspace.de ESMTP

vs.

[webmail@lyra ~]$ telnet localhost 25
Trying 82.98.87.96...
Connected to localhost.
Escape character is '^]'.
220 helium.selfhost.de ESMTP

Wenn User nämlich die /etc/nsswitch.conf nicht mehr lesen können, in der die Zeile hosts: files dns steht und gethostbyname dazu anweist, erst in der /etc/hosts zu schauen, bevor es das DNS bemüht, dann spielt schon mal keine Rolle mehr, was in der /etc/hosts so drinsteht. Und wenn der aufzulösende Hostname keinen Punkt beinhaltet, verhält sich gethostbyname wie folgt:

If name doesn’t end in a dot and the environment variable HOSTALIASES is set, the alias file pointed to by HOSTALIASES will first be searched for name (see hostname(7) for the file format). The current domain and its parents are searched unless name ends in a dot.

Das bedeutet: gethostbyname versucht gar nicht erst, localhost via DNS aufzulösen (denn unser DNS-Resolver würde dafür direkt 127.0.0.1 liefern), sondern hängt direkt den eigenen Domainnamen an – in diesem Fall uberspace.de, so dass also faktisch via DNS nach localhost.uberspace.de gesucht wurde, was wegen eines Wildcard-Records im DNS dann auf die IP 82.98.87.96 auflöste.

Beim CentOS-Projekt gibt es einen Bug-Report dazu: Post-install script for sudo sets /etc/nsswitch.conf to mode 600. Er wurde auch upstream gemeldet, also im Red-Hat-Bugtracker, aber das Problem ist immer noch offen: Wer also heute unter CentOS 5 sein sudo updated, kann auch morgen noch localhost nicht mehr oder falsch auflösen. Vor diesem Hintergrund muss man fast froh über das wenig intuitive Verhalten von MySQL sein, das den Hostnamen localhost als implizite Aufforderung ansieht, sich doch bitte über einen Unix-Socket mit dem Server zu verbinden (möchte man eine TCP-Verbindung, muss man explizit 127.0.0.1 angeben), so dass alle Verbindungen zum MySQL-Server auf localhost von diesem Problem unbeeinträchtigt waren.

Hand aufs Herz, Bugs dieses Kalibers hätte ich nicht in einer Distribution erwartet, die ihren Fokus explizit nicht darauf hat, immer die neuesten Versionen von allem einzusetzen, sondern die bestehenden Versionen so sensibel wie möglich durch Backporting von Bugfixes zu pflegen, damit nur ja keine unvorhergesehenen Probleme auftreten – zumal es ja nicht mal ein Bug in sudo war, sondern sudo lediglich der Transporteur für einen Haufen abenteuerliches und unvorsichtiges Shellscripting war. Ich kann nur hoffen, dass das eine ärgerliche Ausnahme bleibt – und vor allem schnell gefixt wird.

Update 16.08.2012: Wunderbar, in aktualisierten sudo-RPMs findet sich nun im %postinstall:

#
# rhbz#846631: fix permissions on nsswitch.conf
#
if [ "$(stat -c '%a' "/etc/nsswitch.conf")" = "600" ]; then
	chmod 0644 "/etc/nsswitch.conf" || :
fi

(Auch) darum machen wir Uberspace.de

Sonntag, 29. April 2012

Manchmal sind wir fast etwas überrascht davon, dass offenbar als Besonderheit Nr. 1 von Uberspace.de ausgerechnet das freie Preismodell gesehen wird, das wir eigentlich eher als Nebenschauplatz gesehen hatten – und das vornehmlich aus der Bequemlichkeit heraus entstanden ist, nicht ständig über „warum kostet dieses und jenes so und so viel, und woanders kostet das aber so und so viel“ diskutieren zu müssen. Eigentlich lag unser Hauptanliegen vielmehr darin, mal etwas angstfreier damit umzugehen, Usern eine „echte Shell“ an die Hand zu geben, statt eines Webinterfaces, das einem bei dem, was man so machen will, eher hinderlich im Weg steht – und so auch ein bisschen mehr den „Spaß am Gerät“ zu wecken und zu zeigen, wie vielfältig die Möglichkeiten mit Linux so sind.

Um so schöner, wenn dann in einer Supportmail auch mal so etwas zu lesen ist (vielen Dank an den Autor, dass wir ihn zitieren dürfen):

Jetzt mal im Ernst: Ich erinnere mich noch daran, dass ich dir vor einem Jahr geschrieben habe, dass das Tolle an Uberspace das Erfassen der Zusammenhänge ist. Das macht es auch weiterhin so spannend, euer Kunde zu sein, während man sich irgendwann an Confixx sattgesehen hat. Nun bin ich zwar immer noch kein Profi geworden (das habe ich jetzt zwei-, nein, dreimal eindrucksvoll bewiesen), dafür habt ihr mir die Berührungsängste, die ich gegenüber Linux hegte, genommen, auch im Desktopbereich. Allein dafür bin ich froh, dass es euch gibt. Danke.

Lob des Backups

Samstag, 11. Februar 2012

Während ich diesen Artikel schreibe, rauschen im anderen Fenster die Zeilen der rsync-Ausgaben über den Bildschirm. Um 07:12 Uhr bemerkte Icinga die ersten Probleme; um 7:17 Uhr eskalierte es sie auf mein Handy. Seitdem bin ich wach, und weil ich während des nun laufenden Restores ohne nichts weiter unternehmen kann und die nächsten Schritte bereits vorbereitet habe, mache ich das Beste draus: Ich blogge.

Um’s direkt vorwegzunehmen: In technischer Hinsicht bietet dieser Blogpost keinerlei Erkenntnisgewinn. Nur vielleicht eine Moral, die da lautet: Macht Backups. (Und benutzt Sonnencreme, klar.)

Vom Monitoring aus dem Schlaf geholt zu werden, ist erstmal kein großer Aufreger. Nicht selten geht es hier um Probleme vom Kaliber „der Apache braucht mal einen Restart“ oder auch „die Mailqueue ist ungewöhnlich voll“ (was auf einen Spamvorfall hindeutet). Echte Ausfälle sind selten. In diesem Fall war es ein sehr kundenspezifischer Check: Es wird durch ein PHP-Script geprüft, ob eine ganz bestimmte Spalte einer ganz bestimmten Tabelle einer MySQL-Datenbank eine bestimmte Länge hat. Die Gründe für diesen Check führen hier zu weit; es gab vor längerer Zeit einen Vorfall, der dazu Anlass gab, und wir sind eigentlich immer bestrebt, für Vorfälle, die nicht durch unser Monitoring diagnostiziert wurden, anschließend noch bessere Checks zu schreiben. Dieser hier ist ein schönes Beispiel: Der Apache lief. Der MySQL-Server lief. Apache konnte auch mit MySQL kommunizieren. Aber MySQL konnte keine Daten schreiben. Das Gros der Checks des fraglichen Systeme meldete daher fröhlich „alles in Ordnung“ – obwohl überhaupt nichts in Ordnung war. Mir war nur noch nicht bewusst, wie sehr nicht.

Das ausgeführte DESCRIBE kunden führte jedenfalls zu einem Fehler, und zwar konkret zu Can't create/write to file '/tmp/#sql_2fc2_0.MYI' (Errcode: 30). Das muss jetzt erstmal noch keine Katastrophe sein, sondern kann auf simple Probleme wie eine überschrittene Quota oder eine vollgelaufene Partition hindeuten (was wir zwar auch monitoren, aber nur in einer niedrigeren Frequenz, weil sowas typischerweise nicht „von jetzt auf gleich“ passiert). Also habe ich mich dann erstmal per SSH eingeloggt, was auch geklappt hat.

Als df -h dann lediglich Command not found lieferte, und ps ax dann auch, war ich dann sehr schnell richtig wach, und es war klar: Das Problem ist größerer Natur als nur eine vollgelaufene /tmp-Partition. Auch dmesg ließ sich nicht mehr ausführen. Da es sich bei dem System um eine Xen-Instanz handelte, war von daher ein Login auf dem Wirtssystem dran. xm console zeige neben dem Login nur noch einen ext3-Fehler: Journal has aborted. Okay, das kommt mal vor. Selten. Und es ist in der Regel kein Problem, das sich nicht mit einem fsck.ext3 lösen ließe. Also: xm shutdown, xm create -c, … und das sah alles so gar nicht gut aus, denn pygrub mochte keine grub-Konfiguration innerhalb des Blockdevices des Gasts mehr finden.

Nun sieht dieses System so aus, dass eine lokal eingebaute Festplatte im physischen Server lediglich als Boot-Medium fungiert. Die Blockdevices hingegen kommen via iSCSI von unserem Storage-Cluster: Zwei dicke Maschinen mit je einem RAID6 über 16 Platten, die über DRBD repliziert werden, und auf denen dann LVM-Volumes via ietd exportiert werden. Schnell ein Login auf dem derzeit aktiven Filer: Gibt’s Probleme? Ein Blick in die Logfiles, in dmesg, … aber alles sah gut aus. Ich habe mich schnell auf einigen anderen Xen-Gästen eingeloggt, die ebenfalls vom Storage-Cluster aus exportiert werden, aber dort war alles in Ordnung, stabiler Betrieb, auch Schreibzugriffe problemlos möglich.

Im konkreten Fall ist auch der physische Server, auf dem die Xen-Instanz läuft, redundant ausgelegt: Via Heartbeat überwachen sich beide Hosts gegenseitig, und wer aktiv ist, bindet das iSCSI-Target ein und fährt die darauf enthaltene Xen-Instanz hoch. Das Setup läuft an sich prima und hat auch schon einige (geplante) Failover-Vorgänge überstanden. Ich hielt den Zeitpunkt für gegeben, einen Failover durchzuführen. Die Xen-Instanz war schnell gestoppt (tja, wenn das Linux-System erstmal nicht mehr auf sein Blockdevice schreiben kann, geht das verdammt fix …), dann ein iscsiadm --logout – und das hing. Und hing. Und hing. Was von daher merkwürdig war, weil der iSCSI-Server durchaus anpingbar war, also kein Problem mit dem Netzwerkinterface o.ä. vorliegen konnte.

In solchen Fällen ist es wichtig, eine Split-Brain-Situation zu vermeiden, sprich, es sollten nicht zwei Hosts das gleiche Blockdevice bearbeiten. Die einfachste Möglichkeit ist, den ohnehin praktisch toten Host komplett vom Strom zu trennen. Gesagt, getan – ist dank IPMI ja alles kein Problem. Dann die Übernahme des iSCSI-Targets auf den anderen Knoten, die von Heartbeat ohnehin automatisch veranlasst wurde, als der bisher aktive Host nicht mehr anpingbar war. Klappte. Mit einem kleinen Problem: Das Blockdevice beinhaltet eigentlich eine Partitionstabelle, die genau eine Partition umfasst, nämlich die Root-Partition. Wird das iSCSI-Target eingebunden, taucht es insofern als /dev/sdb auf (/dev/sda ist das eingebaute Boot-Plattensystem), und noch dazu ein /dev/sdb1 mit jener Root-Partition.

/dev/sdb war da. /dev/sdb1 fehlte. Ein fdisk -l /dev/sdb behauptete schlicht, es gebe keine Partitionstabelle. Das Blockdevice selbst hatte aber schon mal zumindest die korrekte Größe – der Zugriff via iSCSI an sich schien also zu klappen. Sicherheitshalber habe ich mir dann die Partition direkt auf dem Storage-Cluster angeschaut, um sicherzugehen, dass es wirklich kein iSCSI-Problem ist. Zum Vergleich, so sollte es aussehen (bei einem identisch eingerichteten Xen-Image):

[root@filer01 ~]# fdisk -lu /dev/mapper/vgdrbd1-vserver_working 

Disk /dev/mapper/vgdrbd1-vserver_working: 188.7 GB, 188743680000 bytes
255 heads, 63 sectors/track, 22946 cylinders, total 368640000 sectors
Units = sectors of 1 * 512 = 512 bytes

                              Device Boot      Start         End      Blocks   Id  System
/dev/mapper/vgdrbd1-vserver_working1   *           1   368627489   184313744+  83  Linux

Und so sah es faktisch aus:

[root@filer01 ~]# fdisk -lu /dev/mapper/vgdrbd2-vserver_broken 

Disk /dev/mapper/vgdrbd2-vserver_broken: 188.7 GB, 188743680000 bytes
255 heads, 63 sectors/track, 22946 cylinders, total 368640000 sectors
Units = sectors of 1 * 512 = 512 bytes

Disk /dev/mapper/vgdrbd2-vserver_broken doesn't contain a valid partition table

Okay, iSCSI selbst war als akute Fehlerquelle insofern aus dem Spiel. Es schien an der Zeit, TestDisk ins Spiel zu bringen. Dieses Tool ist Gold wert, wenn es darum geht, defekte Partitionstabellen zu fixen, unter anderem, in dem es die Platte sektorenweise nach Blöcken absucht, die z.B. wie ein ext3-Superblock aussehen – oder nach einem Backup davon, denn ext3 legt immer gleich mehrere Kopien des Superblocks verteilt über das Blockdevice an.

TestDisk fand nichts. Also wirklich überhaupt gar nichts. Insofern hatte ich wenig Hoffnung, dass der darauffolgende Schritt noch etwas bringen würde, nämlich die Partitionstabelle anhand eines identisch aufgesetzten VServers zu rekonstruieren. Aber bevor ich’s nicht versucht habe … erwartungsgemäß scheiterte aber der Versuch, die frisch angelegte Partition zu mounten (die ja eigentlich durchaus noch das Dateisystem hätte enthalten müssen – die Partitionstabelle zu bearbeiten, ändert ja nichts daran, dass auf dem Rest der Platte immer noch unverändert Daten liegen). Aus einem anderen Xen-Image identischer Konfiguration konnte ich noch die Positionen ableiten, die die Backup-Superblocks haben sollten, und habe sie alle durchprobiert – nichts.

An diesem Punkt war dann Schluss. Wir sind kein Datenrettungsunternehmen, und wir können auch nicht mal eben fix ein einzelnes LVM-Volume eines Clusters, auf dem noch ein paar Dutzend weiterer – voll funktionsfähiger – Volumes liegen, an ein solches schicken (wenn, dann müssten wir einen Klon des LVM-Volumes auf eine externe Platte anfertigen), und selbst wenn, würde das das Problem nicht lösen, dass das System im Moment eben nicht läuft. Insofern ist an dieser Stelle der Punkt gekommen, an dem ich mit hängenden Schultern sagen muss: Ich habe absolut keine Ahnung, was hier passiert ist. Der Storage-Cluster meldet weiterhin keinerlei Probleme. Sein RAID6 ist 100% in Ordnung (sagt der Controller). Und vor allem gab es ja keinerlei schreibenden Zugriffe auf Partitionstabelle oder Dateisystem des Images des Xen-Gasts, die irgendwie hätten fehlschlagen und etwas korrumpieren können – und Daten verschwinden eigentlich nicht „einfach so“, nicht ohne dass ein physischer Defekt vorliegen würde, was bei einem RAID-System mit vollkommen intakten Platten nun wirklich ausgesprochen unwahrscheinlich ist. Aber, wie’s aussieht, möglich.

Nun also rasseln die Dateien aus dem letzten Backup via rsync wieder zurück auf ein frisch angelegtes und mit einem Dateisystem versehenen Blockdevice. Der größte Datenblock, nämlich /home, ist schon durch, insofern kann es sich nur noch um Minuten handeln. Deshalb noch kurz ein Punkt im Hinblick auf Backups – darauf legen wir großen Wert: Insbesondere dort machen wir nichts mit proprietärer Software oder irgendwelchem Kompressions- oder diff-Gebastel. Die Gelegenheiten, bei denen uns rdiff-backup (dessen Konzept eigentlich ziemlich cool ist) im Stich gelassen hat, sei es durch absurd lange Restore-Zeiten selbst für einzelne Dateien, oder schlicht durch komplette Abbrüche mitten im Prozess, überwiegt die Zahl der Gelegenheiten, in denen es uns gerettet hat, bei weitem. Deshalb sind wir in Sachen Backup extrem konservativ: Ein extra Server mit eigenen Platten, keine komplizierten LVM-DRBD-Sonstwas-Layer dazwischen, einfach nur eine schnöde Partition angelegt, mit rsync kommen die Daten drauf. Versionierung läuft über Hardlinks. Auf diese Weise liegen zwei Versionen der gleichen Datei zwar nicht so platzsparend wie „erste Version plus Diff“ oder „letzte Version plus Reverse-Diff“ auf der Platte, aber dafür in einem Zustand, bei dem sich jeder Versionsstand mit Bordmitteln wie cp oder eben rsync schnell auf jede beliebige andere Hardware bringen lässt. Der Charme besteht eben nicht in Features, sondern manchmal schlicht im Weglassen selbiger.

So, die letzten Dateien laufen durch. In der Xen-Konfiguration ist bereits die disk-Zeile auf das neue Blockdevice angepasst. rsync fertig, umount, xm create -c. Bootet.

Die Maschine läuft nun also wieder, zwar mit einem Stand von gestern Nacht, aber eine bessere Option gibt es nun mal nicht. Downtime: Von 07:12 Uhr bis 11:01 Uhr – fast vier Stunden, was die Verfügbarkeit im Jahresmittel auf ärgerliche 99,95% herunterkatapultiert (wobei nur etwa eine halbe Stunde auf tatsächliche administrative Arbeiten entfällt – der Rest hingegen auf den Restore der Daten; man sollte also bei einem Backup nicht unterschätzen, dass auch ein Restore eine signifikante Downtime bedeuten kann). Aber der (ziemlich branchenspezifische) Shop wird nachts kaum besucht, am Wochenende noch weniger, von daher hält sich der potentielle Schaden wohl hoffentlich in Grenzen. Dass ein solcher Vorfall nun ausgerechnet bei einem System auftritt, wo sowohl die physischen Maschinen „vorne“ redundant ausgelegt sind als auch der Storage-Cluster „hinten“, ärgert mich um so mehr, aber es ist ein gutes Beispiel dafür, dass Hochverfügbarkeit durch Failover-Cluster auch nur eine Teilmenge möglicher Störungen absichert – und derart tiefgreifende Schäden an Dateisystemen und Partitionstabelle gehören nicht dazu. Die Lehren, die ggf. aus diesem Vorfall zu ziehen sind, werden wir dann am Montag im Teammeeting besprechen.

Also Leute, macht Backups. Gute Backups. Und vor allem vollständige Backups: Seid nicht zu selektiv – kopiert lieber zuviel als zuwenig, auch wenn man im Moment leider immer noch nicht wieder „Plattenplatz kostet doch eh fast nichts“ sagen kann. Und schaut gelegentlich, dass eure Backups auch funktionieren. Ihr wisst nie, wann sie euch mal den Allerwertesten retten.

So, jetzt erstmal Kaffee.

DNS-Resolving mit persistentem Cache

Samstag, 03. September 2011

Wir fahren in Bezug auf DNS-Caching eine einfache, aber sehr effektive Strategie: Alle von uns verwalteten Hosts bekommen einen lokalen DNS-Resolver. Zwar haben wir in unseren Netzen jeweils ebenfalls DNS-Resolver, die direkt mit einem Eintrag in der /etc/resolv.conf netzintern genutzt werden können, aber das brauchen wir eigentlich nur während der Installation von Systemen, die wir typischerweise via PXE-Boot und dann über das Netz machen, wofür ein fertig bereitstehender Resolver vonnöten ist. Sobald eine Maschine läuft, bekommt sie einen eigenen Resolver.

Dieser Ansatz mag erstmal ungewöhnlich wirken, gehört doch das Eintragen von klassischerweise zwei DNS-Resolvern in die eigene Netzwerkkonfiguration zum üblichen Standard. Ein Aspekt ist sicherlich eine gewisse Performancesteigerung – was der gängige Grund sein dürfte, warum oft empfohlen wird, einen lokalen DNS-Cache auf dem heimischen Rechner zu installieren, denn bei gar nicht mal so wenigen Zugangsprovidern sind die von jenen bereitgestellten Resolver oftmals in bedauernswertem Zustand, was die Performance angeht, und gerade die günstigeren Plaste-DSL-Router für Daheim bekleckern sich hier auch oft nicht mit Ruhm – gelegentlich mag auch mein Linksys-Router einfach keine DNS-Anfragen mehr auflösen oder an den Upstream-Resolver durchreichen, obwohl er den Internetzugang an sich noch bereitstellt – nur ein Router-Reboot hilft dann noch. „Stabil“ ist anders.

Im Rechenzentrum zählen diese Argumente weniger. Die dort bereitgestellten DNS-Resolver liegen ja im gleichen Netz wie die anfragenden Hosts, und sie sind zudem mit üppiger Hardware ausgestattet. Was die Performance angeht, liegen wir hier also eher von Optimierungen im Millisekundenbereich, die es wirklich nicht wert wären.

Worum’s uns viel mehr geht, ist, mögliche Störungsquellen zu minimieren. Das DNS an sich ist voller Redundanz und Lastverteilung: Müssen DNS-Anfragen aufgelöst werden, stehen satte 13 IPv4-Adressen von Root-Nameservern bereit; einige sind auch via IPv6 erreichbar. Hinter den meisten stehen nicht etwa einzelne Hosts, sondern gleich etliche, die via Anycast geografisch verteilt sind. Fragt man jene beispielsweise nach der .de-Delegation, erhält man derzeit immerhin gleich fünf mögliche Nameserver, auch wiederum geografisch verteilt und zum Teil über IPv6 erreichbar. Für jede einzelne .de-Domain sind dann auch wieder mindestens zwei Nameserver zuständig, die gemäß der Vorgaben der DENIC in unterschiedlichen Class-C-Netzen stehen müssen (auch wenn das nicht zwingend eine geografische Trennung bedeutet). Es gibt also Redundanzen, wohin das Auge sieht. In den seltenen Fällen, wo tatsächlich einer dieser Hosts gestört ist, hat ein DNS-Resolver also praktisch immer genug Alternativen, um ans Ziel zu kommen – und das auch performant, weil er natürlich auch die Information, dass ein bestimmter Host aktuell nicht ansprechbar ist, eine gewisse Zeit lang cachen kann.

Und diese ganze Redundanz soll man nun aufgeben, in dem man alle Hosts in seinem Netzwerk über einen einzigen DNS-Resolver führt? Natürlich, natürlich: Wenigstens zwei sollten es schon sein; mehr als drei sind aber normalerweise schon gar nicht möglich. Wenn nun einer ausfällt, gönnt sich die C-Library aber defaultmäßig stolze fünf Sekunden, bis sie den nächsten Nameserver versucht – und versucht es ebenso defaultmäßig immer wieder beim ersten. Zwar lässt sich das in gewissen Grenzen anpassen (hint, hint: options timeout:1 rotate), aber so weit, dass dann z.B. ein Resolver, der ein paar Mal nicht antwortet, für eine gewisse Zeit lang gar nicht mehr angesprochen wird, geht die Logik dann eben doch nicht. Es gibt also zwar auch ein bisschen Redundanz, aber wirklich elegant ist sie nicht. Man braucht also wenn dann schon wirklich hoch verfügbare DNS-Resolver im Netz, denn jeder Ausfall zieht empfindliche Probleme nach sich: Logins via SSH brauchen sekundenlang, weil das System versucht, ein Reverse Lookup für die IP durchzuführen und danach auch noch ein Forward Lookup, um zu schauen, ob’s passt, und wehe dem, der kein HostnameLookups Off in seiner Apache-Konfiguration hat.

Ein lokaler DNS-Resolver hingegen fällt eigentlich nur in einer Situation aus: Nämlich dann, wenn die komplette Maschine ausfällt – und dann ist’s ohnehin egal, bzw. dann hat man ohnehin dringendere Probleme. Vor allem aber beträfe ein reiner Resolver-Ausfall dann nur diesen einen Host – und nicht gleich sämtliche Hosts im Netz, die jenen Resolver benutzen. Insofern fahren wir hier gewissermaßen eine Insel-Strategie: Wenn was kaputtgeht, dann jedenfalls nur sehr begrenzt. Und die Realität zeigt nun im Lauf von Jahren auf Dutzenden von Hosts: Es geht nichts kaputt. Das Setup ist rock-solid.

Als lokalen DNS-Cache benutzen wir hierbei dnscache aus dem djbdns-Paket. Das ist, ohne Frage, für jemanden, der sonst nicht mit Software aus dem DJB-Universum, mit einer gewöhnungsbedürften Installation verbunden, die aber schnell ihre Vorzüge ausspielt – und letztlich punktet dnscache genau wie tinydns eben besonders auch mit hoher Stabilität und Sicherheit. Einige Dinge sind aber zu beachten:

Erstens, errno.

Damit djbdns mit einer aktuellen glibc kompiliert werden kann, muss in conf-cc am Ende des Compileraufrufs -include /usr/include/errno.h angehängt werden; ein Umstand, den Dan Bernstein als Bug der glibc ansieht, auch wenn das durchaus diskutiert werden kann (runterscrollen zu „references“).

Zweitens, Root-Nameserver.

Die mit djbdns mitgelieferte Liste von Root-Nameservern ist nicht mehr ganz aktuell. Das lässt sich aber schnell fixen, weil natürlich jeder der (noch verfügbaren) Root-Nameserver eine aktuelle Liste liefern kann. Mit

dnsip `dnsqr ns . | awk '/^answer: \./ { print $5 }'` >
/service/dnscache/root/servers/@

lässt sie sich fix aktualisieren; ein

svc -h /service/dnscache

informiert dnscache zur Laufzeit über die neue Serverliste.

Drittens, Cache-Größe.

Die Standardkonfiguration gibt dnscache eine Cache-Größe von gerade mal 1 MB. Das hat zur Folge, dass insbesondere auf Systemen, die viele DNS-Anfragen machen, der Cache möglicherweise eine stärkere Fluktuation aufweist als nötig wäre, mit der Folge, dass DNS-Anfragen, die er eigentlich aus dem Cache hätte beantworten können, wenn jener nicht zu früh wieder hätte aufgeräumt werden müssen, erneut stellen muss. Angesichts heutiger RAM-Dimensionen stellt es mit Sicherheit kein Problem dar, dem Cache 10 MB zu verpassen:

echo 10000000 > /service/dnscache/env/CACHESIZE

vergrößert den Cache, und

echo 12000000 > /service/dnscache/env/DATALIMIT

teilt dem dnscache begrenzenden softlimit mit, dass dnscache nun mehr RAM belegen darf. Mit

svc -t /service/dnscache

wird die laufende dnscache-Instanz beendet, woraufhin svscan den Dienst automatisch neu startet. Welche Cache-Größe wirklich angemessen ist, führt hier zu weit; entsprechende Strategien sind aber bereits an verschiedenen Stellen dokumentiert.

Viertens sind wir kürzlich auf einen Patch gestoßen, der dnscache etwas äußerst Nützliches beibringen, nämlich die Möglichkeit, seinen aktuellen Cacheinhalt in eine Datei zu dumpen – und bei einem Start seinen Cache auch erstmal direkt aus jener Datei wieder zu befüllen. Auf diese Weise ist der Cache von dnscache auch nach einem Neustart sofort „warm“, ohne dass es erstmal wieder damit beginnen muss, von den Root-Nameservern her die TLD-spezifischen-Nameserver aufzulösen, und so weiter.

Der ursprüngliche Patch stammt von Efgé und bringt die Funktionalität ein, durch Senden eines SIGALRM dnscache dazu zu veranlassen, seinen Cacheinhalt in eine Datei zu dumpen, deren Name durch eine zuvor gesetzte Umgebungsvariable definiert wird, sowie die Funktionalität, jene Datei beim Starten wieder einzulesen. Nikola Vladov hat auf dieser Basis noch einen erweiterten Patch entwickelt, der zum einen dafür sorgt, dass dnscache beim Beenden automatisch einmal seinen Cache speichert, und der selbiges zudem in einem festlegbaren Rhythmus auch von sich aus tut, was insbesondere für Fälle praktisch ist, in denen ein Host crasht und von daher keine Gelegenheit mehr hat, den Cache zu schreiben wie bei einem sauberen Herunterfahren (nicht, dass sowas ständig vorkäme, aber der kluge Mann baut vor).

Lokal ordentlich zu cachen ist in jedem Fall eine gute Praxis, nicht nur der eigenen Performance wegen, sondern auch, um die Last, die man durch DNS-Anfragen auf anderen Hosts erzeugt, gering zu halten – es gewinnen also beide Seiten. Um so wichtiger ist das, wenn man nicht nur „gewöhnliche“ DNS-Daten abfragt, sondern auch Datenbanken, die via DNS publiziert werden, wie beispielsweise das IP to ASN Mapping der Team Cymru Community Services (mit herzlichem Dank an Daniel für den Tipp) – was für uns der Hauptgrund war, uns eingehender mit der Cache-Dumping-Thematik zu befassen. Der konkrete Anlass dafür reicht aber noch problemlos für einen weiteren Blogpost – stay tuned.

mkinitrd und Filesystem-Labels

Freitag, 09. Juli 2010

Au Backe, das hätte deutlich schiefer gehen können.

Nicht selten betreiben wir Server mit einem Xen-Kernel als Dom0 und binden die DomU aber nicht von einer lokalen Festplatte ein, sondern via iSCSI. Das stellt sich dann so dar, dass die Dom0 eine /dev/sda hat (z.B. eine eingebaute Festplatte); das iSCSI-Target wird dann eingebunden und steht als /dev/sdb bereit. Von diesem Device werden dann aber keine Partionen direkt gemountet, sondern das Device wird in der Xen-Konfiguration der DomU als phy:-Device angesprochen. So weit, so gut – klappt auch einwandfrei.

Nun hat der anaconda-Installer von CentOS 5 die Angewohnheit, für die Root- und die /boot-Partition Labels zu vergeben. Diese kann man auf der Shell z. B. mit e2label einsehen und auch nachträglich jederzeit ändern. In der /etc/fstab wird dann die Root-Partition nicht mit /dev/sda2 referenziert, sondern mit LABEL=/, weil die Partition eben das Label namens „/“ erhalten hat. Der Gedanke dahinter ist eigentlich gar nicht mal schlecht: Ändert ein Device seine Bezeichnung, beispielsweise weil ein zusätzliches Gerät am SCSI-Bus hängt und eine sda plötzlich eine sdb ist, so kann es dennoch unverändert angesprochen werden, weil es sein Label behält. Und das funktioniert auch prima.

Bis auf eins.

Wir führten auf einer Dom0 ein Kernel-Update durch. In diesem Zuge wurde auch eine neue initrd erstellt. Hierbei stellt mkinitrd alle für den Bootvorgang nötigen Module zusammen und strickt ein minimales init-Script. Das hat auch tausend Mal prima funktioniert – bis heute. Heute war nämlich gerade ein iSCSI-Target eingebunden – was an sich auch kein Problem ist, denn mkinitrd interessiert sich, was Treiber für Blockdevices angeht, eigentlich nur für die Devices, die in der /etc/fstab als zum Booten nötig referenziert werden. Nun nennt die /etc/fstab aber LABEL=/ als Root-Device, und damit nimmt das Unheil seinen Lauf. Zunächst einen Blick auf die Konfiguration:

# head -1 /etc/fstab 
LABEL=/ / ext3 defaults 1 1

# e2label /dev/sda2
/

# df -h | head -2
Filesystem            Size  Used Avail Use% Mounted on
/dev/sda2              31G  1.9G   27G   7% /

Und jetzt das Problem. Es ist wie gesagt gerade ein iSCSI-Target eingebunden, das vom Kernel auf /dev/sdb gemappt wird und das nur eine Partition hat:

# ls -l /dev/disk/by-path/ip-*
lrwxrwxrwx 1 root root  9 Jul  8 22:32 /dev/disk/by-path/[...]-lun-0 -> ../../sdb
lrwxrwxrwx 1 root root 10 Jul  8 22:32 /dev/disk/by-path/[...]-lun-0-part1 -> ../../sdb1

Und eben jene Partition … hat auch das Label „/“:

# e2label /dev/sdb1
/

mkinitrd -v zeigt dann, dass es zwar versucht, das Richtige zu tun (nämlich herauszubekommen, was für eine Art Device die Root-Partition ist, damit es die dafür nötigen Treiber in die initrd einbinden kann, löst das Label aber prompt auf genau das falsche Device auf:

...
Found root device sdb1 for LABEL=/
Looking for driver for device sdb1
Found iscsi component /sys/devices/platform/host6/session5/target6:0:0/6:0:0:0
...

Damit erstellt es letztlich eine initrd mit dem falschen Root-Device – und baut dazu den gesamten iSCSI-Support mit ein, bis hin zu einem iscsistart-Aufruf, der die Zugangsdaten beinhaltet, die nötig sind, um das Target vom iSCSI-Host einbinden zu können. Ohne das -v dabei sieht man davon … nix. Vor allem sahen wir schon deshalb nichts davon, weil wir nicht mal mkinitrd selbst aufgerufen hatten – das hatte new-kernel-pkg aufgerufen, und auch das wiederum hatten wir nicht aktiv benutzt, sondern es befindet sich im %postinstall-Abschnitt des Kernel-RPMs, das wir gerade mit yum upgrade kernel-xen auf den neuesten Stand gebracht haben. Wie schon einige Male davor, immer ohne Probleme. Davor war eben zum Zeitpunkt der Kernel-Updates auch noch kein iSCSI-Target eingebunden.

Nach einem Reboot zeigte sich ein absurdes Bild. Beim Booten war deutlich erkennbar, wie sich der Server faktisch das iSCSI-Device holte und als /dev/sda vorzeigte; die lokale Festplatte war dadurch nun plötzlich /dev/sdb (in diesem Zusammenhang mag man ein zynisches „Ja und, dafür gibt’s doch die Labels!“ einwerfen. Sehr witzig, haha). Merkwürdigerweise war der Inhalt der Root-Partition definitiv der, der von der Root-Partition der lokalen Platte kam – obwohl jene laut mount das iSCSI-Device sein sollte. Kurz, das System befand sich in nicht wirklich definiertem Zustand, und darüberhinaus mussten wir uns darum sorgen, dass das fragliche iSCSI-Device, das gerade angeblich unsere Root-Partition darstellte (aber irgendwie eben auch wieder nicht) Schaden davontragen könnte, weil es nämlich parallel dazu auf einem anderen Xen-Host bereits eingebunden und aktiv in Benutzung war. Wir können uns bislang nicht erklären, wie es zu dieser merkwürdigen Differenz zwischen angeblichem und realem Mount-Zustand kommen konnte, aber auf wundersame Weise blieb das iSCSI-Target von diesem Schindluder völlig unbeeindruckt. Puh..!

Der Bugfix war letztlich trivial: Wir haben in der /etc/fstab der Dom0 alle Labels durch echte Devicenamen ersetzt und die initrd neu erstellt. Ein Reboot, und das System befand sich wieder in definiertem Zustand. Für heute: Mit einer großen Portion Glück davongekommen. Und viel über mkinitrd gelernt.

RAID1 mit neuen Platten vergrößern

Donnerstag, 24. Juni 2010

In einem Kundenserver, der schon ein paar Jahre auf dem Buckel hat, sollte der Speicherplatz erweitert werden – eingebaut waren zwei Festplatten mit je 160 GB; für heutige Verhältnisse in der Tat nicht mehr zeitgemäß. Möglichkeiten gibt es viele: Zusätzliche Platten einbauen (dann verbraucht die Kiste aber mehr Strom und wird lauter); eine externe Festplatte anschließen (die ist dann aber nicht redundant); die Festplatten austauschen und das System umkopieren (das dauert dann aber ewig). Schließlich haben wir uns für die schickste Möglichkeit mit minimaler Downtime entschieden: Die Festplatten nacheinander im RAID auswechseln. Downtime: Etwa zweimal fünf Minuten jeweils für den Austausch einer Platte. Plus ein bisschen Arbeitsaufwand, der aber im laufenden Betrieb erledigt werden kann.

Zum Nachmachen hier nun das Protokoll. Die Festplatten sind Platten nach S-ATA-Standard; stehen im System also als /dev/sda und /dev/sdb bereit. Sie haben jeweils drei Partionen, die per Software-RAID zusammengefasst sind:

# cat /proc/mdstat 
Personalities : [raid1] 
md1 : active raid1 sda1[0] sdb1[1]
      200704 blocks [2/2] [UU]
      
md2 : active raid1 sda3[0] sdb3[1]
      974550976 blocks [2/2] [UU]
      
md0 : active raid1 sda2[0] sdb2[1]
      2008000 blocks [2/2] [UU]
      
unused devices: 

Dabei fungiert /dev/md0 als Swap-Partition; /dev/md1 als /boot und /dev/md2 als Root-Partition. So, und jetzt geht’s los.

Erstmal wird kurz und schmerzlos eine der beiden Platten ausgebaut – in unserem Fall /dev/sdb, aber welche zuerst kommt, spielt keine Rolle. An ihrer Stelle wurde eine unpartitionierte Platte eingebaut. Der Server wurde wieder hochgefahren; alles kein Problem – dafür ist ein RAID1 ja da. Anschließend galt es, die neue Festplatte zu partitionieren. Das haben wir hier ganz traditionell mit fdisk gemacht und dabei für die ersten beiden kleinen Partitionen (Swap und /boot) die gleiche Zylinderzahl verwendet; die dritte Partition wurde dann entsprechend ausgeweitet. So sieht’s nun aus:

# fdisk -l /dev/sdb

Disk /dev/sdb: 1000.2 GB, 1000204886016 bytes
255 heads, 63 sectors/track, 121601 cylinders
Units = cylinders of 16065 * 512 = 8225280 bytes

   Device Boot      Start         End      Blocks   Id  System
/dev/sdb1   *           1          25      200781   fd  Linux raid autodetect
/dev/sdb2              26         275     2008125   fd  Linux raid autodetect
/dev/sdb3             276      121601   974551095   fd  Linux raid autodetect

Wichtig ist, hier den korrekten Partitionstyp zu vergeben (in diesem Fall fd für Linux raid autodetect) sowie die erste Partition bootfähig zu machen.

Nun fehlt den drei /dev/md*-Devices ja die Redundanz; die stellen wir also erstmal wieder her, in dem wir mittels mdadm die frisch angelegten Partitionen hinzufügen:

mdadm /dev/md1 -a /dev/sdb1
mdadm /dev/md0 -a /dev/sdb2
mdadm /dev/md2 -a /dev/sdb3

Die ersten beiden synchronisieren in unter einer Minute; für /dev/md2 wurde gut eine halbe Stunde fällig. Mittels

watch cat /proc/mdstat

lässt sich die Synchronisation gut beobachten. Dieser Vorgang muss vollständig und fehlerfrei abgeschlossen sein, denn wir haben ja vor, gleich die Platte, von der synchronisiert wird, ebenfalls zu wechseln, und dafür muss die neu eingebaute natürlich komplett auf dem neuesten Stand sein!

Nun möchte man meinen, das wär’s, aber eine ganz wichtige Sache fehlt noch: Auch wenn die Daten jetzt wieder synchron sind, so fehlt noch der Bootloader – sprich, wenn jetzt die verbliebene Platte ausgewechselt würde, könnte das System nicht mehr booten. Es gilt also, GRUB auf /dev/sdb zu installieren, und das ist nicht ganz so einfach, wie es auf den ersten Blick scheint. Die RAID1-Redundanz liegt ja auf Partitionsebene – betrifft also nicht den Bootsektor selbst.

Der Haken an der Sache ist: GRUB führt eine device.map, in der die physischen Festplatten auf die GRUB-internen Bezeichnungen hd0 und hd1 gemappt werden. Kein Problem, solange es die zweite Festplatte ist, die ausfällt – fällt allerdings die erste aus, wird /dev/sdb als einzige verbleibende Platte plötzlich hd0 statt hd1 – und das schmeckt GRUB nun überhaupt nicht. Man kann nun in der device.map zwar beide Platten von Hand als hd0 definieren; das mag jedoch grub-install nicht und quittiert mit:

The drive (hd0) is defined multiple times in the device map /boot/grub/device.map

Es bleibt also letztlich nur, das Spiel von Hand zu spielen, und zwar so:

grub> device (hd0) /dev/sdb
grub> root (hd0,0)
grub> setup (hd0)
grub> quit

Mit anderen Worten: Man verkauft GRUB die zweite Platte als hd0, also als erste, denn wenn die echte erste Platte einmal ausfällt – dann ist die zweite Platte ja auch faktisch die erste. Das klappt prima, und das System ist bereit zum Austausch der verbliebenen kleinen Platte gegen das größere Modell. Hat man alles richtig gemacht, sollte der Server problemlos mit der einen neuen großen Platte booten (die nun ihrerseits auch nur eine Hälfte des RAID1 darstellt).

Nun sind die obigen Schritte für die verbliebene Platte, in unserem Fall /dev/sda, zu wiederholen: Partitionieren; mittels mdadm zum RAID1 hinzufügen; GRUB von Hand in den Bootsektor packen.

Nun haben wir einen Server mit neuen Festplatten, bei denen die Root-Partition deutlich größer ist, aber bisher haben wir davon noch nichts – es fehlen noch zwei entscheidende Schritte. Zunächst muss die Partition /dev/md2, die jetzt nur noch einen Bruchteil des physisch vorhandenen Speicherplatzes nutzt, auf die neue Größe ausgedehnt werden. Das erledigen wir mit:

mdadm --grow -z max /dev/md2

Hiermit wird für den zusätzlichen Speicherplatz ein Resync angestoßen – sprich, dieser Vorgang zieht sich eine ganze Weile, zumal wir ja hier auf Terabyte-Platten aufgerüstet haben. Im konkreten Fall mussten wir rund zwei Stunden dafür einkalkulieren.

Nun sind wir aber immer noch nicht fertig. Zwar ist das Device jetzt endlich so groß, wie wir es haben wollen, aber das Dateisystem weiß davon noch nichts – und das muss schließlich die neu hinzugekommenen Blöcke auch ansprechen können.

Erfreulicherweise gibt es seit einigen Jahren die Möglichkeit, ein ext2-/ext3-Dateisystem im laufenden Betrieb, sprich: im gemounteten Zustand zu vergrößern. Bei CentOS 4, das auf diesem Server noch läuft (kein Problem, wird ja noch bis Anfang 2012 supportet), übernimmt diesen Job noch ext2online; bei neueren Distributionen ist der Live-Resizing-Code in resize2fs aufgegangen, das bis dahin nur eine Größenanpassung von nicht aktiv verwendeten Devices realisierte. Auf geht’s:

ext2online -v /dev/md2

Der Vorgang zieht sich ein bisschen, aber man kann sehr schön beobachten, wie sich die Größe im laufenden Betrieb ändert – und sich der prozentuale Füllstand der Root-Partition entsprechend nach und nach verringert:

# df -h
Filesystem            Size  Used Avail Use% Mounted on
/dev/md2              178G  123G   46G  73% /
/dev/md1              190M   15M  166M   8% /boot
none                  505M     0  505M   0% /dev/shm

… warten …

# df -h
Filesystem            Size  Used Avail Use% Mounted on
/dev/md2              203G  123G   69G  65% /
/dev/md1              190M   15M  166M   8% /boot
none                  505M     0  505M   0% /dev/shm

… warten …

# df -h
Filesystem            Size  Used Avail Use% Mounted on
/dev/md2              213G  123G   79G  62% /
/dev/md1              190M   15M  166M   8% /boot
none                  505M     0  505M   0% /dev/shm

Wenn dieser Vorgang abschlossen ist – war’s das. Viel Spaß mit dem neu hinzugewonnenen Speicherplatz!

LVM DomU unter Dom0

Montag, 14. Juni 2010

Am Wochenende hatten wir den Fall das bei einer Xen-Instanz das Dateisystem geprüft und ggf. repariert werden musste. Die Xen-Instanz liegt in diesem Fall in einem Image (gast.img)  welches mit Hilfe des Block-Tap-Mechanismus (blktap) zur Verfügung gestellt wird.

Inhalt des Images gast.img:

 Partition 1     -> /boot
 Physical Volume -> VolumeGroup00 -> LogVol00 (/)  [80 GB]
                                  -> LogVol01 (swap)

Jetzt ist es so, das durch die damals vorgenommene Defaultinstallation das Dateisystem des Wirtes (xenwirt) ebenso aufgebaut ist wie das des Gastsystems:

 Partition 1     -> /boot
 Physical Volume -> VolumeGroup00 -> LogVol00 (/)   [320 GB]
                                  -> LogVol01 (swap)

Ok, zurück zum Thema. Wir wollen das Dateisystem des Gastes checken (fsck) und anschließend einbinden (mounten). Nachfolgend alle notwendigen Schritte:

Mit Hilfe von kpartx alle Partitionen des Images erkennen und die dazugehörige device-map (/dev/mapper/loop<Loopbackdevice>p<Partitions>) erstellen

kpartx -a /xen/gast.img

Angenommen kpartx verwendet jetzt das Loopbackdevice /dev/loop7, dann findet man jetzt die Partitionen des Images unter /dev/mapper/loop7p*. Die erste Partition (/boot) kann man jetzt schon direkt ansprechen und z.B. einbinden (mount /dev/mapper/loop7p1 /mnt/tmp). Durch das Ausführen von vgscan werden jetzt die neu hinzugekommenen VolumeGroups gefunden und diese könnten jetzt sogar eingebunden und verwendet werden. Hier stoßen wir jetzt leider nur auf unser kleines o.a. Problem: Beide VolumeGroups haben den gleichen Namen, nämlich VolGroup00. Damit wir nun die „richtigen“ LogicalVolumes ansprechen können müssen wir die neu hinzugekommene VolumeGroup umbenennen.  Dazu kopieren wir uns die UUID der VG (die mit der Größe von 80GB) die uns das Kommando vgs liefert und nutzen die ID als Identifikationsmerkmal.

vgscan

vgs -v

Als neuen Namen verwenden wir nun etwas Eindeutiges.

vgrename H3sOYJ-ywTE-vNTf-pm0i-De3zXU VolGroupGuest01

Jetzt können wir die umbenannte VolumeGroup aktivieren damit uns (endlich) unter /dev/VolGroupGuest01/ die Logical Volumes bereitstehen. Einer Überprüfung der  root-Partition des Gastes steht nun nichts mehr im Weg.

vgchange -ay VolGroupGuest01

fsck.ext3 -C -f /dev/VolGroupGuest01/LogVol00

Da wir den Namen der VolumeGroup nach dem fsck jetzt nicht wieder in VolGroup00 umbenennen können (da es diese VG ja schon gibt!), ist es Notwendig das Gastsystem anzupassen. Wir binden das Gastsystem unter /mnt/guest1 ein und ändern die Grubkonfiguration /boot/grub/grub.conf, die /etc/fstab und die initrd folgendermaßen ab:

Einbinden des Gastsystems:

mount /dev/VolGroupGuest01/LogVol00 /mnt/guest1

mount /dev/mapper/loop7p1 /mnt/guest1/boot

Anpassen /etc/fstab

perl -pi -e „s/VolGroup00/VolGroupGuest01/“ /mnt/guest1/etc/fstab

Anpassen /boot/grub/grub.conf

perl -pi -e „s/VolGroup00/VolGroupGuest01/“ /mnt/guest1/boot/grub/grub.conf

Anpassen /boot/initrd-*

mkdir ~/tmp
cd ~/tmp
cp /mnt/guest1/boot/initrd-<KERNELVERSION>.img ./initrd.gz
gunzip initrd.gz
mkdir content
cd content
cpio -id < ../initrd

perl -pi -e "s/VolGroup00/VolGroupGuest01/" ./init
cd ~/tmp/content
find . | cpio --create --format='newc' > ~/tmp/newinitrd
cd ~/tmp
gzip newinitrd
mv newinitrd.gz /mnt/guest1/boot/initrd-<KERNELVERSION>.img

Jetzt können wir das Dateisystem wieder aushängen, die VolumeGroup deaktivieren, das Devicemapping entfernen und das Loop-Device aushängen.

umount /mnt/guest1/boot

umount /mnt/guest1

vgchange -an VolGroupGuest01

kpartx -d /dev/loop7

Das Image steht jetzt wieder wie gewohnt für xen zur Verfügung. In unserem Fall ist der Gast ohne weitere Probleme gebootet.

PlayStation 3 jetzt ganz ohne Linux

Montag, 29. März 2010

Die PS3 bietet von Haus aus ein sogenanntes „Other OS“-Feature, mit dem es möglich ist, zusätzlich zum Sony-eigenen GameOS auch Linux zu installieren. Das haben viele Leute, auch aus meinem persönlichen Umfeld, gerne genutzt – nicht zuletzt, weil die übers Zocken hinausgehenden Fähigkeiten der PS3 als Mediacenter nur recht begrenzt waren und Linux hier viel mehr Möglichkeiten bot. In erster Linie war es aber schon die schiere Rechenleistung, die den Einsatz der PS3 als Rechnerersatz auch im prominenten Umfeld salonfähig machte, so etwa als die US Air Fore 2.200 Konsolen beantragte und zumindest 1.700 bekam, nachdem sie bereits einen High-Performance-Cluster aus 336 Konsolen im Einsatz hatte. Durch die Presse ging aber auch, dass das US Immigration and Customs Enforcement Cyber Crimes Center auch einen interessanten Verwendungszweck für die PS3s gefunden hat, nämlich um die Passwörter zu knacken, mit denen mutmaßliche Pädophile ihre Kinderpornos verschlüsseln.

Damit ist jetzt Schluss. Bereits als Sony den Nachfolger herausbrachte, nämlich die PS3 Slim, war das „Other OS“-Feature weg. Ärgerlich, aber bei einem neuen Produkt natürlich Sonys gutes Recht – das betrifft ja nicht die bestehenden PS3s, bei denen „Other OS“ eben nun mal ein Ausstattungsmerkmal war. Dennoch ging es auf den betreffenden Mailinglisten hoch her, und nicht wenige sorgten sich darum, dass dieses Feature auch auf den älteren Konsolen wegfallen könnte. So hoch, dass sich Geoff Levand, der seit 2000 bei Sony für die Linux-Aktivitäten zuständig ist und auch der Maintainer der entsprechenden Kernel-Erweiterungen ist, sich dazu genötigt sah, in mehreren Postings auf cbe-oss-dev dieses Statement von seiner offiziellen Sony-Mailadresse aus zu verfassen („SCE“ steht für „Sony Computer Entertainment“, also die Tochtergesellschaft von Sony, die für die Playstation verantwortlich zeichnet):

Please be assured that SCE is committed to continue
the support for previously sold models that have the
„Install Other OS“ feature and that this feature will
not be disabled in future firmware releases.

Nun, offensichtlich hat Sony kein sehr langes Gedächtnis. Gerade einmal sieben Monate später verkündet das offizielle Playstation-Blog Informationen zum kommenden Firmware-Update 3.21:

[…] It will disable the “Install Other OS” feature that was available on the PS3 systems prior to the current slimmer models, launched in September 2009. […] If you are one of the few who use the “Other OS” feature, or if you belong to an organisation that does, then you can choose not to upgrade your system. However, doing so will mean that the following features will not be available:

  • Ability to sign in to PlayStation Network and use network features that require signing in to PlayStation Network, such as online features of PS3 games and chat.

[…]

Sehe ich das richtig? Wenn man das Firmware-Update installiert, muss man also damit leben, dass ein offizielles Feature wegfällt – und damit auch die ganzen auf dem „Other OS“ abgelegten Daten. An die kommt man dann nämlich laut Blogpost nicht mehr dran. Wenn man aber das Firmware-Update nicht installiert, muss man damit leben, dass man ein anderes offizielles Feature künftig nicht mehr nutzen kann. Was also hätten’s denn gern: Pest oder Cholera?

Wie man so mit seinen eigenen Kunden, die das Gerät mitsamt der entsprechenden Ausstattungsmerkmale gekauft haben, so behandeln kann, will sich mir einfach nicht erschließen. Aber vielleicht haben die Kunden, wenn bei der nächsten Anschaffung eines technischen Geräts auch ein Sony-Produkt zur Auswahl steht, ja ein ausreichend langes Gedächtnis.

Bevor man das RAID anfaßt

Freitag, 26. März 2010

Nur eine Anmerkung zu dem Bericht meines Kollegen Jonas über die Rettung eines RAID 5:

Ich war mal mit einem RAID 10 in einer ganz ähnlichen Situation, ich hatte kein aktuelles Backup der Daten auf dem RAID und ich konnte leider auch keine Images der Platten im RAID mehr ziehen, um bei einem gescheiterten Reparaturversuch nötigenfalls den Versuch rückgäng zu machen. Es ging auch nicht nur um ein paar Photos, sondern leider um ziemlich wichtige Daten. Ich war also sehr auf Vorsicht bedacht.

Wie schon gesagt war die Situation ganz ähnlich. Ich war mir sehr sicher, daß die Daten auf den Platten noch konsistent waren, aber die Superblöcke wollten partout nicht zusammengehen. Auch ich bin damals auf den Hinweis gestoßen, daß man das RAID auch quasi neu anlegen kann und so wieder zu intakten Superblöcken kommen kann. Aber da ich mir Fehler nicht leisten konnte, zögerte ich etwas mit der Umsetzung.

Und dann hatte ich eine Idee, die letztlich auch funktioniert hat: Man kann sich auf einem anderen Rechner mit Linux auf dem man LVM eingerichtet hat (in diesem Fall war das eine gerade nicht gebrauchte Workstation, auf der im LVM noch viel Platz war) einfach ein paar Logical Volumes einrichten und damit einen RAID einrichten. Natürlich ist ein RAID über Logical Volumes auf derselben Festplatte vollkommen sinnfrei und absolut inperformant, aber es eignet sich hervorragend um das Verhalten von mdadm zu testen. Ich hab mir also vier LVs angelegt, darüber einen RAID 10 erzeugt, darin ein Dateisystem angelegt und einfach ein paar Nutzdaten reinkopiert: eine größere Logdatei, eine MP3, einen Film. Dann habe ich das Dateisystem wieder ausgehängt und den RAID bewußt beschädigt — und zwar so daß derselbe Fehlerzustand hergestellt wurde wie auf dem „echten“ RAID 10. Ich konnte dann die Reparatur in aller Ruhe testen, man könnte sogar sagen üben, und nach erfolgreicher Reparatur des Test-RAIDs wunderbar verifizieren, ob die Daten in dem Dateisystem noch stimmten.

Das beste daran war, daß mdadm eben wirklich nicht auf das neu angelegte RAID schreibt, solange man es in degradiertem Zustand anlegt. D.h. ich konnte mir sogar mehrere Fehlversuche leisten. (Bei einem RAID 10 nicht unwahrscheinlich, man muß ja die korrekte Anordnung der Festplatten finden.) Ob das RAID korrekt zusammengefügt war hab ich jeweils damit getestet, ob ich das Dateisytem darin im Read Only Modus mounten konnte oder nicht.

Ein weiterer Vorteil dieser LVM Testumgebung: Man kann sich mit LVM wunderbar Snapshots der LVs anlegen. Wenn man dann bei den Reparaturversuchen tatsächlich etwas beschädigen sollte (kam mir damals nicht vor, aber es hätte ja), dann kann man seine Testumgebung schnell wieder in den Ausgangszustand zurückversetzen und muß sie nicht komplett neu anlegen.


Impressum