Используем Bash в SQL-стиле
Приветствую! Данная небольшая статья призвана осветить некоторые аспекты применения Bash для анализа файлов в SQL-стиле. Будет интересна для новичков, возможно, опытные пользователи также найдут для себя что-нибудь новое.
Структура задачи:
- projects
- project1/ — проекты
- conf/
*.conf — конфигурации построения отчетов по таблицам
- reports/
/ - report1.json — сами отчеты, содержат статистику по таблицам Apache Hive
- report2.json
- conf/
- project2/
…
- project1/ — проекты
Надо: найти просроченные отчеты.
Итак, расчехляем 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"
(или не выдать).
Код тут