Jak uruchomić polecenie średnio 5 razy na sekundę?

21

Mam skrypt wiersza polecenia, który wykonuje wywołanie interfejsu API i aktualizuje bazę danych o wyniki.

Mam limit 5 wywołań API na sekundę u dostawcy API. Wykonanie skryptu trwa dłużej niż 0,2 sekundy.

  • Jeśli uruchomię polecenie sekwencyjnie, nie będzie ono działać wystarczająco szybko i będę wykonywać tylko 1 lub 2 wywołania API na sekundę.
  • Jeśli uruchomię polecenie sekwencyjnie, ale jednocześnie z kilku terminali, mogę przekroczyć limit 5 połączeń / sekundę.

Czy istnieje sposób na uporządkowanie wątków, aby mój skrypt wiersza polecenia był wykonywany prawie dokładnie 5 razy na sekundę?

Na przykład coś, co działałoby z 5 lub 10 wątkami i żaden wątek nie wykonałby skryptu, jeśli poprzedni wątek wykonał go mniej niż 200 ms temu.

Benzoes
źródło
Wszystkie odpowiedzi zależą od założenia, że ​​skrypt zakończy się w kolejności, w jakiej się nazywa. Czy jest możliwe do zaakceptowania w przypadku użycia, jeśli kończą się one w porządku?
Cody Gustafson
@CodyGustafson Jest całkowicie do przyjęcia, jeśli kończą się w porządku. Przynajmniej nie wierzę, że istnieje takie założenie w przyjętej odpowiedzi?
Benjamin
Co się stanie, jeśli przekroczysz liczbę połączeń na sekundę? Jeśli dostawca interfejsu API dławi się, nie potrzebujesz żadnego mechanizmu na swoim końcu ... prawda?
Floris
@Floris Zwrócą komunikat o błędzie, który zostanie przetłumaczony jako wyjątek w zestawie SDK. Po pierwsze wątpię, aby dostawca interfejsu API był zadowolony, jeśli wygeneruję 50 komunikatów przepustnicy na sekundę (powinieneś odpowiednio reagować na takie komunikaty), a po drugie używam API do innych celów w tym samym czasie, więc nie chcę osiągać limitu, który jest nieco wyższy.
Benjamin

Odpowiedzi:

25

W systemie GNU, a jeśli tak pv, możesz:

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

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

Należy -P20wykonać najwyżej 20 $cmdjednocześnie.

-L10 ogranicza szybkość do 10 bajtów na sekundę, więc 5 linii na sekundę.

Jeśli twoje $cmds stają się dwa wolne i powodują osiągnięcie limitu 20, wtedy xargsprzestaną czytać, aż $cmdprzynajmniej jedna instancja powróci. pvnadal będzie zapisywać do potoku z tą samą szybkością, aż potok się zapełni (co w systemie Linux z domyślnym rozmiarem potoku 64KiB zajmie prawie 2 godziny).

W tym momencie pvprzestanie pisać. Ale nawet wtedy, gdy xargswznowi czytanie, pvspróbuje nadrobić zaległości i wyśle ​​wszystkie wiersze, które powinien był wysłać wcześniej, tak szybko, jak to możliwe, aby utrzymać ogólną średnią 5 linii na sekundę.

Oznacza to, że tak długo, jak to możliwe, przy 20 procesach, aby spełnić te wymagania średnio 5 uruchomień na sekundę, będzie to robić. Jednak gdy limit zostanie osiągnięty, szybkość, z jaką uruchamiane są nowe procesy, nie będzie sterowana przez timer pv, ale przez szybkość, z jaką wracają wcześniejsze instancje cmd. Na przykład, jeśli 20 jest aktualnie uruchomionych i działało przez 10 sekund, a 10 z nich decyduje się zakończyć wszystkie w tym samym czasie, wtedy 10 nowych zostanie uruchomionych jednocześnie.

Przykład:

$ 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

Średnio będzie to 5 razy na sekundę, nawet jeśli opóźnienie między dwoma cyklami nie zawsze będzie wynosić dokładnie 0,2 sekundy.

Za pomocą ksh93(lub za pomocą, zshjeśli twoje sleeppolecenie obsługuje ułamkowe sekundy):

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

Nie ogranicza to jednak liczby współbieżnych your-command.

Stéphane Chazelas
źródło
Po kilku testach pvpolecenie wydaje się być dokładnie tym, czego szukałem, nie mogłem mieć lepszej nadziei! Tylko na tej linii: yes | pv -qL10 | xargs -n1 -P20 sh -c "$cmd" shczy nie jest to ostatnia shzbędna?
Benjamin
1
@Benjamin Druga chwila shdotyczy $0twojego $cmdskryptu. Jest również używany w komunikatach o błędach przez powłokę. Bez tego $0byłby yz yes, więc y: cannot execute cmdyes sh | pv -qL15 | xargs -n1 -P20 sh -c "$cmd"
pojawiałyby
Usiłuję rozłożyć całość na zrozumiałe części, TBH! W twoim przykładzie usunąłeś to ostatnie sh; a w moich testach, kiedy go usuwam, nie widzę żadnej różnicy!
Benjamin
@Benzoes. To nie jest krytyczne. Zmieni się tylko wtedy, $cmdgdy użyjesz $0(dlaczego miałbyś to zrobić?) I wyświetli komunikat o błędzie. Spróbuj na przykład z cmd=/; bez drugiego shzobaczyłbyś coś takiego y: 1: y: /: Permission deniedzamiastsh: 1: sh: /: Permission denied
Stéphane Chazelas
Mam problem z twoim rozwiązaniem: działa dobrze przez kilka godzin, a następnie w pewnym momencie po prostu wychodzi, bez żadnego błędu. Czy może to być związane z zapełnianiem się rury, powodując nieoczekiwane skutki uboczne?
Benjamin,
4

Upraszczając, jeśli twoje polecenie trwa krócej niż 1 sekundę, możesz po prostu uruchomić 5 poleceń na sekundę. Oczywiście jest to bardzo brutalne.

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

Jeśli twoje polecenie może potrwać dłużej niż 1 sekundę i chcesz rozłożyć polecenia, możesz spróbować

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

Alternatywnie możesz mieć 5 oddzielnych pętli, które działają niezależnie, z minimum 1 sekundą.

for i in {1..5}
do    while :
      do   sleep 1 &
           mycmd &
           wait
      done &
      sleep .2
done
meuh
źródło
Całkiem niezłe rozwiązanie. Podoba mi się to, że jest prosty i dokładnie 5 razy na sekundę, ale ma tę wadę, że uruchamia 5 poleceń jednocześnie (zamiast co 200 ms) i może nie ma zabezpieczenia, że ​​może działać co najwyżej n wątków jednocześnie !
Benjamin
@ Benjamin Dodałem 200 ms snu w pętli drugiej wersji. W tej drugiej wersji nie może być uruchomionych więcej niż 5 cmd naraz, ponieważ zaczynamy tylko 5, a następnie czekamy na wszystkie.
Meuh
Problem polega na tym, że nie można uruchomić więcej niż 5 na sekundę; jeśli wykonanie wszystkich skryptów nagle zajmie więcej niż 1 sekundę, to daleko ci do osiągnięcia limitu API. Ponadto, jeśli poczekasz na nich wszystkich, pojedynczy skrypt blokujący zablokuje wszystkie pozostałe?
Benjamin
@Benjamin Więc możesz uruchomić 5 niezależnych pętli, każda z minimalnym czasem snu 1 sekundy, patrz 3. wersja.
Meuh
2

Dzięki programowi C

Możesz na przykład użyć wątku, który śpi przez 0,2 sekundy

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

użyj go, aby wiedzieć, jak utworzyć wątek: utwórz wątek (to jest link, którego użyłem do wklejenia tego kodu)

Couim
źródło
Dziękuję za odpowiedź, chociaż idealnie szukałem czegoś, co nie wymagałoby programowania w języku C, a jedynie używania istniejących narzędzi uniksowych!
Benjamin
Tak, odpowiedź stackoverflow na to może być na przykład korzystać z tokenami współużytkowane przez wiele wątków roboczych, ale z prośbą o Unix.SE sugeruje bardziej „power user”, a nie podejście „programista” jest poszukiwany :-) Mimo to, ccjest istniejące narzędzie uniksowe, a to nie jest dużo kodu!
Steve Jessop
1

Za pomocą node.js możesz uruchomić pojedynczy wątek, który wykonuje skrypt bash co 200 milisekund, bez względu na to, jak długo odpowiedź wraca, ponieważ odpowiedź pochodzi z funkcji wywołania zwrotnego .

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

Ten skrypt javascript jest uruchamiany co 200 milisekund, a odpowiedź jest uzyskiwana przez funkcję wywołania zwrotnego function (error, stdout, stderr).

W ten sposób możesz kontrolować, że nigdy nie przekracza 5 wywołań na sekundę niezależnie od tego, jak powolne lub szybkie jest wykonanie polecenia lub ile musi czekać na odpowiedź.

jcbermu
źródło
Podoba mi się to rozwiązanie: uruchamia dokładnie 5 poleceń na sekundę, w regularnych odstępach czasu. Jedyną wadą, jaką widzę, jest to, że nie ma zabezpieczenia, że ​​może działać jednocześnie co najwyżej n procesów! Jeśli możesz to łatwo dołączyć? Nie jestem zaznajomiony z node.js.
Benjamin
0

Od pvjakiegoś czasu korzystam z rozwiązania opartego na Stéphane Chazelas , ale odkryłem, że po jakimś czasie, gdziekolwiek od kilku minut do kilku godzin, wyszedł losowo (i cicho). - Edycja: Powodem było to, że mój skrypt PHP czasami umarł z powodu przekroczenia maksymalnego czasu wykonania, wychodząc ze statusem 255.

Postanowiłem więc napisać proste narzędzie wiersza polecenia, które robi dokładnie to, czego potrzebuję.

Osiągnięcie mojego pierwotnego celu jest tak proste, jak:

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

Uruchamia prawie dokładnie 5 poleceń na sekundę, chyba że jest już 20 współbieżnych procesów, w którym to przypadku pomija kolejne wykonanie, dopóki nie zostanie udostępnione miejsce.

To narzędzie nie jest wrażliwe na wyjście o statusie 255.

Benzoes
źródło