A maneira correta, se houver alguma, de usar o dd como buffer de pipe?

3

A questão

Quando procurei por ferramentas de buffer de tubulação no * NIX, vejo sugestões de usar buffer , mbuffer ou pv . No entanto, os dois primeiros não estão sempre no repositório oficial das distribuições (como o Arch), enquanto pv (a partir de 1.6.0) tem um bug que impede essa funcionalidade. Em algumas outras perguntas, vejo menções sobre dd usadas como buffers, e gostaria de explorá-las porque dd está sempre lá. No entanto, nenhum é elaborado o suficiente para fazer sentido real, então aqui eu peço uma maneira "adequada" para usá-lo.

As perguntas mencionadas dd incluem link e link

Para facilitar o teste, forneço um script de teste abaixo, com alguns comentários sobre meus próprios experimentos. Detalhes serão explicados após a listagem do código. Por favor, verifique se você tem pv instalado e pelo menos 256M de memória antes de executar!

#!/bin/sh

producer() {
    while [ 1 ]; do
    dd if=/dev/zero bs=64M count=1 iflag=fullblock status=none
    sleep 4
    done
}

buffer() {
    # Works, but long
    # Must at least fill 32M before consumer starts
    # So, must choose small obs and chain more to look
    # more like a proper "buffer"
    dd obs=32M status=none | \
        dd obs=32M status=none| \
        dd obs=32M status=none| \
        dd obs=32M status=none
    # Doesn't work, producer rate limited
    #dd bs=128M status=none 
    # Doesn't work, producer must fill buffer, then
    # must wait until buffer is empty
    #dd obs=128M status=none 
    # Doesn't work, producer rate limited
    #dd ibs=128M status=none 
    # Doesn't work, producer must fill buffer, then
    # must wait until buffer is empty
    #dd bs=128M status=none iflag=fullblock
}

consumer() {
    pv --rate-limit 1M -q | dd of=/dev/null status=none
}

producer | pv -cN produce | buffer | pv -cN consume | consumer

Aqui, o produtor produz 64MB de dados a cada 4 segundos, com um buffer de 128MB, enquanto o consumidor consome a uma taxa constante de 1MB / s. Claro, isso significa que o buffer irá transbordar rapidamente, mas isso é para mostrar efeitos claramente. Idealmente, antes que o buffer fosse preenchido (na terceira produção), deveríamos ver um consumo constante de 1 MB / se rajadas de produção fornecendo dados de 64 MB cada. A saída "correta" é assim:

  produce:  128MiB 0:00:07 [   0 B/s] [  <=>                                                       ]
  consume: 7.25MiB 0:00:07 [1.01MiB/s] [       <=>                                                 ]

Aqui, a solução de trabalho é mostrada da seguinte forma:

dd obs=32M status=none | \
    dd obs=32M status=none| \
    dd obs=32M status=none| \
    dd obs=32M status=none

Isso é construído dividindo-se o buffer de 128MB necessário em 4 partes. Sim, cada bloco deve ser preenchido antes que os dados sejam passados para o próximo nível, mas como 32 MB é menor que o burst de 64 MB, ele funciona para este teste, como se fosse um buffer real. Agora, existem alguns problemas.

  1. Em aplicativos reais, não temos uma explosão instantânea de dados, portanto, os pedaços precisam ser pequenos, mas não muito pequenos. O que significa que haverá uma longa cadeia de dd comandos
  2. E se um EOF for encontrado antes da marca de 32 MB ser atingida? Esse bloco será perdido?
    Eu testei com dd if=test.txt| dd obs=1M | dd obs=1M | dd of=test2.txt e comparei o resultado. Acontece que isso não é um problema. Então, usá-lo para backup não corromperá os dados.
  3. Quanta sobrecarga é criada?
  4. Existe uma maneira mais elegante de conseguir o mesmo, organizando inteligentemente os parâmetros?

Existem algumas outras tentativas incluídas no script e elas não funcionam, conforme explicado nos comentários. E eu tentei usar processos em segundo plano FIFO +, que produz o mesmo resultado.

PS. Só para você saber, bufferizar um pipe é bastante útil ao fazer o backup de A em B, especialmente quando A é um HDD, que tem tempo de busca. Então eu faria algo assim:

tar cSpf - <path> -C <root path> | <a large buffer> | <some parallel compressor> \
| <small buffer if compressor is generally slow and B have seek time> \
| dd bs=<several GB if B is not an SSD> iflag=fullblock oflag=direct of=<archive.tar.??>
    
por Carl Dong 04.05.2017 / 22:58

1 resposta

0

Estou colocando minha própria resposta. Pode não ser o melhor, mas está tudo bem.

Cuidado

Isso está escrito na frente depois de muitos testes.

Não encadeie DDs demais para armazenar em buffer, ou todos os seus núcleos de CPU podem bloquear o IO, e seu computador irá congelar mesmo se você tiver toneladas de memória sobrando!

Especialmente tóxico se você tiver uma unidade USB externa lenta e quebrada que também precise de uma intensidade ridícula de IO para ler / escrever.

Exemplos

Eu basicamente esgotou todas as combinações de opções de DD. Um único DD parece ser impossível para essa tarefa, já que não pode executar IO assíncrono. Caso contrário, na cadeia de buffer DD, o maior bloco deve ser preenchido antes de começar a agir como um FIFO. Então, se você não se importa com o atraso inicial ao encher o tubo ... Uma cadeia de trabalho de dois dds. Espero que alguém possa fornecer uma solução mais elegante, mas aqui está um exemplo de uso.

Exemplo 1 : Tarring todos os arquivos de um HDD A muito fragmentado (tremores de tempo de resposta) para um HDD B altamente fragmentado (jitters), usando XZ como algoritmo de compressão (lento) em paralelo você está realmente usando o computador) (disclaimer: Estou escrevendo isso da minha cabeça, então pequenos detalhes podem estar errados. Use a seu próprio risco):

tar -cpSf - -C /mnt/A . | \
  dd obs=1024M | dd obs=1024M | \
  xz -T 0 -c | \
  dd obs=1024M | dd obs=1024M | \
  dd bs=512M iflag=fullblock of=/mnt/B/A.tar.xz

Adicione pv para ver a velocidade. Aqui, xz é iniciado somente depois que os dados de 1 GB são lidos de A (a menos que tenha menos de 1 GB, ele será concluído). Da mesma forma, a gravação em disco para B começa somente após os dados de 1 GB saírem do xz. Esse código fornece um buffer de 2 GB entre tar e xz e 2 GB entre xz e gravação. O bs=512M no final não é realmente necessário, mas descobri que um tamanho de bloco grande (> 64M) oferece uma velocidade de gravação média melhor, especialmente em discos rígidos USB. Eu suponho que ele cria menos fragmentos também, se a unidade B estiver em uso (não confirmada).

Exemplo 2 . Objetivo: copiar um arquivo gigantesco de um disco strongmente fragmentado A para um disco muito fragmentado B.

dd if=/mnt/A/file obs=<half of cache you want> | dd bs=<half of cache> iflag=fullblock oflag=direct of=/mnt/B/file

Esta é uma das formas mais simples que posso encontrar. Se o arquivo for gigantesco o suficiente, o tempo inicial usado para preencher o cache deve ser insignificante. Enquanto isso, lê / escreve de forma assíncrona, e espera-se que grupos escrevam o suficiente para obter algum desempenho sequencial. Suponho que o SSD não se importe com o tamanho do bloco, no entanto.

Exemplo 3 . Graças a Kamil Maciorowski, agora tenho o seguinte no meu .zshrc :

buffer() {
    if [ "$2" -gt 0 ] ; then
        dd status=none obs="$1" | buffer "$1" $(($2-1))
    else 
        cat 
    fi
}

Agora, se você precisar de 3 blocos de buffer de 512M, encadeie buffer 512M 3 em seu pipeline. Geralmente, se o seu trabalho é grande o suficiente para o seu rendimento (por exemplo, Copiando / Comprimindo 100GB + dados @ 100MB / s em média), um bloco menor não dá vantagem além de preencher o tubo mais rapidamente (o que é irrelevante já que esse tempo é pequeno) . Eu observei que, se você colocar muitos blocos, a CPU pode estar tão ocupada com o IO que o comando congela o computador inteiro.

Agora, o Exemplo 1 se torna

tar -cpSf - -C /mnt/A . | \
buffer 1024M 2 | \
xz -T 0 -c | \
buffer 1024M 2 | \
dd bs=512M iflag=fullblock of=/mnt/B/A/tar.xz
    
por 06.05.2017 / 19:28