Falling Down the Rabbit Hole: Zgodba o eni napaki pri ponovnem zagonu laka – 1. del

Ghostinushanka, potem ko je prejšnjih 20 minut trkal po gumbih, kot da je od tega odvisno njegovo življenje, se obrne proti meni s pol divjim izrazom v očeh in premetenim nasmeškom - "Stari, mislim, da razumem."

»Poglej sem,« reče in pokaže na enega od znakov na zaslonu, »stavim svoj rdeči klobuk, da če sem dodamo, kar sem ti pravkar poslal« - pokaže na drug del kode - »napake ne bo več biti prikazan."

Malo zmeden in utrujen spremenim stavek sed, na katerem smo delali nekaj časa, shranim datoteko in zaženem systemctl varnish reload. Sporočilo o napaki je izginilo ...

»E-poštna sporočila, ki sem si jih izmenjala s kandidatom,« je nadaljeval moj kolega, medtem ko se je njegov nasmešek prelevil v pristen nasmeh, poln veselja, »nenadoma se mi je posvetilo, da je to popolnoma enak problem!«

Kako se je vse začelo

Članek predvideva razumevanje delovanja bash, awk, sed in systemd. Zaželeno je znanje lakiranja, ni pa obvezno.
Časovni žigi v delčkih so bili spremenjeni.
Napisano z Ghostinushanka.
To besedilo je prevod izvirnika, objavljenega v angleščini pred dvema tednoma; prevod boyikoden.

Sonce sije skozi panoramska okna še enega toplega jesenskega jutra, ob strani tipkovnice stoji skodelica sveže pripravljene kofeinske pijače, v slušalkah prek šumenja mehanskih tipkovnic igra priljubljena simfonija zvokov in prvi zapis v seznam zaostankov na kanban plošči igrivo žari z usodnim naslovom »Raziščite varnishreload sh: echo: I/O error in staging« (Raziščite »varnishreload sh: echo: I/O error« in staging). Pri lakiranju napak ni in ne more biti, tudi če ne povzročajo težav, kot je v tem primeru.

Za tiste, ki ne poznate varnishreload, je to preprost lupinski skript, ki se uporablja za ponovno nalaganje konfiguracije lak - imenovan tudi VCL.

Kot pove naslov vstopnice, je do napake prišlo na enem od strežnikov v stopnji in ker sem bil prepričan, da usmerjanje laka v fazi deluje pravilno, sem domneval, da gre za manjšo napako. Torej samo sporočilo, ki je prišlo v že zaprt izhodni tok. Vzamem listek zase, v popolnem zaupanju, da ga bom v manj kot 30 minutah označil pripravljenega, se potrepljam po rami, da počistim tablo naslednje krame in se vrnem k pomembnejšim stvarem.

Trčenje v zid pri 200 km/h

Odpiranje datoteke varnishreload, na enem od strežnikov, ki izvajajo Debian Stretch, sem videl lupinski skript, dolg manj kot 200 vrstic.

Med izvajanjem skripta nisem videl ničesar, kar bi lahko povzročilo težave pri večkratnem zagonu neposredno s terminala.

Saj je to oder, tudi če se zlomi, se ne bo nihče pritoževal, no ... ne preveč. Zaženem skript in vidim, kaj bo izpisano na terminalu, vendar napake niso več vidne.

Še nekaj zagonov, da se prepričam, da ne morem reproducirati napake brez dodatnega truda, in začel sem ugotavljati, kako spremeniti ta skript in narediti, da še vedno vrže napako.

Ali lahko skript blokira STDOUT (z uporabo > &-)? Ali STDERR? Nobeden na koncu ni deloval.

Očitno systemd na nek način spremeni okolje izvajanja, toda kako in zakaj?
Vklopim vim in urejam varnishreload, dodajanje set -x tik pod shebangom, v upanju, da bo razhroščevanje izhoda skripta osvetlilo.

Datoteka je popravljena, zato ponovno naložim varnish in vidim, da je sprememba popolnoma pokvarila vse ... Izpuh je popolna zmešnjava, s tonami C-like kode v njem. Tudi pomikanje po terminalu ni dovolj, da bi našli, kje se začne. Čisto sem zmeden. Ali lahko način za odpravljanje napak vpliva na delo programov, ki se izvajajo v skriptu? Brez nakladanja. Hrošč v lupini? Več možnih scenarijev mi leta kot ščurki v različne smeri. Skodelica pijače, polne kofeina, se takoj izprazni, hiter izlet v kuhinjo po zaloge in ... gremo. Odprem scenarij in si pobližje pogledam shebang: #!/bin/sh.

/bin/sh - to je samo simbolna povezava bash, zato se skript interpretira v načinu, združljivem s POSIX, kajne? Ni ga bilo! Privzeta lupina v Debianu je dash, kar je točno to se nanaša /bin/sh.

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

Zavoljo poskusa sem spremenil shebang v #!/bin/bash, odstranjeno set -x in poskusil znova. Končno se je pri naslednjem ponovnem nalaganju laka v izhodu pojavila dopustna napaka:

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

Vrstica 124, tukaj je!

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 }

A kot se je izkazalo, je linija 124 precej prazna in nič zanimiva. Lahko samo domnevam, da je do napake prišlo kot del večvrstične vrstice, ki se začne v vrstici 116.
Kaj se končno zapiše v spremenljivko VCL_FILE kot rezultat izvajanja zgornje podlupine?

Na začetku pošlje vsebino spremenljivke VLC_SHOW, ustvarjen v vrstici 115, do naslednjega ukaza skozi cev. In kaj se potem zgodi tam?

Prvič, uporablja varnishadm, ki je del namestitvenega paketa varnish, za konfiguracijo varnish brez ponovnega zagona.

podukaz vcl.show -v se uporablja za izpis celotne konfiguracije VCL, navedene v ${VCL_NAME}, v STDOUT.

Za prikaz trenutno aktivne konfiguracije VCL kot tudi več prejšnjih različic konfiguracij usmerjanja varnish, ki so še vedno v pomnilniku, lahko uporabite ukaz varnishadm vcl.list, katerega rezultat bo podoben naslednjemu:

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

Spremenljiva vrednost ${VCL_NAME} nastavite v drugem delu scenarija varnishreload na ime trenutno aktivnega VCL, če obstaja. V tem primeru bo to »reload_20190101_120000_12397«.

V redu, spremenljivo. ${VCL_SHOW} vsebuje celotno konfiguracijo za lak, do sedaj jasno. Zdaj končno razumem, zakaj izpis pomišljaja z set -x se je izkazalo za tako pokvarjeno - vključevalo je vsebino nastale konfiguracije.

Pomembno je razumeti, da je celotno konfiguracijo VCL pogosto mogoče sestaviti iz več datotek. Komentarji v slogu C se uporabljajo za določanje, kje je bila ena konfiguracijska datoteka vključena v drugo, in točno o tem govori naslednja vrstica izrezka kode.
Sintaksa za komentarje, ki opisujejo vključene datoteke, ima naslednjo obliko:

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

Številke v tem kontekstu niso pomembne, zanima nas ime datoteke.

Kaj se torej zgodi v močvirju ukazov, ki se začne v vrstici 116?
Sprijaznimo se.
Ukaz je sestavljen iz štirih delov:

  1. Preprosto echo, ki prikaže vrednost spremenljivke ${VCL_SHOW}
    echo "$VCL_SHOW"
  2. awk, ki išče vrstico (zapis), kjer bo prvo polje po razdelitvi besedila “//”, drugo pa “VCL.SHOW”.
    Awk bo izpisal prvo vrstico, ki se ujema s temi vzorci, in nato takoj ustavil obdelavo.

    awk '$1 == "//" && $2 == "VCL.SHOW" {print; exit}'
  3. Blok kode, ki hrani vrednosti polja v petih spremenljivkah, ločenih s presledki. Peta spremenljivka FILE prejme preostanek vrstice. Na koncu zadnji odmev izpiše vsebino spremenljivke ${FILE}.
    { read -r DELIM VCL_SHOW INDEX SIZE FILE; echo "$FILE" }
  4. Ker so vsi koraki od 1 do 3 zaprti v podlupino, je izhod vrednosti $FILE bo zapisan v spremenljivko VCL_FILE.

Kot nakazuje komentar v vrstici 119, to služi edinemu namenu zanesljive obravnave primerov, ko se bo VCL nanašal na datoteke s presledki v njihovih imenih.

Komentiral sem prvotno logiko obdelave za ${VCL_FILE} in poskušal spremeniti zaporedje ukazov, vendar ni pripeljalo do ničesar. Čisto mi je vse delalo, v primeru zagona servisa pa je dalo napako.

Zdi se, da napake preprosto ni mogoče ponoviti pri ročnem izvajanju skripta, medtem ko se je ocenjenih 30 minut že šestkrat končalo, poleg tega pa se je pojavila naloga z višjo prioriteto, ki je preostale primere potisnila na stran. Preostanek tedna je bil poln najrazličnejših nalog in le malo razredčen s pogovorom na sed in razgovorom s kandidatom. Težava z napako v varnishreload nepovratno izgubljen v pesku časa.

Tvoj tako imenovani sed-fu... pravzaprav... smeti

Naslednji teden je bil en dokaj prost dan, zato sem se odločil, da ponovno vzamem to karto. Upal sem, da je v mojih možganih nek proces v ozadju ves ta čas iskal rešitev za ta problem in tokrat bom zagotovo razumel, kaj je narobe.

Ker zadnjič samo spreminjanje kode ni pomagalo, sem se le odločil, da jo prepišem od 116. vrstice. V vsakem primeru je bila obstoječa koda neumna. In absolutno ni potrebe po uporabi read.

Ponovno pogledam napako:
sh: echo: broken pipe - v tem ukazu je echo na dveh mestih, vendar sumim, da je prvo verjetnejši krivec (no, ali pa vsaj sostorilec). Tudi Awk ne vzbuja zaupanja. In če je res tako awk | {read; echo} dizajn vodi do vseh teh težav, zakaj ga ne bi zamenjali? Ta enovrstični ukaz ne uporablja vseh funkcij awk in niti te dodatne read v dodatku.

Od prejšnjega tedna je bilo poročilo o sedŽelel sem preizkusiti svoje na novo pridobljene veščine in se poenostaviti echo | awk | { read; echo} v bolj razumljivo echo | sed. Čeprav to zagotovo ni najboljši pristop za odkrivanje hrošča, sem pomislil, da bi vsaj poskusil svoj sed-fu in se morda naučil kaj novega o težavi. Med potjo sem svojega kolega, pisca pogovorov sed, prosil, naj mi pomaga sestaviti učinkovitejši skript sed.

Izpustil sem vsebino varnishadm vcl.show -v "$VCL_NAME" v datoteko, tako da se lahko osredotočim na pisanje skripta sed brez kakršnih koli težav s ponovnim zagonom storitve.

Kratek opis, kako točno sed obravnava vnos, lahko najdete v njegov priročnik GNU. V virih sed simbol n izrecno podana kot ločilo vrstic.

V več prehodih in po nasvetu mojega kolega smo napisali skript sed, ki je dal enak rezultat kot celotna prvotna vrstica 116.

Spodaj je vzorčna datoteka z vhodnimi podatki:

> 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

Morda iz zgornjega opisa ni razvidno, vendar nas zanima le prvi komentar // VCL.SHOW, v vhodnih podatkih pa jih je lahko več. Zato se prvotni awk konča po prvem ujemanju.

# шаг первый, вывести только строки с комментариями
# используя возможности 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.vcl

Torej bi bila vsebina skripta varnishreload videti nekako takole:

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

Zgornjo logiko lahko povzamemo takole:
Če se niz ujema z regularnim izrazom // VCL.SHOW, nato pa pohlepno požrite besedilo, ki vključuje obe številki v tej vrstici, in shranite vse, kar ostane po tej operaciji. Izdajte shranjeno vrednost in končajte program.

Preprosto, kajne?

Zadovoljni smo bili s skriptom sed in dejstvom, da nadomešča vso izvirno kodo. Vsi moji testi so dali želene rezultate, zato sem spremenil »varnishreload« na strežniku in znova zagnal systemctl reload varnish. Umazana napaka echo: write error: Broken pipe se nam je spet zasmejal v obraz. Utripajoči kazalec je čakal na vnos novega ukaza v temni praznini terminala ...

Vir: www.habr.com

Dodaj komentar