Освоение внешних команд: оптимизация производительности Bash-скриптов
Раскройте скрытые возможности повышения производительности ваших Bash-скриптов, освоив использование внешних команд. В этом руководстве объясняются значительные накладные расходы, вызванные многократным запуском таких процессов, как `grep` или `sed`. Изучите практические, действенные методы замены внешних вызовов эффективными встроенными функциями Bash, пакетными операциями с использованием мощных утилит и оптимизации циклов чтения файлов для значительного сокращения времени выполнения в задачах автоматизации с высокой пропускной способностью.
Освоение внешних команд: оптимизация производительности Bash-скриптов
Самый быстрый Bash-скрипт часто тот, который запускает меньше программ.
Bash хорош для склеивания: прочитать файл, решить, что делать, запустить другой инструмент, проверить код возврата и продолжить. Это не высокопроизводительный язык обработки данных. Ловушка в использовании Bash так, как будто каждая крошечная строковая операция требует sed, каждое сравнение — expr, а каждый файловый цикл — свежий grep. Такой стиль работает на десяти строках. Он становится мучительным на 200 000 строках.
Цена — запуск процесса. Когда скрипт запускает grep, sed, awk, cut, tr, date или basename, оболочка должна создать еще один процесс и ждать его. Один вызов — не проблема. Один вызов внутри большого цикла — это шаблон, который стоит исправить.
Начните с поиска команд внутри циклов:
grep -nE 'for |while ' script.sh
grep -nE 'grep|sed|awk|cut|tr|expr|basename|dirname|cat' script.sh
Это не значит, что каждое совпадение плохо. Один awk над целым файлом обычно нормален. Один sed, запускаемый для каждой строки, — это то, что превращает скрипт обслуживания в таинственный сбой во время развертывания.
Замените крошечные внешние вызовы самим Bash
Самые легкие победы — это арифметика, длина строки, префиксы, суффиксы и простые подстановки. Bash уже умеет это делать.
Внешняя арифметика:
# Использует внешнюю утилиту 'expr'
RESULT=$(expr $A + $B)
Встроенная арифметика:
RESULT=$((A + B))
Внешняя подстановка строк:
MY_STRING="hello world"
NEW_STRING=$(echo "$MY_STRING" | sed 's/world/universe/')
Расширение параметров:
MY_STRING="hello world"
NEW_STRING=${MY_STRING/world/universe}
printf '%s\n' "$NEW_STRING"
| Задача | Неэффективный метод (внешний) | Эффективный метод (встроенный) |
|---|---|---|
| Извлечение подстроки | `echo "$STR" | cut -c 1-5` |
| Проверка длины | expr length "$STR" |
${#STR} |
| Удаление суффикса | basename "$file" .log |
${file%.log} |
| Удаление пути | basename "$path" |
${path##*/} |
| Удаление имени файла | dirname "$path" |
${path%/*} |
| Замена первого совпадения | sed 's/foo/bar/' |
${value/foo/bar} |
| Замена всех совпадений | sed 's/foo/bar/g' |
${value//foo/bar} |
Отдавайте предпочтение [[ ... ]] для условных операторов Bash. Это ключевое слово оболочки, оно чисто обрабатывает сопоставление с образцом и избегает некоторых сюрпризов с кавычками, которые возникают с [ ... ].
if [[ $name == *.log && -s $name ]]; then
printf 'непустой лог: %s\n' "$name"
fi
Не заходите слишком далеко. Замена по шаблону Bash — это не полноценный движок регулярных выражений. Если правило действительно сложное, один проход awk или perl чище и обычно быстрее, чем хитроумное расширение оболочки.
Пакетная обработка вместо повторяющейся работы
Если инструмент может обработать много входных данных за один запуск, передайте ему много входных данных. Это наиболее важно для grep, awk, sed, find, инструментов сжатия, клиентов загрузки и всего, что подключается к сетевому сервису.
Этот цикл запускает один grep на файл:
for file in *.log; do
grep "ERROR" "$file" > "${file}.errors"
done
Если вам нужен только один объединенный результат, используйте один grep:
grep "ERROR" *.log > all_errors.txt
Если вам нужен вывод для каждого файла, подумайте, действительно ли требуется разделение. Иногда последующий инструмент может прочитать префикс имени файла из grep -H:
grep -H "ERROR" *.log > errors-with-filenames.txt
Для построчных преобразований сверните простые цепочки grep | awk в одну программу awk:
awk '/data/ {print $1}' input.txt | sort > output.txt
Это все еще запускает sort, и это нормально. Сортировка — это именно та работа, которую должен выполнять внешний инструмент. Полезное изменение — удаление бесполезного cat и отдельного grep.
Читайте файлы без cat
Стандартный цикл чтения строк скучен не просто так:
while IFS= read -r line; do
printf 'Обработка: %s\n' "$line"
done < file.txt
IFS= сохраняет начальные и конечные пробелы. -r предотвращает интерпретацию обратной косой черты как escape-символа. Перенаправление сохраняет цикл в текущей оболочке, что важно, если цикл обновляет переменные, которые понадобятся позже.
Эта версия выглядит безобидно, но обычно хуже:
cat file.txt | while read -r line; do
count=$((count + 1))
done
printf '%s\n' "$count"
В Bash сегмент конвейера обычно выполняется в подоболочке, поэтому count может не обновиться в родительской оболочке. Он также запускает cat без всякой пользы.
Используйте подстановку процессов, когда входные данные действительно создаются командой:
while IFS= read -r file; do
printf 'большой файл: %s\n' "$file"
done < <(find /var/log -type f -size +100M)
Здесь find выполняет реальную работу. Сохранение цикла в текущей оболочке по-прежнему полезно.
Используйте find -exec ... + и xargs осторожно
Файловые циклы — частый источник случайной медлительности:
for file in $(find . -name '*.tmp'); do
rm "$file"
done
Это ломается на пробелах и запускает rm многократно. Используйте пакетное выполнение:
find . -name '*.tmp' -exec rm -f {} +
Форма + передает много путей каждому вызову rm. Более старая форма \; запускает команду один раз для каждого пути.
Для команд, которые выигрывают от параллелизма, xargs -P может сократить реальное время:
xargs -n 1 -P 4 curl -fsS -O < urls.txt
Используйте -0, когда задействованы имена файлов:
find uploads -type f -print0 | xargs -0 -n 50 -P 4 ./process-file
Параллелизм не бесплатен. Четыре задания curl могут быть быстрее одного. Сорок могут привести к ограничению со стороны API или насытить небольшой хост.
Измеряйте, прежде чем переписывать все
Правильная оптимизация зависит от того, где тратится время. Сначала используйте простое измерение времени:
time ./script.sh
Для скриптов с интенсивным использованием процессов strace -c в Linux может показать, тратит ли скрипт время на создание процессов, открытие файлов или ожидание ввода-вывода:
strace -f -c ./script.sh
Трассировка оболочки может выявить повторяющиеся команды:
PS4='+ $SECONDS ${BASH_SOURCE}:${LINENO}: '
bash -x ./script.sh
Если скрипт тратит 95 процентов времени на ожидание экспорта базы данных, замена ${value/foo/bar} не поможет. Если он запускает sed 300 000 раз, поможет.
Знайте, когда внешние инструменты лучше
| Цель | Лучший инструмент (обычно) | Примечания |
|---|---|---|
| Извлечение и фильтрация полей | awk |
Лучше, чем циклы Bash для табличного текста. |
| Потоковое редактирование | sed |
Хорош для одного прохода по файлу. |
| Обход файлов | find |
Безопаснее, чем разбор ls. |
| JSON | jq |
Не разбирайте JSON с помощью cut. |
| Параллельные задания | xargs -P или GNU parallel |
Добавьте ограничения и обрабатывайте ошибки. |
| Обработка больших текстов | awk, perl, Python |
Часто понятнее, чем героический Bash. |
Встроенные функции Bash быстры, но поддерживаемость все равно важна. Я бы предпочел поддерживать один понятный скрипт awk, чем 40 строк хрупкого расширения параметров, которое понимает только исходный автор.
Практический контрольный список для проверки
Когда Bash-скрипт кажется медленным, пройдитесь по нему в таком порядке:
- Найдите внешние команды внутри циклов.
- Замените простые арифметические и строковые операции расширением Bash.
- Удалите бесполезные вызовы
cat. - Пакетируйте аргументы файлов с помощью
grep,awk,sed,find -exec ... +илиxargs. - Сохраняйте циклы чтения строк в текущей оболочке, если переменные должны пережить цикл.
- Измерьте снова.
Вам не нужно превращать каждый скрипт в упражнение по бенчмаркингу. Большие выигрыши обычно приходят из нескольких очевидных мест: одна команда на строку, одна команда на файл или одна команда на элемент API. Исправьте их, сохраните скрипт читаемым и остановитесь, когда время выполнения перестанет быть проблемой.