Cair em uma toca de coelho: uma história sobre um erro de reinicialização de um verniz - Parte 1

ghostinushanka , depois de apertar os botões nos 20 minutos anteriores, como se sua vida dependesse disso, se vira para mim com uma expressão meio selvagem nos olhos e um sorriso malicioso - "Cara, acho que entendi."


“Olhe aqui” - diz, apontando para um dos símbolos na tela - “Aposto no meu chapéu vermelho que se adicionarmos aqui o que eu acabei de enviar” - apontando para outro pedaço de código - “o erro não é mais será exibido. "


Um pouco confuso e cansado, mudo a expressão sed em que trabalhamos há algum tempo, salve o arquivo e execute o systemctl varnish reload . A mensagem de erro desapareceu ...


"Os e-mails que eu troquei com o candidato", continuou meu colega, enquanto o sorriso dele se transforma em um sorriso genuíno e cheio de alegria, "de repente me ocorreu que esse é exatamente o mesmo problema!"


Como tudo começou


Este artigo pressupõe uma compreensão de como bash, awk, sed e systemd funcionam. O conhecimento do verniz é bem-vindo, mas não obrigatório.
Os carimbos de hora do snippet foram alterados.
Escrito com ghostinushanka .
Este texto é uma tradução do original publicado em inglês há duas semanas; tradução boikoden .


O sol brilha através das janelas panorâmicas em outra manhã quente de outono, a xícara da bebida com cafeína preparada na hora repousa longe do teclado, a sinfonia favorita de sons nos fones de ouvido, sobrepondo o farfalhar dos teclados mecânicos, e o fatídico título "Investigar varnishre" brilha de brincadeira a primeira entrada na lista de pedidos pendentes no quadro do Kanban. sh: echo: erro de E / S no teste ”(Investigue o“ verniz reload sh: echo: erro de E / S ”no estágio). Quando se trata de verniz, não há erros e não pode haver lugar, mesmo que eles não se traduzam em problemas, como neste caso.


Para aqueles que não estão familiarizados com o varnishreload , este é um script de shell simples usado para recarregar uma configuração de verniz - também chamada VCL.


Como o nome do ticket sugere, ocorreu um erro em um dos servidores no palco e, como eu tinha certeza de que o roteamento de verniz no palco funcionava corretamente, presumi que esse seria um erro menor. Portanto, apenas uma mensagem que entrou em um fluxo de saída já fechado. Pego o bilhete para mim, confiante em que o marcarei pronto em menos de 30 minutos, dou um tapinha no meu ombro para limpar o tabuleiro do próximo lixo e voltar a assuntos mais importantes.


Batendo contra uma parede a uma velocidade de 200 km / h


Depois de abrir o arquivo varnishreload , em um dos servidores executando o Debian Stretch, vi um shell script com menos de 200 linhas.


Depois de executar o script, não notei nada que pudesse resultar em problemas quando ele foi executado várias vezes diretamente do terminal.


No final, este é um estágio, mesmo que quebre, ninguém vai reclamar, bem ... não muito. Eu corro o script e vejo o que será gravado no terminal, mas não vejo erros.


Mais algumas etapas para garantir que eu não possa reproduzir o erro sem nenhum esforço adicional, e começo a descobrir como alterar esse script e ainda assim dar um erro.


Um script pode substituir STDOUT (usando > &- )? Ou STDERR? Nenhum deles funcionou como resultado.


Obviamente, o systemd de alguma forma modifica o ambiente de inicialização, mas como e por quê?
Eu varnishreload vim e edito varnishreload , adicionando set -x diretamente sob o shebang, esperando que a saída do script de depuração varnishreload um pouco mais.


O arquivo foi corrigido, então eu reinicio o verniz e vejo que a mudança quebrou tudo completamente ... O escape é uma bagunça completa, na qual existem toneladas de código C. Mesmo a rolagem no terminal não é suficiente para descobrir onde começa. Estou completamente confuso. O modo de depuração pode afetar o trabalho dos programas iniciados em um script? Não, bobagem. Um bug no shell? Vários cenários possíveis correm pela minha cabeça como baratas em diferentes direções. Um copo de bebida cheia de cafeína foi esvaziado instantaneamente, uma rápida viagem à cozinha para reabastecer o estoque e ... vamos lá. Abro o script e olho para o shebang: #!/bin/sh .


/bin/sh é simplesmente o link simbólico do bash; portanto, o script é interpretado no modo compatível com POSIX, certo? Lá estava! O shell padrão no Debian é dash, e é exatamente a isso que /bin/sh se refere .


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

Para fins de teste, mudei o shebang para #!/bin/bash , excluí set -x e tentei novamente. Finalmente, durante a reinicialização subsequente do verniz, um erro tolerável apareceu na saída:


 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 

Linha 124, aí está!


 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 } 

Mas, como se viu, a linha 124 é bastante vazia e não interessa. Só pude supor que o erro surgiu como parte de uma linha múltipla iniciando na linha 116.
O que é finalmente gravado na variável VCL_FILE como resultado da execução do sub-shell mencionado acima?


No início, ele envia o conteúdo da variável VLC_SHOW criada na linha 115 para o próximo comando através do pipe. E então o que acontece então?


Primeiro, ele usa o varnishadm , que faz parte do pacote de instalação do verniz, para configurar o verniz sem reiniciar.


O vcl.show -v usado para vcl.show -v toda a configuração da VCL especificada em ${VCL_NAME} para STDOUT.


Para exibir a configuração atual da VCL ativa, bem como várias versões anteriores das configurações de roteamento de verniz que ainda estão na memória, você pode usar o varnishadm vcl.list , cuja saída será semelhante à abaixo:


 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 

O valor da variável ${VCL_NAME} definido em outra parte do script varnishreload para o nome da VCL atualmente ativa, se houver. Nesse caso, será "reload_20190101_120000_12397".


Ótimo, a variável ${VCL_SHOW} contém a configuração completa do verniz, até agora está claro. Agora, finalmente entendi por que a saída do traço com o set -x estava tão quebrada - incluía o conteúdo da configuração resultante.


É importante entender que uma configuração completa da VCL geralmente pode ser conectada a partir de vários arquivos. Os comentários no estilo C são usados ​​para determinar onde alguns arquivos de configuração foram incluídos em outros, e é exatamente sobre isso que trata toda a linha de trecho de código abaixo.
A sintaxe dos comentários que descrevem os arquivos incluídos possui o seguinte formato:


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

Os números neste contexto não são importantes, estamos interessados ​​no nome do arquivo.


Então, o que está acontecendo no pântano de equipes começando na linha 116?
Vamos descobrir.
A equipe consiste em quatro partes:


  1. Um echo simples que exibe o valor da variável ${VCL_SHOW}
     echo "$VCL_SHOW" 
  2. awk , que está procurando uma linha (registro), onde o primeiro campo, depois de quebrar o texto, será "//" e o segundo "VCL.SHOW".
    O Awk gravará a primeira linha correspondente a esses padrões e interromperá imediatamente o processamento.
     awk '$1 == "//" && $2 == "VCL.SHOW" {print; exit}' 
  3. Um bloco de código que armazena em cinco valores de campos variáveis ​​separados por espaços. A quinta variável FILE obtém o restante da string. Finalmente, o último eco grava o conteúdo da variável ${FILE} .
     { read -r DELIM VCL_SHOW INDEX SIZE FILE; echo "$FILE" } 
  4. Como todas as etapas de 1 a 3 estão incluídas em um VCL_FILE , a saída do valor $FILE será gravada na variável VCL_FILE .

Como segue o comentário na linha 119, isso tem um único objetivo: tratar de forma confiável os casos em que a VCL fará referência a arquivos com caracteres de espaço no nome.


Comentei a lógica de processamento original de ${VCL_FILE} e tentei alterar a sequência de comandos, mas isso não levou a nada. Tudo funcionou corretamente para mim e, no caso de iniciar o serviço, deu um erro.


Parece que o erro simplesmente não é reproduzível quando você executa o script manualmente, enquanto os 30 minutos esperados já terminaram seis vezes e, no apêndice, uma tarefa de maior prioridade apareceu, afastando o restante dos casos. O resto da semana foi repleto de tarefas e foi apenas ligeiramente diluído com um relatório sobre sed e uma entrevista com o candidato. O problema com o varnishreload foi irremediavelmente perdido nas areias do tempo.


Seu chamado sed-fu ... realmente ... lixo


A semana seguinte acabou sendo um dia bastante livre, então, novamente, decidi pegar esse ingresso. Eu esperava que, no meu cérebro, algum processo em segundo plano estivesse procurando uma solução para esse problema, e dessa vez eu certamente entenda o que é.


Desde a última vez que uma simples alteração de código não ajudou, decidi reescrevê-la a partir da 116ª linha. De qualquer forma, o código existente era péssimo. E não há absolutamente nenhuma necessidade de usar a read .


Olhando para o erro novamente:
sh: echo: broken pipe - neste comando echo está em dois lugares, mas suspeito que o primeiro seja o culpado mais provável (bem, ou pelo menos um cúmplice). Awk também não é credível. E caso seja realmente awk | {read; echo} awk | {read; echo} awk | {read; echo} construção leva a todos esses problemas, por que não substituí-lo? Este comando de uma linha não usa todos os recursos do awk, e mesmo essa read extra no apêndice.


Como houve um relatório sobre o sed na semana passada, eu queria experimentar minhas habilidades recém-adquiridas e simplificar o echo | awk | { read; echo} echo | awk | { read; echo} echo | awk | { read; echo} em um echo | sed mais compreensível echo | sed echo | sed . Embora essa definitivamente não seja a melhor abordagem para detectar um erro, pensei que pelo menos tentaria meu sed-fu e talvez aprendesse algo novo sobre o problema. No processo, pedi ao meu colega, autor do relatório sobre sed, que me ajudasse a criar um script sed mais eficaz.


varnishadm vcl.show -v "$VCL_NAME" o conteúdo do varnishadm vcl.show -v "$VCL_NAME" no arquivo, para que eu pudesse me concentrar em escrever um script sed sem qualquer aborrecimento associado à recarga do serviço.


Uma breve descrição de como o sed lida com a entrada pode ser encontrada em seu manual GNU . Nas fontes sed, o caractere \n é especificado explicitamente como um separador de linhas.


Em várias passagens e com as recomendações do meu colega, escrevemos um script sed que deu o mesmo resultado que toda a linha de origem 116.


A seguir está um arquivo de entrada de amostra:


 > 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 

Isso pode não ser óbvio na descrição acima, mas estamos interessados ​​apenas no primeiro comentário // VCL.SHOW , e pode haver vários deles na entrada. É por isso que o awk original termina seu trabalho após a primeira partida.


 #  ,      #   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 

Portanto, o conteúdo do script varnishreload será mais ou menos assim:


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

A lógica acima pode ser resumida da seguinte maneira:
Se a linha corresponder à expressão regular // VCL.SHOW , coma avidamente o texto que inclui os dois números nessa linha e salve tudo o que resta após esta operação. Dê o valor salvo e termine o programa.


Simples, certo?


Ficamos satisfeitos com o script sed e com o fato de ele substituir todo o código original. Todos os meus testes deram os resultados desejados, então mudei o “varnishreload” no servidor e executei o systemctl reload varnish novamente. O erro imundo echo: write error: Broken pipe riu de novo em nossos rostos. Um cursor piscando aguardava a entrada de um novo comando no vazio escuro do terminal ...

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


All Articles