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

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

  • projects
    1. project1/ — проекты
      • conf/
        • <run_configurations>*.conf — конфигурации построения отчетов по таблицам
      • reports/
        • <run_configurations>/
          • 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 <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, разделить имя файла с отчетом и имя таблицы пробелом, и напечатать те отчеты, где дата создания отчета меньше даты изменения таблицы, затем ищем уникальные отчеты, и работа готова.

Заключение


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

Код тут

Комментарии (36)


  1. zoroda
    04.12.2017 07:49

    Плюсую.
    Часто приходится обрабатывать таким образом значительные объёмы данных. Раньше загонял данные в SQL таблицы, строил индексы, писал заросы, экспортировал результат. Лет 7 назад открыл для себя возможность разгребать такие файлы при помощи unix команд. Результаты просто фантастические! Скорость обработки увеличилась даже не в разы — на порядки.


    1. mihmig
      04.12.2017 10:19

      Эмм, а скорость «раскуривания» такого скрипта следующим админом (или Вами же, но через полгода-год) как изменилась?

      Что мешает установить язык более высокого уровня (с IDE, отладчиком, автокомплитом, подсветкой синтаксиса и проверкой ошибок «на лету»)?

      Нет, мы откроем ман в соседней консоли и будем отлаживать скрипт стопятьсотый раз прогоняя скрипт…


      1. Cobolorum
        04.12.2017 10:34

        Если «админ» не знает текстовые утилиты unix он просто не компетентен.
        Автор просто не напиасл самое важное, что выше приведенный скрипт будет работать десятилетиями и не требовать какого либо сопровождения, в отличии от писанины на «языке высокого уровня»


        1. poxvuibr
          04.12.2017 12:02

          Автор просто не напиасл самое важное, что выше приведенный скрипт будет работать десятилетиями и не требовать какого либо сопровождения, в отличии от писанины на «языке высокого уровня»

          А писанина на языке высокого уровня что? в 12 часов превратится в тыкву? :)


        1. tgz
          04.12.2017 12:39

          А вы помните все ключи для вызова read? А в мане за сколько найдете что делает ключ -r например?
          И я молчу про постоянный эскейпинг кавычек, неявный субшел, башизмы и прочие IFS'ы.
          Питон предпочтительнее везде, где bash переваливает на 10 строк.


          1. iboltaev Автор
            04.12.2017 12:51

            Ну тогда я спокоен, у меня 9)


          1. zoroda
            06.12.2017 06:56

            Да, последнее время занчительно больше делаю на Питоне: заимодействие с API, JSON, HTTP запросы. Но такого рода обработка файлов — это то, с чем башовые утилиты справляются значительно быстрее.


            1. tgz
              06.12.2017 09:03

              bash скрипт работает быстрее, чем python? Не верю (с)


              1. poxvuibr
                06.12.2017 22:19

                bash скрипт работает быстрее, чем python? Не верю (с)

                Ну собственно первая строка этого конкретного баш скрипта проходит по всем файлам и ищет там строки с "table". А вторая строка ещё раз проходит по тем же файлам и ищет в них же строки с "created_date". Эти данные записываются в файлы. И потом содержимое файлов джойнится.


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


                И чем бы ты не описал эти альтернативные действия — питоном ли, джавой или рубями или другим, более ядрёным, башем — всё равно получится быстрее, чем в данном конкретном случае, за счёт более вменяемого алгоритма.


                Так что, если речь идёт именно о скорости работы программы, а не о скорости создания скрипта — правильно не верите :)


      1. nekt
        04.12.2017 10:36

        Как будто IDE, отладчик, автокомплик, подсветка синтаксиса и проверка ошибок на лету — это панацея и не придется раскуривать ни свою реализацию поиска по файлам, ни свою реализацию sed, ни кастомную грепалку…


        1. poxvuibr
          04.12.2017 12:21

          Как будто IDE, отладчик, автокомплик, подсветка синтаксиса и проверка ошибок на лету — это панацея

          Не панацея, но писать будет легче и быстрее.


          и не придется раскуривать ни свою реализацию поиска по файлам

          Это вы про поиск файлов, содержащия строку? Код будет тривиальным, раскуривать его не придётся.


          ни свою реализацию sed

          sed там в коде используется для того, чтобы делать замены в строках. Код ещё более тривиальный, чем поиск по файлам, там вообще даже думать не придётся, чтобы понять что происходит.


          ни кастомную грепалку

          grep используется либо для поиска файлов (см первый пункт), либо для отсева строк, что в языке программирования высокого уровня тоже очень просто.


          1. iboltaev Автор
            04.12.2017 12:36

            Так и на баше не rocket science. Ну и по опыту, на баше такие штуки пишутся гораздо быстрее, получаются гораздо короче и оказываются гораздо более надежными. Меньше кода — меньше багов (с). Проще поставить пайп, чем написать лишний if или фильтр/замену по регекспу. Если брать support, то да, код write-only, но он таким и задумывался — решить задачу наименьшими силами и забить.


            1. poxvuibr
              04.12.2017 12:50

              Так и на баше не rocket science.

              Для баша нет IDE, которая тебе подскажет и поможет.


              Ну и по опыту, на баше такие штуки пишутся гораздо быстрее, получаются гораздо короче и оказываются гораздо более надежными.

              Из того, что есть в скриптах, навскидку какие-то трудности представляет только join, потому что его, возможно, в стандартной библиотеке не будет.


              Если брать support, то да, код write-only, но он таким и задумывался

              Предыдущий оратор тут говорил, что код на языке программирования высокого уровня читать будет не сильно легче, чем вот этот баш, а то и тяжелее.


              1. ymn
                04.12.2017 13:51

                Для баша нет IDE, которая тебе подскажет и поможет.

                для баша есть set -x. Для условных однострочников этого хватает за глаза. Люди, которые хотят рефакторинг и go-to-definition для скриптов на баше, явно делают что-то не так.


                1. poxvuibr
                  04.12.2017 16:28

                  Люди, которые хотят рефакторинг и go-to-definition для скриптов на баше, явно делают что-то не так.

                  Ну да, используют баш там, где лучше использовать язык программирования высокого уровня.


                  1. iboltaev Автор
                    04.12.2017 16:56

                    Не, ну правда. Не нравится баш — не пишите. Не нравится читать код на баше — не читайте. Начальство заставляет — смените начальство. Как-то так.


                    1. poxvuibr
                      04.12.2017 17:13

                      Не, ну правда. Не нравится баш — не пишите.

                      Вопрос то вроде в том, что потом легче читать и модифицировать — баш или код на языке высокого уровня, а не в том, нравится мне писать на баше, или нет.


                      1. iboltaev Автор
                        04.12.2017 17:22

                        Вкратце ответ — кому как. Я напишу на баше и уволюсь, придете вы и скажете, что это все г**но и надо на питоне, перепишете на питоне, уволитесь, придет условный Ваня и скажет, что питон г**но и надо на баше, и так далее. И это — нормально.

                        Кто как хочет — тот так и доставляет себе радость и удовольствие.

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


                        1. poxvuibr
                          04.12.2017 17:44

                          Вкратце ответ — кому как. Я напишу на баше и уволюсь, придете вы и скажете, что это все г**но и надо на питоне, перепишете на питоне, уволитесь, придет условный Ваня и скажет, что питон г**но и надо на баше, и так далее. И это — нормально.

                          Вы тут какое-то колесо сансары описали. Бессмысленный и беспощадный джаггернаут. Я против того, чтобы это было нормой :).


                          Кто как хочет — тот так и доставляет себе радость и удовольствие.

                          Спорить не буду, нередко решать проблему лучше не тем инструментом, который её лучше решает, а тем, которым ты лучше умеешь пользоваться. Особенно, когда надо сделать так, чтобы заработало прямо сейчас. Если бы в ветке не подняли вопрос поддерживаемости, в общем-то и разговора бы не было.


                          Хотя, пользуясь случаем, хочу сделать замечание статье. Стиль, используемый в скрипте — не SQL. SQL это декларативный язык, а тут функциональный конвейер.


      1. ihouser
        04.12.2017 12:01

        Вот написал человек на Delphi программульку и слился. В свое время этот IDE был на вершине популярности. Прога была хороша, мы были рады до того момента, когда данных стало много и начались тормоза (что то не оптимально написал, не предвидел).

        Купили крутую программку от уважаемой компании. Делает все, но чуточку не то что нужно. И сделать так как нам надо они немогут — навороты не позволяют. Вынуждены жрать кактус.

        Так что скрипт на bash'е переживет все модные веяния. И понимающий unix команды всегда найдутся.


        1. poxvuibr
          04.12.2017 12:28

          Вот написал человек на Delphi программульку и слился.

          Я так понимаю проблема в том, что нет человека, который может поправить исходники и собрать код заново? Ну, то есть, надо было выбирать другй язык программирования?


          Так что скрипт на bash'е переживет все модные веяния. И понимающий unix команды всегда найдутся.

          Программа на java тоже переживёт. Да что там. Мне неприятно это говорить, но даже программа на Питоне будет жить ещё очень долго. Ну и работать не только там, где есть unix tools, конечно :)


          1. ihouser
            04.12.2017 13:03

            Ну, то есть, надо было выбирать другой язык программирования?


            В то время выбор Delphi был естественным. Сама программа 4 года радовала всех.

            Мне неприятно это говорить, но даже программа на Питоне будет жить ещё очень долго.


            А вот Питон — г… о. Постоянно имею проблемы с прогами на питоне. Какой нибудь редкий модуль отвалится а из логов никак непонят что отвалилось. А когда разберешься, окажется что нужна версия модуля которая при сборке вываливается с ошибкой. С прогами на других языках столько гемора не имели.

            Конечно, можно винить руки из ж, но в бизнесе чинить программы — муда. Инструменты должны работать незаметно и исправно.


            1. poxvuibr
              04.12.2017 13:36

              В то время выбор Delphi был естественным.

              Я не сомневаюсь.


              Сама программа 4 года радовала всех.

              Вне всякого сомнения это успешный код. Я даже не совсем понимаю, что мешало поправить его и собрать ещё раз. Врядли обошлось бы дороже покупки стороннего решения. Дельфи то сейчас не то чтобы совсем мёртвая штука .


              А вот Питон — г… о.

              Ну, я бы не был так категоричен. Тем не менее я его не люблю и именно поэтому мне неприятно говорить, что код на питоне — таки штука долговечная.


              Постоянно имею проблемы с прогами на питоне.

              Сочувствую. У меня немного обратная ситуация — я их писать не люблю. Но питон однозначно лучше баша.


              1. ihouser
                04.12.2017 19:07

                Я даже не совсем понимаю, что мешало поправить его и собрать ещё раз. Врядли обошлось бы дороже покупки стороннего решения.


                Для компании несвязанной с IT найти нужного программиста задача нетривиальная, точнее — лотерея. И в это далекое время мы не знали слова «freelance». Оглядываясь назад, мы многое могли сделать лучше. Был бы тогда теперешний опыт…


            1. Crandel
              04.12.2017 13:58

              Если вы не платите за это деньги — то вам никто ничего не должен. А руки да — они из одного места получается


      1. iboltaev Автор
        04.12.2017 14:34

        Вообще, это скорее повод следующему админу таки раскурить баш. А то мало ли, вдруг воинствующий джавист придет сопроводжать. Давайте для него версию на java напишем, с GOF-ом и интерфейсами.


        1. poxvuibr
          04.12.2017 16:31

          А можно попросить у вас какие-нибудь живые данные? Что там в этих файликах, на которых скрипты бегают?


          1. iboltaev Автор
            04.12.2017 16:52

            Данные-то дать могу, но вот Hive-кластер, который в статье упоминается, вам самому поднимать приедтся


            1. poxvuibr
              04.12.2017 16:57

              Как я понимаю, от кластера там только xargs -n1 -I dr hdfs dfs -ls dr . Это можно и чем-нибудь другим подменить для целей тестирования.


  1. onix74
    04.12.2017 10:36
    +2

    Вместо sed 's/:/ /g' | awk '{print $1 " " $3}' можно использовать awk -F: '{print $1 " " $3}'
    Конструкция упростится и станет более читабельной


    1. iboltaev Автор
      04.12.2017 10:38

      Да, можно. Спасибо, как-то сразу не догадался)


      1. guestinger
        04.12.2017 15:39

        Вот эту конструкцию тоже можно заменить для читабельности:
        sed 's/[\r\n",:]//g'
        на
        tr -d '\r",:'

        Зачем у вас идёт замена переноса строки (\n)?
        Во-первых, сэд её так не сможет заменить — надо считать хотя бы две строки для этого (link).
        Во-вторых, если это удастся сделать (тем же tr), то у вас будет всё в одну строку, что нарушит дальнейшую логику.


  1. Dreyk
    04.12.2017 10:58

    jq для работы с JSON не пробовали?


    1. iboltaev Автор
      04.12.2017 12:02

      Посмотрел. Не сильно код укорачивает, если честно) Задача для него слишком примитивна


  1. RPG
    05.12.2017 00:39

    Хотя и сам не прочь крутить подобные портянки, хочу предупредить читателей, этот код — не пример для подражания. Выход за границу 80 символов — сама по себе проблема, и если скрипт "причесать" до читаемого состояния, то в нём будет уже не 9, а без малого сотня строк. И гарантировать, что этот скрипт не развалится от случайно затесавшегося неформатного файлика, в отличие от настоящего парсера, невозможно. А когда он развалится, отладка превратится в ад (если этот факт вообще кто-то заметит).


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


    while read -r line; do
      echo $((line**2))
    done < <(seq 123)

    В отличие от command | while ...do ... done она не создаёт новый сабшелл и позволяет избежать ошибок, связанных с потерей переменных.


  1. sub31
    05.12.2017 07:49

    cut -f 1,3 -d ':' вместо sed 's/:/ /g' | awk '{print $1 " " $3}'
    не работает?
    А sed 's#\(.*\):.*:\(.*\)#\1 \2#g'?