Down the Rabbit Hole: Eine Geschichte von einem Varnishreload-Fehler - Teil 1

Nachdem er in den letzten 20 Minuten die Tastatur gedrückt hat, als würde er für sein Leben tippen, dreht sich Ghostinushanka mit einem halb verrückten Blick in den Augen und einem schlauen Lächeln zu mir. „Alter, ich glaube, ich habe es verstanden.


Sehen Sie sich das an “- als er auf eines der Zeichen auf dem Bildschirm zeigt -„ Ich wette, mein roter Hut, wenn wir hinzufügen, was ich Ihnen gerade hierher geschickt habe “- als er auf eine andere Stelle im Code zeigt -„ wird es geben kein Fehler mehr. "
Etwas verwirrt und müde systemctl varnish reload ich den Sed-Ausdruck, den wir seit einiger Zeit herausfinden, speichere die Datei und systemctl varnish reload das systemctl varnish reload . Fehlermeldung weg ...


"Diese E-Mails, die ich mit dem Kandidaten ausgetauscht habe", fährt mein Kollege fort, während sich sein Lächeln in ein breites und echtes Grinsen verwandelt. "Mir ist plötzlich aufgefallen, dass dies genau dasselbe Problem ist!"


Wie alles begann


Dieser Artikel setzt voraus, dass Sie mit bash, awk und systemd vertraut sind. Einige Lackkenntnisse sind von Vorteil, aber nicht erforderlich.
Zeitstempel in Beispiel-Snippets wurden überarbeitet.
Zusammen mit ghostinushanka verfasst.


An einem weiteren warmen Herbstmorgen scheint die Sonne durch die wandgroßen Fenster, eine Tasse frisch gebrühte koffeinhaltige Flüssigkeit steht neben der Tastatur, Kopfhörer sprechen die geliebte Symphonie von Klängen, die das Rascheln mechanischer Tastaturen überdecken, und der erste Eintrag im Rückstand auf der Kanbantafel wird spielerisch der Titel des schicksalhaften Tickets angezeigt sh: echo: I/O error „Untersuche das Laden von Daten sh: echo: I/O error bei der Bereitstellung“. Immer wenn es um Lack geht, gibt es keinen Platz für Fehler, auch wenn dieser keine wirklichen Probleme zu verursachen schien.


Für diejenigen unter Ihnen, die mit varnishreload nicht vertraut sind , ist es einfach ein Shell-Skript, das zum erneuten Laden der Konfiguration - auch VCL genannt - des Varnish-Caching-Servers verwendet wird .


Wie der Titel des Tickets andeutet, ist der Fehler auf einem der Staging-Computer aufgetreten, und ich war mir ziemlich sicher, dass das Varnish-Routing in der Staging-Umgebung funktioniert. Daher war meine Annahme, dass dies ein geringfügiges Problem sein muss. Nur eine benutzerfreundliche Ausgabenachricht, die in einen geschlossenen Stream geschrieben wird. Ich greife nach dem Ticket und bin fest davon überzeugt, dass ich es in weniger als 30 Minuten als erledigt markieren kann. Ich klopfe mir auf den Rücken, um eine weitere weltliche Aufgabe zu erledigen und mich wieder wichtigeren Dingen zuzuwenden.


Mit 200 km / h gegen die Wand


Beim Öffnen der Datei varnishreload auf einem der betroffenen Server unter Debian Stretch varnishreload ich fest, dass ein Shell-Skript weniger als 200 Zeilen lang ist. Wenn ich es kurz durchlese, sehe ich nichts Gefährliches, das mich daran hindern würde, das Skript immer wieder vom Terminal aus auszuführen. Immerhin ist dies eine Inszenierung, auch wenn sie kaputt geht, niemand wird sich beschweren, na ja ... nicht zu viel. Ich starte das Skript und beobachte, nur um herauszufinden, dass keine Fehler zu sehen sind. Ein paar weitere Durchläufe stellen sicher, dass ich den Fehler nicht ohne zusätzlichen Aufwand reproduzieren kann, und ich beginne Pläne zu entwickeln, um die Umgebung des Skripts zu optimieren und zu verbessern. Hilft es überhaupt, STDOUT für das Skript zu schließen (mit > &- )? Oder stderr? Weder noch.


Offensichtlich beeinträchtigt systemd die Umwelt auf irgendeine Weise, aber wie und warum? Ich varnishreload vim und bearbeite das varnishreload des Systems, varnishreload set -x direkt unter dem varnishreload hinzu und hoffe, dass die detaillierte Skriptausgabe etwas Licht ins Dunkel bringt.


Da die Datei gepatcht ist, lade ich den Firnis neu, nur um zu sehen, dass die Änderung das Skript vollständig zerstört hat ... Die Ausgabe ist ein komplettes Durcheinander, in dem jede Menge C-Code angezeigt wird und der standardmäßige Scrollback-Puffer nicht ausreicht, um herauszufinden, woher er kommt. Ich fühle mich verwirrt. Könnte das Festlegen der Debug-Option für das Shell-Skript das aufgerufene Programm beschädigen? Nein, kann nicht sein. Ein Fehler in der Shell? In meinem Kopf laufen mehrere mögliche Szenarien wild in verschiedene Richtungen. Eine Tasse koffeinhaltiges Getränk ist sofort fertig, schnelle Fahrt in die Küche zum Nachfüllen und schon geht es wieder los. Ich öffne die Datei und schaue mir den shebang genau an: #!/bin/sh .


Aber /bin/sh ist sicherlich nur ein Symlink zu bash, so dass das Skript im POSIX-kompatiblen Modus interpretiert wird, oder? Falsch! Die nicht interaktive Standard-Shell unter Debian ist dash, und genau darauf zeigt /bin/sh .


 # ls -l /bin/sh lrwxrwxrwx 1 root root 4 Jan 24 2017 /bin/sh -> dash 

Wenn auch nur zum Debuggen, habe ich den shebang in #!/bin/bash geändert, die set -x und es erneut versucht. Zum Schluss noch ein vernünftiger Fehler beim nächsten Nachladen des Lacks:


 Jan 01 12:00:00 hostname varnishreload[32604]: /usr/sbin/varnishreload: line 124: echo: write error: Broken pipe Jan 01 12:00:00 hostname varnishreload[32604]: VCL 'reload_20190101_120000_32604' compiled 

Linie 124, jetzt reden wir!


 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 this ceremony to handle blanks in FILE 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 "failed to get the VCL file name" 129 fi 130 131 echo "$VCL_FILE" 132 } 

Aber wie sich herausstellt, ist die Linie 124 ziemlich ereignislos. Ich konnte nur vermuten, dass der Fehler als Teil des in Zeile 116 ausgeführten mehrzeiligen Befehls erzeugt wurde.


Was erzeugt die obige Subshell überhaupt, um sie in der Variablen VCL_FILE zu speichern? Im ersten Teil sendet es den Inhalt der VCL_SHOW Variablen, die in der Zeile 115 erstellt wurde, in die Pipe. Was passiert dann dort?


Zunächst wird varnishadm , ein Standardbestandteil einer Varnish-Installation, mit der Varnish konfiguriert wird, ohne dass ein Neustart erforderlich ist. Mit dem Unterbefehl vcl.show -v wird die gesamte durch ${VCL_NAME} angegebene VCL-Konfiguration an ${VCL_NAME} ausgegeben.


Um die aktuell aktive VCL-Konfiguration sowie mehrere frühere Versionen des varnishadm vcl.list , die sich noch im Speicher befinden, können Sie einen anderen Befehl varnishadm vcl.list , dessen Ausgabe der folgenden ähnelt:


 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_12587 

Die Variable ${VCL_NAME} wird an anderer Stelle im varnishreload Skript auf den Namen der derzeit aktiven VCL gesetzt, sofern vorhanden. In diesem Fall wäre das "reload_20190101_120000_12397".


Toll, also enthält ${VCL_SHOW} jetzt eine vollständige Konfiguration für Varnish, die bisher ${VCL_SHOW} einfach war. Jetzt habe ich endlich verstanden, warum die Dash-Ausgabe mit set -x so kaputt zu sein schien - sie enthielt den Inhalt der resultierenden Lackkonfiguration.


Wichtig hierbei ist, dass die vollständige VCL-Konfiguration häufig aus mehreren Dateien zusammengefügt wird. Mit Kommentaren im C-Stil wird festgelegt, wo Konfigurationsdateien in andere Konfigurationsdateien eingefügt wurden. Genau darum geht es in der nächsten Zeile des Code-Snippets.


Die Syntax der dateibezogenen Kommentare hat das folgende Format


 // VCL.SHOW <NUM> <NUM> <FILENAME> 

Die Zahlen sind hier nicht wichtig, was uns interessiert, ist der Dateiname.


Was in aller Welt passiert also in der Menge der Befehle, die in Zeile 116 beginnen?
Lass es uns auseinander nehmen.
Der Befehl besteht aus vier Teilen:


  1. Ein einfaches echo , das den Wert von ${VCL_SHOW}
     echo "$VCL_SHOW" 
  2. awk sucht nach einer Zeile (Datensatz), in der das erste Feld '//' und das zweite "VCL.SHOW" ist.
    Awk wird angewiesen, die erste Zeile zu drucken, die mit diesen Mustern übereinstimmt, und die Verarbeitung dann sofort zu beenden.
     awk '$1 == "//" && $2 == "VCL.SHOW" {print; exit}' 
  3. Ein Codeblock, der die durch Leerzeichen getrennten Felder in fünf Variablen einliest. Die fünfte Variable FILE erhält den Rest der Zeile. Schließlich gibt ein letztes Echo den Inhalt der Variablen ${FILE} .
     { read -r DELIM VCL_SHOW INDEX SIZE FILE; echo "$FILE" }. 
  4. Da die Schritte 1 bis 3 alle in eine Subshell eingeschlossen sind, wird die Ausgabe von $FILE in der Variablen VCL_FILE .

Wie aus dem Kommentar in Zeile 119 hervorgeht, dient diese Vorgehensweise einem einzigen Zweck: dem zuverlässigen Behandeln des Falls, in dem VCL auf Dateinamen mit Leerzeichen verweist.


Ich habe die ursprüngliche Verarbeitungslogik für ${VCL_FILE} und versucht, die Befehlskette zu optimieren, aber ohne vernünftiges Ende. Alles hat in meiner Shell funktioniert, aber nie als Dienst ausgeführt.


Es scheint, dass der Fehler bei meiner Ausführung überhaupt nicht reproduzierbar ist - mittlerweile sind die geschätzten 30 Minuten sechs Mal vergangen, und eine neue Aufgabe mit hoher Priorität hat alles beiseite gelegt. Der Rest der Woche war ziemlich voll mit verschiedenen Aufgaben, die beiden Ausnahmen waren ein internes Gespräch unseres Teams über die Verwendung von sed und ein Interview mit einem vielversprechenden Kandidaten. Das Problem, dass der varnishreload Fehler verschwunden ist, ist für die ganze Zeit verloren gegangen.


Dein sogenanntes Sed-Fu ... ist wirklich ... ziemlich erbärmlich


Einer der folgenden Wochentage war ziemlich frei, also nahm ich die Aufgabe wieder auf. Ich hatte gehofft, dass vielleicht immer noch ein Hintergrundprozess in meinem Gehirn das Problem in den Griff bekommt und ich es endlich knacken kann.


Da das letzte Mal das Verbiegen des Codes nicht geholfen hat, habe ich mich für eine Neufassung von Zeile 116 entschieden. Der vorhandene Code war sowieso verrückt. Es ist absolut nicht nötig, hier zu read .


Schauen Sie sich den Fehler noch einmal an:
sh: echo: broken pipe - Echo ist an zwei Stellen in diesem Befehl, aber ich vermute, dass der erste ein wahrscheinlicherer Täter (oder Komplize) ist. Awk schafft auch kein Vertrauen. Nun, falls es sich wirklich um die awk | {read; echo} awk | {read; echo} awk | {read; echo} construct verursacht all diese Probleme, warum nicht etwas anderes verwenden? Awk ist auf diesem Einzeiler nicht wirklich voll ausgelastet, und dann ist da noch dieser Überschuss.


Da wir in der sed Woche ein internes Gespräch über sed , wollte ich meine neu erworbenen Fähigkeiten ausprobieren und das echo | awk | { read; echo } optimieren echo | awk | { read; echo } in ein einfacheres echo | sed echo | sed . Obwohl dies definitiv nicht der richtige Weg ist, um das Debuggen in Angriff zu nehmen, dachte ich, ich würde zumindest mein Sed-Fu ausprobieren und vielleicht etwas Neues über das Problem in diesem Prozess erfahren. Dabei habe ich meinen Kollegen - den Autor des Sed-Talks - gebeten, mir zu helfen, einen effizienteren Sed-Befehl zu entwickeln.


Ich habe die Datei varnishadm vcl.show -v "$VCL_NAME" in eine Datei geschrieben, damit ich mich auf das Schreiben von sed konzentrieren kann, ohne den ganzen Aufwand für das Neuladen von Diensten.


Eine kurze Einführung, wie genau sed Prozesse eingegeben werden, findet sich in seinem GNU-Handbuch . In sed sources wird das Zeichen \n explizit als Zeilentrennzeichen angegeben.


Nach mehreren Iterationen und Eingaben meines Kollegen haben wir einen sed-Ausdruck erstellt, der genau das gleiche Ergebnis wie die ursprüngliche Zeile 116 liefert.


Lassen Sie uns hier eine Beispiel-Eingabedatei erstellen,


 > cat vcl-example.vcl Text // VCL.SHOW 0 1578 file with 3 spaces.vcl More text // VCL.SHOW 0 1578 file.vcl Even more text // VCL.SHOW 0 1578 file with TWOspaces.vcl Final text 

Es ist aus der obigen Beschreibung möglicherweise nicht ersichtlich, aber wir interessieren uns nur für den ersten // VCL.SHOW Kommentar, und bei der Eingabe sind möglicherweise mehrere vorhanden. Genau deshalb gibt awk nach dem ersten Match auf.


 # step 1, capture just the comment lines # using sed capability to specify delimiter character with '\#' instead of the commonly used '/' so there is no need to escape slashes themselves # and the “address” capability defined as regex “// VCL.SHOW” to search for lines with specific pattern # -n flag makes sure that the sed does not print all as it does by default (see above link) # -E switches to the extended regex > 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 # step 2, only print out the file name # using the “substitute” command with regex capture groups to print just that group # and this is done only for the matches of the previous search > 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 # step 3, make sure to only get the first result # same as with the awk before, add an immediate exit after the first processed match is printed > 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 # step 4, wrap it up into a one-liner using the colon to separate commands > sed -En -e '\#// VCL.SHOW#{s#.* [0-9]+ [0-9]+ (.*)$#\1#p;q;}' vcl-example.vcl file with 3 spaces.vcl 

Der Inhalt des varnishreload-Skripts würde also ungefähr so ​​aussehen:


 VCL_FILE="$(echo "$VCL_SHOW" | sed -En '\#// VCL.SHOW#{s#.*[0-9]+ [0-9]+ (.*)$#\1#p;q;};')" 

Die obige Logik kann prägnant ausgedrückt werden durch:
Wenn eine Zeile mit dem // VCL.SHOW , // VCL.SHOW gierig mit dem Text einschließlich der beiden Zahlen in dieser Zeile // VCL.SHOW , und erfassen Sie, was danach kommt. Capture ausgeben und beenden.


Einfach, nicht wahr?


Wir waren mit dem sed-Skript und der Tatsache, welchen Originalcode es ersetzt, zufrieden. Alle von mir durchgeführten varnishreload ergaben die gewünschten Ergebnisse. varnishreload habe ich den varnishreload auf dem Server geändert und den systemctl reload varnish erneut systemctl reload varnish . Das gefürchtete echo: write error: Broken pipe lächelte in unseren Gesichtern. Der blinkende Cursor erwartete eine neue Befehlseingabe in der dunklen Leere des Terminals ...

Source: https://habr.com/ru/post/de475698/


All Articles