company_banner

Заметки о Unix: два сценария работы с конвейерами

Автор оригинала: Chris Siebenmann
  • Перевод
Мне встречалось множество рекомендаций о повышении безопасности использования shell-скриптов в Bash путём включения опции pipefail (например — это рекомендуется в данном материале 2015 года). Это, с одной стороны, хорошая рекомендация. Но включение pipefail может привести к конфликту. В одном из двух сценариев использования конвейеров эта опция показывает себя замечательно, а вот в другом то, к чему приводит её включение, выглядит просто ужасно.



Для того чтобы понять суть этой проблемы давайте сначала разберёмся с тем, за что именно отвечает опция Bash pipefail. Обратимся к документации:

Статусом выхода из конвейера, в том случае, если не включена опция pipefail, служит статус завершения последней команды конвейера. Если опция pipefail включена — статус выхода из конвейера является значением последней (самой правой) команды, завершённой с ненулевым статусом, или ноль — если работа всех команд завершена успешно.

Причина использования pipefail заключается в том, что иначе команда, неожиданно завершившаяся с ошибкой и находящаяся где-нибудь в середине конвейера, обычно остаётся незамеченной. Она, если использовалась опция set -e, не приведёт к аварийному завершению скрипта. Можно пойти другим путём и тщательно проверять всё с использованием $PIPESTATUS, но это означает необходимость выполнения больших объёмов дополнительной работы.

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

generate --thing | sed 1000q | gronkulate

Команда sed, после получения 1000 строк, завершит работу и закроет конвейер, в который пишет данные generate. А generate получит сигнал SIGPIPE и, по умолчанию, остановится. Статус выхода команды будет отличаться от нуля, а это значит, что с использованием pipefail работа всего конвейера «завершится с ошибкой» (а с использованием set -e скрипт нормально завершит работу).

(В некоторых случаях то, что происходит, может, от запуска к запуску, меняться. Причина этого — в системе планирования выполнения процессов. Это может зависеть и от того, какой объём данных производят процессы, находящиеся ближе к началу конвейера, и как он соотносится с тем, что фильтруют процессы, расположенные ближе к концу конвейера. Так, если в нашем примере generate создаст 1000 строк или меньше — sed примет все эти данные.)

Это ведёт к двум шаблонам использования конвейеров командной оболочки. При использовании первого конвейер потребляет все входные данные, действуя так в тех случаях, если всё работает без сбоев. Так как подобным образом работают все процессы — ни у одного процесса никогда не должно возникнуть необходимости выполнять запись в закрытый конвейер. А значит — никогда не появится и сигнал SIGPIPE. Второй шаблон использования конвейеров предусматривает ситуацию, когда хотя бы один процесс завершает обработку входных данных раньше, чем обычно. Часто подобные процессы специально помещают в конвейер для остановки обработки данных в определённый момент (как sed в вышеприведённом примере). Подобные конвейеры иногда или даже всегда генерируют сигналы SIGPIPE, некоторые процессы в них завершаются с ненулевым кодом.

Конечно, пользоваться подобными конвейерами можно и в окружении, где применяется pipefail, и даже с set -e. Например, можно сделать так, чтобы один из шагов конвейера всегда сообщал бы об успешном завершении:

(generate --thing || true) | sed 1000q | gronkulate

Но об этой проблеме нужно помнить, нужно обращать внимание на то, какие команды могут окончить работу раньше, чем обычно, не читая всех входных данных. Если что-то упустить — то в «награду» за это, вероятно, можно получить ошибку из скрипта. Если автору скрипта будет сопутствовать успех, то это будет ошибка, возникающая регулярно. В противном случае ошибки будут возникать время от времени, случаясь тогда, когда одна из команд конвейера выдаёт необычно большой объём выходных данных, или тогда, когда другая команда завершает свою работу необычно рано или необычно быстро.

(Кроме того, очень хорошо было бы игнорировать только ошибки, связанные с SIGPIPE, но не другие ошибки. Если generate завершится с ошибкой по причинам, не связанным с SIGPIPE, то нам хотелось бы, чтобы весь конвейер выглядел бы так, как если бы он завершился с ошибкой.)

Чутьё подсказывает мне, что шаблон использования конвейеров, основанный на полном потреблении всех входных данных, распространён гораздо сильнее, чем шаблон, когда работа конвейера завершается раньше, чем обычно. Правда, я не пытался оценить свои скрипты на предмет особенностей использования в них конвейеров. Это, определённо, совершенно естественный шаблон использования конвейеров, когда в них выполняется фильтрация, трансформация или исследование всех сущностей из некоего набора (например — для того чтобы их посчитать или вывести некие сводные данные по ним).

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

В блоге автора этой статьи есть ещё много подобных материалов. Как вы думаете, стоит ли нам перевести некоторые из них?

  • 88,4%Да38
  • 11,6%Нет5
RUVDS.com
VDS/VPS-хостинг. Скидка 10% по коду HABR

Комментарии 2

    0
    Мы сталкивались с таким поведением, когда надо было обеспечить семплирование tsv.gz файлов. То есть надо было вычитать из файла N строк, что в принципе решается при помощи zcat | head -N | gzip конвейера. И понятно, что нет никакого смысла команде head дочитывать вход до конца.
    Но и команде zcat тоже надо понимать когда ей надо закончить работу. Тот же SIGPIPE можно и проигнорировать разными способами. Тут наиболее правильным решением, наверное, окажется не попытки организации нестандартных конвейеров, а всё ж создание элементов конвейера более подходящих для задачи. Например, сделать команду аналогичную zcat, но умеющую семплировать данные.
    PS. Параллелизация в bash возможна как минимум 3 разными способами и, на мой взгляд, bash совсем не подходит для столь сложных задач, потому что даже проконтролировать все коды выхода не всегда возможно…
      0
      Добавлю, почему нельзя просто так игнорировать ошибку выполнения первой команды. Дело в том, что мы хотим заигнорировать один конкретный вид ошибки — ошибку записи в закрытый пайп. Но ведь причин, почему команда отвалилась может быть множество: segmentation fault, ошибка чтения диска, ошибка в читаемых данных, отсутствие файла для чтения, неверное окружение на сервере, отсутствие какого-нибудь модуля. Ведь мы не хотим игнорировать вообще всё это?

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

    Самое читаемое