Por que os pipes às vezes ficam “presos”: buffer


Aqui está um problema terminal de nicho que me incomoda há anos, mas que eu realmente nunca entendi até algumas semanas atrás. Digamos que você esteja executando este comando para observar alguma saída específica em um arquivo de log:

tail -f /some/log/file | grep thing1 | grep thing2

Se as linhas de log estiverem sendo adicionadas ao arquivo de forma relativamente lenta, o resultado que verei é… nada! Não importa se houve correspondências no arquivo de log ou não, simplesmente não haveria saída.

Eu internalizei isso como “uh, acho que os pipes ficam presos às vezes e não me mostram a saída, isso é estranho”, e eu lidaria com isso apenas executando grep thing1 /some/log/file | grep thing2 em vez disso, o que funcionaria.

Então, como tenho feito um mergulho profundo nos últimos meses, fiquei muito animado para finalmente aprender exatamente por que isso acontece.

por que isso acontece: buffer

A razão pela qual “pipes ficam presos” às vezes é que é MUITO comum que os programas armazenem sua saída em buffer antes de gravá-la em um pipe ou arquivo. Então o pipe está funcionando bem, o problema é que o programa nunca gravou os dados no pipe!

Isso ocorre por motivos de desempenho: escrever toda a saída imediatamente assim que possível usa mais chamadas do sistema, por isso é mais eficiente salvar dados até que você tenha cerca de 8 KB de dados para gravar (ou até o programa terminar) e ENTÃO gravá-los no canal.

Neste exemplo:

tail -f /some/log/file | grep thing1 | grep thing2

o problema é que grep thing1 está salvando todas as suas correspondências até ter 8 KB de dados para gravar, o que pode literalmente nunca acontecer.

programas não armazenam buffer ao gravar em um terminal

Parte do motivo pelo qual achei isso tão desorientador é que tail -f file | grep thing
funcionará perfeitamente bem, mas quando você adicionar o segundo grepele para de funcionar!! A razão para isso é que a maneira grep lida com o buffer depende se está gravando em um terminal ou não.

Veja como grep (e muitos outros programas) decide armazenar em buffer sua saída:

  • Verifique se stdout é um terminal ou não usando o isatty função
    • Se for um terminal, use buffer de linha (imprima todas as linhas imediatamente assim que as tiver)
    • Caso contrário, use “buffering de bloco” – imprima dados apenas se você tiver pelo menos 8 KB ou mais de dados para imprimir

Então se grep estiver escrevendo diretamente em seu terminal, você verá a linha assim que for impressa, mas se estiver escrevendo em um canal, você não verá.

É claro que o tamanho do buffer nem sempre é de 8 KB para cada programa, depende da implementação. Para grep o buffer é tratado pela libc, e o tamanho do buffer da libc é definido no BUFSIZ variável. É aqui que isso é definido na glibc.

(como um aparte: “os programas não usam buffers de saída de 8 KB ao gravar em um terminal” não é, tipo, uma lei da física do terminal, um programa PODERIA usar um buffer de 8 KB ao gravar a saída em um terminal, se quisesse, seria extremamente estranho se fizesse isso, não consigo pensar em nenhum programa que se comporte dessa maneira)

comandos que armazenam buffer e comandos que não

Uma coisa irritante sobre esse comportamento de buffer é que você precisa lembrar quais comandos armazenam sua saída em buffer ao gravar em um pipe.

Alguns comandos que não buffer sua saída:

Acho que quase todo o resto armazenará a saída em buffer, especialmente se for um comando em que você provavelmente o usará para processamento em lote. Aqui está uma lista de alguns comandos comuns que armazenam sua saída em buffer ao gravar em um pipe, junto com o sinalizador que desativa o buffer de bloco.

  • grep (--line-buffered)
  • sed (-u)
  • ahk (há um fflush() função)
  • tcpdump(-l)
  • jq (-u)
  • tr (-u)
  • cut (não é possível desabilitar o buffer)

Esses são todos os que consigo pensar, muitos comandos unix (como sort) pode ou não armazenar em buffer sua saída, mas isso não importa porque sort não pode fazer nada até terminar de receber a entrada.

Também fiz o meu melhor para testar as versões Mac OS e GNU, mas há muitas variações e posso ter cometido alguns erros.

linguagens de programação onde a instrução “print” padrão armazena buffers

Além disso, aqui estão algumas linguagens de programação em que a instrução print padrão armazenará a saída em buffer ao gravar em um canal e algumas maneiras de desativar o buffer, se desejar:

  • C (desativar com setvbuf)
  • Python (desativar com python -uou PYTHONUNBUFFERED=1ou sys.stdout.reconfigure(line_buffering=False)ou print(x, flush=True))
  • Ruby (desativar com STDOUT.sync = true)
  • Perl (desativar com $| = 1)

Presumo que essas linguagens sejam projetadas dessa maneira para que a função de impressão padrão seja rápida quando você estiver processando em lote.

Além disso, se a saída é armazenada em buffer ou não, pode depender de como você imprime, por exemplo, em C++ cout buffers when writing to a pipe but cout will flush its output.

quando você pressiona Ctrl-C em um tubo, o conteúdo do buffer é perdido

Digamos que você esteja executando este comando como uma maneira hackeada de observar solicitações de DNS para example.come você esqueceu de passar -l para tcpdump:

sudo tcpdump -ni any port 53 | grep example.com

Quando você pressiona Ctrl-CO que acontece? Em um mundo mágico perfeito, o que eu faria
querer acontecer é para tcpdump para liberar seu buffer, grep iria procurar
example.come veria toda a saída que perdi.

Mas no mundo real, o que acontece é que todos os programas são eliminados e a saída em tcpdumpO buffer de está perdido.

Acho que este problema é provavelmente inevitável – passei um pouco de tempo com
strace para ver como isso funciona e grep recebe o SIGINT antes
tcpdump de qualquer maneira, mesmo que tcpdump tentou liberar seu buffer grep já estaria morto.


Depois de um pouco mais de investigação, há uma solução alternativa: se você encontrar
tcpdumpPID e kill -TERM $PIDentão o tcpdump liberará o buffer para que você possa ver a saída. Isso é meio chato, mas eu testei e parece funcionar.

redirecionar para um arquivo também armazena em buffer

Não são apenas pipes, isso também irá armazenar em buffer:

sudo tcpdump -ni any port 53 > output.txt

Redirecionar para um arquivo não tem o mesmo “Ctrl-C irá destruir totalmente o conteúdo do buffer” – na minha experiência, ele geralmente se comporta mais como você deseja, onde o conteúdo do buffer é gravado no arquivo antes de o programa sair. Não tenho 100% de certeza se isso é algo em que você sempre pode confiar ou não.

um monte de maneiras possíveis de evitar o buffer

Ok, vamos conversar sobre soluções. Digamos que você executou este comando:

tail -f /some/log/file | grep thing1 | grep thing2

Perguntei às pessoas do Mastodon como elas resolveriam isso na prática e havia 5 abordagens básicas. Aqui estão eles:

solução 1: execute um programa que termine rapidamente

Historicamente, minha solução para isso tem sido apenas evitar completamente a situação de “escrita de comando no canal lentamente” e, em vez disso, executar um programa que terminará rapidamente assim:

cat /some/log/file | grep thing1 | grep thing2 | tail

Isso não faz a mesma coisa que o comando original, mas significa que você evita pensar nesses estranhos problemas de buffer.

(você também pode fazer grep thing1 /some/log/file mas muitas vezes prefiro usar um “desnecessário” cat)

solução 2: lembre-se do sinalizador “line buffer” para grep

Você deve se lembrar que o grep tem um sinalizador para evitar buffer e passá-lo assim:

tail -f /some/log/file | grep --line-buffered thing1 | grep thing2

solução 3: use awk

Algumas pessoas disseram que se estiverem lidando especificamente com uma situação de múltiplos greps, eles irão reescrevê-la para usar um único awk em vez disso, assim:

tail -f /some/log/file |  awk '/thing1/ && /thing2/'

Ou você escreveria um texto mais complicado grepassim:

tail -f /some/log/file |  grep -E 'thing1.*thing2'

(awk também buffers, então para que isso funcione você vai querer awk para ser o último comando no pipeline)

solução 4: usar stdbuf

stdbuf usa LD_PRELOAD para desligar o buffer da libc, e você pode usá-lo para desligar o buffer de saída assim:

tail -f /some/log/file | stdbuf -o0 grep thing1 | grep thing2

Como qualquer LD_PRELOAD solução não é confiável – não funciona em binários estáticos, acho que não funcionará se o programa não estiver usando o buffer da libc e nem sempre funciona no Mac OS. Harry Marr tem uma postagem muito boa sobre como o stdbuf funciona.

solução 5: usar unbuffer

unbuffer program forçará a saída do programa a ser um TTY, o que significa que ele se comportará como normalmente faria em um TTY (menos buffer, saída colorida, etc.). Você poderia usá-lo neste exemplo assim:

tail -f /some/log/file | unbuffer grep thing1 | grep thing2

Diferente stdbuf sempre funcionará, embora possa ter efeitos colaterais indesejados, por exemplo grep thing1também combinará cores.

Se você deseja instalar o unbuffer, está no expect pacote.

essas são todas as soluções que conheço!

É um pouco difícil para mim dizer qual é o “melhor”, acho que pessoalmente provavelmente usarei unbuffer porque sei que sempre vai dar certo.

Se eu aprender mais soluções, tentarei adicioná-las a este post.

Não tenho certeza de quantas vezes isso acontece

Acho que não é muito comum eu ter um programa que lentamente transfere dados para um canal como este. Normalmente, se estou usando um canal, um monte de dados é gravado muito rapidamente, processado por tudo no pipeline e então tudo sai. Os únicos exemplos que posso apresentar agora são:

  • tcpdump
  • tail -f
  • assistindo arquivos de log de uma maneira diferente, como com kubectl logs
  • a saída de um cálculo lento

e se houvesse uma variável de ambiente para desativar o buffer?

Acho que seria legal se houvesse uma variável de ambiente padrão para desativar o buffer, como PYTHONUNBUFFERED em Python. Tive essa ideia em algumas postagens de Mark Dominus em 2018. Talvez NO_BUFFER como NO_COLOR?

O design parece difícil de acertar; Mark ressalta que o NETBSD possui variáveis ​​de ambiente chamadas STDBUF, STDBUF1etc, o que oferece muito controle sobre o buffer, mas imagino que a maioria dos desenvolvedores não deseja implementar muitas variáveis ​​de ambiente diferentes para lidar com um caso extremo relativamente menor.

Também estou curioso para saber se existem programas que liberam automaticamente seus buffers de saída após algum período de tempo (como 1 segundo). Parece que seria bom em teoria, mas não consigo pensar em nenhum programa que faça isso, então imagino que haja algumas desvantagens.

coisas que deixei de fora

Algumas coisas que eu não falei neste post já que essas postagens estão ficando muito longas recentemente e sério, alguém REALMENTE quer ler 3.000 palavras sobre buffer?

  • a diferença entre buffer de linha e saída totalmente sem buffer
  • como o buffer para stderr é diferente do buffer para stdout
  • este post é apenas sobre buffer que acontece dentro do programao driver TTY do seu sistema operacional também faz um pouco de buffer às vezes
  • outros motivos pelos quais você pode precisar liberar sua saída além de “você está gravando em um canal”



Source link

Deixe um comentário

O seu endereço de email não será publicado. Campos obrigatórios marcados com *

Botão Voltar ao Topo