Como executar um comando em média 5 vezes por segundo?

21

Eu tenho um script de linha de comando que executa uma chamada de API e atualiza um banco de dados com os resultados.

Eu tenho um limite de 5 chamadas de API por segundo com o provedor de API. O script leva mais de 0,2 segundos para ser executado.

  • Se eu executar o comando sequencialmente, ele não será executado rápido o suficiente e só farei 1 ou 2 chamadas de API por segundo.
  • Se eu executar o comando seqüencialmente, mas simultaneamente em vários terminais, posso exceder o limite de 5 chamadas / segundo.

Existe uma maneira de orquestrar threads para que meu script de linha de comando seja executado quase exatamente 5 vezes por segundo?

Por exemplo, algo que seria executado com 5 ou 10 threads, e nenhum thread executaria o script se um thread anterior o executasse há menos de 200 ms.

Benjamin
fonte
Todas as respostas dependem da suposição de que seu script será finalizado na ordem em que é chamado. É aceitável para o seu caso de uso se eles terminarem fora de ordem?
Cody Gustafson
@CodyGustafson É perfeitamente aceitável se eles terminarem fora de ordem. Não acredito que exista essa suposição na resposta aceita, pelo menos?
Benjamin
O que acontece se você exceder o número de chamadas por segundo? Se o provedor da API diminuir, você não precisará de nenhum mecanismo ... precisa?
Floris 04/04
@Floris Eles retornarão uma mensagem de erro que será traduzida em uma exceção no SDK. Antes de tudo, duvido que o provedor da API fique satisfeito se eu gerar 50 mensagens de aceleração por segundo (você deve agir de acordo com essas mensagens de acordo) e, em segundo lugar, estou usando a API para outros fins ao mesmo tempo. não quero atingir o limite que é realmente um pouco maior.
227 Benjamin Benjamin

Respostas:

25

Em um sistema GNU e, se houver pv, você pode fazer:

cmd='
   that command | to execute &&
     as shell code'

yes | pv -qL10 | xargs -n1 -P20 sh -c "$cmd" sh

O -P20é executar no máximo 20 $cmdao mesmo tempo.

-L10 limita a taxa a 10 bytes por segundo, portanto, 5 linhas por segundo.

Se seus $cmds se tornarem lentos e fizer com que o limite de 20 seja atingido, a xargsleitura será interrompida até que $cmdpelo menos uma instância retorne. pvainda continuará gravando no pipe na mesma taxa, até que o pipe fique cheio (o que no Linux com um tamanho de pipe padrão de 64KiB levará quase 2 horas).

Nesse ponto, pvparará de escrever. Mas mesmo assim, quando xargsretomar a leitura, pvtentará recuperar e enviar todas as linhas que deveria ter enviado mais cedo o mais rápido possível, para manter uma média geral de 5 linhas por segundo.

O que isso significa é que, contanto que seja possível, com 20 processos, atender a 5 rodadas por segundo no requisito médio, isso será feito. No entanto, quando o limite é atingido, a taxa na qual novos processos são iniciados não será conduzida pelo timer do pv, mas pela taxa na qual as instâncias anteriores do cmd retornam. Por exemplo, se 20 estão em execução no momento e duram 10 segundos, e 10 deles decidem terminar todos ao mesmo tempo, 10 novos serão iniciados ao mesmo tempo.

Exemplo:

$ cmd='date +%T.%N; exec sleep 2'
$ yes | pv -qL10 | xargs -n1 -P20 sh -c "$cmd" sh
09:49:23.347013486
09:49:23.527446830
09:49:23.707591664
09:49:23.888182485
09:49:24.068257018
09:49:24.338570865
09:49:24.518963491
09:49:24.699206647
09:49:24.879722328
09:49:25.149988152
09:49:25.330095169

Em média, será 5 vezes por segundo, mesmo que o atraso entre duas execuções nem sempre seja exatamente de 0,2 segundos.

Com ksh93(ou zshse o seu sleepcomando suportar segundos fracionários):

typeset -F SECONDS=0
n=0; while true; do
  your-command &
  sleep "$((++n * 0.2 - SECONDS))"
done

Isso não vincula o número de your-commands concorrentes .

Stéphane Chazelas
fonte
Após um pouco de teste, o pvcomando parece ser exatamente o que eu estava procurando, não poderia esperar melhor! Apenas nesta linha yes | pv -qL10 | xargs -n1 -P20 sh -c "$cmd" sh:, não é o último shredundante?
Benjamin
1
@ Benjamin Esse segundo shé para o $0seu $cmdscript. Também é usado em mensagens de erro pelo shell. Sem ele, $0seria yde yes, assim que você receber mensagens de erro como y: cannot execute cmd... Você também pode fazeryes sh | pv -qL15 | xargs -n1 -P20 sh -c "$cmd"
Stéphane Chazelas
Estou lutando para decompor tudo em pedaços compreensíveis, TBH! No seu exemplo, você removeu esse último sh; e nos meus testes, quando o removo, não vejo diferença!
Benjamin
@Benjamin. Não é crítico. Ele só mudará se você $cmdusar $0(por que usaria?) E para mensagens de erro. Tente, por exemplo, com cmd=/; sem o segundo sh, você veria algo como, em y: 1: y: /: Permission deniedvez desh: 1: sh: /: Permission denied
Stéphane Chazelas
Estou tendo um problema com sua solução: ela funciona bem por algumas horas e, em algum momento, sai, sem nenhum erro. Isso pode estar relacionado ao cano ficar cheio, com alguns efeitos colaterais inesperados?
1075 Benjamin Benjamin
4

De maneira simplista, se o seu comando durar menos de 1 segundo, você poderá iniciar 5 comandos por segundo. Obviamente, isso é muito explosivo.

while sleep 1
do    for i in {1..5}
      do mycmd &
      done
done

Se o seu comando demorar mais de 1 segundo e você desejar espalhar os comandos, tente

while :
do    for i in {0..4}
      do  sleep .$((i*2))
          mycmd &
      done
      sleep 1 &
      wait
done

Como alternativa, você pode ter 5 loops separados que são executados de forma independente, com um mínimo de 1 segundo.

for i in {1..5}
do    while :
      do   sleep 1 &
           mycmd &
           wait
      done &
      sleep .2
done
meuh
fonte
Solução bastante agradável também. Eu gosto do fato de ser simples e de exatamente 5 vezes por segundo, mas tem a desvantagem de iniciar 5 comandos ao mesmo tempo (em vez de cada 200ms) e talvez não tenha a garantia de ter no máximo n threads em execução por vez !
Benjamin
@ Benjamin eu adicionei um sono de 200ms no loop da segunda versão. Esta segunda versão não pode ter mais de 5 cmds em execução ao mesmo tempo, pois somente no início 5, aguardamos todos eles.
meuh
O problema é que você não pode ter mais de 5 por segundo iniciado; se todos os scripts demorarem mais de 1s para serem executados, você estará longe de atingir o limite da API. Além disso, se você esperar por todos eles, um único script de bloqueio bloqueará todos os outros?
Benjamin
@ Benjamin Para que você possa executar 5 loops independentes, cada um com um sono mínimo de 1 segundo, consulte a terceira versão.
meuh
2

Com um programa C,

Você pode, por exemplo, usar um fio que dorme por 0,2 segundos em um tempo

#include<stdio.h>
#include<string.h>
#include<pthread.h>
#include<stdlib.h>
#include<unistd.h>

pthread_t tid;

void* doSomeThing() {
    While(1){
         //execute my command
         sleep(0.2)
     } 
}

int main(void)
{
    int i = 0;
    int err;


    err = pthread_create(&(tid), NULL, &doSomeThing, NULL);
    if (err != 0)
        printf("\ncan't create thread :[%s]", strerror(err));
    else
        printf("\n Thread created successfully\n");



    return 0;
}

use-o para saber como criar um tópico: crie um tópico (este é o link que usei para colar este código)

Couim
fonte
Obrigado pela sua resposta, embora eu estivesse idealmente procurando por algo que não envolvesse programação C, mas apenas usando as ferramentas Unix existentes!
Benjamin
Sim, a resposta stackoverflow a este poder, por exemplo, a utilização de um token bucket compartilhado entre vários segmentos de trabalho, mas perguntando sobre Unix.SE sugere mais um "usuário avançado" ao invés de abordagem de "programador" é procurado :-) Ainda assim, ccé uma ferramenta Unix existente, e isso não é muito código!
Steve Jessop
1

Usando o node.js, é possível iniciar um único encadeamento que executa o script bash a cada 200 milissegundos, não importa quanto tempo a resposta leve para voltar, porque a resposta vem de uma função de retorno de chamada .

var util = require('util')
exec = require('child_process').exec

setInterval(function(){
        child  = exec('fullpath to bash script',
                function (error, stdout, stderr) {
                console.log('stdout: ' + stdout);
                console.log('stderr: ' + stderr);
                if (error !== null) {
                        console.log('exec error: ' + error);
                }
        });
},200);

Esse javascript é executado a cada 200 milissegundos e a resposta é obtida através da função de retorno de chamada function (error, stdout, stderr).

Dessa forma, você pode controlar que ele nunca exceda as 5 chamadas por segundo, independentemente de quão lenta ou rápida é a execução do comando ou de quanto ele precisa esperar por uma resposta.

jcbermu
fonte
Eu gosto dessa solução: ele inicia exatamente 5 comandos por segundo, em intervalos regulares. A única desvantagem que vejo é a falta de uma garantia de ter no máximo n processos em execução por vez! Se isso é algo que você pode incluir facilmente? Não estou familiarizado com o node.js.
Benjamin
0

Eu uso a pvsolução baseada em Stéphane Chazelas há algum tempo, mas descobri que ela saiu aleatoriamente (e silenciosamente) após algum tempo, de alguns minutos a algumas horas. - Editar: o motivo foi que meu script PHP ocasionalmente morria devido a um tempo máximo de execução excedido, saindo com o status 255.

Então, decidi escrever uma ferramenta simples de linha de comando que faz exatamente o que eu preciso.

Atingir meu objetivo original é tão simples quanto:

./parallel.phar 5 20 ./my-command-line-script

Inicia quase exatamente 5 comandos por segundo, a menos que já existam 20 processos simultâneos; nesse caso, pula as próximas execuções até que um slot fique disponível.

Esta ferramenta não é sensível a uma saída de status 255.

Benjamin
fonte