Как выполнить команду в среднем 5 раз в секунду?

21

У меня есть сценарий командной строки, который выполняет вызов API и обновляет базу данных с результатами.

У меня есть ограничение в 5 вызовов API в секунду с поставщиком API. Выполнение сценария занимает более 0,2 секунды.

  • Если я запускаю команду последовательно, она не будет работать достаточно быстро, и я буду делать только 1 или 2 вызова API в секунду.
  • Если я запускаю команду последовательно, но одновременно с нескольких терминалов, я могу превысить ограничение в 5 вызовов / секунду.

Есть ли способ организовать потоки так, чтобы мой сценарий командной строки выполнялся почти ровно 5 раз в секунду?

Например, что-то, что будет работать с 5 или 10 потоками, и ни один поток не выполнит сценарий, если предыдущий поток выполнил его менее 200 мс назад.

Вениамин
источник
Все ответы зависят от предположения, что ваш скрипт будет завершен в том порядке, в котором он вызывается. Приемлемо ли это для вашего случая использования, если они заканчиваются из строя?
Коди Густафсон
@CodyGustafson Это вполне приемлемо, если они заканчиваются в порядке. Я не верю, что в принятом ответе есть такое предположение, по крайней мере?
Бенджамин
Что произойдет, если вы превысите количество вызовов в секунду? Если провайдер API ограничивается, вам не нужен какой-либо механизм с вашей стороны ... не так ли?
Флорис
@Floris Они вернут сообщение об ошибке, которое преобразуется в исключение в SDK. Во-первых, я сомневаюсь, что провайдер API будет рад, если я сгенерирую 50 сообщений газа в секунду (вы должны действовать соответственно с такими сообщениями), а во-вторых, я использую API для других целей одновременно, поэтому я не хочу достигать предела, который на самом деле немного выше.
Бенджамин

Ответы:

25

В системе GNU, и если у вас есть pv, вы можете сделать:

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

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

-P20Это выполнить не более 20 $cmdодновременно.

-L10 ограничивает скорость до 10 байтов в секунду, поэтому 5 строк в секунду.

Если у вас $cmdдва медленных и приводит к достижению предела 20, то xargsчтение прекратится, пока $cmdне вернется хотя бы один экземпляр. pvбудет продолжать запись в канал с той же скоростью, пока канал не заполнится (что в Linux с размером канала по умолчанию 64 КБ займет почти 2 часа).

В этот момент pvперестану писать. Но даже тогда, когда xargsвозобновит чтение, pvпопытается перехватить и отправить все строки, которые должны были быть отправлены ранее, как можно быстрее, чтобы поддерживать среднее значение 5 строк в секунду.

Это означает, что до тех пор, пока это будет возможно с 20 процессами, удовлетворяющими этим 5 запускам в секунду в среднем требовании, он будет делать это. Однако при достижении предела скорость запуска новых процессов будет зависеть не от таймера pv, а от скорости, с которой возвращаются более ранние экземпляры cmd. Например, если 20 в данный момент работают и были в течение 10 секунд, и 10 из них решили завершить все одновременно, то 10 новых будут запущены одновременно.

Пример:

$ 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

В среднем это будет 5 раз в секунду, даже если задержка между двумя запусками не всегда будет ровно 0,2 секунды.

С ksh93(или с, zshесли ваша sleepкоманда поддерживает доли секунды):

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

Это не ограничивает число одновременных your-commands.

Стефан Шазелас
источник
После небольшого тестирования pvкоманда, кажется, как раз то, что я искал, не мог надеяться на лучшее! Просто на этой линии: yes | pv -qL10 | xargs -n1 -P20 sh -c "$cmd" shне последний ли shизлишний?
Бенджамин
1
@ Бенджамин Эта секунда shдля $0вашего $cmdсценария. Это также используется в сообщениях об ошибках оболочкой. Без этого, $0было бы yот yes, так что вы получите сообщения об ошибках, как y: cannot execute cmd... Вы могли бы также сделатьyes sh | pv -qL15 | xargs -n1 -P20 sh -c "$cmd"
Стефан Шазелас
Я изо всех сил пытаюсь разложить все это на понятные кусочки, ТБХ! В вашем примере вы удалили это последнее sh; и в моих тестах, когда я удаляю его, я не вижу никакой разницы!
Бенджамин
@Benjamin. Это не критично. Это только изменится, если вы $cmdиспользуете $0(почему бы это?) И для сообщений об ошибках. Попробуйте, например, с cmd=/; без второго shвы бы увидели что-то вроде y: 1: y: /: Permission deniedвместоsh: 1: sh: /: Permission denied
Стефан Шазелас
У меня возникла проблема с вашим решением: оно работает нормально в течение нескольких часов, а затем в какой-то момент оно просто завершается без каких-либо ошибок. Может ли это быть связано с переполнением трубы с неожиданными побочными эффектами?
Бенджамин
4

Проще говоря, если ваша команда длится менее 1 секунды, вы можете просто запустить 5 команд каждую секунду. Очевидно, это очень бурный.

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

Если ваша команда может занять более 1 секунды, и вы хотите распространить команды, вы можете попробовать

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

Кроме того, вы можете иметь 5 отдельных циклов, которые работают независимо, с минимумом 1 секунды.

for i in {1..5}
do    while :
      do   sleep 1 &
           mycmd &
           wait
      done &
      sleep .2
done
meuh
источник
Довольно неплохое решение. Мне нравится тот факт, что это просто и выполняется ровно 5 раз в секунду, но у него есть недостаток - запускать 5 команд одновременно (вместо каждых 200 мс), и, возможно, не хватает гарантии, что одновременно может быть запущено не более n потоков. !
Бенджамин
@ Бенджамин Я добавил сон 200 мс в цикле второй версии. Эта вторая версия не может иметь более 5 cmds одновременно, так как мы только запускаем 5, а затем ждем их всех.
meuh
Проблема в том, что вы не можете запустить более 5 в секунду; если для всех сценариев внезапно требуется более 1 с, то вы далеки от достижения предела API. Плюс, если вы будете ждать их всех, один сценарий блокировки заблокирует все остальные?
Бенджамин
@ Benjamin Таким образом, вы можете запустить 5 независимых циклов, каждый с минимальным сном 1 секунда, см. 3-ю версию.
Мех
2

С программой на С,

Вы можете, например, использовать нить, которая спит в течение 0,2 секунды

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

используйте его, чтобы узнать, как создать поток: создайте поток (это ссылка, которую я использовал для вставки этого кода)

Couim
источник
Спасибо за ваш ответ, хотя я в идеале искал что-то, что не подразумевало бы программирования на C, а только с использованием существующих инструментов Unix!
Бенджамин
Да, ответом на решение stackoverflow может быть, например, использование маркера, совместно используемого несколькими рабочими потоками, но при запросе в Unix.SE предлагается больше подхода «Опытный пользователь», а не «программист» :-) Тем не менее, ccесть существующий инструмент Unix, и это не так много кода!
Стив Джессоп
1

Используя node.js, вы можете запустить один поток, который выполняет сценарий bash каждые 200 миллисекунд, независимо от того, сколько времени потребуется для ответа, поскольку ответ приходит через функцию обратного вызова .

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

Этот javascript запускается каждые 200 миллисекунд, и ответ получается через функцию обратного вызова function (error, stdout, stderr).

Таким образом, вы можете контролировать, что оно никогда не превышает 5 вызовов в секунду, независимо от того, насколько медленным или быстрым является выполнение команды или сколько времени он должен ждать ответа.

jcbermu
источник
Мне нравится это решение: оно запускает ровно 5 команд в секунду через равные промежутки времени. Единственный недостаток, который я вижу, заключается в том, что в нем нет гарантии, что одновременно может быть запущено не более n процессов! Если это то, что вы могли бы легко включить? Я не знаком с node.js.
Бенджамин
0

Я pvкакое-то время пользовался решением на основе Стефана Шазеля , но обнаружил, что оно выходило случайно (и молча) через некоторое время, от нескольких минут до нескольких часов. - Изменить. Причина была в том, что мой PHP-скрипт иногда умирал из-за превышения максимального времени выполнения, выход из него со статусом 255.

Поэтому я решил написать простой инструмент командной строки, который делает именно то, что мне нужно.

Достижение моей первоначальной цели так же просто, как:

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

Он запускает почти ровно 5 команд в секунду, если не существует уже 20 одновременных процессов, и в этом случае он пропускает следующее выполнение, пока слот не станет доступным.

Этот инструмент не чувствителен к состоянию 255 выхода.

Вениамин
источник