Come eseguire un comando in media 5 volte al secondo?


21

Ho uno script da riga di comando che esegue una chiamata API e aggiorna un database con i risultati.

Ho un limite di 5 chiamate API al secondo con il provider API. L'esecuzione dello script richiede più di 0,2 secondi.

  • Se eseguo il comando in sequenza, non funzionerà abbastanza velocemente e effettuerò solo 1 o 2 chiamate API al secondo.
  • Se eseguo il comando in sequenza, ma contemporaneamente da più terminali, potrei superare il limite di 5 chiamate / secondo.

Se esiste un modo per orchestrare i thread in modo che il mio script da riga di comando venga eseguito quasi esattamente 5 volte al secondo?

Ad esempio qualcosa che verrebbe eseguito con 5 o 10 thread e nessun thread eseguirà lo script se un thread precedente lo ha eseguito meno di 200ms fa.


Tutte le risposte dipendono dal presupposto che lo script finirà nell'ordine in cui viene chiamato. È accettabile per il tuo caso d'uso se si esauriscono?
Cody Gustafson,

@CodyGustafson È perfettamente accettabile se finiscono fuori servizio. Non credo che ci sia un tale presupposto nella risposta accettata, almeno?
Benjamin,

Cosa succede se si supera il numero di chiamate al secondo? Se il provider API accelera, non hai bisogno di alcun meccanismo alla tua fine ... vero?
Floris,

@Floris Restituiranno un messaggio di errore che si tradurrà in un'eccezione nell'SDK. Prima di tutto dubito che il fornitore dell'API sarà felice se genererò 50 messaggi al secondo (dovresti agire di conseguenza su tali messaggi), e in secondo luogo sto usando l'API per altri scopi allo stesso tempo, quindi non voglio raggiungere il limite che è in realtà leggermente più alto.
Benjamin,

Risposte:


25

Su un sistema GNU e se hai pv, potresti fare:

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

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

L' -P20esecuzione è al massimo 20 $cmdcontemporaneamente.

-L10 limita la velocità a 10 byte al secondo, quindi 5 righe al secondo.

Se i tuoi $cmds diventano due lenti e provoca il raggiungimento del limite di 20, allora xargssmetterà di leggere fino $cmda quando almeno un'istanza non ritorna. pvcontinuerà comunque a scrivere sulla pipe alla stessa velocità, fino a quando la pipe non si riempie (che su Linux con una dimensione di pipe predefinita di 64 KiB richiederà quasi 2 ore).

A quel punto, pvsmetterà di scrivere. Ma anche allora, quando xargsriprenderà a leggere, pvproverà a recuperare e inviare tutte le righe che avrebbe dovuto inviare prima il più rapidamente possibile in modo da mantenere una media di 5 righe al secondo nel complesso.

Ciò significa che, finché sarà possibile con 20 processi per soddisfare quel 5 run al secondo in media, lo farà. Tuttavia, quando viene raggiunto il limite, la velocità con cui vengono avviati i nuovi processi non sarà guidata dal timer di pv ma dalla velocità con cui ritornano le precedenti istanze cmd. Ad esempio, se 20 sono attualmente in esecuzione e sono stati per 10 secondi e 10 di loro decidono di terminare tutti contemporaneamente, quindi ne verranno avviati 10 nuovi contemporaneamente.

Esempio:

$ 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

In media, sarà 5 volte al secondo anche se il ritardo tra due corse non sarà sempre esattamente di 0,2 secondi.

Con ksh93(o con zshse il tuo sleepcomando supporta i secondi frazionari):

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

Ciò non pone limiti al numero di concorrenti contemporaneamente your-command.


Dopo un po 'di test, il pvcomando sembra essere esattamente quello che stavo cercando, non potevo sperare di meglio! Solo su questa linea: yes | pv -qL10 | xargs -n1 -P20 sh -c "$cmd" shnon è l'ultimo shridondante?
Benjamin,

1
@Benjamin Quel secondo shè per la $0tua $cmdsceneggiatura. Viene anche utilizzato nei messaggi di errore dalla shell. Senza di essa, $0verrebbe yda yes, quindi riceveresti messaggi di errore come y: cannot execute cmd... Potresti anche fareyes sh | pv -qL15 | xargs -n1 -P20 sh -c "$cmd"
Stéphane Chazelas,

Sto lottando per scomporre il tutto in pezzi comprensibili, TBH! Nel tuo esempio, hai rimosso quest'ultimo sh; e nei miei test, quando lo rimuovo, non vedo alcuna differenza!
Benjamin,

@Benjamin. Non è critico. Sarà diverso solo se $cmdlo usi $0(perché dovrebbe?) E per i messaggi di errore. Prova ad esempio con cmd=/; senza il secondo sh, vedresti qualcosa di simile y: 1: y: /: Permission deniedinvece dish: 1: sh: /: Permission denied
Stéphane Chazelas,

Sto riscontrando un problema con la tua soluzione: funziona bene per alcune ore, poi a un certo punto esce, senza errori. Questo potrebbe essere correlato al fatto che la pipa si riempie, con alcuni effetti collaterali inaspettati?
Benjamin,

4

Semplicisticamente, se il comando dura meno di 1 secondo, è possibile avviare 5 comandi al secondo. Ovviamente, questo è molto pieno.

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

Se il tuo comando potrebbe richiedere più di 1 secondo e desideri distribuire i comandi, puoi provare

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

In alternativa, puoi avere 5 loop separati che funzionano in modo indipendente, con un minimo di 1 secondo.

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

Piuttosto bella soluzione pure. Mi piace il fatto che sia semplice ed è esattamente 5 volte al secondo, ma ha lo svantaggio di avviare 5 comandi contemporaneamente (anziché ogni 200ms), e forse manca la salvaguardia di avere al massimo n thread in esecuzione alla volta !
Benjamin,

@Benjamin Ho aggiunto un sonno di 200 ms nel loop della seconda versione. Questa seconda versione non può avere più di 5 cmd in esecuzione alla volta, poiché iniziamo solo 5, quindi li aspettiamo tutti.
Meuh

Il problema è che non è possibile avviare più di 5 al secondo; se tutti gli script impiegano improvvisamente più di 1 secondo per essere eseguiti, allora sei lontano dal raggiungere il limite API. Inoltre, se li aspetti tutti, un singolo script di blocco bloccherebbe tutti gli altri?
Benjamin,

@Benjamin Quindi puoi eseguire 5 loop indipendenti, ognuno con un sonno minimo di 1 secondo, vedi terza versione.
Meuh

2

Con un programma C,

Ad esempio, puoi usare un thread che dorme per 0,2 secondi in un istante

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

usalo per sapere come creare un thread: crea un thread (questo è il link che ho usato per incollare questo codice)


Grazie per la tua risposta, anche se stavo idealmente cercando qualcosa che non coinvolgesse la programmazione in C, ma usando solo gli strumenti Unix esistenti!
Benjamin,

Sì, la risposta dello stackoverflow a questo potrebbe essere, ad esempio, quella di utilizzare un bucket token condiviso tra più thread di lavoro, ma chiedendo su Unix.SE si suggerisce più un approccio "Power user" piuttosto che "programmatore" :-) Tuttavia, ccè uno strumento Unix esistente, e questo non è molto codice!
Steve Jessop,

1

Utilizzando node.js è possibile avviare un singolo thread che esegue lo script bash ogni 200 millisecondi, indipendentemente da quanto tempo impiega la risposta a tornare perché la risposta arriva attraverso una funzione di callback .

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

Questo javascript viene eseguito ogni 200 millisecondi e la risposta viene ottenuta tramite la funzione di richiamata function (error, stdout, stderr).

In questo modo puoi controllare che non superi mai le 5 chiamate al secondo indipendentemente da quanto è lenta o veloce l'esecuzione del comando o da quanto deve attendere una risposta.


Mi piace questa soluzione: avvia esattamente 5 comandi al secondo, a intervalli regolari. L'unico inconveniente che posso vedere è che manca una protezione di avere al massimo n processi in esecuzione alla volta! Se questo è qualcosa che potresti includere facilmente? Non ho familiarità con node.js.
Benjamin,

0

Ho usato la pvsoluzione basata su Stéphane Chazelas per un po 'di tempo, ma ho scoperto che è uscita casualmente (e silenziosamente) dopo qualche tempo, da qualche minuto a qualche ora. - Modifica: il motivo era che il mio script PHP occasionalmente moriva a causa del superamento di un tempo massimo di esecuzione, uscendo con lo stato 255.

Così ho deciso di scrivere un semplice strumento da riga di comando che fa esattamente quello di cui ho bisogno.

Raggiungere il mio obiettivo originale è semplice come:

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

Inizia quasi esattamente 5 comandi al secondo, a meno che non ci siano già 20 processi simultanei, nel qual caso salta le successive esecuzioni fino a quando non diventa disponibile uno slot.

Questo strumento non è sensibile all'uscita 255 di stato.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.