Falling Down the Rabbit Hole: Povestea unei erori de repornire a lacului - Partea 1

Ghostinushanka, молотив по кнопкам в течение предыдущих 20 минут, как если бы от этого зависела его жизнь, поворачивается ко мне с полу-диким выражением в глазах и хитрой ухмылкой — «Чувак, я кажется понял.»

«Посмотри вот сюда,» — говорит, показывая на один из символов на экране — «Спорим на мою красную шляпу, что если мы добавим вот сюда то, что я тебе только что послал» — показывая на другой участок кода — «ошибка уже не будет выводиться.»

Немного озадаченный и уставший, я изменяю sed выражение, над которым мы какое-то время уже работали, сохраняю файл и запускаю systemctl varnish reload. Mesajul de eroare a dispărut...

«Мейлы, которыми я обменивался с кандидатом,» продолжил мой коллега, в то время как его ухмылка перерастает в неподдельную улыбку полную радости, «До меня вдруг дошло что это точно такая же проблема!»

Cum a început totul

Статья предполагает понимание принципов работы bash, awk, sed и systemd. Знание varnish приветствуется, но не является обязательным.
Marcajele temporale din fragmente au fost modificate.
Scris cu Ghostinushanka.
Этот текст является переводом оригинала, опубликованного на английском языке две недели назад; перевод boyikoden.

Soarele strălucește prin ferestrele panoramice într-o altă dimineață caldă de toamnă, o ceașcă de băutură cu cofeină proaspăt preparată se odihnește departe de tastatură, o simfonie preferată de sunete se aude peste foșnetul tastaturilor mecanice din căști și prima intrare în lista de bilete întârziate de pe panoul kanban strălucește jucăuș cu titlul fatidic „Investigați: „Investigați” poarta „varnishreload sh : echo: I/O error” în stadiu). Când vine vorba de lac, nu există și nu pot exista greșeli, chiar dacă nu duc la probleme, ca în acest caz.

Pentru cei care nu sunt familiarizați cu lacîncărcare, acesta este un script shell simplu folosit pentru a reîncărca configurația lac - numit și VCL.

După cum sugerează titlul biletului, eroarea a apărut pe unul dintre serverele din scenă și, din moment ce eram încrezător că rutarea lacului în scenă funcționează corect, am presupus că aceasta ar fi o greșeală minoră. Deci, doar un mesaj care a intrat într-un flux de ieșire deja închis. Îmi iau un bilet, cu toată încrederea că îl voi marca gata în mai puțin de 30 de minute, mă bat pe umăr pentru a curăța tabla de următorul junk și revin la lucruri mai importante.

Lovindu-se de un perete cu 200 km/h

Deschiderea unui fișier varnishreload, на одном из серверов под управлением Debian Stretch, я увидел шелл скрипт длиной менее 200 строк.

Пробежавшись по скрипту, я не заметил ничего такого, что могло бы вылиться в проблемы при многократном его запуске прямо из терминала.

В конце концов, это стейдж, даже если оно и сломается, никто не будет жаловаться, ну… не слишком много. Запускаю скрипт и смотрю что будет выписываться на терминал, вот только ошибок уже и не видно.

Еще пару запусков, чтобы убедиться, что я не могу воспроизвести ошибку без каких-либо дополнительных усилий, и я начинаю придумывать как этот скрипт изменить и заставить его таки выдавать ошибку.

Poate bloca scriptul STDOUT (folosind > &-)? Sau STDERR? Nici unul nu a funcționat până la urmă.

Evident, systemd modifică într-un fel mediul de rulare, dar cum și de ce?
Pornesc vim și editez varnishreload, adăugând set -x прямо под шебанг, надеясь, что дебаг вывод скрипта прольёт чуточку света.

Fișierul este corectat, așa că reîncarc lacul și văd că schimbarea a rupt totul... Evacuarea este o mizerie completă, în care sunt tone de cod asemănător C. Nici măcar defilarea în terminal nu este suficientă pentru a găsi de unde începe. Sunt complet confuz. Modul de depanare poate afecta funcționarea programelor lansate într-un script? Nu, e o prostie. Bug în coajă? Mai multe scenarii posibile îmi trec prin cap ca niște gândaci în direcții diferite. Cupa băuturii cu cofeină se golește instantaneu, o călătorie rapidă la bucătărie pentru a reumple stocul și... plecăm. Deschid scenariul și mă uit mai atent la shebang: #!/bin/sh.

/bin/sh — это ведь просто симлинк на bash, так что скрипт интерпретируется в POSIX-совместимом режиме, верно? Не тут-то было! Оболочка по умолчанию в Debian — это dash, и это именно то, на что se referă /bin/sh.

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

De dragul procesului, am schimbat shebang-ul în #!/bin/bash, șters set -x и попробовал ещё раз. Наконец-то, при последующей перезагрузке varnish-а, в выводе появилась сносная ошибка:

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

Linia 124, iată-l!

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 }

Но как оказалось, строка 124 довольно пуста и интереса не представляет. Я мог лишь только предположить, что ошибка возникла как часть многострочника, начинающегося на 116-й строке.
Ce se scrie în cele din urmă la variabilă VCL_FILE ca urmare a executării sub-shell-ului de mai sus?

La început, trimite conținutul variabilei VLC_SHOW, созданной на строке 115, следующей команде через пайпу. А там-то что тогда происходит?

În primul rând, folosește varnishadm, который является частью установочного пакета varnish, для настройки varnish-а без перезапуска.

subcomanda vcl.show -v este utilizat pentru a scoate întreaga configurație VCL specificată în ${VCL_NAME}, la STDOUT.

Чтобы отобразить текущую активную конфигурацию VCL, а также несколько предыдущих версий конфигураций маршрутизации varnish-а, которые все еще находятся в памяти, можно использовать команду varnishadm vcl.list, a cărui ieșire va fi similară cu următoarea:

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

Valoare variabilă ${VCL_NAME} stabilit într-o altă parte a scenariului varnishreload на имя активного в данный момент VCL, если таковой имеется. В данном случае это будет “reload_20190101_120000_12397”.

Bine, variabil. ${VCL_SHOW} содержит полную конфигурацию для varnish, пока ясно. Теперь я, наконец, понял, почему вывод dash с set -x оказался таким битым — он включал в себя содержимое получившейся конфигурации.

Важно понимать, что полная конфигурация VCL часто может быть слеплена из нескольких файлов. Комментарии в Си стиле используются для определения того, где одни файлы конфигурации были включены в другие, и это именно то, о чем, собственно, вся приведённая ниже строка фрагмента кода.
Синтаксис комментариев, описывающих включенные файлы, имеет следующий формат:

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

Цифры в данном контексте не важны, нас интересует имя файла.

Deci, ce se întâmplă în mlaștina de comenzi care începe pe linia 116?
Să recunoaștem.
Comanda este formată din patru părți:

  1. simplu echo, care afișează valoarea variabilei ${VCL_SHOW}
    echo "$VCL_SHOW"
  2. awk, который ищет строку (запись), где первым полем, после разбиения текста, будет “//”, а вторым — «VCL.SHOW».
    Awk выпишет первую строку, соответствующую этим шаблонам, а затем немедленно прекратит обработку.

    awk '$1 == "//" && $2 == "VCL.SHOW" {print; exit}'
  3. Блок кода, который сохраняет в пять переменных значения полей, разделенных пробелами. Пятая переменная FILE получает остаток строки. Наконец, последний echo выписывает содержимое переменной ${FILE}.
    { read -r DELIM VCL_SHOW INDEX SIZE FILE; echo "$FILE" }
  4. Поскольку все шаги с 1 по 3 заключены в саб-шелл, вывод значения $FILE va fi scris într-o variabilă VCL_FILE.

Как следует из комментария на 119-й строке, это служит единственной цели: надежно обрабатывать случаи, когда VCL будет ссылаться на файлы с символами пробела в названии.

Am comentat logica originală de procesare pentru ${VCL_FILE} и попытался изменить последовательность команд, но это ни к чему не привело. У меня всё работало чисто, а в случае запуска сервиса выдавало ошибку.

Se pare că eroarea pur și simplu nu este reproductibilă la rularea manuală a scriptului, în timp ce cele 30 de minute estimate s-au încheiat deja de șase ori și, în plus, a apărut o sarcină cu prioritate mai mare, împingând restul cazurilor deoparte. Restul săptămânii a fost plin de o varietate de sarcini și a fost doar ușor diluat cu o discuție despre sed și un interviu cu candidatul. Problemă de eroare în varnishreload pierdut iremediabil în nisipurile timpului.

Așa-zisul tău sed-fu... de fapt... gunoi

На следующей неделе выдался один довольно свободный день, поэтому я снова решил заняться этим тикетом. Я надеялся, что в моём мозгу, какой-то фоновый процесс всё это время искал решение этой проблемы и в этот раз я уж точно пойму в чём дело.

Поскольку в прошлый раз простое изменение кода не помогло, я просто решил его переписать начиная со 116-й строки. В любом случае существующий код был дурковатым. И в нём нет абсолютно никакой необходимости использовать read.

Privind din nou eroarea:
sh: echo: broken pipe — в этой команде echo находится в двух местах, но я подозреваю, что первая — более вероятный виновник (ну или хотя-бы соучастник). Awk также не внушает доверия. И в случае, если действительно это awk | {read; echo} конструкция приводит ко всем этим проблемам, почему бы её не заменить? Эта однострочная команда не использует все возможности awk, да ещё и этот лишний read în anexă.

De săptămâna trecută a fost un raport despre sedAm vrut să încerc abilitățile mele nou dobândite și să simplific echo | awk | { read; echo} într-un mod mai înțeles echo | sed. Хотя это определенно не лучший подход к выявлению ошибки, я подумал, что по крайней мере попробую свое sed-fu и, возможно, узнаю что-то новое о проблеме. По ходу дела я попросил своего коллегу, автора доклада о sed, помочь мне придумать более эффективный sed скрипт.

Am renunțat la conținut varnishadm vcl.show -v "$VCL_NAME" в файл, так я мог сосредоточиться на написании sed скрипта без каких-либо хлопот, связанных с перезагрузками сервиса.

Краткое описание того, как именно sed обрабатывает входные данные, можно найти в manualul său GNU. În sursele sed, simbolul n specificat explicit ca separator de linii.

В несколько проходов и с рекомендациями моего коллеги мы написали sed скрипт, который давал тот же результат, что и вся исходная строка 116.

Mai jos este un fișier exemplu cu date de intrare:

> 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

Это может быть не очевидным из приведенного выше описания, но нас интересует только первый комментарий // VCL.SHOW, причём во входных данных их может быть несколько. Именно поэтому оригинальный awk заканчивает свою работу после первого совпадения.

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

Deci, conținutul scriptului varnishreload ar arăta cam așa:

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

Logica de mai sus poate fi rezumată după cum urmează:
Dacă șirul se potrivește cu expresia regulată // VCL.SHOW, тогда жадно сожри текст, включающий оба числа в этой строке, и сохрани всё, что останется после этой операции. Выдай сохранённое значение и закончи программу.

Simplu, nu-i așa?

Мы были довольны sed скриптом и тем фактом, что он заменяет собой весь оригинальный код. Все мои тесты дали желаемые результаты, поэтому я изменил “varnishreload” на сервере и снова запустил systemctl reload varnish. Greșeală murdară echo: write error: Broken pipe вновь смеялась нам в лицо. Подмигивающий курсор ожидал ввода новой команды в темной пустоте терминала…

Sursa: www.habr.com

Adauga un comentariu