Executando comandos canalizados em paralelo

16

Considere o seguinte cenário. Eu tenho dois programas A e B. O programa A produz saídas padrão de linhas de strings enquanto o programa B processa linhas de stdin. A maneira de usar esses dois programas é obviamente:

foo @ bar: ~ $ A | B

Agora notei que isso consome apenas um núcleo; por isso estou me perguntando:

Os programas A e B estão compartilhando os mesmos recursos computacionais? Em caso afirmativo, existe uma maneira de executar A e B simultaneamente?

Outra coisa que notei é que A roda muito mais rápido que B, portanto, estou pensando se poderia, de alguma forma, executar mais programas B e deixá-los processar as linhas que A produz em paralelo.

Ou seja, A produziria suas linhas e haveria N instâncias dos programas B que liam essas linhas (quem as lê primeiro) as processam e as produzem no stdout.

Então, minha pergunta final é:

Existe uma maneira de canalizar a saída para A entre vários processos B sem precisar cuidar das condições da corrida e de outras inconsistências que possam surgir?

Jernej
fonte
11
Embora A | B | Cseja paralelo como em processos separados, devido à natureza dos tubos (B precisa aguardar a saída de A, C deve aguardar a saída de B), ainda pode ser linear em alguns casos. Depende inteiramente do tipo de produção que eles produzem. Não há muitos casos em que executar múltiplos Bajudaria muito, é perfeitamente possível que o exemplo wc paralelo seja mais lento que o normal, wcpois a divisão pode levar mais recursos do que contar linhas normalmente. Use com cuidado.
Frostschutz

Respostas:

14

Um problema split --filteré que a saída pode ser misturada, de modo que você obtém meia linha do processo 1, seguida por meia linha do processo 2.

O GNU Parallel garante que não haverá confusão.

Então, suponha que você queira fazer:

 A | B | C

Mas esse B é terrivelmente lento e, portanto, você deseja paralelizar isso. Então você pode fazer:

A | parallel --pipe B | C

Por padrão, o GNU Parallel divide-se em \ n e um tamanho de bloco de 1 MB. Isso pode ser ajustado com --recend e --block.

Você pode encontrar mais informações sobre o GNU Parallel em: http://www.gnu.org/s/parallel/

Você pode instalar o GNU Parallel em apenas 10 segundos com:

wget -O - pi.dk/3 | sh 

Assista ao vídeo de introdução em http://www.youtube.com/playlist?list=PL284C9FF2488BC6D1

Ole Tange
fonte
11
Embora eu discorde totalmente do método de instalação :-), +1 porque sua solução resolve a maioria dos problemas com os meus.
LSerni
Este é realmente bom. Você também tem alguma sugestão para os parâmetros a serem usados? Eu sei que o programa A produzirá mais de 1 TB de dados, aproximadamente 5 GB por minuto. O programa B processa dados 5 vezes mais devagar que A emite e tenho 5 núcleos à minha disposição para esta tarefa.
Jernej
Atualmente, o GNU Parallel pode lidar com no máximo 100 MB / s, então você atingirá esse limite. O ideal --block-sizedependerá da quantidade de RAM e da rapidez com que você pode iniciar um novo B. Na sua situação, eu usaria --block 100Me veria como isso funcionava.
Ole Tange
@lserni Você pode criar um método de instalação melhor, que funcione na maioria das máquinas UNIX e exija uma quantidade semelhante de trabalho do usuário?
Ole Tange
4
Desculpe, eu não me deixei claro. O método de instalação - o script passado para sh- é ótimo. O problema está em passar para sh: baixar e executar código executável de um site . Lembre-se, talvez eu esteja sendo paranóico demais, já que alguém poderia objetar que um RPM ou DEB personalizado é basicamente a mesma coisa, e até postar o código em uma página a ser copiada e colada resultaria em pessoas fazendo isso cegamente de qualquer forma.
LSerni
13

Quando você escreve A | B, os dois processos já são executados em paralelo. Se você vê-los usando apenas um núcleo, provavelmente é por causa das configurações de afinidade da CPU (talvez haja alguma ferramenta para gerar um processo com afinidade diferente) ou porque um processo não é suficiente para armazenar um núcleo inteiro e o sistema " prefere "não espalhar a computação.

Para executar vários B's com um A, você precisa de uma ferramenta como splita --filteropção:

A | split [OPTIONS] --filter="B"

No entanto, isso pode atrapalhar a ordem das linhas na saída, porque os trabalhos B não serão executados todos na mesma velocidade. Se este for um problema, pode ser necessário redirecionar a saída i para um arquivo intermediário e costurá-las no final usando cat. Por sua vez, isso pode exigir um espaço considerável em disco.

Existem outras opções (por exemplo, você pode limitar cada instância de B a uma única saída com buffer de linha, esperar até que toda uma "rodada" de B seja concluída, executar o equivalente a uma redução no mapa desplit s e a saída temporária em conjunto), com níveis variados de eficiência. A opção 'round' descrita acima, por exemplo, aguardará a conclusão da instância mais lenta de B , portanto, dependerá muito do buffer disponível para B; pode ajudar, ou não, dependendo de quais são as operações.cat[m]buffer

Exemplos

Gere os primeiros 1000 números e conte as linhas em paralelo:

seq 1 1000 | split -n r/10 -u --filter="wc -l"
100
100
100
100
100
100
100
100
100
100

Se "marcássemos" as linhas, veríamos que cada primeira linha é enviada para o processo 1, cada quinta linha para o processo 5 e assim por diante. Além disso, no tempo necessário splitpara gerar o segundo processo, o primeiro já é um bom caminho para sua cota:

seq 1 1000 | split -n r/10 -u --filter="sed -e 's/^/$RANDOM - /g'" | head -n 10
19190 - 1
19190 - 11
19190 - 21
19190 - 31
19190 - 41
19190 - 51
19190 - 61
19190 - 71
19190 - 81

Ao executar numa máquina de dois núcleos, seq, splite os wcprocessos compartilham os núcleos; mas olhando mais de perto, o sistema deixa os dois primeiros processos na CPU0 e divide a CPU1 entre os processos de trabalho:

%Cpu0  : 47.2 us, 13.7 sy,  0.0 ni, 38.1 id,  1.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu1  : 15.8 us, 82.9 sy,  0.0 ni,  1.0 id,  0.0 wa,  0.3 hi,  0.0 si,  0.0 st
  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM     TIME+ COMMAND
 5314 lserni    20   0  4516  568  476 R 23.9  0.0   0:03.30 seq
 5315 lserni    20   0  4580  720  608 R 52.5  0.0   0:07.32 split
 5317 lserni    20   0  4520  576  484 S 13.3  0.0   0:01.86 wc
 5318 lserni    20   0  4520  572  484 S 14.0  0.0   0:01.88 wc
 5319 lserni    20   0  4520  576  484 S 13.6  0.0   0:01.88 wc
 5320 lserni    20   0  4520  576  484 S 13.3  0.0   0:01.85 wc
 5321 lserni    20   0  4520  572  484 S 13.3  0.0   0:01.84 wc
 5322 lserni    20   0  4520  576  484 S 13.3  0.0   0:01.86 wc
 5323 lserni    20   0  4520  576  484 S 13.3  0.0   0:01.86 wc
 5324 lserni    20   0  4520  576  484 S 13.3  0.0   0:01.87 wc

Observe especialmente que splitestá consumindo uma quantidade considerável de CPU. Isso diminuirá proporcionalmente às necessidades de A; isto é, se A for um processo mais pesado que seq, a sobrecarga relativa splitdiminuirá. Mas se A é um processo muito leve e B é bastante rápido (para que você não precise de mais do que 2-3 B's para acompanhar A), paralelizar com split(ou tubos em geral) pode não valer a pena.

LSerni
fonte
Interessante que a divisão encontrada no Ubuntu não tenha a opção --filter. Que tipo de sistema operacional está usando para isso?
Jernej
Linux OpenSuSE 12.3, com coreutils ( gnu.org/software/coreutils/manual/html_node/… ). Vou tentar encontrar um Ubuntu, eles podem ter mudado o nome para acomodar alguma ferramenta com nome semelhante.
LSerni
Tem certeza de que a split --filteropção está faltando? No meu Ubuntu 12.04-LTS ("wheezy / sid"), ele está lá, e meus exemplos funcionam. Você poderia ter instalado um diferente splitdo GNU coreutils?
LSerni
Obrigado por isso. Eu tive que instalar uma versão mais recente do Coreutils. BTW, notei que se eu executar o programa A sozinho, ele consumirá um núcleo inteiro (100%) se eu executar o A | B então eles juntos comem um núcleo inteiro, processo A comendo 15% e processo B comendo 85% .. Você percebe por que isso acontece?
Jernej
2
Isso é provável por causa do bloqueio . Se B for mais pesado que A, então A não pode enviar sua saída e fica mais lento. Outra possibilidade é A ceder a B durante sua operação (por exemplo, disco / rede). Em um sistema diferente, você pode ver B consumindo 100% da CPU1 e A sendo atribuídos 18% da CPU0. Você provavelmente precisa de 85/15 ~ 5.67 = entre 5 e 6 instâncias de B para obter uma única instância A para saturar um único núcleo. E / S, se presente, pode distorcer esses valores, no entanto.
LSerni