¿Cómo ejecutar un comando a un promedio de 5 veces por segundo?

21

Tengo un script de línea de comandos que realiza una llamada a la API y actualiza una base de datos con los resultados.

Tengo un límite de 5 llamadas API por segundo con el proveedor de API. El script tarda más de 0.2 segundos en ejecutarse.

  • Si ejecuto el comando secuencialmente, no se ejecutará lo suficientemente rápido y solo haré 1 o 2 llamadas API por segundo.
  • Si ejecuto el comando secuencialmente, pero simultáneamente desde varias terminales, podría exceder el límite de 5 llamadas / segundo.

Si hay una manera de orquestar hilos para que mi script de línea de comandos se ejecute casi exactamente 5 veces por segundo?

Por ejemplo, algo que se ejecutaría con 5 o 10 subprocesos, y ningún subproceso ejecutaría el script si un subproceso anterior lo ejecutó hace menos de 200 ms.

Benjamín
fuente
Todas las respuestas dependen de la suposición de que su secuencia de comandos finalizará en el orden en que se llama. ¿Es aceptable para su caso de uso si terminan fuera de servicio?
Cody Gustafson
@CodyGustafson Es perfectamente aceptable si terminan fuera de servicio. No creo que exista tal suposición en la respuesta aceptada, al menos.
Benjamin
¿Qué sucede si excedes la cantidad de llamadas por segundo? Si el proveedor de API acelera, no necesita ningún mecanismo en su extremo ... ¿verdad?
Floris
@Floris Devolverán un mensaje de error que se traducirá en una excepción en el SDK. En primer lugar, dudo que el proveedor de API esté contento si genero 50 mensajes de aceleración por segundo (se supone que debe actuar sobre tales mensajes en consecuencia), y en segundo lugar estoy usando la API para otros fines al mismo tiempo, así que no quiero alcanzar el límite que en realidad es un poco más alto.
Benjamin

Respuestas:

25

En un sistema GNU y si lo tiene pv, podría hacer:

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

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

El -P20es ejecutar como máximo 20 $cmdal mismo tiempo.

-L10 limita la velocidad a 10 bytes por segundo, por lo que 5 líneas por segundo.

Si sus $cmds se vuelven dos lentos y hace que se alcance el límite de 20, xargsdejará de leer hasta que $cmdal menos una instancia regrese. pvseguirá escribiendo en la tubería a la misma velocidad, hasta que la tubería se llene (lo que en Linux con un tamaño de tubería predeterminado de 64 KB tomará casi 2 horas).

En ese punto, pvdejará de escribir. Pero incluso entonces, cuando xargsreanude la lectura, pvintentará ponerse al día y enviar todas las líneas que debería haber enviado antes lo antes posible para mantener un promedio general de 5 líneas por segundo.

Lo que eso significa es que, siempre que sea posible con 20 procesos para cumplir con ese requisito promedio de 5 ejecuciones por segundo, lo hará. Sin embargo, cuando se alcanza el límite, la velocidad a la que se inician los nuevos procesos no será controlada por el temporizador de pv, sino por la velocidad a la que regresan las instancias de cmd anteriores. Por ejemplo, si 20 se están ejecutando actualmente y lo han estado durante 10 segundos, y 10 de ellos deciden terminar todo al mismo tiempo, se iniciarán 10 nuevos a la vez.

Ejemplo:

$ 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

En promedio, será 5 veces por segundo, incluso si el retraso entre dos ejecuciones no siempre será exactamente 0.2 segundos.

Con ksh93(o con zshsi su sleepcomando admite segundos fraccionarios):

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

Sin your-commandembargo, eso no limita el número de correos electrónicos concurrentes .

Stéphane Chazelas
fuente
Después de un poco de prueba, el pvcomando parece ser exactamente lo que estaba buscando, ¡no podría esperar mejor! Solo en esta línea: yes | pv -qL10 | xargs -n1 -P20 sh -c "$cmd" sh¿no es el último shredundante?
Benjamin
1
@Benjamin Ese segundo shes para el $0en su $cmdscript. También es utilizado en mensajes de error por el shell. Sin él, $0sería yde yes, por lo que obtendría mensajes de error como y: cannot execute cmd... También podría hacerloyes sh | pv -qL15 | xargs -n1 -P20 sh -c "$cmd"
Stéphane Chazelas
¡Estoy luchando por descomponer todo en piezas entendibles, TBH! En su ejemplo, ha eliminado esto último sh; y en mis pruebas, cuando lo elimino, ¡no veo diferencia!
Benjamin
@Benjamín. No es critico. Solo hará una diferencia si la $cmdusa $0(¿por qué lo haría?) Y para mensajes de error. Prueba por ejemplo con cmd=/; sin el segundo sh, y: 1: y: /: Permission deniedsh: 1: sh: /: Permission denied
verías
Tengo un problema con su solución: funciona bien durante unas horas, luego, en algún momento, simplemente sale, sin ningún error. ¿Podría esto estar relacionado con que la tubería se llene y tenga algunos efectos secundarios inesperados?
Benjamin
4

Simplísticamente, si su comando dura menos de 1 segundo, puede iniciar 5 comandos por segundo. Obviamente, esto es muy explosivo.

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

Si su comando puede tomar más de 1 segundo y desea extender los comandos, puede probar

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

Alternativamente, puede tener 5 bucles separados que se ejecutan independientemente, con un mínimo de 1 segundo.

for i in {1..5}
do    while :
      do   sleep 1 &
           mycmd &
           wait
      done &
      sleep .2
done
meuh
fuente
Muy buena solución también. Me gusta el hecho de que es simple y es exactamente 5 veces por segundo, pero tiene la desventaja de iniciar 5 comandos al mismo tiempo (en lugar de cada 200 ms), y tal vez carece de la protección de tener al menos n hilos ejecutándose a la vez !
Benjamin
@Benjamin agregué un sueño de 200 ms en el ciclo de la segunda versión. Esta segunda versión no puede tener más de 5 cmds ejecutándose a la vez, ya que solo comenzamos cada 5, luego los esperamos a todos.
meuh
El problema es que no puede iniciar más de 5 por segundo; Si todos los scripts de repente tardan más de 1s en ejecutarse, entonces está lejos de alcanzar el límite API. Además, si los espera a todos, ¿un solo script de bloqueo bloquearía a todos los demás?
Benjamin
@Benjamin Para que pueda ejecutar 5 bucles independientes, cada uno con un sueño mínimo de 1 segundo, consulte la tercera versión.
meuh
2

Con un programa en C,

Por ejemplo, puede usar un hilo que duerme durante 0.2 segundos en un momento

#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;
}

úselo para saber cómo crear un hilo: cree un hilo (este es el enlace que he usado para pegar este código)

Couim
fuente
Gracias por su respuesta, aunque idealmente estaba buscando algo que no involucrara la programación en C, ¡sino que solo usara las herramientas Unix existentes!
Benjamin
Sí, la respuesta de stackoverflow a esto podría ser, por ejemplo, usar un cubo de tokens compartido entre múltiples subprocesos de trabajo, pero preguntar en Unix.SE sugiere que se necesita más un enfoque de "Usuario avanzado" en lugar de "programador" :-) Aún así, cces una herramienta existente de Unix, ¡y esto no es mucho código!
Steve Jessop
1

Usando node.js puede iniciar un solo subproceso que ejecuta el script bash cada 200 milisegundos, sin importar cuánto tiempo demore la respuesta en regresar porque la respuesta se realiza a través de una función de devolución de llamada .

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);

Este javascript se ejecuta cada 200 milisegundos y la respuesta se obtiene a través de la función de devolución de llamada function (error, stdout, stderr).

De esta manera, puede controlar que nunca supere las 5 llamadas por segundo, independientemente de cuán lenta o rápida sea la ejecución del comando o cuánto tenga que esperar una respuesta.

jcbermu
fuente
Me gusta esta solución: comienza exactamente 5 comandos por segundo, a intervalos regulares. El único inconveniente que puedo ver es que carece de la protección de tener como máximo n procesos ejecutándose a la vez. Si esto es algo que podrías incluir fácilmente? No estoy familiarizado con node.js.
Benjamin
0

He usado la pvsolución basada en Stéphane Chazelas durante algún tiempo, pero descubrí que salió al azar (y en silencio) después de algún tiempo, desde unos pocos minutos hasta unas pocas horas. - Editar: La razón fue que mi script PHP ocasionalmente murió debido a un tiempo de ejecución máximo excedido, saliendo con el estado 255.

Así que decidí escribir una herramienta simple de línea de comandos que haga exactamente lo que necesito.

Lograr mi objetivo original es tan simple como:

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

Inicia casi exactamente 5 comandos por segundo, a menos que ya haya 20 procesos concurrentes, en cuyo caso omite las siguientes ejecuciones hasta que haya un espacio disponible.

Esta herramienta no es sensible a una salida de estado 255.

Benjamín
fuente