Comment exécuter une commande à une moyenne de 5 fois par seconde?

21

J'ai un script de ligne de commande qui effectue un appel d'API et met à jour une base de données avec les résultats.

J'ai une limite de 5 appels d'API par seconde avec le fournisseur d'API. L'exécution du script prend plus de 0,2 seconde.

  • Si j'exécute la commande séquentiellement, elle ne s'exécutera pas assez rapidement et je ne ferai que 1 ou 2 appels d'API par seconde.
  • Si j'exécute la commande séquentiellement, mais simultanément à partir de plusieurs terminaux, je peux dépasser la limite de 5 appels / seconde.

S'il existe un moyen d'orchestrer les threads afin que mon script de ligne de commande soit exécuté presque exactement 5 fois par seconde?

Par exemple, quelque chose qui s'exécuterait avec 5 ou 10 threads, et aucun thread n'exécuterait le script si un thread précédent l'avait exécuté il y a moins de 200 ms.

Benjoin
la source
Toutes les réponses dépendent de l'hypothèse que votre script se terminera dans l'ordre dans lequel il est appelé. Est-il acceptable pour votre cas d'utilisation s'ils finissent en panne?
Cody Gustafson
@CodyGustafson Il est parfaitement acceptable qu'ils finissent en panne. Je ne crois pas qu'il y ait une telle hypothèse dans la réponse acceptée, au moins?
Benjamin
Que se passe-t-il si vous dépassez le nombre d'appels par seconde? Si le fournisseur d'API s'étrangle, vous n'avez besoin d'aucun mécanisme de votre côté ... n'est-ce pas?
Floris
@Floris Ils renverront un message d'erreur qui se traduira par une exception dans le SDK. Tout d'abord, je doute que le fournisseur d'API soit satisfait si je génère 50 messages d'accélération par seconde (vous êtes censé agir en fonction de ces messages en conséquence), et d'autre part j'utilise l'API à d'autres fins en même temps, donc je ne veulent pas atteindre la limite qui est en fait légèrement supérieure.
Benjamin

Réponses:

25

Sur un système GNU et si vous l'avez fait pv, vous pouvez faire:

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

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

Le -P20est d'exécuter au plus 20 $cmden même temps.

-L10 limite le débit à 10 octets par seconde, donc 5 lignes par seconde.

Si votre $cmds devient lent et fait atteindre la limite de 20, alors la xargslecture s'arrêtera jusqu'à ce qu'une $cmdinstance au moins revienne. pvcontinuera à écrire sur le canal au même rythme, jusqu'à ce que le canal soit plein (ce qui sous Linux avec une taille de canal par défaut de 64 Ko prendra près de 2 heures).

À ce stade, pvarrêtera d'écrire. Mais même alors, lorsque la xargslecture reprendra, pvil tentera de rattraper son retard et d'envoyer toutes les lignes qu'il aurait dû envoyer le plus rapidement possible afin de maintenir une moyenne globale de 5 lignes par seconde.

Ce que cela signifie, c'est que tant qu'il est possible avec 20 processus de répondre à 5 besoins par seconde en moyenne, il le fera. Cependant, lorsque la limite est atteinte, la vitesse à laquelle les nouveaux processus sont démarrés ne sera pas pilotée par le temporisateur de pv mais par la vitesse à laquelle les instances cmd précédentes reviennent. Par exemple, si 20 sont en cours d'exécution et durent 10 secondes, et que 10 d'entre eux décident de tout terminer en même temps, 10 nouveaux seront démarrés en même temps.

Exemple:

$ 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 moyenne, il sera de 5 fois par seconde même si le délai entre deux runs ne sera pas toujours exactement de 0,2 seconde.

Avec ksh93(ou avec zshsi votre sleepcommande prend en charge les fractions de seconde):

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

Cela ne met cependant aucune limite sur le nombre de your-commands simultanés .

Stéphane Chazelas
la source
Après un peu de test, la pvcommande semble être exactement ce que je cherchais, je ne pouvais pas espérer mieux! Juste sur cette ligne:, yes | pv -qL10 | xargs -n1 -P20 sh -c "$cmd" shn'est-ce pas le dernier shredondant?
Benjamin
1
@Benjamin Cette seconde shest pour le $0dans votre $cmdscript. Il est également utilisé dans les messages d'erreur par le shell. Sans elle, $0serait yde yes, donc vous auriez des messages d'erreur comme y: cannot execute cmd... Vous pourriez aussi faireyes sh | pv -qL15 | xargs -n1 -P20 sh -c "$cmd"
Stéphane Chazelas
J'ai du mal à décomposer le tout en morceaux compréhensibles, TBH! Dans votre exemple, vous avez supprimé ce dernier sh; et dans mes tests, quand je l'enlève, je ne vois aucune différence!
Benjamin
@Benjamin. Ce n'est pas critique. Cela ne changera que si vous l' $cmdutilisez $0(pourquoi le ferait-il?) Et pour les messages d'erreur. Essayez par exemple avec cmd=/; sans le second sh, vous verriez quelque chose comme y: 1: y: /: Permission deniedau lieu desh: 1: sh: /: Permission denied
Stéphane Chazelas
J'ai un problème avec votre solution: cela fonctionne bien pendant quelques heures, puis à un moment donné, il se termine, sans aucune erreur. Cela pourrait-il être lié au fait que le tuyau est plein, ce qui a des effets secondaires inattendus?
Benjamin
4

Simplement, si votre commande dure moins de 1 seconde, vous pouvez simplement démarrer 5 commandes par seconde. De toute évidence, c'est très éclatant.

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

Si votre commande peut prendre plus d'une seconde et que vous souhaitez étaler les commandes, vous pouvez essayer

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

Alternativement, vous pouvez avoir 5 boucles distinctes qui s'exécutent indépendamment, avec un minimum de 1 seconde.

for i in {1..5}
do    while :
      do   sleep 1 &
           mycmd &
           wait
      done &
      sleep .2
done
meuh
la source
Une très bonne solution également. J'aime le fait qu'il soit simple et soit exactement 5 fois par seconde, mais il a l'inconvénient de démarrer 5 commandes en même temps (au lieu de toutes les 200 ms), et manque peut-être de la garantie d'avoir au plus n threads en cours d'exécution à la fois !
Benjamin
@Benjamin J'ai ajouté un sommeil de 200 ms dans la boucle de la deuxième version. Cette deuxième version ne peut pas avoir plus de 5 cmds en cours d'exécution car nous ne démarrons qu'à 5, puis les attendons tous.
meuh
Le problème est que vous ne pouvez pas démarrer plus de 5 par seconde; si tous les scripts prennent soudainement plus de 1 seconde à exécuter, alors vous êtes loin d'atteindre la limite de l'API. De plus, si vous les attendez tous, un seul script de blocage bloquerait tous les autres?
Benjamin
@Benjamin Vous pouvez donc exécuter 5 boucles indépendantes, chacune avec un sommeil minimum de 1 seconde, voir 3ème version.
meuh
2

Avec un programme C,

Vous pouvez par exemple utiliser un thread qui dort pendant 0,2 seconde dans un certain temps

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

l'utiliser pour savoir comment créer un fil: créer un fil (c'est le lien que j'ai utilisé pour coller ce code)

Couim
la source
Merci pour votre réponse, même si je recherchais idéalement quelque chose qui n'impliquerait pas la programmation C, mais uniquement en utilisant les outils Unix existants!
Benjamin
Oui, la réponse stackoverflow à cela pourrait par exemple être d'utiliser un compartiment de jetons partagé entre plusieurs threads de travail, mais demander sur Unix.SE suggère plus d'une approche "utilisateur avancé" plutôt que "programmeur" :-) Pourtant, ccest un outil Unix existant, et ce n'est pas beaucoup de code!
Steve Jessop
1

À l'aide de node.js, vous pouvez démarrer un seul thread qui exécute le script bash toutes les 200 millisecondes quel que soit le temps nécessaire à la réponse pour revenir car la réponse passe par une fonction de rappel .

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

Ce javascript s'exécute toutes les 200 millisecondes et la réponse est obtenue via la fonction de rappel function (error, stdout, stderr).

De cette façon, vous pouvez contrôler qu'il ne dépasse jamais les 5 appels par seconde, indépendamment de la lenteur ou de la rapidité de l'exécution de la commande ou de la durée d'attente d'une réponse.

jcbermu
la source
J'aime cette solution: elle démarre exactement 5 commandes par seconde, à intervalles réguliers. Le seul inconvénient que je peux voir, c'est qu'il manque une garantie d'avoir au plus n processus en cours d'exécution à la fois! Si c'est quelque chose que vous pourriez facilement inclure? Je ne connais pas node.js.
Benjamin
0

J'ai utilisé la pvsolution basée sur Stéphane Chazelas pendant un certain temps, mais j'ai découvert qu'elle s'est arrêtée de manière aléatoire (et silencieuse) après un certain temps, de quelques minutes à quelques heures. - Edit: La raison en est que mon script PHP est parfois mort à cause d'un temps d'exécution maximum dépassé, se terminant avec le statut 255.

J'ai donc décidé d'écrire un outil de ligne de commande simple qui fait exactement ce dont j'ai besoin.

Atteindre mon objectif initial est aussi simple que:

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

Il démarre presque exactement 5 commandes par seconde, sauf s'il existe déjà 20 processus simultanés, auquel cas il ignore la ou les prochaines exécutions jusqu'à ce qu'un emplacement devienne disponible.

Cet outil n'est pas sensible à une sortie d'état 255.

Benjoin
la source