, premendo i tasti per i 20 minuti precedenti come se la sua vita dipendesse da questo, si gira verso di me con uno sguardo semi selvaggio e un sorriso furbo — «Ehi, credo di aver capito.»
«Guarda qui,» dice, indicando uno dei simboli sullo schermo — «Scommettiamo sul mio cappello rosso che se aggiungiamo qui quello che ti ho appena mandato» — indicando un'altra parte del codice — «l'errore non verrà più visualizzato.»
Un po' confuso e stanco, modifico l'espressione sed su cui avevamo lavorato per un po’, salvo il file e lo avvio systemctl varnish reload. Il messaggio di errore è scomparso…
«Le email che ho scambiato con il candidato,» continua il mio collega, mentre il suo sorriso si trasforma in un sorriso autentico pieno di gioia, «mi sono reso conto che è proprio lo stesso problema!»
Da dove è tutto iniziato
L'articolo presuppone una comprensione dei principi di funzionamento di bash, awk, sed e systemd. La conoscenza di varnish è gradita, ma non è obbligatoria.
Le timestamp nei frammenti sono state modificate.
Scritto insieme a .
Questo testo è una traduzione dell'originale pubblicato in inglese due settimane fa; la traduzione .
Il sole filtra attraverso le finestre panoramiche in un'altra calda mattina d'autunno, una tazza di bevanda ricca di caffeina è posata a lato della tastiera, i miei brani preferiti risuonano nelle cuffie coprendo il fruscio delle tastiere meccaniche, e il primo elemento nella lista dei ticket del backlog sulla bacheca kanban brilla con il fatidico titolo “Investigate varnishreload sh: echo: I/O error in staging”. Quando si parla di varnish, non c'è posto per errori, anche se non si manifestano come problemi in questo caso.
Per chi non è familiare con , è un semplice script shell usato per ricaricare la configurazione — nota anche come VCL.
Come suggerisce il titolo del ticket, l’errore è apparso su uno dei server in staging, e poiché ero sicuro che il routing di varnish in staging funzionasse correttamente, ho supposto che si trattasse di un errore minore. Quindi, solo un messaggio finito in un flusso di output già chiuso. Prendo il ticket per me, sicuro di poterlo contrassegnare come completato in meno di 30 minuti, battermi sulla spalla per liberare la board da un'altra spazzatura e tornare a faccende più importanti.
Scontrandomi con un muro a 200 km/h
Apertura del file varnishreload, su uno dei server in esecuzione Debian Stretch, ho visto uno script shell di meno di 200 righe.
Scorrendo lo script, non ho notato nulla che potesse causare problemi se venisse eseguito più volte direttamente dal terminale.
Dopotutto, si tratta di staging, anche se si rompe, nessuno si lamenterà, beh… non troppo. Eseguo lo script e vedo cosa viene visualizzato nel terminale, ma non ci sono errori da vedere.
Altre due esecuzioni, per assicurarmi di non poter riprodurre l'errore senza alcuno sforzo aggiuntivo, e inizio a pensare a come modificare questo script per fargli restituire effettivamente un errore.
Potrebbe essere utile accorciare STDOUT (utilizzando >&-)? O STDERR? Nessuna delle due ha funzionato.
Ovviamente, systemd in qualche modo modifica l'ambiente di esecuzione, ma come e perché?
Apro vim e modifico varnishreload, aggiungendo set -x proprio sotto lo shebang, sperando che l'output di debug dello script faccia luce.
Il file è stato corretto, quindi riavvio varnish e vedo che la modifica ha completamente rotto tutto... L'output è un disastro totale, con tonnellate di codice in stile C. Anche lo scorrimento nel terminale non è sufficiente per trovare dove inizia. Sono completamente confuso. Può la modalità di debug influenzare l'esecuzione dei programmi avviati nello script? No, assurdità. Un bug nella shell? Diversi scenari possibili scorrono nella mia mente come scarafaggi in direzioni diverse. La mia tazza di bevanda ricca di caffeina si svuota immediatamente, rapido viaggio in cucina per rifornire e… andiamo. Apro lo script e controllo lo shebang: #!/bin/sh.
/bin/sh — è solo un symlink su bash, quindi lo script è interpretato in modalità compatibile con POSIX, giusto? Non così in fretta! La shell predefinita in Debian è dash, e questo è esattamente ciò che /bin/sh.
# ls -l /bin/sh
lrwxrwxrwx 1 root root 4 Jan 24 2017 /bin/sh -> dashPer provare, ho cambiato lo shebang in #!/bin/bash, ho rimosso set -x e ho provato di nuovo. Finalmente, durante il successivo riavvio di varnish, è apparso un errore decente nell'output:
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' compiledLinea 124, eccolo!
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 # tutto questo cerimoniale per gestire spazi 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 "impossibile ottenere il nome del file VCL"
129 fi
130
131 echo "$VCL_FILE"
132 }Ma come si è scoperto, la riga 124 è piuttosto vuota e non suscita interesse. Non potevo che ipotizzare che l'errore fosse parte di un multi-line che inizia alla riga 116.
Cosa viene quindi registrato nella variabile VCL_FILE a seguito dell'esecuzione del sopra citato subshell?
Inizialmente, trasmette il contenuto della variabile VLC_SHOW, creata alla riga 115, al comando successivo tramite pipe. E cosa succede lì?
Prima di tutto, viene utilizzato varnishadm, parte del pacchetto installativo di varnish, per configurare varnish senza riavviare.
Il comando vcl.show -v viene utilizzato per mostrare l'intera configurazione VCL specificata in ${VCL_NAME}, in STDOUT.
Per visualizzare l'attuale configurazione VCL attiva, così come alcune versioni precedenti delle configurazioni di routing di varnish ancora in memoria, si può usare il comando varnishadm vcl.list, il cui output sarà simile a quello qui sotto:
dismesso freddo/occupato 1 reload_20190101_120000_11903
dismesso freddo/occupato 2 reload_20190101_120000_12068
dismesso freddo/occupato 16 reload_20190101_120000_12259
dismesso freddo/occupato 16 reload_20190101_120000_12299
dismesso freddo/occupato 28 reload_20190101_120000_12357
attivo auto/caldo 32 reload_20190101_120000_12397
disponibile auto/caldo 0 reload_20190101_120000_12587Valore della variabile ${VCL_NAME} viene impostato in un'altra parte dello script varnishreload sul nome del VCL attivo al momento, se presente. In questo caso sarà “reload_20190101_120000_12397”.
Ottimo, la variabile ${VCL_SHOW} contiene l'intera configurazione per varnish, fin qui chiaro. Ora ho finalmente capito perché l'output dash di set -x era così danneggiato — includeva il contenuto della configurazione risultante.
È importante capire che l'intera configurazione VCL può essere spesso assemblata da più file. I commenti in stile C vengono utilizzati per indicare dove un file di configurazione è stato incluso in un altro, ed è esattamente questo di cui tratta tutta la seguente riga di codice.
La sintassi dei commenti che descrivono i file inclusi ha il seguente formato:
// VCL.SHOW <NUM> <NUM> <FILENAME>I numeri in questo contesto non sono importanti, ci interessa il nome del file.
Cosa succede quindi nel guazzabuglio dei comandi che inizia alla riga 116?
Analizziamo.
Il comando è composto da quattro parti:
- Un semplice
echo, che stampa il valore della variabile${VCL_SHOW}echo "$VCL_SHOW" awk, che cerca la riga (record) dove il primo campo, dopo aver diviso il testo, è “//”, e il secondo è «VCL.SHOW».
Awk stamperà la prima riga che corrisponde a questi schemi e poi smetterà immediatamente di elaborare.awk '$1 == "//" && $2 == "VCL.SHOW" {print; exit}'- Un blocco di codice che salva in cinque variabili i valori dei campi divisi da spazi. La quinta variabile FILE ottiene il resto della riga. Infine, l'ultimo echo stampa il contenuto della variabile
${FILE}.{ read -r DELIM VCL_SHOW INDEX SIZE FILE; echo "$FILE" } - Poiché tutti i passaggi da 1 a 3 sono inclusi nel subshell, l'output del valore
$FILEsarà registrato nella variabileVCL_FILE.
Come indicato dal commento alla riga 119, questo serve a uno scopo unico: gestire in modo affidabile i casi in cui il VCL fa riferimento a file con spazi nel nome.
Ho commentato la logica originale di gestione per ${VCL_FILE} e ho cercato di modificare la sequenza di comandi, ma non ha portato a nulla. Tutto funzionava perfettamente, e nel caso di avvio del servizio restituiva un errore.
Sembra che l'errore non sia semplicemente riproducibile quando si esegue lo script manualmente, mentre venti minuti presunti sono già scaduti sei volte e, in aggiunta, è emersa un'altra priorità che ha messo da parte le altre faccende. Il resto della settimana è stato pieno di vari compiti e solo un po' alleggerito da una presentazione su sed e un colloquio con un candidato. Il problema dell'errore in varnishreload è andato irreparabilmente perso nelle sabbie del tempo.
Il tuo presunto sed-fu... in realtà... è spazzatura
La settimana prossima ho avuto un giorno piuttosto libero, quindi ho deciso di tornare su questo ticket. Speravo che nella mia mente, un qualche processo in background avesse cercato una soluzione a questo problema tutto questo tempo e che stavolta avrei capito davvero di cosa si trattasse.
Poiché l'ultima volta una semplice modifica del codice non ha aiutato, ho deciso di riscriverlo a partire dalla riga 116. In ogni caso il codice esistente era goffo. E non c'era assolutamente bisogno di usare read.
Guardando di nuovo l'errore:
sh: echo: pipe rotta — in questo team, echo si trova in due posti, ma sospetto che il primo sia il colpevole più probabile (o almeno un complice). Anche Awk non ispira fiducia. E nel caso in cui davvero sia questo awk | {read; echo} costrutto che porta a tutti questi problemi, perché non sostituirlo? Questo comando su una sola riga non sfrutta tutte le capacità di awk, e c'è anche quella superflua read .
Poiché la settimana scorsa è stata fatta una Presentazione su sed, volevo provare le mie abilità appena acquisite e semplificare echo | awk | { read; echo} in un modo più chiaro echo | sed. Anche se questo non è certamente il miglior approccio per diagnosticare l'errore, ho pensato che almeno proverò il mio sed-fu e potrei scoprire qualcosa di nuovo sul problema. Durante il processo, ho chiesto al mio collega, l'autore della presentazione su sed, di aiutarmi a ideare uno script sed più efficace.
Ho inviato il contenuto varnishadm vcl.show -v "$VCL_NAME" in un file, così potevo concentrarmi sulla scrittura dello script sed senza preoccupazioni riguardo ai riavvii del servizio.
Una breve descrizione di come sed gestisce i dati in ingresso può essere trovata nel . Nei sorgenti di sed, il simbolo n è esplicitamente indicato come delimitatore di righe.
Con alcuni passaggi e i consigli del mio collega, abbiamo scritto uno script sed che dava lo stesso risultato della stringa originale 116.
Ecco un esempio del file contenente i dati in ingresso:
> cat vcl-example.vcl
Text
// VCL.SHOW 0 1578 file con 3 spazi.vcl
More text
// VCL.SHOW 0 1578 file.vcl
Even more text
// VCL.SHOW 0 1578 file con DUEspazi.vcl
Final textQuesto potrebbe non essere ovvio dalla descrizione sopra, ma ci interessa solo il primo commento // VCL.SHOW, e nei dati in ingresso potrebbero essercene più. È per questo che l'originale awk termina la sua esecuzione dopo la prima corrispondenza.
# шаг первый, вывести только строки с комментариями
# используя возможности 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.vclQuindi, il contenuto dello script varnishreload apparirà all'incirca così:
VCL_FILE="$(echo "$VCL_SHOW" | sed -En '#// VCL.SHOW#{s#.*[0-9]+ [0-9]+ (.*)$#1#p;q;};')"La logica sopra può essere espressa brevemente come segue:
Se la stringa corrisponde a un'espressione regolare // VCL.SHOW, allora consuma avidamente il testo che include entrambi i numeri in quella stringa e salva tutto ciò che rimane dopo questa operazione. Restituisci il valore salvato e termina il programma.
Semplice, vero?
Eravamo soddisfatti dello script sed e del fatto che sostituisse l'intero codice originale. Tutti i miei test hanno dato i risultati desiderati, quindi ho cambiato "varnishreload" sul server e ho riavviato systemctl reload varnish. L'orribile errore echo: write error: Broken pipe sorrideva di nuovo in faccia a noi. Il cursore lampeggiante aspettava un nuovo comando nell'oscurità vuota del terminale...
Fonte: habr.com
