Используем Bash в SQL-стиле

habr.png

Приветствую! Данная небольшая статья призвана осветить некоторые аспекты применения Bash для анализа файлов в SQL-стиле. Будет интересна для новичков, возможно, опытные пользователи также найдут для себя что-нибудь новое.

Структура задачи:

  • projects
    1. project1/ — проекты
      • conf/
        • *.conf — конфигурации построения отчетов по таблицам
      • reports/
        • /
          • report1.json — сами отчеты, содержат статистику по таблицам Apache Hive
          • report2.json
    2. project2/


Надо: найти просроченные отчеты.

Итак, расчехляем Bash, открываем отдельный терминал для man-ов и приступаем)

Всех, кому интересно — прошу под кат.

Имеем: внутреннюю систему построения отчетов в виде папки с проектами. В каждом проекте в папке conf лежат конфигурации построения отчетов, содержащие в себе имена Hive-овых баз данных в полях "schema", по таблицам которых строятся отчеты. В папке reports — сами отчеты, разложенные в папки с именами конфигураций. Каждый отчет — это json, содержащий статистику по Hive-овым таблицам в массиве объектов "table", а также дату создания в поле "created_date". Возьмем ее вместо даты создания файла, раз уж есть. Нам надо найти такие отчеты, в которых содержатся таблицы, которые были изменены после создания отчета.

Почему в SQL-стиле? Bash предоставляет большие возможности работы с текстом, разделенным на колонки (обычно пробелами), напоминающие обработку таблиц в SQL.

Инструментарий:

  • cat, find, grep и прочее — в представлении не нуждаются)
  • sed — используем для тупой автозамены sed s/что/на что/g
  • awk — позволяет отображать/переставлять/сливать колонки, фильтровать строки по содержимому колонок
  • sort, uniq — наверное, любимые инструменты разгребателей логов) Первый — сортирует, второй — удаляет/подсчитывает дубликаты. Используются часто для всяких
    top N
    awk '...' log | sort -k field_n | uniq -c | sort -n -r | head -n N

  • xargs — обрабатывает поток строк одной командой. Может развернуть строки в argument-list для заданной команды, а может для каждой строки эту команду выполнить.
  • join — натуральный SQL-евский INNER JOIN. Сливает 2 сортированных файла по значению одного одинакового поля в один, сначала идет общее поле, затем оставшиеся поля первого файла, потом — второго.


Приступим. Для начала — просто нагрепаем используемые таблицы:

 grep -r "\"table\":" projects/*/reports/* | ...


Он отдает данные в таком виде:

projects/project1/reports/run1/report1.json: «table»: «table1»,
projects/project1/reports/run2/report2.json: «table»: «table2»,
projects/project2/reports/run3/report3.json: «table»: «table3»,
 ... | sed 's/:/ /g' |  awk '{print $1 " " $3}' | sed 's/[\r\n",:]//g' | ...
... | sort -k 1b,1 | uniq > report_tables


Меняем ':' на пробел, чтобы точно отделить имя файла от колонки «table», печатаем первую (файл отчета) и третью (имя таблицы) колонки, чистим мусор sed-ом, пересортировываем и сохраняем в нашу первую таблицу — report_tables.

Затем таким же способом строим таблицу report_dates, только грепаем created_date и выводим чуть больше колонок (дату и время):

grep -r "\"created_date\":" projects/*/reports/* | sed 's/:/ /g' | ...
... | awk '{print $1 " " $3"T"$4":"$5":"$6}' | sed 's/[\r\n",:]//g' | ...
... | sort -k 1b,1 | uniq > report_dates


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

join report_tables report_dates | awk '{print $1"#"$2 " " $3}' | ...
... | sort -k 1b,1 > report_table_date
projects/project1/reports/run1/report1.json#table1 2017–08–07T070918.024907
projects/project1/reports/run1/report1.json#table2 2017–08–07T070918.024907
projects/project1/reports/run1/report1.json#table3 2017–08–07T070918.024907


Первая часть вроде бы готова. Теперь по аналогии нагрепаем используемые базы:

grep -r "schema\":" projects/*/conf/* | sed 's/:/ /g' | ...
... | awk '{print $3 " " $1}' | sed 's/[\r\n":,]//g' | ...
... | sort -k 1b,1 | uniq > schema_configs
schema1 projects/project1/conf/run1.conf
schema1 projects/project1/conf/run2.conf
schema2 projects/project2/conf/run1.conf


Вот и первая трудность. Предыдущая таблица построена по файлам отчетов, а эта — по файлам конфигов. Надо проставить между ними соответствие:

cat schema_configs | awk '{print $2}' | sort | uniq | ...


А теперь задумаемся. Просто поставить xargs -n1 find ... мы не можем, так как потеряем саму строку с конфигом, а она нужна. Значит, будем итерироваться циклом. Ну да ладно. Ставим пайп и поехали:

... | while read line; do ; done | sort -k 1b,1 > config_reports


Далее пишем все внутри statements:

dir=$(dirname $line); dir2=$(dirname $dir); ...
run=$(echo $line | sed "s/.*\///" | sed 's/\.conf//g'); ...
reps=$(find $dir2/reports/$run/ -name *.json); ...
for r in $reps; do echo $line $r ; done


Выглядит сложно. dirname вытаскивает из пути к файлу путь до последнего слеша, этим мы и воспользовались, чтобы подняться выше файла с отчетом на пару уровней ($dir2). Следующее выражение run=... вытаскивает из $line имя файла run.conf и обрезает расширение, получая имя конфигурации запуска. Далее reps — имена файлов с отчетами для данного конфига, и циклом по ним выводим файл с конфигом $line и файл с отчетом $r. Пересортировываем и пишем табличку config_reports.

projects/project1/conf/run1.conf projects/project1/reports/run1/report1.json
projects/project1/conf/run1.conf projects/project1/reports/run1/report2.json
projects/project1/conf/run2.conf projects/project1/reports/run2/report3.json


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

cat schema_configs | awk '{print $1}' | sort | uniq | ...
... |sed 's/^/path_in_hive/g' | sed 's/$/\.db/g' | ...
... | xargs -n1 -I dr hdfs dfs -ls dr | sed 's/\// /g' | ...
... | sed 's/\.db//g' | awk '{print $12 " " $13 " " $6"T"$7}' | ...
... | sort -k 1b,1 | uniq > schema_tables


Несмотря на длину, тут все просто. Сначала берем schema_configs, оттуда выделяем уникальные схемы, затем sed-ом приписываем к началу путь к Hive-вому хранилищу, в конец — расширение .db. Теперь для каждой такой строки выполняем hdfs dfs -ls, это показывает нам все таблицы в заданной базе с датами их последнего изменения. Меняем все слеши на пробелы, вытаскиваем имя базы, имя таблицы и дату ее изменения, пересортировываем и готова табличка schema_tables.

Теперь заключительная часть:

# configs - tables
join schema_configs schema_tables | awk '{print $2 " " $3 " " $4}' | ...
... | sort -k 1b,1 | uniq > config_tables

# reports - tables hive dates
join config_reports config_tables | awk '{print $2"#"$3 " " $4}' | ...
... | sort -k 1b,1 > report_table_hive_dates

# final!
join report_table_date report_table_hive_dates | sed 's/#/ /g' | ...
... | awk '{if ($3<$4) print $1}' | sort | uniq > outdated_reports


Сначала джойним schema_configs и schema_tables по полю с именем бд, и получаем табличку config_tables — конфиг, таблица и дата ее последнего изменения. Затем джойним config_reports и config_tables, чтобы наконец-то получить соответствие отчет — таблица в Hive. Причем имя файла с отчетом и имя таблицы объединяем в одно поле с помощью #. Ну и последний штрих — сджойнить report_table_date и report_table_hive_dates, разделить имя файла с отчетом и имя таблицы пробелом, и напечатать те отчеты, где дата создания отчета меньше даты изменения таблицы, затем ищем уникальные отчеты, и работа готова.

Заключение


Девять довольно простых строк на баше оказалось достаточно, чтобы решить данную задачу. Далее этот скрипт запускаем по крону, и вебморда, ориентируясь на файл outdated_reports, может выдать для отчета заголовок "Report is outdated" (или не выдать).

Код тут

© Habrahabr.ru