, während er auf die Tasten hämmerte, als hinge sein Leben davon ab, dreht er sich mit einem halb wilden Ausdruck in den Augen und einem schelmischen Grinsen zu mir um — „Alter, ich glaube, ich hab's verstanden.“
„Schau mal hierher,“ sagt er und zeigt auf eines der Symbole auf dem Bildschirm — „Wetten um meinen roten Hut, dass wenn wir hier das hinzufügen, was ich dir gerade geschickt habe“ — zeigt auf einen anderen Teil des Codes — „die Fehlermeldung nicht mehr erscheint.“
Etwas verwirrt und müde ändere ich den sed-Ausdruck, an dem wir schon eine Weile gearbeitet haben, speichere die Datei und starte systemctl varnish reload. Die Fehlermeldung ist verschwunden...
„Die Mails, die ich mit dem Kandidaten ausgetauscht habe,“ fuhr mein Kollege fort, während sein Grinsen in ein aufrichtiges, freudiges Lächeln übergeht, „plötzlich wurde mir klar, dass es genau dasselbe Problem ist!“
Wie alles begann
Der Artikel setzt ein Verständnis der Funktionsweise von bash, awk, sed und systemd voraus. Kenntnisse in varnish sind von Vorteil, aber nicht zwingend erforderlich.
Die Zeitstempel in den Snippets wurden geändert.
Zusammen geschrieben mit .
Dieser Text ist eine Übersetzung des Originals, das vor zwei Wochen auf Englisch veröffentlicht wurde; Übersetzung .
Die Sonne scheint an einem weiteren warmen Herbstmorgen durch die Panoramafenster, eine Tasse frisch zubereitetes, koffeinhaltiges Getränk steht abseits der Tastatur, in den Kopfhörern erklingt die Lieblingssymfonie, die das Geräusch der mechanischen Tastaturen übertönt, und der erste Eintrag in der Ticketliste auf dem Kanban-Board leuchtet mit dem schicksalhaften Titel „Untersuchen Sie varnishreload sh: echo: I/O-Fehler im Staging“ (Untersuchen Sie „varnishreload sh: echo: I/O-Fehler“ im Staging) auf. Wenn es um Varnish geht, ist kein Platz für Fehler, auch wenn sie nicht zu Problemen führen, wie in diesem Fall.
Für diejenigen, die nicht vertraut sind mit , ist dies ein einfaches Shell-Skript, das zur Neuinstallation der Konfiguration eingesetzt wird — auch bekannt als VCL.
Wie der Name des Tickets andeutet, ist der Fehler auf einem der Server in der Staging-Umgebung aufgetreten. Da ich mir sicher war, dass das Routing des Varnish-Servers in der Staging-Umgebung einwandfrei funktioniert, nahm ich an, dass es sich um einen kleinen Fehler handelt. Ein einfaches Protokoll, das in einen bereits geschlossenen Ausgangs-Stream geraten ist. Ich nehme mir das Ticket, überzeugt, dass ich es in weniger als 30 Minuten als erledigt markieren werde, klopfe mir selbst auf die Schulter für die Bereinigung des Boards von erneutem Müll und widme mich wichtigeren Aufgaben.
Gegen eine Wand mit 200 km/h krachen
Als ich die Datei öffnete varnishreload, auf einem der Server mit Debian Stretch, sah ich ein Shell-Skript mit weniger als 200 Zeilen.
Nachdem ich das Skript durchgegangen bin, habe ich nichts bemerkt, was bei mehrfacher Ausführung direkt aus dem Terminal Probleme verursachen könnte.
Schließlich ist das ja nur die Staging-Umgebung; selbst wenn es kaputtgeht, wird sich niemand beschweren, naja… nicht zu viele. Ich starte das Skript und schaue, was im Terminal ausgegeben wird, nur leider sind auch keine Fehler mehr sichtbar.
Noch ein paar Ausführungen, um sicherzustellen, dass ich den Fehler nicht reproduzieren kann, ohne zusätzlichen Aufwand und ich beginne, darüber nachzudenken, wie ich dieses Skript ändern und zum Auslösen des Fehlers bringen kann.
Kann das Skript STDOUT überdecken (mit > &-)? Oder STDERR? Keines von beiden hat schließlich funktioniert.
Offensichtlich verändert systemd irgendwie die Ausführungsumgebung, aber wie und warum?
Ich öffne vim und bearbeite varnishreload, indem ich set -x direkt unter dem Shebang hinzufüge, in der Hoffnung, dass die Debug-Ausgabe des Skripts ein wenig Licht ins Dunkel bringt.
Die Datei ist geändert, also starte ich varnish neu und sehe, dass die Änderung alles kaputt gemacht hat... Der Output ist ein komplettes Chaos, in dem Unmengen von C-ähnlichem Code sind. Selbst das Scrollen im Terminal reicht nicht aus, um herauszufinden, wo das Ganze anfängt. Ich bin völlig ratlos. Kann der Debug-Modus das Verhalten von Programmen, die im Skript aufgerufen werden, beeinflussen? Nein, das ist Quatsch. Ein Fehler im Shell? Mehrere mögliche Szenarien rasen in meinem Kopf herum wie Kakerlaken in verschiedene Richtungen. Eine Tasse mit einem koffeinreichen Getränk wird sofort geleert, ein schneller Gang zur Küche, um Nachschub zu holen, und... los geht's. Ich öffne das Skript und schaue mir den Shebang genau an: #!/bin/sh.
/bin/sh Das ist doch einfach ein Symlink auf Bash, also wird das Skript im POSIX-kompatiblen Modus interpretiert, oder? Weit gefehlt! Die Standard-Shell in Debian ist dash, und genau darauf basiert es. /bin/sh.
# ls -l /bin/sh
lrwxrwxrwx 1 root root 4 Jan 24 2017 /bin/sh -> dashUm es auszuprobieren, habe ich den Shebang geändert in #!/bin/bash, habe entfernt set -x und es nochmals versucht. Schließlich, nach dem nächsten Neustart von varnish, erschien eine akzeptable Fehlermeldung in der Ausgabe:
Jan 01 12:00:00 hostname varnishreload[32604]: /usr/sbin/varnishreload: Zeile 124: echo: Schreibfehler: Brechende Pipe
Jan 01 12:00:00 hostname varnishreload[32604]: VCL 'reload_20190101_120000_32604' kompiliertZeile 124, da ist es!
114 find_vcl_file() {
115 VCL_SHOW=$(varnishadm vcl.show -v "$VCL_NAME" 2>&1) || :
116 VCL_FILE=$(
117 echo "$VCL_SHOW" |
118 awk '$1 == "//" && $2 == "VCL.SHOW" {print; exit}' | {
119 # all diese Zeremonie, um Leerzeichen in FILE zu handhaben
120 read -r DELIM VCL_SHOW INDEX SIZE FILE
121 echo "$FILE"
122 }
123 ) || :
124
125 if [ -z "$VCL_FILE" ]
126 then
127 echo "$VCL_SHOW" >&2
128 fail "konnte den VCL-Dateinamen nicht abrufen"
129 fi
130
131 echo "$VCL_FILE"
132 }Aber wie sich herausstellte, ist Zeile 124 ziemlich leer und uninteressant. Ich konnte nur vermuten, dass der Fehler als Teil des mehrzeiligen Codes aufgetreten ist, der in Zeile 116 beginnt.
Was wird letztendlich in die Variable VCL_FILE gepackt, als das oben genannte Sub-Shell ausgeführt wird?
Zunächst sendet er den Inhalt der Variablen VLC_SHOW, die in Zeile 115, dem folgenden Befehl durch die Pipe, erstellt wurde. Was passiert dann dort?
Zuerst wird hier varnishadm, das Teil des Installationspakets varnish ist, verwendet, um varnish ohne Neustart zu konfigurieren.
Der Unterbefehl vcl.show -v wird verwendet, um die gesamte in ${VCL_NAME}, definierte VCL-Konfiguration in STDOUT auszugeben.
Um die aktuell aktive VCL-Konfiguration sowie einige frühere Versionen der Routing-Konfigurationen von varnish, die noch im Speicher sind, anzuzeigen, kann der Befehl varnishadm vcl.list, dessen Ausgabe der folgenden ähnlich sein wird:
discarded cold/busy 1 reload_20190101_120000_11903
discarded cold/busy 2 reload_20190101_120000_12068
discarded cold/busy 16 reload_20190101_120000_12259
discarded cold/busy 16 reload_20190101_120000_12299
discarded cold/busy 28 reload_20190101_120000_12357
active auto/warm 32 reload_20190101_120000_12397
available auto/warm 0 reload_20190101_120000_12587Der Wert der Variablen ${VCL_NAME} wird in einem anderen Teil des Skripts varnishreload auf den Namen der derzeit aktiven VCL gesetzt, falls vorhanden. In diesem Fall wird es „reload_20190101_120000_12397“ sein.
Gut, die Variable ${VCL_SHOW} enthält die vollständige Konfiguration für varnish, das ist klar. Jetzt habe ich endlich verstanden, warum die Ausgabe dash mit set -x war so fehlerhaft – er umfasste den Inhalt der resultierenden Konfiguration.
Es ist wichtig zu verstehen, dass eine vollständige VCL-Konfiguration oft aus mehreren Dateien zusammengestellt werden kann. Kommentare im C-Stil werden verwendet, um anzugeben, wo eine Konfigurationsdatei in eine andere eingebunden wurde, und genau das beschreibt die folgende Zeile des Codeausschnitts.
Die Syntax der Kommentare, die eingebundene Dateien beschreiben, hat folgendes Format:
// VCL.SHOW <NUM> <NUM> <FILENAME>Die Zahlen sind in diesem Kontext nicht wichtig, uns interessiert der Dateiname.
Was passiert also im Sumpf der Befehle, die mit Zeile 116 beginnen?
Lassen Sie uns das klären.
Der Befehl besteht aus vier Teilen:
- Ein einfaches
echo, das den Wert der Variablen ausgibt${VCL_SHOW}echo "$VCL_SHOW" awk, das die Zeile (Eintrag) sucht, bei der das erste Feld, nach der Trennung des Textes, „//“ und das zweite „VCL.SHOW“ ist.
Awk wird die erste Zeile, die diesen Mustern entspricht, ausgeben und dann die Verarbeitung sofort beenden.awk '$1 == "//" && $2 == "VCL.SHOW" {print; exit}'- Ein Codeblock, der die Werte von Feldern, die durch Leerzeichen getrennt sind, in fünf Variablen speichert. Die fünfte Variable FILE erhält den Rest der Zeile. Schließlich gibt das letzte Echo den Inhalt der Variable aus.
${FILE}.{ read -r DELIM VCL_SHOW INDEX SIZE FILE; echo "$FILE" } - Da alle Schritte von 1 bis 3 in einem Sub-Shell eingeschlossen sind, wird der Wert ausgegeben.
$FILEwird in der Variable gespeichert.VCL_FILE.
Wie aus dem Kommentar in Zeile 119 hervorgeht, dient dies nur dem Zweck, zuverlässig mit Fällen umzugehen, in denen VCL auf Dateien mit Leerzeichen im Namen verweist.
Ich habe die ursprüngliche Verarbeitungslogik für ${VCL_FILE} auskommentiert und versucht, die Reihenfolge der Befehle zu ändern, aber das führte zu nichts. Bei mir hat alles einwandfrei funktioniert, während der Dienststart einen Fehler ausgegeben hat.
Es scheint, dass der Fehler einfach nicht reproduzierbar ist, wenn das Skript manuell ausgeführt wird, während die angesetzten 30 Minuten bereits sechs Mal abgelaufen sind und zudem eine wichtigere Aufgabe aufgetaucht ist, die die anderen Dinge in den Hintergrund gedrängt hat. Der Rest der Woche war mit den verschiedensten Aufgaben gefüllt und wurde nur ein wenig durch einen Bericht über sed und ein Vorstellungsgespräch mit einem Kandidaten aufgelockert. Das Problem mit dem Fehler in varnishreload ging unwiderruflich in den Sand der Zeit verloren.
Ihr angebliches sed-Fu... ist tatsächlich... Mist.
In der nächsten Woche hatte ich einen ziemlich freien Tag, also habe ich beschlossen, mich wieder um dieses Ticket zu kümmern. Ich hoffte, dass in meinem Kopf ein Hintergrundprozess die ganze Zeit nach einer Lösung für dieses Problem gesucht hat, und diesmal würde ich definitiv verstehen, worum es geht.
Da beim letzten Mal eine einfache Änderung des Codes nicht geholfen hat, habe ich einfach beschlossen, ihn ab Zeile 116 neu zu schreiben. Der bestehende Code war sowieso nicht gut, und es gibt absolut keinen Grund, ihn zu verwenden. read.
Wenn ich mir den Fehler nochmal ansehe:
sh: echo: broken pipe — in diesem Befehl kommt echo an zwei Stellen vor, aber ich vermute, dass die erste der wahrscheinliche Übeltäter ist (oder zumindest Mitwisser). Awk ist ebenfalls nicht vertrauenswürdig. Und falls dies wirklich das Problem ist: awk | {read; echo} warum sollte man dann diese Konstruktion nicht ersetzen? Dieser Einzeiler nutzt nicht alle Möglichkeiten von awk und hat zudem diesen überflüssigen read Zusatz.
Da letzte Woche ein Bericht über sed, wollte ich meine neu erworbenen Fähigkeiten ausprobieren und echo | awk | { read; echo} in eine verständlichere Form bringen. echo | sed. Obwohl dies sicherlich nicht der beste Ansatz zur Fehlersuche ist, dachte ich, dass ich zumindest mein sed-fu ausprobieren und vielleicht etwas Neues über das Problem lernen könnte. Dabei bat ich meinen Kollegen, den Autor des sed-Berichts, mir zu helfen, ein effizienteres sed-Skript zu entwickeln.
Ich habe den Inhalt varnishadm vcl.show -v "$VCL_NAME" in eine Datei geschrieben, damit ich mich ganz auf das Schreiben des sed-Skripts konzentrieren konnte, ohne mich um Service-Neustarts kümmern zu müssen.
Eine kurze Beschreibung, wie sed die Eingabedaten verarbeitet, finden Sie in . In den sed-Quellcodes ist das Zeichen n deutlich als Zeilenbegrenzer angegeben.
In mehreren Durchgängen und mit den Empfehlungen meines Kollegen haben wir ein sed-Skript geschrieben, das dasselbe Ergebnis wie die gesamte ursprüngliche Zeile 116 lieferte.
Hier ist ein Beispiel für die Eingabedatei:
> cat vcl-example.vcl
Text
// VCL.SHOW 0 1578 datei mit 3 Leerzeichen.vcl
Weitere Texte
// VCL.SHOW 0 1578 datei.vcl
Noch mehr Texte
// VCL.SHOW 0 1578 datei mit ZWEILeerzeichen.vcl
Finaler TextDas mag aus der obigen Beschreibung nicht offensichtlich sein, aber uns interessiert nur der erste Kommentar. // VCL.SHOW, wobei es in den Eingabedaten mehrere geben kann. Genau aus diesem Grund endet das originale awk seine Ausführung nach der ersten Übereinstimmung.
# шаг первый, вывести только строки с комментариями
# используя возможности sed, определяется символ-разделитель с помощью конструкции '#' вместо обычно используемого '/', за счёт этого не придётся экранировать косые в искомом комментарии
# определяется регулярное выражение “// VCL.SHOW”, для поиска строк с определенным шаблоном
# флаг -n позаботится о том, чтобы sed не выводил все входные данные, как он это делает по умолчанию (см. ссылку выше)
# -E позволяет использовать расширенные регулярные выражения
> cat vcl-processor-1.sed
#// VCL.SHOW#p
> sed -En -f vcl-processor-1.sed vcl-example.vcl
// VCL.SHOW 0 1578 file with 3 spaces.vcl
// VCL.SHOW 0 1578 file.vcl
// VCL.SHOW 0 1578 file with TWOspaces.vcl
# шаг второй, вывести только имя файла
# используя команду “substitute”, с группами внутри регулярных выражений, отображается только нужная группa
# и это делается только для совпадений, ранее описанного поиска
> cat vcl-processor-2.sed
#// VCL.SHOW# {
s#.* [0-9]+ [0-9]+ (.*)$#1#
p
}
> sed -En -f vcl-processor-2.sed vcl-example.vcl
file with 3 spaces.vcl
file.vcl
file with TWOspaces.vcl
# шаг третий, получить только первый из результатов
# как и в случае с awk, добавляется немедленное завершения после печати первого найденного совпадения
> cat vcl-processor-3.sed
#// VCL.SHOW# {
s#.* [0-9]+ [0-9]+ (.*)$#1#
p
q
}
> sed -En -f vcl-processor-3.sed vcl-example.vcl
file with 3 spaces.vcl
# шаг четвертый, схлопнуть всё в однострочник, используя двоеточия для разделения команд
> sed -En -e '#// VCL.SHOW#{s#.* [0-9]+ [0-9]+ (.*)$#1#p;q;}' vcl-example.vcl
file with 3 spaces.vclSo wird der Inhalt des Skripts varnishreload ungefähr so aussehen:
VCL_FILE="$(echo "$VCL_SHOW" | sed -En '#// VCL.SHOW#{s#.*[0-9]+ [0-9]+ (.*)$#1#p;q;};')"Die oben dargestellte Logik kann kurz wie folgt zusammengefasst werden:
Wenn die Zeile dem regulären Ausdruck entspricht, // VCL.SHOW, dann friss gierig den Text, der beide Zahlen in dieser Zeile enthält, und speichere alles, was nach dieser Operation übrig bleibt. Gib den gespeicherten Wert aus und beende das Programm.
Einfach, oder?
Wir waren mit dem sed-Skript und der Tatsache zufrieden, dass es den gesamten originalen Code ersetzt. Alle meine Tests ergaben die gewünschten Ergebnisse, also änderte ich „varnishreload“ auf dem Server und startete erneut systemctl reload varnish. Ein fieser Fehler echo: Schreibfehler: Brechen der Leitung lächelte uns erneut ins Gesicht. Der blinkende Cursor wartete darauf, einen neuen Befehl in der dunklen Leere des Terminals einzugeben…
Quelle: habr.com
