GNU parallel и xargs. Параллельный запуск нескольких копий команды с разными аргументами
Submitted by Ромка on Сб, 19/09/2015 - 16:07
Задача
Есть консольная команда вида:
./do-something.sh -x 1
Значение аргумента x
может меняться в диапазоне от 1 до 30 000. Выполнение команды для одного аргумента занимает от 30 секунд до 15 минут. Нужно максимально быстро выполнить эту команду для заданного диапазона аргументов на N-ядерном сервере максимально используя ресурсы сервера.
Возможные варианты решения
- Простой цикл от 1 до 30 тысяч с запуском команды на каждой итерации будет использовать только 1 ядро. Это решение неприемлемо: оно будет работать слишком долго и не задействует все доступные ресурсы сервера.
- Можно вручную разбить диапазон на N частей и запустить N циклов вида:
for i in `seq 1 1000` do ./do-something.sh -x $i done
Второе решение лучше первого — оно задействует все доступные ядра процессора, но оно все равно неприемлемо. Команды выполняются с непостоянной скоростью. В каком-то из диапазонов могут попасться только легкие команды, которые выполнятся, предположим, за несколько минут, а в каком-то — тяжелые и их выполнение затянется на несколько часов. Таким образом, часть ядер быстро освободится, будет простаивать и ресурсы сервера опять будут использованы неоптимально.
Решение с xargs
Утилита xargs
, входящая во все современные дистрибутивы Linux, позволяет выполнить заданную команду для списка аргументов поступивших на стандартный ввод. Полезные ссылки:
- http://offbytwo.com/2011/06/26/things-you-didnt-know-about-xargs.html
- http://habrahabr.ru/company/selectel/blog/248207/
В следующем примере берется список файлов текущей директории ls
(в примере использован корень проекта на фреймворке Yii2, ничего секретного) и для каждого файла в директории применяется команда file
, определяющая тип файла:
ls | xargs file assets: directory commands: directory composer.json: ASCII text composer.lock: UTF-8 Unicode text config: directory controllers: directory mail: directory migrations: directory models: directory modules: directory requirements.php: PHP script, ASCII text runtime: directory tests: directory vendor: directory views: directory web: directory yii: a /usr/bin/env php script, ASCII text executable yii.bat: DOS batch file, ASCII text
Аргумент -P
позволяет задать сколько параллельных потоков будет использовано для выполнения задачи.
Поэкспериментируем. Возьмем следующий скрипт и назовем его do-something.sh
:
#!/usr/bin/env bash # Check for command line arguments if [ $# -lt 1 ] then echo "No options found!" exit 1 fi # Get number while getopts "x:" opt do case $opt in x) num=$OPTARG ;; *) echo "No reasonable options found!";; esac done rnd=$(shuf -i 1-100 -n 1) rnd=$(echo "$rnd 100" | awk '{printf "%.2f \n", $1/$2}') sleep $rnd echo $num
Этот скрипт берет на вход число и выводит его на экран с задержкой от 0 до 1 секунды. Теперь запустим этот скрипт командой time echo {1..10} | xargs -n 1 ./do-something.sh -x
. Эта команда выполняет следующие задачи:
- генерирует последовательность чисел от 1 до 10:
echo {1..10}
, - передает эти числа по одному в наш скрипт (за это отвечает аргумент
-n 1
, без него вся последовательность будет воспринята как один длинный аргумент, так как значения разделены пробелом, а не переводом строки), - в конце работы скрипта командой
time
выводит затраченное время.
В результате мы получим примерно такой вывод:
time echo {1..20} | xargs -n 1 ./do-something.sh -x 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 real 0m10.301s user 0m0.042s sys 0m0.194s
Результат 1
А теперь запустим ту же команду с опцией -P 4
, что заставит скрипт выполняться в 4 потока:
time echo {1..20} | xargs -n 1 -P 4 ./do-something.sh -x 4 2 6 1 3 8 10 5 12 9 7 11 13 15 14 19 16 17 20 18 real 0m2.651s user 0m0.032s sys 0m0.215s
Результат 2
В этом примере мы видим, что команды выполнились в 4 раза быстрее, так как запускались параллельно. То что на результат практически не влияет генератор случайных чисел легко убедиться заменив случайную паузу константой.
Теоретически, можно для числа потоков задать значение превышающее число доступных ядер на компьютере. В зависимости от выполняемой программы это может привести как к повышению, так и к понижению скорости выполнения, поэтому оптимальное значение числа потоков должно быть выбрано индивидуально для каждого конкретного приложения.
У xargs
есть один недостаток. Давайте заменим в скрипте do-something.sh sleep $rnd
на sleep 0.1
. Это сделает задержку не случайной, а постоянной. Теперь еще раз выполним time echo {1..20} | xargs -n 1 -P 4 ./do-something.sh -x
:
time echo {1..20} | xargs -n 1 -P 4 ./do-something.sh -x 1 3 2 4 5 6 7 8 9 10 12 11 13 14 15 16 17 18 19 20 real 0m0.560s user 0m0.034s sys 0m0.186s
Результат 3
Видно, что результаты выводятся не последовательно, это не всегда приемлемо.
Решение с GNU Parallel
Ниже перевод введения из мануала к утилите:
Если вы используете xargs, то вы легко сможете использовать parallel, так как эта утилита поддерживает те же аргументы командной строки что и xargs. Если вы используете циклы в шелл-скриптах, то, вероятно, parallel поможет вам избавиться от них и ускорить выполнение за счет параллельного запуска команд.
GNU parallel возаращает результаты выполнения команд в том же порядке как если бы они были запущены последовательно. Это делает возможным использование результатов работы parallel как входных данных для других программ.
Для каждой входящей строки GNU parallel запустит команду и передаст ей эту строку в качетсве аргументов. Если команда не задана, то входящая строка будет исполнена. Несколько строк будут выполнены одновременно. GNU parallel может быть использована как замена для xargs и cat | bash.
У этой утилиты как минмум 2 видимых преимущества перед xargs:
- она позволяет запускать команды не в рамках одного сервера, а сразу на нескольких,
- руководство обещает, что результаты будут выводиться последовательно.
Испытаем. Поверим обещаниям того, что parallel
принимает те же аргументы, что и xargs
и просто заменим имя одной утилиты на другую в команде, которую использовали ранее:
time echo {1..20} | parallel -n 1 -P 4 ./do-something.sh -x 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 real 0m0.562s user 0m0.135s sys 0m0.096s
Результат 4
Работает! Команда выполнилась примено за те же 0,5 секунд, что и xargs
и результат возвращен в правильной последовательности.
Теперь попробуем вернуть обратно случайную задержку, зменим в скрипте do-something.sh sleep 0.1
на sleep $rnd
и запустим еще раз. Результат будет возвращен опять в правильной последовательности, несмотря на то, что из-за разной задержки команды запущенные позже могут быть выполнены раньше предыдущих команд (это хорошо видно во втором результате выше).
Единственным недостатком является то, что xargs
возвращает результаты как только они готовы, а parallel
— только тогда когда выполнение всех команд завершено. Но это цена, которую приходится платить за корректную последовательность результатов. Если запустить parallel
с аргументом --bar
, то во время работы будет выводиться прогресс бар, показывающий процент выполненных команд.
Теперь испытаем еще одну киллер-фичу parallel
— возможность запустить команду на нескольких серверах сразу. Для этого воспользуемся примером из доки: https://www.gnu.org/software/parallel/man.html#EXAMPLE:-Parallel-grep.
# Добавим список серверов в конфиг. В моем случае сервера имеют имена dev и test (echo dev; echo test) > .parallel/my_cluster # Убедимся, что существует файл .ssh/config и забэкапим его touch .ssh/config cp .ssh/config .ssh/config.backup # Временно отключим StrictHostKeyChecking (echo 'Host *'; echo StrictHostKeyChecking no) >> .ssh/config parallel --slf my_cluster --nonall true # Откатываем назад изменения StrictHostKeyChecking в конфиге SSH mv .ssh/config.backup .ssh/config
Теперь сервера из файла .parallel/my_cluster
добавлены в .ssh/known_hosts
.
Наконец, нужно скопировать скрипт do-something.sh в домашнюю директорию текущего пользователя на удаленных серверах (в моем примере test и dev).
После выполненной подготовки мы можем запустить команду на серверах dev
и test
добавив к вызову parallel опцию --sshlogin dev,test
.
Попробуем:
time echo {1..3200} | parallel -n 1 -P 4 --sshlogin test,dev ./do-something.sh -x real 0m0.334s user 0m0.080s sys 0m0.032s
Результат 5
Виден выигрыш в скорости даже на такой элементарной операции, несмотря на оверхед связанный с установкой соединения по сети. В случае с действительно тяжелыми командами, выполнение которых может занимать десятки секунд или минут, выигрыш от такого распределенного выполнения может оказаться еще заметнее.
Последние комментарии