Последовательная обработка на многоядерном сервере выглядит расточительно: восемь ядер стоят, а один архив сжимается за сорок минут. Утилита GNU Parallel закрывает разрыв между списком задач и простаивающими ядрами: она читает вход, строит из него команды и запускает их одновременно, соблюдая управление потоком и сбор результата. Для резервных задач и любой массовой обработки файлов это часто разница между ночью и часом. Статья разбирает способы подачи заданий, управление потоками, границы ускорения, контроль результата и сценарии применения с конкретными командами.

Утилита Parallel и её отличие от простых конвейеров

Утилита построена вокруг одной идеи: вход превращается в список заданий, задания выполняются одновременно, результаты собираются в общем потоке. Заданием может быть файл для обработки, каталог для архивирования, строка со списком параметров или имя машины. Каждое задание подставляется в шаблон команды на место подстановочного выражения, и утилита сама решает, сколько команд держать в полёте одновременно.

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

Пример превращения списка файлов в параллельное сжатие читается как предложение:

parallel -j8 "gzip -9 {}" ::: /var/log/app/*.log

Аргументы после разделителя с тремя двоеточиями становятся источником заданий, каждый файл подставляется на место выражения в скобках, и восемь задач сжатия работают одновременно. Тот же шаблон масштабируется от трёх файлов до десятков тысяч, и структура команды не меняется.

Отдельное достоинство, вежливость к потокам. Утилита понимает разницу между задачами на процессор и задачами на ввод-вывод, и настройки потока для сжатия и для копирования это два разных разговора. Материал ниже разбирает эту разницу отдельно, потому что именно на ней спотыкаются первые попытки ускорения.

Место утилиты в наборе инструментов выросло из нужды: склеивать списки однотипных задач с простаивающими ядрами. За годы она обросла именно тем, чего не хватало циклам в сценариях и конвейерам со сборщиками аргументов: учётом заданий, повтором неудач, журналом и сохранением порядка вывода.

Способы подачи заданий от списков аргументов до файлов

Источник заданий выбирается по месту, где список живёт. Короткий список удобнее передать аргументами после разделителя. Список в файле подключается вторым разделителем, по одной задаче на строку. Вход из другой команды приходит по стандартному потоку, и конвейер из поиска файлов прямо в параллельный запуск работает без промежуточных файлов:

find /srv -name "*.sql" -print0 | parallel -0 -j4 "gzip {}"

Разделитель нулевых байтов в связке с ключом нулевого ввода переживает любые имена, включая пробелы и переводы строк. Обычный текстовый вход делится по строкам, и этого достаточно в большинстве случаев, но привычка использовать безопасную пару для файловых списков экономит объяснения с самим собой в ночь переаттестации.

Шаблон команды гибче, чем подстановка одного аргумента. Доступны модификации подстановочного выражения: имя без каталога, имя без расширения, номер задания, и комбинации. Для команд, где аргумент нужен дважды, выражение просто повторяется в шаблоне. Для задач с несколькими аргументами в одной строке вход разбивается по столбцам, и шаблон берёт нужные столбцы по отдельности.

Списки машин открывают следующий уровень: задание выполняется на удалённых хостах, и утилита раздаёт их по списку через безопасное соединение. Сценарий для больших хозяйств выглядит эффектно, но в статье остаётся упоминанием, потому что резервные задачи на одной машине закрывают большинство потребностей без новой инженерии.

Управление числом параллельных потоков и наблюдение за прогрессом

Число потоков задаётся ключом с числом, и выбор числа это половина искусства. Вторая половина в понимании, куда упирается задача. Сжатие и хэширование это работа на процессор: потоков назначают по числу ядер, иногда с небольшим запасом, и ускорение близко к линейному. Копирование и синхронизация упираются в диски и сеть: потоков назначают умеренно, потому что десять параллельных копирований одного массива не ускоряют его, а сворачивают в очередь из случайных частей.

Индикаторы прогресса превращают ожидание из медитации в наблюдение. Ключ ожидаемого времени показывает оценку завершения по текущей скорости, ключ прогресса рисует живую линию работающих и завершённых заданий, а режим перечисления номеров обновляет счёт в терминале. Для длительных прогонов это не украшение, а страховка: видно, что процесс жив, и видно, примерно сколько осталось.

Журнал заданий закрывает требования протокола. Каждая строка журнала несёт номер задания, время старта, длительность, объёмы принятого и отправленного, код возврата и саму команду. После прогона журнал это готовый материал для отчёта: сколько заданий, сколько успешных, какие зависели дольше прочих. Сводка по журналу строится парой команд, и разбор ночного прогона превращается из археологии в чтение таблицы.

Скромная настройка деликатности завершает управление: утилите можно запретить запускать новые задания, а текущим дать закончиться, и она корректно завершится по сигналу прерывания. Массовые операции, которые нельзя остановить без потери половины работы, проектируются с этой возможностью сразу, а не додумываются потом.

Резервные задачи как классический сценарий параллелизма

Резервное копирование по своей природе это список однотипных задач: много каталогов, много файлов, много баз. Последовательная обработка делает из списка один длинный поток, параллельная превращает его в веер. Типовые сценарии читаются одинаково:

parallel -j6 "tar czf /backup/{/.}.tar.gz -C {} ." :::: /etc/backup-dirs.txt

Сжатие каталогов по списку: каждый каталог получает собственный архив с понятным именем, задания распределяются по ядрам, и общий прогон занимает время самого долгого каталога вместо суммы всех. Список каталогов живёт в файле, читается как документация, и добавление нового раздела в резервирование это одна строка в списке.

Синхронизация множества деревьев на приёмник строится так же, с оговоркой: каждое задание пишет в собственный целевой каталог, и параллельные записи не пересекаются. Хэширование больших объёмов для проверки целостности ускоряется на порядок, потому что каждый файл обрабатывается своим потоком. Дампы баз, разложенные по файлам, сжимаются и раздаются параллельно с тем же шаблоном.

Отдельный класс, обработка после копирования: распаковка тестовой среды из архива, пересчёт контрольных сумм, конвертация форматов. Всё, что раньше писалось циклом с вечера и заканчивалось к утру, переписывается одной параллельной командой и заканчивается к полуночи, оставляя утро для разбора.

Границы ускорения и поиск узких мест

Параллелизм не ускоряет то, что и так не может работать быстрее. Узкое место в диске остаётся узким местом при любом числе потоков: массив отдаёт свои мегабайты в секунду независимо от того, сколько процессов за ними стоят в очереди. Сеть между машинами ограничена своим каналом, и параллельные копирования делят его по-честному, то есть делят, а не умножают.

Практика границ проста. Если утилита мониторинга показывает загрузку процессоров в потолок, а диски отдыхают, задача на процессор, и поток добавлять разумно. Если процессоры спят, а диски или сеть в пике, задача на ввод-вывод, и потоки убавляют до уровня, при котором канал работает стабильно. Замер до и после изменения числа потоков это единственная честная процедура: ускорение проверяется секундомером, а не ощущением.

Очереди общей инфраструктуры наказывают жадность. Восемь параллельных заданий на сервере с общей сетевой полкой для соседей это восемь отрезов полки, и добрые отношения с соседями по стойке стоят дороже полутора часов ожидания. Ночные окна и умеренные потоки держат ускорение честным ко всем участникам железа.

Мелкие задания открывают отдельный эффект: накладные расходы на запуск каждого процесса съедают выигрыш на заданиях длиной в миллисекунды. Параллелизм окупается на секундах и минутах, а на тысячи микрозадач разумнее сначала объединить их в группы, а потом распараллелить объединённое.

Контроль результата и повтор неудавшихся заданий

Параллельный прогон обязан заканчиваться проверяемым результатом, и утилита даёт для этого три инструмента. Первый, код возврата самого запуска: успех только если все задания успешны, и планировщик сразу видит провал по ненулевому коду. Второй, журнал заданий с кодами каждой строки: после прогона выборка провалившихся команд строится простым фильтром по коду. Третий, режим возобновления: повторный запуск умеет брать из журнала только неудавшиеся задания и не трогать успешные.

Режим повтора неудач меняет культуру массовых операций. Прогон на тысячи файлов с сетевыми сбоями перестаёт быть лотереей: первый проход выполняет основную массу, повтор доезжает хвост из неудач, и суммарно это быстрее и спокойнее, чем один идеальный по надежности последовательный проход. Сценарии резервирования ценят именно это свойство, потому что ночные задания обязаны уметь доезжать после мелких сбоев без человека.

Проверка результата для резервных задач дополняется контролем самих изделий: архивы открываются тестовым чтением, контрольные суммы сверяются, и параллельная проверка после параллельного создания закрывает цикл по той же утилите. Задание, которое создало изделие, и задание, которое его проверило, живут в одном журнале, и протокол ночного прогона складывается сам.

Дисциплина сообщений завершает контроль: вывод заданий собирается в общий поток в порядке заданий, режим пометки добавляет к каждой строке имя задания, и длинный вывод остаётся читаемым. Разбор провала начинается с журнала и заканчивается точной строкой вывода конкретного задания, без чтения всего потока глазами.

Рабочие сценарии применения параллельного запуска

Сценарии, в которых утилита окупает вечер изучения, выглядят так:

  1. Сжатие и архивирование каталогов по списку с распределением по ядрам;
  2. Хэширование больших объёмов для контрольных сумм и проверки целостности;
  3. Параллельная синхронизация независимых деревьев на приёмник;
  4. Обработка разросшихся логов по частям с сохранением порядка строк;
  5. Массовая конвертация и распаковка файлов в тестовых средах;
  6. Ночные прогоны с журналом и повтором неудавшихся заданий.

За сценариями стоит общая формула: список однотипных заданий, тратящих секунды или минуты, и свободные ядра. Всё, что ей соответствует, ускоряется с минимальным риском, всё, что ей не соответствует, сначала измеряется, потом решается.

Итоговое суждение об утилите практичное. GNU Parallel не создаёт скорости из ничего, она лишь перестаёт расточать то, что уже оплачено: ядра, полки и ночь. Резервные задачи, обработка архивов и массовые проверки логов переписываются с циклов на параллельные шаблоны за час, и выигрыш измеряется в часах каждую ночь. Инструмент с честным журналом, оценкой времени и повтором неудач это редкий случай, когда ускорение приходит не ценой надёжности, а вместе с ней.
Мелкая привычка с большим эффектом: перед боевым прогоном всегда выполняется холостой режим с показом команд. Тридцать секунд чтения сгенерированного списка спасают от опечатки в шаблоне, которая в параллельном режиме повторилась бы тысячи раз за минуты.