, an den Tasten hämmernd die vorherigen 20 Minuten, als hinge sein Leben davon ab, dreht er sich mit einem halb wilden Ausdruck in den Augen und einem listigen Grinsen zu mir um – „Kumpel, ich glaube, ich habe es verstanden.“
„Schau mal hier“, sagt er, während er auf eines der Symbole auf dem Bildschirm zeigt – „Ich wette um meinen roten Hut, dass, wenn wir hier das hinzufügen, was ich dir gerade geschickt habe“ – zeigt auf einen anderen Teil des Codes – „der Fehler nicht mehr angezeigt wird.“
Etwas verwirrt und müde ändere ich den sed-Ausdruck, an dem wir 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 echtes Lächeln voller Freude übergeht, „plötzlich wurde mir klar, dass das genau das gleiche Problem war!“
Wie alles begann
Der Artikel setzt ein Verständnis der Funktionsweise von bash, awk, sed und systemd voraus. Kenntnisse über varnish sind willkommen, aber nicht unbedingt erforderlich.
Die Zeitstempel in den Snippets wurden geändert.
Geschrieben zusammen mit .
Dieser Text ist eine Übersetzung des Originals, das vor zwei Wochen in englischer Sprache veröffentlicht wurde; die Übersetzung .
Die Sonne scheint an einem weiteren warmen Herbstmorgen durch die Panoramafenster, eine Tasse frisch zubereiteten koffeinhaltigen Getränks steht neben der Tastatur, in den Kopfhörern erklingt meine Lieblingssymphonie, die das Geräusch der mechanischen Tastaturen übertönt, und die erste Eintragung in der Liste der Tickets im Backlog auf dem Kanban-Board leuchtet verspielt mit dem schicksalhaften Titel „Untersuchen varnishreload sh: echo: I/O-Fehler in der Staging-Umgebung“ (Untersuchen „varnishreload sh: echo: I/O-Fehler“ in der Staging-Umgebung). Wenn es um varnish geht, gibt es keinen Platz für Fehler, selbst wenn sie sich nicht in irgendwelchen Problemen äußern, wie in diesem Fall.
Für diejenigen, die nicht vertraut sind mit , es handelt sich um ein einfaches Shell-Skript, das verwendet wird, um die Konfiguration von — auch bekannt als VCL.
Wie der Ticketname bereits andeutet, trat der Fehler auf einem der Server auf der Staging-Umgebung auf. Da ich überzeugt war, dass das Routing von Varnish auf dem Staging korrekt funktioniert, vermutete ich, dass es sich um einen kleinen Fehler handelt. So ein einfaches Ereignis, das in den bereits geschlossenen Output-Stream gelangte. Ich nehme das Ticket an mich, in der vollen Überzeugung, dass ich es in weniger als 30 Minuten als gelöst markieren kann. Ich klopfe mir selbst auf die Schulter für die Reinigung des Boards von wieder einmalem Müll und kehre zu wichtigeren Aufgaben zurück.
Gegen die Wand mit einer Geschwindigkeit von 200 km/h
Als ich die Datei öffnete varnishreload, auf einem der Server, die von Debian Stretch verwaltet werden, sah ich ein Shell-Skript mit weniger als 200 Zeilen.
Nachdem ich das Skript durchgesehen hatte, bemerkte ich nichts, was zu Problemen führen könnte, wenn ich es mehrfach direkt aus dem Terminal heraus ausführe.
Schließlich ist das ja nur ein Staging. Selbst wenn es kaputt geht, wird sich niemand beschweren… nun, nicht zu viele. Ich führe das Skript aus und sehe, was im Terminal ausgegeben wird, aber Fehler sind nicht zu sehen.
Ein paar weitere Ausführungen, um sicherzustellen, dass ich den Fehler ohne zusätzlichen Aufwand nicht reproduzieren kann, und ich beginne mir zu überlegen, wie ich dieses Skript ändern und es dazu bringen kann, tatsächlich einen Fehler auszugeben.
Vielleicht sollte ich STDOUT umleiten (mit > &-)? Oder STDERR? Beides hat letztendlich nicht 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 den Shebang einfüge, in der Hoffnung, dass die Debug-Ausgabe des Skripts ein wenig Licht ins Dunkel bringt.
Die Datei ist bearbeitet, also lade ich Varnish neu und sehe, dass die Änderung alles kaputt gemacht hat… Die Ausgabe ist ein totales Chaos, in dem tonnenweise C-ähnlicher Code geschrieben ist. Selbst das Scrollen im Terminal reicht nicht aus, um zu finden, wo es beginnt. Ich bin völlig verwirrt. Kann der Debug-Modus die Ausführung von Programmen, die im Skript gestartet werden, beeinflussen? Nein, Blödsinn. Ein Fehler in der Shell? Mehrere mögliche Szenarien rasen in meinem Kopf wie Kakerlaken in alle Richtungen. Eine Tasse mit koffeinhaltigem Getränk wird sofort geleert, ich mache eine schnelle Reise in die Küche, um nachfüllen zu können, und dann geht es weiter. Ich öffne das Skript und schaue mir den Shebang genauer an: #!/bin/sh.
/bin/sh — das ist doch nur ein Symlink auf bash, also wird das Skript im POSIX-kompatiblen Modus interpretiert, richtig? Das dachte ich zumindest! Die Standard-Shell in Debian ist dash, und genau das ist es, was ich /bin/sh.
# ls -l /bin/sh
lrwxrwxrwx 1 root root 4 Jan 24 2017 /bin/sh -> dashProbiere es aus, ich ändere den Shebang zu #!/bin/bash, gelöscht set -x und noch einmal versucht. Schließlich erschien beim nächsten Neustart von Varnish eine annehmbare Fehlermeldung im Output:
Jan 01 12:00:00 hostname varnishreload[32604]: /usr/sbin/varnishreload: Zeile 124: echo: Schreibfehler: Broken 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 der Datei 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 "Fehler beim Abrufen des VCL-Dateinamens"
129 fi
130
131 echo "$VCL_FILE"
132 }Wie sich herausstellte, ist Zeile 124 ziemlich leer und uninteressant. Ich konnte nur spekulieren, dass der Fehler als Teil des Multiline-Kommandos aufgetreten ist, das in Zeile 116 beginnt.
Was wird letztendlich in die Variable VCL_FILE geschrieben, als Ergebnis der Ausführung des oben genannten Sub-Shells?
Zuerst sendet er den Inhalt der Variable VLC_SHOW, die in Zeile 115 erstellt wurde, an den folgenden Befehl über die Pipe. Was passiert dann dort?
Erstens wird dort varnishadm, das Teil des Installationspakets von Varnish ist, verwendet, um Varnish ohne Neustart zu konfigurieren.
Der Unterbefehl vcl.show -v wird verwendet, um die gesamte VCL-Konfiguration auszugeben, die in ${VCL_NAME}, auf STDOUT angegeben ist.
Um die aktuell aktive VCL-Konfiguration sowie einige frühere Versionen der Varnish-Routing-Konfigurationen, die sich noch im Speicher befinden, anzuzeigen, kann der Befehl varnishadm vcl.list, dessen Ausgabe ähnlich ist wie die folgende:
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 Variable ${VCL_NAME} wird an einer anderen Stelle im Skript gesetzt varnishreload auf den Namen des aktuellen aktiven VCL, falls vorhanden. In diesem Fall wird es "reload_20190101_120000_12397" sein.
Großartig, die Variable ${VCL_SHOW} enthält die vollständige Konfiguration für Varnish, das ist klar. Jetzt habe ich endlich verstanden, warum die Ausgabe von Dash set -x so kaputt war – sie enthielt den Inhalt der resultierenden Konfiguration.
Es ist wichtig zu verstehen, dass die vollständige VCL-Konfiguration häufig aus mehreren Dateien zusammengesetzt werden kann. Kommentare im C-Stil werden verwendet, um anzugeben, wo eine Konfigurationsdatei in eine andere eingefügt wurde, und genau darum geht es in der folgenden Zeile des Codefragmentes.
Die Syntax der Kommentare, die die eingefügten Dateien beschreiben, hat folgendes Format:
// VCL.SHOW <NUM> <NUM> <FILENAME>Die Zahlen sind in diesem Kontext unwichtig, uns interessiert der Dateiname.
Was passiert also im Befehlssumpf, der mit Zeile 116 beginnt?
Lassen Sie uns das klären.
Der Befehl besteht aus vier Teilen:
- Einfach
echo, welcher den Wert der Variablen${VCL_SHOW}echo "$VCL_SHOW" awk, der nach einer Zeile (Eintrag) sucht, wo das erste Feld nach der Textzerlegung „//“ und das zweite „VCL.SHOW“ ist.
Awk gibt die erste Zeile, die diesen Mustern entspricht, aus und stoppt dann sofort die Verarbeitung.awk '$1 == "//" && $2 == "VCL.SHOW" {print; exit}'- Ein Codeblock, der die Werte der durch Leerzeichen getrennten Felder 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 Variablen
${FILE}.{ read -r DELIM VCL_SHOW INDEX SIZE FILE; echo "$FILE" } - Da alle Schritte von 1 bis 3 in einer Subshell eingeschlossen sind, wird der Wert von
$FILEin der Variablen gespeichert.VCL_FILE.
Wie aus dem Kommentar in Zeile 119 hervorgeht, dient dies einem einzigen Zweck: die zuverlässige Verarbeitung von Fällen, in denen VCL auf Dateien mit Leerzeichen im Namen verweist.
Ich habe die ursprüngliche Logik der Verarbeitung für ${VCL_FILE} auskommentiert und versucht, die Reihenfolge der Befehle zu ändern, aber das führte zu nichts. Bei mir funktionierte alles einwandfrei, und beim Start des Dienstes trat ein Fehler auf.
Es scheint, dass der Fehler einfach nicht reproduzierbar ist, wenn das Skript manuell ausgeführt wird. Die angesprochenen 30 Minuten waren bereits sechs Mal vergangen, und außerdem gab es eine prioritärere Aufgabe, die die anderen Dinge zur Seite schob. Der Rest der Woche war mit den unterschiedlichsten Aufgaben gefüllt und wurde nur ein wenig durch einen Vortrag über sed und ein Vorstellungsgespräch mit einem Kandidaten aufgelockert. Das Problem mit dem Fehler in varnishreload ging unwiderruflich im Sand der Zeit verloren.
Ihr angebliches sed-Fu... ist in Wirklichkeit... Müll.
In der nächsten Woche gab es einen recht freien Tag, also beschloss ich, mich wieder mit diesem Ticket zu beschäftigen. Ich hoffte, dass in meinem Kopf ein Hintergrundprozess die ganze Zeit über nach einer Lösung für dieses Problem gesucht hatte und dass ich es diesmal ganz sicher verstehen würde.
Da beim letzten Mal die einfache Änderung des Codes nicht geholfen hat, beschloss ich einfach, ihn ab der 116. Zeile neu zu schreiben. In jedem Fall war der bestehende Code schlecht strukturiert. Und es gibt absolut keinen Grund, ihn zu verwenden. lesen.
Noch einmal auf den Fehler schauend:
sh: echo: broken pipe — in diesem echo-Befehl kommt es an zwei Stellen vor, aber ich vermute, dass die erste die wahrscheinlichere Ursache ist (oder zumindest ein Mitbewohner). Auch awk ist nicht vertrauenswürdig. Und falls das tatsächlich der Fall ist, awk | {read; echo} führt diese Konstruktion zu all diesen Problemen, warum sollte man sie nicht ersetzen? Dieser Einzeiler nutzt nicht alle Möglichkeiten von awk und hat zusätzlich diesen überflüssigen lesen auf der Seite.
Da es letzte Woche einen Bericht über sed, wollte ich versuchen, meine neu erworbenen Fähigkeiten auszuprobieren und echo | awk | { read; echo} in ein verständlicheres echo | sed. Obwohl das definitiv nicht die beste Methode zur Fehlersuche ist, dachte ich, dass ich zumindest mein sed-fu ausprobieren und vielleicht etwas Neues über das Problem lernen könnte. Unterwegs bat ich einen Kollegen, den Autor des sed-Berichts, mir zu helfen, ein effizienteres sed-Skript zu entwickeln.
Ich habe den Inhalt von varnishadm vcl.show -v "$VCL_NAME" in eine Datei gespeichert, damit ich mich ohne die Umstände des Neustarts des Dienstes auf das Schreiben des sed-Skripts konzentrieren konnte.
Eine kurze Beschreibung, wie sed die Eingabedaten verarbeitet, findet man in . Im Quellcode von sed ist das Zeichen n deutlich als Zeilen-Trennzeichen 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.
Im Folgenden ein Beispiel für eine Datei mit Eingangsdaten:
> cat vcl-example.vcl
Text
// VCL.SHOW 0 1578 Datei mit 3 Leerzeichen.vcl
Mehr Text
// VCL.SHOW 0 1578 Datei.vcl
Noch mehr Text
// VCL.SHOW 0 1578 Datei mit ZWEILeerzeichen.vcl
Finaler TextDas ist aus der obigen Beschreibung vielleicht nicht offensichtlich, aber wir interessieren uns nur für den ersten Kommentar // VCL.SHOW, und in den Eingangsdaten kann es mehrere davon geben. Deshalb hört das ursprüngliche awk nach dem ersten Treffer auf zu arbeiten.
# шаг первый, вывести только строки с комментариями
# используя возможности 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.vclAlso 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 angegebene Logik kann folgendermaßen zusammengefasst werden:
Wenn die Zeichenfolge dem regulären Ausdruck entspricht // VCL.SHOW, dann verschlinge gierig den Text, der beide Zahlen in dieser Zeichenfolge 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 ursprünglichen Code ersetzt. Alle meine Tests lieferten die gewünschten Ergebnisse, deshalb habe ich "varnishreload" auf dem Server geändert und erneut gestartet systemctl reload varnish. Der fiese Fehler echo: Schreibfehler: Brechen der Pipe lachte uns wieder ins Gesicht. Der blinkende Cursor wartete darauf, einen neuen Befehl in der dunklen Leere des Terminals einzugeben…
Quelle: habr.com
