#bash, #tutorial, #linux, #sql
Приветствую! Данная небольшая статья призвана осветить некоторые аспекты применения Bash для анализа файлов в SQL-стиле. Думаю, будет интересна для новичков, возмжно, опытные пользователи также найдут для себя что-нибудь новое.
Структура задачи:
- projects/
- project1/
- conf/
- [run configurations]*.conf -- конфигурации построения отчетов по таблицам
- reports/
- [run configurations]/ -- папки с конфигурациями
- report1.json -- сами отчеты, содержат статистику по таблицам Apache Hive
- report2.json
- [run configurations]/ -- папки с конфигурациями
- 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 | grep ".conf$" | ...
А теперь задумаемся. Просто поставить xargs -n1 find ... мы не можем, так как потеряем саму строку с конфигом, а она нужна. Придется итерироваться циклом, ну да ладно. Ставим пайп и поехали:
... | while read line; do <statements>; 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, разделить имя файла с отчетом и имя таблицы пробелом, и напечатать те отчеты, где дата создания отчета меньше даты изменения таблицы, затем ищем уникальные отчеты, и работа готова.
Заключение
9 довольно простых строк на баше оказалось достаточно, чтобы решить данную задачу. Далее этот скрипт запускаем по крону, и вебморда, ориентируясь на файл outdated_reports, может выдать для отчета заголовок "Report is outdated" (или не выдать).
Надеюсь, было интересно. Код тут: https://github.com/iboltaev/bash_article/example.sh