Русский
Русский
English
Статистика
Реклама

Etl

Когда у вас сберовские масштабы. Использование Ab Initio при работе с Hive и GreenPlum

07.07.2020 18:11:11 | Автор: admin
Некоторое время назад перед нами встал вопрос выбора ETL-средства для работы с BigData. Ранее использовавшееся решение Informatica BDM не устраивало нас из-за ограниченной функциональности. Её использование свелось к фреймворку по запуску команд spark-submit. На рынке имелось не так много аналогов, в принципе способных работать с тем объёмом данных, с которым мы имеем дело каждый день. В итоге мы выбрали Ab Initio. В ходе пилотных демонстраций продукт показал очень высокую скорость обработки данных. Информации об Ab Initio на русском языке почти нет, поэтому мы решили рассказать о своём опыте на Хабре.

Ab Initio обладает множеством классических и необычных трансформаций, код которых может быть расширен с помощью собственного языка PDL. Для мелкого бизнеса такой мощный инструмент, вероятно, будет избыточным, и большинство его возможностей могут оказаться дорогими и невостребованными. Но если ваши масштабы приближаются к сберовским, то вам Ab Initio может быть интересен.

Он помогает бизнесу глобально копить знания и развивать экосистему, а разработчику прокачивать свои навыки в ETL, подтягивать знания в shell, предоставляет возможность освоения языка PDL, даёт визуальную картину процессов загрузки, упрощает разработку благодаря обилию функциональных компонентов.

В посте я расскажу о возможностях Ab Initio и приведу сравнительные характеристики по его работе с Hive и GreenPlum.

  • Описание фреймворка MDW и работ по его донастройке под GreenPlum
  • Сравнительные характеристики производительности Ab Initio по работе с Hive и GreenPlum
  • Работа Ab Initio с GreenPlum в режиме Near Real Time


Функционал этого продукта очень широк и требует немало времени на своё изучение. Однако, при должных навыках работы и правильных настройках производительности результаты обработки данных получаются весьма впечатляющие. Использование Ab Initio для разработчика может дать ему интересный опыт. Это новый взгляд на ETL-разработку, гибрид между визуальной средой и разработкой загрузок на скрипто-подобном языке.
Бизнес развивает свои экосистемы и этот инструмент оказывается ему как никогда кстати. С помощью Ab Initio можно копить знания о текущем бизнесе и использовать эти знания для расширения старых и открытия новых бизнесов. Альтернативами Ab Initio можно назвать из визуальных сред разработки Informatica BDM и из невизуальных сред Apache Spark.

Описание Ab Initio


Ab Initio, как и другие ETL-средства, представляет собой набор продуктов.

Ab Initio GDE (Graphical Development Environment) это среда для разработчика, в которой он настраивает трансформации данных и соединяет их потоками данных в виде стрелочек. При этом такой набор трансформаций называется графом:

Входные и выходные соединения функциональных компонентов являются портами и содержат поля, вычисленные внутри преобразований. Несколько графов, соединённых потоками в виде стрелочек в порядке их выполнения называются планом.
Имеется несколько сотен функциональных компонентов, что очень много. Многие из них узкоспециализированные. Возможности классических трансформаций в Ab Initio шире, чем в других ETL-средствах. Например, Join имеет несколько выходов. Помимо результата соединения датасетов можно получить на выходе записи входных датасетов, по ключам которых не удалось соединиться. Также можно получить rejects, errors и лог работы трансформации, который можно в этом же графе прочитать как текстовый файл и обработать другими трансформациями:

Или, например, можно материализовать приёмник данных в виде таблицы и в этом же графе считать из него данные.
Есть оригинальные трансформации. Например, трансформация Scan имеет функционал, как у аналитических функций. Есть трансформации с говорящими названиями: Create Data, Read Excel, Normalize, Sort within Groups, Run Program, Run SQL, Join with DB и др. Графы могут использовать параметры времени выполнения, в том числе возможна передача параметров из операционной системы или в операционную систему. Файлы с готовым набором передаваемых графу параметров называются parameter sets (psets).
Как и полагается, Ab Initio GDE имеет свой репозиторий, именуемый EME (Enterprise Meta Environment). Разработчики имеют возможность работать с локальными версиями кода и делать check in своих разработок в центральный репозиторий.
Имеется возможность во время выполнения или после выполнения графа кликнуть по любому соединяющему трансформации потоку и посмотреть на данные, прошедшие между этими трансформациями:

Также есть возможность кликнуть по любому потоку и посмотреть tracking details в сколько параллелей работала трансформация, сколько строк и байт в какой из параллелей загрузилось:

Есть возможность разбить выполнение графа на фазы и пометить, что одни трансформации нужно выполнять первым делом (в нулевой фазе), следующие в первой фазе, следующие во второй фазе и т.д.
У каждой трансформации можно выбрать так называемый layout (где она будет выполняться): без параллелей или в параллельных потоках, число которых можно задать. При этом временные файлы, которые создаёт Ab Initio при работе трансформаций, можно размещать как в файловой системе сервера, так и в HDFS.
В каждой трансформации на базе шаблона по умолчанию можно создать свой скрипт на языке PDL, который немного напоминает shell.
С помощью языка PDL вы можете расширять функционал трансформаций и, в частности, вы можете динамически (во время выполнения) генерировать произвольные фрагменты кода в зависимости от параметров времени выполнения.
Также в Ab Initio хорошо развита интеграция с ОС через shell. Конкретно в Сбербанке используется linux ksh. Можно обмениваться с shell переменными и использовать их в качестве параметров графов. Можно из shell вызывать выполнение графов Ab Initio и администрировать Ab Initio.
Помимо Ab Initio GDE в поставку входит много других продуктов. Есть своя Co>Operation System с претензией называться операционной системой. Есть Control>Center, в котором можно ставить на расписание и мониторить потоки загрузки. Есть продукты для осуществления разработки на более примитивном уровне, чем позволяет Ab Initio GDE.

Описание фреймворка MDW и работ по его донастройке под GreenPlum


Вместе со своими продуктами вендор поставляет продукт MDW (Metadata Driven Warehouse), который представляет собой конфигуратор графов, предназначенный для помощи в типичных задачах по наполнению хранилищ данных или data vaults.
Он содержит пользовательские (специфичные для проекта) парсеры метаданных и готовые генераторы кода из коробки.

На входе MDW получает модель данных, конфигурационный файл по настройке соединения с базой данных (Oracle, Teradata или Hive) и некоторые другие настройки. Специфическая для проекта часть, например, разворачивает модель в базе данных. Коробочная часть продукта генерирует графы и настроечные файлы к ним по загрузке данных в таблицы модели. При этом создаются графы (и psets) для нескольких режимов инициализирующей и инкрементальной работы по обновлению сущностей.
В случаях Hive и RDBMS генерируются различающиеся графы по инициализирующему и инкрементальному обновлению данных.
В случае Hive поступившие данные дельты соединяется посредством Ab Initio Join с данными, которые были в таблице до обновления. Загрузчики данных в MDW (как в Hive, так и в RDBMS) не только вставляют новые данные из дельты, но и закрывают периоды актуальности данных, по первичным ключам которых поступила дельта. Кроме того, приходится переписать заново неизменившуюся часть данных. Но так приходится делать, поскольку в Hive нет операций delete или update.

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

Поступившая дельта загружается в промежуточную таблицу в базу данных. После этого происходит соединение дельты с данными, которые были в таблице до обновления. И делается это силами SQL посредством сгенерированного SQL-запроса. Далее с помощью SQL-команд delete+insert в целевую таблицу происходит вставка новых данных из дельты и закрытие периодов актуальности данных, по первичным ключам которых поступила дельта. Неизменившиеся данные переписывать нет нужды.
Таким образом, мы пришли к выводу, что в случае Hive MDW должен пойти на переписывание всей таблицы, потому что Hive не имеет функции обновления. И ничего лучше полного переписывания данных при обновлении не придумано. В случае же RDBMS, наоборот, создатели продукта сочли нужным доверить соединение и обновление таблиц использованию SQL.
Для проекта в Сбербанке мы создали новую многократно используемую реализацию загрузчика базы данных для GreenPlum. Сделано это было на основе версии, которую MDW генерирует для Teradata. Именно Teradata, а не Oracle подошла для этого лучше и ближе всего, т.к. тоже является MPP-системой. Способы работы, а также синтаксис Teradata и GreenPlum оказались близки.
Примеры критичных для MDW различий между разными RDBMS таковы. В GreenPlum в отличии от Teradata при создании таблиц нужно писать клаузу
distributed by

В Teradata пишут
delete <table> all

, а в GreеnPlum пишут
delete from <table>

В Oracle в целях оптимизации пишут
delete from t where rowid in (<соединение t с дельтой>)

, а в Teradata и GreenPlum пишут
delete from t where exists (select * from delta where delta.pk=t.pk)

Ещё отметим, что для работы Ab Initio с GreenPlum потребовалось установить клиент GreenPlum на все ноды кластера Ab Initio. Это потому, что мы подключились к GreenPlum одновременно со всех узлов нашего кластера. А для того, чтобы чтение из GreenPlum было параллельным и каждый параллельный поток Ab Initio читал свою порцию данных из GreenPlum, пришлось в секцию where SQL-запросов поместить понимаемую Ab Initio конструкцию
where ABLOCAL()

и определить значение этой конструкции, указав читающей из БД трансформации параметр
ablocal_expr=string_concat("mod(t.", string_filter_out("{$TABLE_KEY}","{}"), ",", (decimal(3))(number_of_partitions()),")=", (decimal(3))(this_partition()))

, которая компилируется в что-то типа
mod(sk,10)=3

, т.е. приходится подсказывать GreenPlum явный фильтр для каждой партиции. Для других баз данных (Teradata, Oracle) Ab Initio может выполнить это распараллеливание автоматически.

Сравнительные характеристики производительности Ab Initio по работе с Hive и GreenPlum


В Сбербанке был проведён эксперимент по сравнению производительности сгенерированных MDW графов применительно к Hive и применительно к GreenPlum. В рамках эксперимента в случае Hive имелось 5 нод на том же кластере, что и Ab Initio, а в случае GreenPlum имелось 4 ноды на отдельном кластере. Т.е. Hive имел некоторое преимущество над GreenPlum по железу.
Было рассмотрено две пары графов, выполняющих одну и ту же задачу обновления данных в Hive и в GreenPlum. При этом запускали графы, сгенерированные конфигуратором MDW:

  • инициализирующая загрузка + инкрементальная загрузка случайно сгенерированных данных в таблицу Hive
  • инициализирующая загрузка + инкрементальная загрузка случайно сгенерированных данных в такую же таблицу GreenPlum

В обоих случаях (Hive и GreenPlum) запускали загрузки в 10 параллельных потоков на одном и том же кластере Ab Initio. Промежуточные данные для расчётов Ab Initio сохранял в HDFS (в терминах Ab Initio был использован MFS layout using HDFS). Одна строка случайно сгенерированных данных занимала в обоих случаях по 200 байт.
Результат получился такой:

Hive:
Инициализирующая загрузка в Hive
Вставлено строк 6 000 000 60 000 000 600 000 000
Продолжительность инициализирующей
загрузки в секундах
41 203 1 601
Инкрементальная загрузка в Hive
Количество строк, имевшихся в
целевой таблице на начало эксперимента
6 000 000 60 000 000 600 000 000
Количество строк дельты, применённых к
целевой таблице в ходе эксперимента
6 000 000 6 000 000 6 000 000
Продолжительность инкрементальной
загрузки в секундах
88 299 2 541

GreenPlum:
Инициализирующая загрузка в GreenPlum
Вставлено строк 6 000 000 60 000 000 600 000 000
Продолжительность инициализирующей
загрузки в секундах
72 360 3 631
Инкрементальная загрузка в GreenPlum
Количество строк, имевшихся в
целевой таблице на начало эксперимента
6 000 000 60 000 000 600 000 000
Количество строк дельты, применённых к
целевой таблице в ходе эксперимента
6 000 000 6 000 000 6 000 000
Продолжительность инкрементальной
загрузки в секундах
159 199 321

Видим, что скорость инициализирующей загрузки как в Hive, так и в GreenPlum линейно зависит от объёма данных и по причинам лучшего железа она несколько быстрее для Hive, чем для GreenPlum.
Инкрементальная загрузка в Hive также линейно зависит от объёма имеющихся в целевой таблице ранее загруженных данных и проходит достаточно медленно с ростом объёма. Вызвано это необходимостью перезаписывать целевую таблицу полностью. Это означает, что применение маленьких изменений к огромным таблицам не очень хороший вариант использования для Hive.
Инкрементальная же загрузка в GreenPlum слабо зависит от объёма имеющихся в целевой таблице ранее загруженных данных и проходит достаточно быстро. Получилось это благодаря SQL Joins и архитектуре GreenPlum, допускающей операцию delete.

Итак, GreenPlum вливает дельту методом delete+insert, а в Hive нету операций delete либо update, поэтому весь массив данных при инкрементальном обновлении были вынуждены переписывать целиком. Наиболее показательно сравнение выделенных жирным ячеек, так как оно соответствует наиболее частому варианту эксплуатации ресурсоёмких загрузок. Видим, что GreenPlum выиграл у Hive в этом тесте в 8 раз.

Работа Ab Initio с GreenPlum в режиме Near Real Time


В этом эксперименте проверим возможность Ab Initio производить обновление таблицы GreenPlum случайно формируемыми порциями данных в режиме, близком к реальному времени. Рассмотрим таблицу GreenPlum dev42_1_db_usl.TESTING_SUBJ_org_finval, с которой будет вестись работа.
Будем использовать три графа Ab Initio по работе с ней:

1) Граф Create_test_data.mp создаёт в 10 параллельных потоков файлы с данными в HDFS на 6 000 000 строк. Данные случайные, структура их организована для вставки в нашу таблицу



2) Граф mdw_load.day_one.current.dev42_1_db_usl_testing_subj_org_finval.pset сгенерированный MDW граф по инициализирующей вставке данных в нашу таблицу в 10 параллельных потоков (используются тестовые данные, сгенерированные графом (1))


3) Граф mdw_load.regular.current.dev42_1_db_usl_testing_subj_org_finval.pset сгенерированный MDW граф по инкрементальному обновлению нашей таблицы в 10 параллельных потоков с использованием порции свежих поступивших данных (дельты), сгенерированных графом (1)


Выполним нижеприведённый сценарий в режиме NRT:

  • сгенерировать 6 000 000 тестовых строк
  • произвести инициализирующую загрузку вставить 6 000 000 тестовых строк в пустую таблицу
  • повторить 5 раз инкрементальную загрузку
    • сгенерировать 6 000 000 тестовых строк
    • произвести инкрементальную вставку 6 000 000 тестовых строк в таблицу (при этом старым данным проставляется время истечения актуальности valid_to_ts и вставляются более свежие данные с тем же первичным ключом)

Такой сценарий эмулирует режим реальной работы некой бизнес-системы в режиме реального времени появляется достаточно объёмная порция новых данных и тут же вливается в GreenPlum.

Теперь посмотрим лог работы сценария:
Start Create_test_data.input.pset at 2020-06-04 11:49:11
Finish Create_test_data.input.pset at 2020-06-04 11:49:37
Start mdw_load.day_one.current.dev42_1_db_usl_testing_subj_org_finval.pset at 2020-06-04 11:49:37
Finish mdw_load.day_one.current.dev42_1_db_usl_testing_subj_org_finval.pset at 2020-06-04 11:50:42
Start Create_test_data.input.pset at 2020-06-04 11:50:42
Finish Create_test_data.input.pset at 2020-06-04 11:51:06
Start mdw_load.regular.current.dev42_1_db_usl_testing_subj_org_finval.pset at 2020-06-04 11:51:06
Finish mdw_load.regular.current.dev42_1_db_usl_testing_subj_org_finval.pset at 2020-06-04 11:53:41
Start Create_test_data.input.pset at 2020-06-04 11:53:41
Finish Create_test_data.input.pset at 2020-06-04 11:54:04
Start mdw_load.regular.current.dev42_1_db_usl_testing_subj_org_finval.pset at 2020-06-04 11:54:04
Finish mdw_load.regular.current.dev42_1_db_usl_testing_subj_org_finval.pset at 2020-06-04 11:56:51
Start Create_test_data.input.pset at 2020-06-04 11:56:51
Finish Create_test_data.input.pset at 2020-06-04 11:57:14
Start mdw_load.regular.current.dev42_1_db_usl_testing_subj_org_finval.pset at 2020-06-04 11:57:14
Finish mdw_load.regular.current.dev42_1_db_usl_testing_subj_org_finval.pset at 2020-06-04 11:59:55
Start Create_test_data.input.pset at 2020-06-04 11:59:55
Finish Create_test_data.input.pset at 2020-06-04 12:00:23
Start mdw_load.regular.current.dev42_1_db_usl_testing_subj_org_finval.pset at 2020-06-04 12:00:23
Finish mdw_load.regular.current.dev42_1_db_usl_testing_subj_org_finval.pset at 2020-06-04 12:03:23
Start Create_test_data.input.pset at 2020-06-04 12:03:23
Finish Create_test_data.input.pset at 2020-06-04 12:03:49
Start mdw_load.regular.current.dev42_1_db_usl_testing_subj_org_finval.pset at 2020-06-04 12:03:49
Finish mdw_load.regular.current.dev42_1_db_usl_testing_subj_org_finval.pset at 2020-06-04 12:06:46


Получается такая картина:
Graph Start time Finish time Length
Create_test_data.input.pset 04.06.2020 11:49:11 04.06.2020 11:49:37 00:00:26
mdw_load.day_one.current.
dev42_1_db_usl_testing_subj_org_finval.pset
04.06.2020 11:49:37 04.06.2020 11:50:42 00:01:05
Create_test_data.input.pset 04.06.2020 11:50:42 04.06.2020 11:51:06 00:00:24
mdw_load.regular.current.
dev42_1_db_usl_testing_subj_org_finval.pset
04.06.2020 11:51:06 04.06.2020 11:53:41 00:02:35
Create_test_data.input.pset 04.06.2020 11:53:41 04.06.2020 11:54:04 00:00:23
mdw_load.regular.current.
dev42_1_db_usl_testing_subj_org_finval.pset
04.06.2020 11:54:04 04.06.2020 11:56:51 00:02:47
Create_test_data.input.pset 04.06.2020 11:56:51 04.06.2020 11:57:14 00:00:23
mdw_load.regular.current.
dev42_1_db_usl_testing_subj_org_finval.pset
04.06.2020 11:57:14 04.06.2020 11:59:55 00:02:41
Create_test_data.input.pset 04.06.2020 11:59:55 04.06.2020 12:00:23 00:00:28
mdw_load.regular.current.
dev42_1_db_usl_testing_subj_org_finval.pset
04.06.2020 12:00:23 04.06.2020 12:03:23 00:03:00
Create_test_data.input.pset 04.06.2020 12:03:23 04.06.2020 12:03:49 00:00:26
mdw_load.regular.current.
dev42_1_db_usl_testing_subj_org_finval.pset
04.06.2020 12:03:49 04.06.2020 12:06:46 00:02:57

Видим, что 6 000 000 строк инкремента обрабатываются за 3 минуты, что достаточно быстро.
Данные в целевой таблице получились распределёнными следующим образом:
select valid_from_ts, valid_to_ts, count(1), min(sk), max(sk) from dev42_1_db_usl.TESTING_SUBJ_org_finval group by valid_from_ts, valid_to_ts order by 1,2;


Можно разглядеть соответствие вставленных данных моментам запуска графов.
Значит можно запускать в Ab Initio инкрементальную загрузку данных в GreenPlum с очень высокой частотой и наблюдать высокую скорость вставки этих данных в GreenPlum. Конечно, раз в секунду запускаться не получится, так как Ab Initio, как и любое ETL-средство, при запуске требует времени на раскачку.

Заключение


Сейчас Ab Initio используется в Сбербанке для построения Единого семантического слоя данных (ЕСС). Этот проект подразумевает построение единой версии состояния различных банковских бизнес-сущностей. Информация приходит из различных источников, реплики которых готовятся на Hadoop. Исходя из потребностей бизнеса, готовится модель данных и описываются трансформации данных. Ab Initio загружает информацию в ЕСС и загруженные данные не только представляют интерес для бизнеса сами по себе, но и служат источником для построения витрин данных. При этом функционал продукта позволяет использовать в качестве приёмника различные системы (Hive, Greenplum, Teradata, Oracle), что даёт возможность без особых усилий подготавливать данные для бизнеса в различных требуемых ему форматах.

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

Автор эксперт профессионального сообщества Сбербанка SberProfi DWH/BigData. Профессиональное сообщество SberProfi DWH/BigData отвечает за развитие компетенций в таких направлениях, как экосистема Hadoop, Teradata, Oracle DB, GreenPlum, а также BI инструментах Qlik, SAP BO, Tableau и др.
Подробнее..

10 приёмов работы с Oracle

01.10.2020 10:13:11 | Автор: admin
В Сбере есть несколько практик Oracle, которые могут оказаться вам полезны. Думаю, часть вам знакома, но мы используем для загрузки не только ETL-средства, но и хранимые процедуры Oracle. На Oracle PL/SQL реализованы наиболее сложные алгоритмы загрузки данных в хранилища, где требуется прочувствовать каждый байт.

  • Автоматическое журналирование компиляций
  • Как быть, если хочется сделать вьюшку с параметрами
  • Использование динамической статистики в запросах
  • Как сохранить план запроса при вставке данных через database link
  • Запуск процедур в параллельных сессиях
  • Протягивание остатков
  • Объединение нескольких историй в одну
  • Нормалайзер
  • Визуализация в формате SVG
  • Приложение поиска по метаданным Oracle


Автоматическое журналирование компиляций


На некоторых базах данных Oracle в Сбере стоит триггер на компиляцию, который запоминает, кто, когда и что менял в коде серверных объектов. Тем самым из таблицы журнала компиляций можно установить автора изменений. Также автоматически реализуется система контроля версий. Во всяком случае, если программист забыл сдать изменения в Git, то этот механизм его подстрахует. Опишем пример реализации такой системы автоматического журналирования компиляций. Один из упрощённых вариантов триггера на компиляцию, пишущего в журнал в виде таблицы ddl_changes_log, выглядит так:

create table DDL_CHANGES_LOG(  id               INTEGER,  change_date      DATE,  sid              VARCHAR2(100),  schemaname       VARCHAR2(30),  machine          VARCHAR2(100),  program          VARCHAR2(100),  osuser           VARCHAR2(100),  obj_owner        VARCHAR2(30),  obj_type         VARCHAR2(30),  obj_name         VARCHAR2(30),  previous_version CLOB,  changes_script   CLOB);create or replace trigger trig_audit_ddl_trg  before ddl on databasedeclare  v_sysdate              date;  v_valid                number;  v_previous_obj_owner   varchar2(30) := '';  v_previous_obj_type    varchar2(30) := '';  v_previous_obj_name    varchar2(30) := '';  v_previous_change_date date;  v_lob_loc_old          clob := '';  v_lob_loc_new          clob := '';  v_n                    number;  v_sql_text             ora_name_list_t;  v_sid                  varchar2(100) := '';  v_schemaname           varchar2(30) := '';  v_machine              varchar2(100) := '';  v_program              varchar2(100) := '';  v_osuser               varchar2(100) := '';begin  v_sysdate := sysdate;  -- find whether compiled object already presents and is valid  select count(*)    into v_valid    from sys.dba_objects   where owner = ora_dict_obj_owner     and object_type = ora_dict_obj_type     and object_name = ora_dict_obj_name     and status = 'VALID'     and owner not in ('SYS', 'SPOT', 'WMSYS', 'XDB', 'SYSTEM')     and object_type in ('TRIGGER', 'PROCEDURE', 'FUNCTION', 'PACKAGE', 'PACKAGE BODY', 'VIEW');  -- find information about previous compiled object  select max(obj_owner) keep(dense_rank last order by id),         max(obj_type) keep(dense_rank last order by id),         max(obj_name) keep(dense_rank last order by id),         max(change_date) keep(dense_rank last order by id)    into v_previous_obj_owner, v_previous_obj_type, v_previous_obj_name, v_previous_change_date    from ddl_changes_log;  -- if compile valid object or compile invalid package body broken by previous compilation of package then log it  if (v_valid = 1 or v_previous_obj_owner = ora_dict_obj_owner and     (v_previous_obj_type = 'PACKAGE' and ora_dict_obj_type = 'PACKAGE BODY' or     v_previous_obj_type = 'PACKAGE BODY' and ora_dict_obj_type = 'PACKAGE') and     v_previous_obj_name = ora_dict_obj_name and     v_sysdate - v_previous_change_date <= 1 / 24 / 60 / 2) and     ora_sysevent in ('CREATE', 'ALTER') then    -- store previous version of object (before compilation) from dba_source or dba_views in v_lob_loc_old    if ora_dict_obj_type <> 'VIEW' then      for z in (select substr(text, 1, length(text) - 1) || chr(13) || chr(10) as text                  from sys.dba_source                 where owner = ora_dict_obj_owner                   and type = ora_dict_obj_type                   and name = ora_dict_obj_name                 order by line) loop        v_lob_loc_old := v_lob_loc_old || z.text;      end loop;    else      select sys.dbms_metadata_util.long2clob(v.textlength, 'SYS.VIEW$', 'TEXT', v.rowid) into v_lob_loc_old        from sys."_CURRENT_EDITION_OBJ" o, sys.view$ v, sys.user$ u       where o.obj# = v.obj#         and o.owner# = u.user#         and u.name = ora_dict_obj_owner         and o.name = ora_dict_obj_name;    end if;    -- store new version of object (after compilation) from v_sql_text in v_lob_loc_new    v_n := ora_sql_txt(v_sql_text);    for i in 1 .. v_n loop      v_lob_loc_new := v_lob_loc_new || replace(v_sql_text(i), chr(10), chr(13) || chr(10));    end loop;    -- find information about session that changed this object    select max(to_char(sid)), max(schemaname), max(machine), max(program), max(osuser)      into v_sid, v_schemaname, v_machine, v_program, v_osuser      from v$session     where audsid = userenv('sessionid');    -- store changes in ddl_changes_log    insert into ddl_changes_log      (id, change_date, sid, schemaname, machine, program, osuser,       obj_owner, obj_type, obj_name, previous_version, changes_script)    values      (seq_ddl_changes_log.nextval, v_sysdate, v_sid, v_schemaname, v_machine, v_program, v_osuser,       ora_dict_obj_owner, ora_dict_obj_type, ora_dict_obj_name, v_lob_loc_old, v_lob_loc_new);  end if;exception  when others then    null;end;

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

Как быть, если хочется сделать вьюшку с параметрами


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

create table DIVISION_SALES(  division_id INTEGER,  dt          DATE,  sales_amt   NUMBER);

Такой запрос сравнивает продажи по подразделениям за два дня. В данном случае, 30.04.2020 и 11.09.2020.

select t1.division_id,       t1.dt          dt1,       t2.dt          dt2,       t1.sales_amt   sales_amt1,       t2.sales_amt   sales_amt2  from (select dt, division_id, sales_amt          from division_sales         where dt = to_date('30.04.2020', 'dd.mm.yyyy')) t1,       (select dt, division_id, sales_amt          from division_sales         where dt = to_date('11.09.2020', 'dd.mm.yyyy')) t2 where t1.division_id = t2.division_id;

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

create or replace view vw_division_sales_report(in_dt1 date, in_dt2 date) asselect t1.division_id,       t1.dt          dt1,       t2.dt          dt2,       t1.sales_amt   sales_amt1,       t2.sales_amt   sales_amt2  from (select dt, division_id, sales_amt          from division_sales         where dt = in_dt1) t1,       (select dt, division_id, sales_amt          from division_sales         where dt = in_dt2) t2 where t1.division_id = t2.division_id;

Предлагается такое обходное решение. Создадим тип под строку из этой вьюшки.

create type t_division_sales_report as object(  division_id INTEGER,  dt1         DATE,  dt2         DATE,  sales_amt1  NUMBER,  sales_amt2  NUMBER);

И создадим тип под таблицу из таких строк.

create type t_division_sales_report_table as table of t_division_sales_report;

Вместо вьюшки напишем pipelined функцию с входными параметрами-датами.

create or replace function func_division_sales(in_dt1 date, in_dt2 date)  return t_division_sales_report_table  pipelined asbegin  for z in (select t1.division_id,                   t1.dt          dt1,                   t2.dt          dt2,                   t1.sales_amt   sales_amt1,                   t2.sales_amt   sales_amt2              from (select dt, division_id, sales_amt                      from division_sales                     where dt = in_dt1) t1,                   (select dt, division_id, sales_amt                      from division_sales                     where dt = in_dt2) t2             where t1.division_id = t2.division_id) loop    pipe row(t_division_sales_report(z.division_id,                                     z.dt1,                                     z.dt2,                                     z.sales_amt1,                                     z.sales_amt2));  end loop;end;

Обращаться к ней можно так:

select *  from table(func_division_sales(to_date('30.04.2020', 'dd.mm.yyyy'),                                 to_date('11.09.2020', 'dd.mm.yyyy')));

Этот запрос и выдаст нам такой же результат, как запрос в начале этой заметки с явным образом подставленными датами.
Pipelined функции могут быть также полезны, если нужно передать параметр внутрь сложного запроса.
Например, рассмотрим сложную вьюшку, в которой поле field1, по которому хочется отфильтровать данные, запрятано где-то глубоко во вьюшке.

create or replace view complex_view as select field1, ...   from (select field1, ...           from (select field1, ... from deep_table), table1          where ...),        table2  where ...;

И запрос из вьюшки с фиксированным значением field1 может иметь плохой план выполнения.

select field1, ... from complex_view where field1 = 'myvalue';

Т.е. вместо того, чтобы сначала отфильтровать deep_table по условию field1 = 'myvalue', запрос может сначала соединить все таблицы, обработав излишне большой объём данных, а потом уже фильтровать результат по условию field1 = 'myvalue'. Такой сложности можно избежать, если сделать вместо вьюшки pipelined функцию с параметром, значение которого присваивается полю field1.

Использование динамической статистики в запросах


Бывает, что один и тот же запрос в БД Oracle обрабатывает всякий раз различный объём данных в использующихся в нём таблицах и подзапросах. Как заставить оптимизатор всякий раз понимать, какой из способов соединения таблиц на этот раз лучше и какие индексы использовать? Рассмотрим, например, запрос, который соединяет порцию изменившихся с последней загрузки остатков по счетам со справочником счетов. Порция изменившихся остатков по счетам сильно меняется от загрузки к загрузке, составляя то сотни строк, то миллионы строк. В зависимости от размера этой порции требуется соединять изменившиеся остатки со счетами то способом /*+ use_nl*/, то способом /*+ use_hash*/. Всякий раз повторно собирать статистику неудобно, особенно, если от загрузки к загрузке изменяется количество строк не в соединяемой таблице, а в соединяемом подзапросе. На помощь тут может прийти хинт /*+ dynamic_sampling()*/. Покажем, как он влияет, на примере запроса. Пусть таблица change_balances содержит изменения остатков, а accounts справочник счетов. Соединяем эти таблицы по полям account_id, имеющимся в каждой из таблиц. В начале эксперимента запишем в эти таблицы побольше строк и не будем менять их содержимое.
Сначала возьмём 10% изменений остатков в таблице change_balances и посмотрим, какой план будет с использованием dynamic_sampling:

SQL> EXPLAIN PLAN  2   SET statement_id = 'test1'  3   INTO plan_table  4  FOR  with c as  5   (select /*+ dynamic_sampling(change_balances 2)*/  6     account_id, balance_amount  7      from change_balances  8     where mod(account_id, 10) = 0)  9  select a.account_id, a.account_number, c.balance_amount 10    from c, accounts a 11   where c.account_id = a.account_id;Explained.SQL>SQL> SELECT * FROM table (DBMS_XPLAN.DISPLAY);Plan hash value: 874320301----------------------------------------------------------------------------------------------| Id  | Operation          | Name            | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |----------------------------------------------------------------------------------------------|   0 | SELECT STATEMENT   |                 |  9951K|   493M|       |   140K  (1)| 00:28:10 ||*  1 |  HASH JOIN         |                 |  9951K|   493M|  3240K|   140K  (1)| 00:28:10 ||*  2 |   TABLE ACCESS FULL| CHANGE_BALANCES |   100K|  2057K|       |  7172   (1)| 00:01:27 ||   3 |   TABLE ACCESS FULL| ACCOUNTS        |    10M|   295M|       |   113K  (1)| 00:22:37 |----------------------------------------------------------------------------------------------Predicate Information (identified by operation id):---------------------------------------------------   1 - access("ACCOUNT_ID"="A"."ACCOUNT_ID")   2 - filter(MOD("ACCOUNT_ID",10)=0)Note-----   - dynamic sampling used for this statement (level=2)20 rows selected.

Итак, видим, что предлагается пройти таблицы change_balances и accounts с помощью full scan и соединить их посредством hash join.
Теперь резко уменьшим выборку из change_balances. Возьмём 0.1% изменений остатков и посмотрим, какой план будет с использованием dynamic_sampling:

SQL> EXPLAIN PLAN  2   SET statement_id = 'test2'  3   INTO plan_table  4  FOR  with c as  5   (select /*+ dynamic_sampling(change_balances 2)*/  6     account_id, balance_amount  7      from change_balances  8     where mod(account_id, 1000) = 0)  9  select a.account_id, a.account_number, c.balance_amount 10    from c, accounts a 11   where c.account_id = a.account_id;Explained.SQL>SQL> SELECT * FROM table (DBMS_XPLAN.DISPLAY);Plan hash value: 2360715730-------------------------------------------------------------------------------------------------------| Id  | Operation                    | Name                   | Rows  | Bytes | Cost (%CPU)| Time     |-------------------------------------------------------------------------------------------------------|   0 | SELECT STATEMENT             |                        | 73714 |  3743K| 16452   (1)| 00:03:18 ||   1 |  NESTED LOOPS                |                        |       |       |            |          ||   2 |   NESTED LOOPS               |                        | 73714 |  3743K| 16452   (1)| 00:03:18 ||*  3 |    TABLE ACCESS FULL         | CHANGE_BALANCES        |   743 | 15603 |  7172   (1)| 00:01:27 ||*  4 |    INDEX RANGE SCAN          | IX_ACCOUNTS_ACCOUNT_ID |   104 |       |     2   (0)| 00:00:01 ||   5 |   TABLE ACCESS BY INDEX ROWID| ACCOUNTS               |    99 |  3069 |   106   (0)| 00:00:02 |-------------------------------------------------------------------------------------------------------Predicate Information (identified by operation id):---------------------------------------------------   3 - filter(MOD("ACCOUNT_ID",1000)=0)   4 - access("ACCOUNT_ID"="A"."ACCOUNT_ID")Note-----   - dynamic sampling used for this statement (level=2)22 rows selected.

На этот раз к таблице change_balances таблица accounts присоединяется посредством nested loops и используется индекс для чтения строк из accounts.
Если же хинт dynamic_sampling убрать, то во втором случае план останется такой же, как в первом случае, и это не оптимально.
Подробности о хинте dynamic_sampling и возможных значениях его числового аргумента можно найти в документации.

Как сохранить план запроса при вставке данных через database link


Решаем такую задачу. На сервере-источнике данных имеются таблицы, которые нужно соединить и загрузить в хранилище данных. Допустим, на сервере-источнике написана вьюшка, которая содержит в себе всю нужную ETL-логику преобразований. Вьюшка написана оптимально, в ней указаны хинты оптимизатору, подсказывающие, как соединять таблицы и какие индексы использовать. На стороне сервера хранилища данных нужно сделать несложную вещь вставить данные из вьюшки в целевую таблицу. И тут могут возникнуть сложности. Если вставку в целевую таблицу осуществить командой вида

insert into dwh_table  (field1, field2)  select field1, field2 from vw_for_dwh_table@xe_link;

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

SQL> EXPLAIN PLAN  2   SET statement_id = 'test'  3   INTO plan_table  4  FOR  insert into dwh_table  5    (field1, field2)  6    select field1, field2 from vw_for_dwh_table@xe_link;Explained.SQL>SQL> SELECT * FROM table (DBMS_XPLAN.DISPLAY);Plan hash value: 1788691278-------------------------------------------------------------------------------------------------------------| Id  | Operation                | Name             | Rows  | Bytes | Cost (%CPU)| Time     | Inst   |IN-OUT|-------------------------------------------------------------------------------------------------------------|   0 | INSERT STATEMENT         |                  |     1 |  2015 |     2   (0)| 00:00:01 |        |      ||   1 |  LOAD TABLE CONVENTIONAL | DWH_TABLE        |       |       |            |          |        |      ||   2 |   REMOTE                 | VW_FOR_DWH_TABLE |     1 |  2015 |     2   (0)| 00:00:01 | XE_LI~ | R->S |-------------------------------------------------------------------------------------------------------------Remote SQL Information (identified by operation id):----------------------------------------------------   2 - SELECT /*+ OPAQUE_TRANSFORM */ "FIELD1","FIELD2" FROM "VW_FOR_DWH_TABLE" "VW_FOR_DWH_TABLE"       (accessing 'XE_LINK' )16 rows selected.

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

declare  cursor cr is    select field1, field2 from vw_for_dwh_table@xe_link;  cr_row cr%rowtype;begin  open cr;  loop    fetch cr      into cr_row;    insert into dwh_table      (field1, field2)    values      (cr_row.field1, cr_row.field2);    exit when cr%notfound;  end loop;  close cr;end;

Запрос из курсора

select field1, field2 from vw_for_dwh_table@xe_link;

в отличие от вставки

insert into dwh_table  (field1, field2)  select field1, field2 from vw_for_dwh_table@xe_link;

сохранит план запроса, заложенный во вьюшку на сервере-источнике.

Запуск процедур в параллельных сессиях


Часто стоит задача запустить из некоторой родительской процедуры несколько параллельных расчётов и, дождавшись завершения каждого из них, продолжить выполнение родительской процедуры. Это может быть полезно при параллельных вычислениях, если ресурсы сервера позволяют это. Есть много способов сделать это.
Опишем очень простой вариант реализации такого механизма. Параллельные процедуры будем выполнять в параллельных одноразовых джобах, родительская же процедура в цикле будет ожидать завершения всех этих джобов.
Создадим таблицы с метаданными для этого механизма. Для начала сделаем таблицу с группами параллельно запускаемых процедур:

create table PARALLEL_PROC_GROUP_LIST(  group_id   INTEGER,  group_name VARCHAR2(4000));comment on column PARALLEL_PROC_GROUP_LIST.group_id  is 'Номер группы параллельно запускаемых процедур';comment on column PARALLEL_PROC_GROUP_LIST.group_name  is 'Название группы параллельно запускаемых процедур';

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

create table PARALLEL_PROC_LIST(  group_id    INTEGER,  proc_script VARCHAR2(4000),  is_active   CHAR(1) default 'Y');comment on column PARALLEL_PROC_LIST.group_id  is 'Номер группы параллельно запускаемых процедур';comment on column PARALLEL_PROC_LIST.proc_script  is 'Pl/sql блок с кодом процедуры';comment on column PARALLEL_PROC_LIST.is_active  is 'Y - active, N - inactive. С помощью этого поля можно временно отключать процедуру из группы';

И сделаем таблицу журнала, где будем собирать лог того, какая процедура когда в каком джобе запускалась:

create table PARALLEL_PROC_LOG(  run_id      INTEGER,  group_id    INTEGER,  proc_script VARCHAR2(4000),  job_id      INTEGER,  start_time  DATE,  end_time    DATE);comment on column PARALLEL_PROC_LOG.run_id  is 'Номер запуска процедуры run_in_parallel';comment on column PARALLEL_PROC_LOG.group_id  is 'Номер группы параллельно запускаемых процедур';comment on column PARALLEL_PROC_LOG.proc_script  is 'Pl/sql блок с кодом процедуры';comment on column PARALLEL_PROC_LOG.job_id  is 'Job_id джоба, в котором была запущена эта процедура';comment on column PARALLEL_PROC_LOG.start_time  is 'Время начала работы';comment on column PARALLEL_PROC_LOG.end_time  is 'Время окончания работы';create sequence Seq_Parallel_Proc_Log;

Теперь приведём код процедуры по запуску параллельных потоков:

create or replace procedure run_in_parallel(in_group_id integer) as  -- Процедура по параллельному запуску процедур из таблицы parallel_proc_list.  -- Параметр - номер группы из parallel_proc_list  v_run_id             integer;  v_job_id             integer;  v_job_id_list        varchar2(32767);  v_job_id_list_ext    varchar2(32767);  v_running_jobs_count integer;begin  select seq_parallel_proc_log.nextval into v_run_id from dual;  -- submit jobs with the same parallel_proc_list.in_group_id  -- store seperated with ',' JOB_IDs in v_job_id_list  v_job_id_list     := null;  v_job_id_list_ext := null;  for z in (select pt.proc_script              from parallel_proc_list pt             where pt.group_id = in_group_id               and pt.is_active = 'Y') loop    dbms_job.submit(v_job_id, z.proc_script);    insert into parallel_proc_log      (run_id, group_id, proc_script, job_id, start_time, end_time)    values      (v_run_id, in_group_id, z.proc_script, v_job_id, sysdate, null);    v_job_id_list     := v_job_id_list || ',' || to_char(v_job_id);    v_job_id_list_ext := v_job_id_list_ext || ' union all select ' ||                         to_char(v_job_id) || ' job_id from dual';  end loop;  commit;  v_job_id_list     := substr(v_job_id_list, 2);  v_job_id_list_ext := substr(v_job_id_list_ext, 12);  -- loop while not all jobs finished  loop    -- set parallel_proc_log.end_time for finished jobs    execute immediate 'update parallel_proc_log set end_time = sysdate where job_id in (' ||                      v_job_id_list_ext ||                      ' minus select job from user_jobs where job in (' ||                      v_job_id_list ||                      ') minus select job_id from parallel_proc_log where job_id in (' ||                      v_job_id_list || ') and end_time is not null)';    commit;    -- check whether all jobs finished    execute immediate 'select count(1) from user_jobs where job in (' ||                      v_job_id_list || ')'      into v_running_jobs_count;    -- if all jobs finished then exit    exit when v_running_jobs_count = 0;    -- sleep a little    sys.dbms_lock.sleep(0.1);  end loop;end;

Проверим, как работает процедура run_in_parallel. Создадим тестовую процедуру, которую будем вызывать в параллельных сессиях.

create or replace procedure sleep(in_seconds integer) asbegin  sys.Dbms_Lock.Sleep(in_seconds);end;

Заполним название группы и таблицу со cкриптами, которые будут выполняться параллельно.

insert into PARALLEL_PROC_GROUP_LIST(group_id, group_name) values(1, 'Тестовая группа');insert into PARALLEL_PROC_LIST(group_id, proc_script, is_active) values(1, 'begin sleep(5); end;', 'Y');insert into PARALLEL_PROC_LIST(group_id, proc_script, is_active) values(1, 'begin sleep(10); end;', 'Y');

Запустим группу параллельных процедур.

begin  run_in_parallel(1);end;

По завершении посмотрим лог.

select * from PARALLEL_PROC_LOG;

RUN_ID GROUP_ID PROC_SCRIPT JOB_ID START_TIME END_TIME
1 1 begin sleep(5); end; 1 11.09.2020 15:00:51 11.09.2020 15:00:56
1 1 begin sleep(10); end; 2 11.09.2020 15:00:51 11.09.2020 15:01:01

Видим, что время выполнения экземпляров тестовой процедуры соответствует ожиданиям.

Протягивание остатков


Опишем вариант решения достаточно типовой банковской задачи по протягиванию остатков. Допустим, имеется таблица фактов изменения остатков по счетам. Требуется на каждый день календаря указать актуальный остаток по счёту (последний за день). Такая информация часто бывает нужна в хранилищах данных. Если в какой-то день не было движений по счёту, то нужно повторить последний известный остаток. Если объёмы данных и вычислительные мощности сервера позволяют, то можно решить такую задачу с помощью SQL-запроса, даже не прибегая к PL/SQL. Поможет нам в этом функция last_value(* ignore nulls) over(partition by * order by *), которая протянет последний известный остаток на последующие даты, в которых не было изменений.
Создадим таблицу и заполним её тестовыми данными.

create table ACCOUNT_BALANCE(  dt           DATE,  account_id   INTEGER,  balance_amt  NUMBER,  turnover_amt NUMBER);comment on column ACCOUNT_BALANCE.dt  is 'Дата и время остатка по счёту';comment on column ACCOUNT_BALANCE.account_id  is 'Номер счёта';comment on column ACCOUNT_BALANCE.balance_amt  is 'Остаток по счёту';comment on column ACCOUNT_BALANCE.turnover_amt  is 'Оборот по счёту';insert into account_balance(dt, account_id, balance_amt, turnover_amt) values(to_date('01.01.2020 00:00:00','dd.mm.yyyy hh24.mi.ss'), 1, 23, 23);insert into account_balance(dt, account_id, balance_amt, turnover_amt) values(to_date('05.01.2020 01:00:00','dd.mm.yyyy hh24.mi.ss'), 1, 45, 22);insert into account_balance(dt, account_id, balance_amt, turnover_amt) values(to_date('05.01.2020 20:00:00','dd.mm.yyyy hh24.mi.ss'), 1, 44, -1);insert into account_balance(dt, account_id, balance_amt, turnover_amt) values(to_date('05.01.2020 00:00:00','dd.mm.yyyy hh24.mi.ss'), 2, 67, 67);insert into account_balance(dt, account_id, balance_amt, turnover_amt) values(to_date('05.01.2020 20:00:00','dd.mm.yyyy hh24.mi.ss'), 2, 77, 10);insert into account_balance(dt, account_id, balance_amt, turnover_amt) values(to_date('07.01.2020 00:00:00','dd.mm.yyyy hh24.mi.ss'), 2, 72, -5);

Нижеприведённый запрос решает нашу задачу. Подзапрос cld содержит календарь дат, в подзапросе ab группируем остатки за каждый день, в подзапросе a запоминаем перечень всех счетов и дату начала истории по каждому счёту, в подзапросе pre для каждого счёта составляем календарь дней с начала его истории. Финальный запрос присоединяет к календарю дней активности каждого счёта последние остатки на каждый день и протягивает их на дни, в которых не было изменений.

with cld as (select /*+ materialize*/   to_date('01.01.2020', 'dd.mm.yyyy') + level - 1 dt    from dual  connect by level <= 10),ab as (select trunc(dt) dt,         account_id,         max(balance_amt) keep(dense_rank last order by dt) balance_amt,         sum(turnover_amt) turnover_amt    from account_balance   group by trunc(dt), account_id),a as (select min(dt) min_dt, account_id from ab group by account_id),pre as (select cld.dt, a.account_id from cld left join a on cld.dt >= a.min_dt)select pre.dt,       pre.account_id,       last_value(ab.balance_amt ignore nulls) over(partition by pre.account_id order by pre.dt) balance_amt,       nvl(ab.turnover_amt, 0) turnover_amt  from pre  left join ab    on pre.dt = ab.dt   and pre.account_id = ab.account_id order by 2, 1;

Результат запроса соответствует ожиданиям.
DT ACCOUNT_ID BALANCE_AMT TURNOVER_AMT
01.01.2020 1 23 23
02.01.2020 1 23 0
03.01.2020 1 23 0
04.01.2020 1 23 0
05.01.2020 1 44 21
06.01.2020 1 44 0
07.01.2020 1 44 0
08.01.2020 1 44 0
09.01.2020 1 44 0
10.01.2020 1 44 0
05.01.2020 2 77 77
06.01.2020 2 77 0
07.01.2020 2 72 -5
08.01.2020 2 72 0
09.01.2020 2 72 0
10.01.2020 2 72 0

Объединение нескольких историй в одну


При загрузке данных в хранилища часто решается задача, когда нужно выстроить единую историю по сущности, имея отдельные истории атрибутов этой сущности, пришедшие из различных источников. Допустим, имеется некоторая сущность с первичным ключом primary_key_id, о которой известна история (start_dt end_dt) трёх её различных атрибутов, расположенная в трёх различных таблицах.

create table HIST1(  primary_key_id INTEGER,  start_dt       DATE,  attribute1     NUMBER);insert into HIST1(primary_key_id, start_dt, attribute1) values(1, to_date('2014-01-01','yyyy-mm-dd'), 7);insert into HIST1(primary_key_id, start_dt, attribute1) values(1, to_date('2015-01-01','yyyy-mm-dd'), 8);insert into HIST1(primary_key_id, start_dt, attribute1) values(1, to_date('2016-01-01','yyyy-mm-dd'), 9);insert into HIST1(primary_key_id, start_dt, attribute1) values(2, to_date('2014-01-01','yyyy-mm-dd'), 17);insert into HIST1(primary_key_id, start_dt, attribute1) values(2, to_date('2015-01-01','yyyy-mm-dd'), 18);insert into HIST1(primary_key_id, start_dt, attribute1) values(2, to_date('2016-01-01','yyyy-mm-dd'), 19);create table HIST2(  primary_key_id INTEGER,  start_dt       DATE,  attribute2     NUMBER); insert into HIST2(primary_key_id, start_dt, attribute2) values(1, to_date('2015-01-01','yyyy-mm-dd'), 4);insert into HIST2(primary_key_id, start_dt, attribute2) values(1, to_date('2016-01-01','yyyy-mm-dd'), 5);insert into HIST2(primary_key_id, start_dt, attribute2) values(1, to_date('2017-01-01','yyyy-mm-dd'), 6);insert into HIST2(primary_key_id, start_dt, attribute2) values(2, to_date('2015-01-01','yyyy-mm-dd'), 14);insert into HIST2(primary_key_id, start_dt, attribute2) values(2, to_date('2016-01-01','yyyy-mm-dd'), 15);insert into HIST2(primary_key_id, start_dt, attribute2) values(2, to_date('2017-01-01','yyyy-mm-dd'), 16);create table HIST3(  primary_key_id INTEGER,  start_dt       DATE,  attribute3     NUMBER); insert into HIST3(primary_key_id, start_dt, attribute3) values(1, to_date('2016-01-01','yyyy-mm-dd'), 10);insert into HIST3(primary_key_id, start_dt, attribute3) values(1, to_date('2017-01-01','yyyy-mm-dd'), 20);insert into HIST3(primary_key_id, start_dt, attribute3) values(1, to_date('2018-01-01','yyyy-mm-dd'), 30);insert into HIST3(primary_key_id, start_dt, attribute3) values(2, to_date('2016-01-01','yyyy-mm-dd'), 110);insert into HIST3(primary_key_id, start_dt, attribute3) values(2, to_date('2017-01-01','yyyy-mm-dd'), 120);insert into HIST3(primary_key_id, start_dt, attribute3) values(2, to_date('2018-01-01','yyyy-mm-dd'), 130);

Целью является загрузка единой истории изменения трёх атрибутов в одну таблицу.
Ниже приведён запрос, решающий такую задачу. В нём сначала формируется диагональная таблица q1 с данными из разных источников по разным атрибутам (отсутствующие в источнике атрибуты заполняются null-ами). Затем с помощью функции last_value(* ignore nulls) диагональная таблица схлопывается в единую историю, а последние известные значения атрибутов протягиваются вперёд на те даты, в которые изменений по ним не было:

select primary_key_id,       start_dt,       nvl(lead(start_dt - 1)           over(partition by primary_key_id order by start_dt),           to_date('9999-12-31', 'yyyy-mm-dd')) as end_dt,       last_value(attribute1 ignore nulls) over(partition by primary_key_id order by start_dt) as attribute1,       last_value(attribute2 ignore nulls) over(partition by primary_key_id order by start_dt) as attribute2,       last_value(attribute3 ignore nulls) over(partition by primary_key_id order by start_dt) as attribute3  from (select primary_key_id,               start_dt,               max(attribute1) as attribute1,               max(attribute2) as attribute2,               max(attribute3) as attribute3          from (select primary_key_id,                       start_dt,                       attribute1,                       cast(null as number) attribute2,                       cast(null as number) attribute3                  from hist1                union all                select primary_key_id,                       start_dt,                       cast(null as number) attribute1,                       attribute2,                       cast(null as number) attribute3                  from hist2                union all                select primary_key_id,                       start_dt,                       cast(null as number) attribute1,                       cast(null as number) attribute2,                       attribute3                  from hist3) q1         group by primary_key_id, start_dt) q2 order by primary_key_id, start_dt;

Результат получается такой:
PRIMARY_KEY_ID START_DT END_DT ATTRIBUTE1 ATTRIBUTE2 ATTRIBUTE3
1 01.01.2014 31.12.2014 7 NULL NULL
1 01.01.2015 31.12.2015 8 4 NULL
1 01.01.2016 31.12.2016 9 5 10
1 01.01.2017 31.12.2017 9 6 20
1 01.01.2018 31.12.9999 9 6 30
2 01.01.2014 31.12.2014 17 NULL NULL
2 01.01.2015 31.12.2015 18 14 NULL
2 01.01.2016 31.12.2016 19 15 110
2 01.01.2017 31.12.2017 19 16 120
2 01.01.2018 31.12.9999 19 16 130

Нормалайзер


Иногда встаёт задача о нормализации данных, пришедших в формате поля с разделителями. Например, в виде такой таблицы:

create table DENORMALIZED_TABLE(  id  INTEGER,  val VARCHAR2(4000));insert into DENORMALIZED_TABLE(id, val) values(1, 'aaa,cccc,bb');insert into DENORMALIZED_TABLE(id, val) values(2, 'ddd');insert into DENORMALIZED_TABLE(id, val) values(3, 'fffff,e');

Такой запрос нормализует данные, расклеив соединённые запятой поля в виде нескольких строк:

select id, regexp_substr(val, '[^,]+', 1, column_value) val, column_value  from denormalized_table,       table(cast(multiset                  (select level                     from dual                   connect by regexp_instr(val, '[^,]+', 1, level) > 0) as                  sys.odcinumberlist)) order by id, column_value;

Результат получается такой:
ID VAL COLUMN_VALUE
1 aaa 1
1 cccc 2
1 bb 3
2 ddd 1
3 fffff 1
3 e 2

Визуализация в формате SVG


Часто возникает желание как-то визуализировать числовые показатели, хранящиеся в базе данных. Например, построить графики, гистограммы, диаграммы. В этом могут помочь специализированные средства, например, Oracle BI. Но лицензии на эти средства могут стоить денег, а настройка их может занять больше времени, чем написание на коленке SQL-запроса к Oracle, который выдаст готовую картинку. Продемонстрируем на примере, как с помощью запроса быстро нарисовать такую картинку в формате SVG.
Предположим, у нас есть таблица с данными

create table graph_data(dt date, val number, radius number);insert into graph_data(dt, val, radius) values (to_date('01.01.2020','dd.mm.yyyy'), 12, 3);insert into graph_data(dt, val, radius) values (to_date('02.01.2020','dd.mm.yyyy'), 15, 4);insert into graph_data(dt, val, radius) values (to_date('05.01.2020','dd.mm.yyyy'), 17, 5);insert into graph_data(dt, val, radius) values (to_date('06.01.2020','dd.mm.yyyy'), 13, 6);insert into graph_data(dt, val, radius) values (to_date('08.01.2020','dd.mm.yyyy'),  3, 7);insert into graph_data(dt, val, radius) values (to_date('10.01.2020','dd.mm.yyyy'), 20, 8);insert into graph_data(dt, val, radius) values (to_date('11.01.2020','dd.mm.yyyy'), 18, 9);

dt это дата актуальности,
val это числовой показатель, динамику которого по времени мы визуализируем,
radius это ещё один числовой показатель, который будем рисовать в виде кружка с таким радиусом.
Скажем пару слов о формате SVG. Это формат векторной графики, который можно смотреть в современных браузерах и конвертировать в другие графические форматы. В нём, среди прочего, можно рисовать линии, кружки и писать текст:

<line x1="94" x2="94" y1="15" y2="675" style="stroke:rgb(150,255,255); stroke-width:1px"/><circle cx="30" cy="279" r="3" style="fill:rgb(255,0,0)"/><text x="7" y="688" font-size="10" fill="rgb(0,150,255)">2020-01-01</text>

Ниже SQL-запрос к Oracle, который строит график из данных в этой таблице. Здесь подзапрос const содержит различные константные настройки размеры картинки, количество меток на осях графика, цвета линий и кружочков, размеры шрифта и т.д. В подзапросе gd1 мы приводим данные из таблицы graph_data к координатам x и y на рисунке. Подзапрос gd2 запоминает предыдущие по времени точки, из которых нужно вести линии к новым точкам. Блок header это заголовок картинки с белым фоном. Блок vertical lines рисует вертикальные линии. Блок dates under vertical lines подписывает даты на оси x. Блок horizontal lines рисует горизонтальные линии. Блок values near horizontal lines подписывает значения на оси y. Блок circles рисует кружочки указанного в таблице graph_data радиуса. Блок graph data строит из линий график динамики показателя val из таблицы graph_data. Блок footer добавляет замыкающий тэг.

with const as (select 700 viewbox_width,         700 viewbox_height,         30 left_margin,         30 right_margin,         15 top_margin,         25 bottom_margin,         max(dt) - min(dt) + 1 num_vertical_lines,         11 num_horizontal_lines,         'rgb(150,255,255)' stroke_vertical_lines,         '1px' stroke_width_vertical_lines,         10 font_size_dates,         'rgb(0,150,255)' fill_dates,         23 x_dates_pad,         13 y_dates_pad,         'rgb(150,255,255)' stroke_horizontal_lines,         '1px' stroke_width_horizontal_lines,         10 font_size_values,         'rgb(0,150,255)' fill_values,         4 x_values_pad,         2 y_values_pad,         'rgb(255,0,0)' fill_circles,         'rgb(51,102,0)' stroke_graph,         '1px' stroke_width_graph,         min(dt) min_dt,         max(dt) max_dt,         max(val) max_val    from graph_data),gd1 as (select graph_data.dt,         const.left_margin +         (const.viewbox_width - const.left_margin - const.right_margin) *         (graph_data.dt - const.min_dt) / (const.max_dt - const.min_dt) x,         const.viewbox_height - const.bottom_margin -         (const.viewbox_height - const.top_margin - const.bottom_margin) *         graph_data.val / const.max_val y,         graph_data.radius    from graph_data, const),gd2 as (select dt,         round(nvl(lag(x) over(order by dt), x)) prev_x,         round(x) x,         round(nvl(lag(y) over(order by dt), y)) prev_y,         round(y) y,         radius    from gd1)/* header */select '<?xml version="1.0" encoding="UTF-8" standalone="no"?>' txt  from dualunion allselect '<svg version="1.1" width="' || viewbox_width || '" height="' ||       viewbox_height || '" viewBox="0 0 ' || viewbox_width || ' ' ||       viewbox_height ||       '" style="background:yellow" baseProfile="full" xmlns="http://personeltest.ru/away/www.w3.org/2000/svg" xmlns:xlink="http://personeltest.ru/away/www.w3.org/1999/xlink" xmlns:ev="http://personeltest.ru/away/www.w3.org/2001/xml-events">'  from constunion allselect '<title>Test graph</title>'  from dualunion allselect '<desc>Test graph</desc>'  from dualunion allselect '<rect width="' || viewbox_width || '" height="' || viewbox_height ||       '" style="fill:white" />'  from constunion all/* vertical lines */select '<line x1="' ||       to_char(round(left_margin +                     (viewbox_width - left_margin - right_margin) *                     (level - 1) / (num_vertical_lines - 1))) || '" x2="' ||       to_char(round(left_margin +                     (viewbox_width - left_margin - right_margin) *                     (level - 1) / (num_vertical_lines - 1))) || '" y1="' ||       to_char(round(top_margin)) || '" y2="' ||       to_char(round(viewbox_height - bottom_margin)) || '" style="stroke:' ||       const.stroke_vertical_lines || '; stroke-width:' ||       const.stroke_width_vertical_lines || '"/>'  from constconnect by level <= num_vertical_linesunion all/* dates under vertical lines */select '<text x="' ||       to_char(round(left_margin +                     (viewbox_width - left_margin - right_margin) *                     (level - 1) / (num_vertical_lines - 1) - x_dates_pad)) ||       '" y="' ||       to_char(round(viewbox_height - bottom_margin + y_dates_pad)) ||       '" font-size="' || font_size_dates || '" fill="' || fill_dates || '">' ||       to_char(min_dt + level - 1, 'yyyy-mm-dd') || '</text>'  from constconnect by level <= num_vertical_linesunion all/* horizontal lines */select '<line x1="' || to_char(round(left_margin)) || '" x2="' ||       to_char(round(viewbox_width - right_margin)) || '" y1="' ||       to_char(round(top_margin +                     (viewbox_height - top_margin - bottom_margin) *                     (level - 1) / (num_horizontal_lines - 1))) || '" y2="' ||       to_char(round(top_margin +                     (viewbox_height - top_margin - bottom_margin) *                     (level - 1) / (num_horizontal_lines - 1))) ||       '" style="stroke:' || const.stroke_horizontal_lines ||       '; stroke-width:' || const.stroke_width_horizontal_lines || '"/>'  from constconnect by level <= num_horizontal_linesunion all/* values near horizontal lines */select '<text text-anchor="end" x="' ||       to_char(round(left_margin - x_values_pad)) || '" y="' ||       to_char(round(viewbox_height - bottom_margin -                     (viewbox_height - top_margin - bottom_margin) *                     (level - 1) / (num_horizontal_lines - 1) +                     y_values_pad)) || '" font-size="' || font_size_values ||       '" fill="' || fill_values || '">' ||       to_char(round(max_val / (num_horizontal_lines - 1) * (level - 1), 2)) ||       '</text>'  from constconnect by level <= num_horizontal_linesunion all/* circles */select '<circle cx="' || to_char(gd2.x) || '" cy="' || to_char(gd2.y) ||       '" r="' || gd2.radius || '" style="fill:' || const.fill_circles ||       '"/>'  from gd2, constunion all/* graph data */select '<line x1="' || to_char(gd2.prev_x) || '" x2="' || to_char(gd2.x) ||       '" y1="' || to_char(gd2.prev_y) || '" y2="' || to_char(gd2.y) ||       '" style="stroke:' || const.stroke_graph || '; stroke-width:' ||       const.stroke_width_graph || '"/>'  from gd2, constunion all/* footer */select '</svg>' from dual;

Результат запроса можно сохранить в файл с расширением *.svg и посмотреть в браузере. При желании можно с помощью какой-либо из утилит конвертировать его в другие графические форматы, размещать на веб-страницах своего приложения и т.д.
Получилась такая картинка:



Приложение поиска по метаданным Oracle


Представьте себе, что стоит задача найти что-либо в исходном коде на Oracle, поискав информацию сразу на нескольких серверах. Речь идёт о поиске по объектам словаря данных Oracle. Рабочим местом для поиска является веб-интерфейс, куда пользователь-программист вводит искомую строку и выбирает галочками, на каких серверах Oracle осуществить этот поиск.
Веб-поисковик умеет искать строку по серверным объектам Oracle одновременно в нескольких различных базах данных банка. Например, можно поискать:
  • Где в коде Oracle зашита константа 61209, представляющая собой номер счёта второго порядка?
  • Где в коде и на каких серверах используется таблица accounts (в т.ч. через database link)?
  • С какого сервера из какой хранимой процедуры или триггера приходит сгенерированная программистом ошибка, например, ORA-20001 Курс валюты не найден?
  • Прописан ли планируемый к удалению индекс IX_CLIENTID где-либо явным образом в хинтах оптимизатора в SQL-запросах?
  • Используются ли где-либо (в т.ч. через database link) планируемые к удалению таблица, поле, процедура, функция и т.д.?
  • Где в коде явно зашит чей-то е-мэйл или номер телефона? Такие вещи лучше выносить из серверных объектов в настроечные таблицы.
  • Где в коде на серверах используется зависящий от версии Oracle функционал? Например, функция wm_concat выдаёт различный тип данных на выходе в зависимости от версии Oracle. Это может быть критично и требует внимания при миграции на более новую версию.
  • Где в коде используется какой-либо редкий приём, на который программисту хочется посмотреть, как на образец? Например, поискать в коде Oracle примеры использования функций sys_connect_by_path, regexp_instr или хинта push_subq.

По результатам поиска пользователю выдаётся информация, на каком сервере в коде каких функций, процедур, пакетов, триггеров, вьюшек и т.п. найдены требуемые результаты.
Опишем, как реализован такой поисковик.
Клиентская часть не сложная. Веб-интерфейс получает введённую пользователем поисковую строку, список серверов для поиска и логин пользователя. Веб-страница передаёт их в хранимую процедуру Oracle на сервере-обработчике. История обращений к поисковику, т.е. кто какой запрос выполнял, на всякий случай журналируется.
Получив поисковый запрос, серверная часть на поисковом сервере Oracle запускает в параллельных джобах несколько процедур, которые по database links на выбранных серверах Oracle сканируют следующие представления словаря данных в поисках искомой строки: dba_col_comments, dba_jobs, dba_mviews, dba_objects, dba_scheduler_jobs, dba_source, dba_tab_cols, dba_tab_comments, dba_views. Каждая из процедур, если что-то обнаружила, записывает найденное в таблицу результатов поиска (с соответствующим ID поискового запроса). Когда все поисковые процедуры завершили работу, клиентская часть выдаёт пользователю всё, что записалось в таблицу результатов поиска с соответствующим ID поискового запроса.
Но это ещё не всё. Помимо поиска по словарю данных Oracle в описанный механизм прикрутили ещё и поиск по репозиторию Informatica PowerCenter. Informatica PowerCenter является популярным ETL-средством, использующимся в Сбербанке при загрузке различной информации в хранилища данных. Informatica PowerCenter имеет открытую хорошо задокументированную структуру репозитория. По этому репозиторию есть возможность искать информацию так же, как и по словарю данных Oracle. Какие таблицы и поля используются в коде загрузок, разработанном на Informatica PowerCenter? Что можно найти в трансформациях портов и явных SQL-запросах? Вся эта информация имеется в структурах репозитория и может быть найдена. Для знатоков PowerCenter напишу, что наш поисковик сканирует следующие места репозитория в поисках маппингов, сессий или воркфловов, содержащих в себе где-то искомую строку: sql override, mapplet attributes, ports, source definitions in mappings, source definitions, target definitions in mappings, target_definitions, mappings, mapplets, workflows, worklets, sessions, commands, expression ports, session instances, source definition fields, target definition fields, email tasks.

Автор: Михаил Гричик, эксперт профессионального сообщества Сбербанка SberProfi DWH/BigData.

Профессиональное сообщество SberProfi DWH/BigData отвечает за развитие компетенций в таких направлениях, как экосистема Hadoop, Teradata, Oracle DB, GreenPlum, а также BI инструментах Qlik, SAP BO, Tableau и др.
Подробнее..

Как мы, сотрудники Сбера, считаем и инвестируем свои деньги

19.10.2020 10:13:52 | Автор: admin


Нужно ли покупать автомобиль за 750 тысяч рублей при том, что вы ездите 18 раз в месяц или дешевле пользоваться такси? Если вы работаете на заднем сидении или слушаете музыку как это меняет оценку? Как правильнее покупать квартиру в какой момент оптимально заканчивать копить на депозите и делать первый взнос по ипотеке? Или даже тривиальный вопрос: выгоднее положить деньги на депозит под 6% с ежемесячной капитализацией или под 6,2% с ежегодной капитализацией? Большинство людей даже не пытается производить такие подсчёты и даже не хотят собирать детальную информацию о своих деньгах. Вместо подсчётов подключают чувства и эмоции. Либо делают какую-то узкую оценку, например, детально подсчитывают годовую стоимость владения автомобилем, в то время как все эти расходы могут составлять лишь 5% от общих трат (а траты на другие стороны жизни при этом не подсчитывают). Мозг человека подвержен когнитивным искажениям. Например, сложно бросить, несмотря на неокупаемость, дело, в которое вложены масса времени и денег. Люди обычно излишне оптимистичны и недооценивают риски, а также легко внушаемы и могут купить дорогую безделушку или вложиться в финансовую пирамиду.
Понятное дело, в случае банка эмоциональная оценка не работает. Поэтому я хочу сначала рассказать о том, как оценивает деньги обычное физлицо (я, в том числе), и как это делает банк. Ниже будет немного финансового ликбеза и много про аналитику данных в Сбербанке для всего банка в целом.
Полученные выводы приведены только в качестве примера и не могут расцениваться как рекомендации для частных инвесторов, поскольку не учитывают множества факторов, оставшихся за рамками данной статьи.
Например, любое событие типа черный лебедь в макроэкономике, в корпоративном управлении любой из компаний и пр., может привести к кардинальным изменениям.

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

  • не безразлично, сколько у вас накопилось имущества и как вести его учёт
  • интересно, как заставить своё имущество приносить вам дополнительный доход
  • хочется понять, какой же из способов вложения денег лучший: недвижимость, депозиты или акции
  • любопытно, что посоветует по этому вопросу анализ данных Сбербанка

Часто люди принимают финансовые решения, не располагая полной информацией о динамике своих собственных доходов и расходов, не имея оценки стоимости собственного имущества, не учитывая в расчётах инфляцию и т.п.
Иногда люди допускают ошибки, например, берут кредит, думая, что смогут его выплачивать, а затем не справляются. При этом ответ на вопрос, осилит ли человек обслуживание кредита, зачастую известен заранее. Нужно просто знать, сколько зарабатываешь, сколько тратишь, какова динамика изменений этих показателей.
Или, например, человек получает какую-то зарплату на работе, её периодически повышают, преподнося, как оценку заслуг. А в действительности по сравнению с инфляцией заработок этого человека может падать, и он может этого не осознавать, если не ведёт учёт доходов.
Некоторые люди не могут оценить, какой выбор выгоднее в сложившейся у них ситуации: снимать квартиру или взять ипотеку под такую-то ставку.
И вместо того, чтобы произвести подсчёт, каковы будут расходы в том и ином случае, как-то монетизировав в расчётах нефинансовые показатели (выгоду от московской прописки оцениваю в M рублей в месяц, удобство от проживания в снимаемой возле работы квартире оцениваю в N рублей в месяц), люди бегут в интернет дискутировать с собеседниками, у которых может быть иная финансовая ситуация и другие приоритеты в оценке нефинансовых показателей.
Я за ответственное финансовое планирование. Прежде всего, предлагается осуществлять сбор следующих данных о собственном финансовом положении:

  • учёт и оценка всего имеющегося в наличии имущества
  • учёт доходов и расходов, а также разницы между доходами и расходами, т.е. динамики накопления имущества

Учёт и оценка всего имеющегося в наличии имущества


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

Оцените, что у вас есть:

  • недвижимость
  • земельные участки
  • транспортные средства
  • вклады в банках
  • кредитные обязательства (с минусом)
  • инвестиции (акции, облигации, ...)
  • стоимость собственного бизнеса
  • прочее имущество

Среди имущества можно отметить ликвидную долю, которую возможно достаточно быстро отозвать и конвертировать в другие формы. Например, долю в квартире, которой владеешь совместно с проживающими в ней родственниками, можно отнести к неликвидному имуществу. Долгосрочные вложения во вклады или акции, которые нельзя отозвать без потерь, также можно считать неликвидными. В свою очередь, недвижимость, которой владеешь, но не проживаешь, транспортные средства, краткосрочные и отзываемые вклады можно отнести к ликвидному имуществу. Например, если понадобятся деньги на срочное лечение, то польза от каких-то инструментов примерно нулевая, поэтому ликвидная доля более ценна.
Далее, среди имущества можно выделить неприбыльное и прибыльное. Например, недвижимость, которая не сдаётся в аренду, а также транспортные средства можно расценить как неприбыльные. А сдаваемые в аренду объекты недвижимости, вложенные под ставку выше инфляции депозиты и акции прибыльное имущество.
Получится, например, такая картинка (данные сгенерированы случайным образом):



У многих людей подобная картинка выглядит сильно перекошенным образом. Например, бедная бабушка может жить в дорогой квартире в Москве, не приносящей прибыль, при этом жить впроголодь от пенсии до пенсии, не помышляя о реструктуризации своего имущества. Ей было бы разумно обменяться квартирами с внуком с доплатой. Наоборот, некий инвестор может столь увлечься инвестированием в акции, что не иметь других видов имущества на чёрный день, что может быть рискованно. Можно нарисовать такую картинку о своём имуществе и задуматься, не разумно ли переложить имущество более прибыльным образом.

Учёт доходов, расходов и динамики накопления имущества


Предлагается регулярно записывать в электронной форме свои доходы и расходы. В эпоху интернет-банков это не требует особых усилий. При этом доходы и расходы можно делить по категориям. Далее, агрегируя их по годам, можно делать выводы об их динамике. Важно учитывать инфляцию, чтобы иметь представление о том, как выглядят суммы за прошлые годы в современных ценах. У каждого своя потребительская корзина. Бензин и еда дорожают с разной скоростью. Но посчитать свою личную инфляцию достаточно сложно. Поэтому, с некоторой погрешностью, можно пользоваться данными об официальном уровне инфляции.
Данные по ежемесячной инфляции имеются во многих открытых источниках, в том числе загружены и в озеро данных Сбербанка.
Пример визуализации динамики доходов-расходов (данные сгенерированы случайным образом, динамика инфляции настоящая):



Имея такую полную картину, можно делать выводы о своём реальном росте/снижении доходов и реальном росте/снижении накоплений, анализировать динамику расходов по категориям и принимать обоснованные финансовые решения.
Какой способ вложения свободных денежных средств обыгрывает инфляцию и приносит наибольший пассивный доход?
В озере данных Сбербанка есть ценные данные на эту тему:

  • динамика стоимости квадратного метра в Москве
  • база предложений по продаже и аренде недвижимости в Москве и ближнем Подмосковье
  • динамика средней годовой процентной ставки по депозитам
  • динамика уровня рублёвой инфляции
  • динамика индекса Мосбиржи полной доходности брутто (MCFTR)
  • котировки акций Мосбиржи и данные о выплаченных дивидендах

Эти данные позволят нам сравнить доходность и риски от вложений в сдаваемую в аренду недвижимость, в банковские депозиты и в рынок акций. При этом не забудем учесть инфляцию.
Сразу скажу, что в этом посте мы занимаемся исключительно анализом данных и не прибегаем к использованию каких-либо экономических теорий. Просто посмотрим, что говорят наши данные какой способ сохранить и преумножить сбережения в России за последние годы дал наилучший результат.
Кратко расскажем, о том, как собираются и анализируются данные, использующиеся в этой статье, и прочие данные в Сбербанке. Имеется слой реплик источников, которые хранятся в формате parquet на hadoop. Используются как внутренние источники (различные АС банка), так и внешние. Реплики источников собираются разными способами. Есть продукт stork, в основе которого лежит spark, набирает обороты и второй продукт Ab Initio AIR. Реплики источников загружаются на различные кластеры hadoop под управлением Cloudera, в том числе могут быть прилинкованы с одного кластера на другой. Кластеры разделены преимущественно по бизнес-блокам, имеются также и кластеры Лаборатории данных. На базе реплик источников строятся различные витрины данных, доступные бизнес-пользователям и data scientist-ам. Для написания этой статьи были использованы различные приложения spark, запросы к hive, приложения по анализу данных и визуализации результатов в формате графики SVG.

Исторический анализ рынка недвижимости


Анализ показывает, что недвижимость в долгосрочной перспективе растёт пропорционально инфляции, т.е. в реальных ценах не растёт и не снижается. Приведём графики динамики цен на жилую недвижимость в Москве, демонстрирующий имеющиеся исходные данные.

График цен в рублях без учёта инфляции:



График цен в рублях с учётом инфляции (в современных ценах):



Видим, что исторически цена колебалась около 200 000 руб./кв.м. в современных ценах и изменчивость была достаточно низкая.

Сколько же процентов годовых сверх инфляции приносят инвестиции в жилую недвижимость? Как доходность зависит от количества комнат в квартире? Проанализируем имеющуюся у Сбербанка базу объявлений о продаже и аренде квартир в Москве и ближнем Подмосковье.
В нашей базе нашлось достаточно много многоквартирных домов, в которых имеются одновременно объявления о продаже квартир и объявления об аренде квартир, причём количество комнат в продаваемых и сдаваемых в аренду квартирах одинаково. Мы сопоставили такие случаи, сгруппировав по домам и количеству комнат в квартире. Если предложений в такой группе имелось несколько, вычисляли среднюю цену. Если площадь продаваемой и сдаваемой в аренду квартиры отличалась, то пропорционально изменяли цену предложения, чтобы площади сравниваемых квартир соответствовали. В результате, предложения нанесли на график. Каждый кружок это фактически квартира, которую одновременно предлагают приобрести и снять. На горизонтальной оси видим стоимость приобретения квартиры, а на вертикальной оси стоимость снятия этой же квартиры. Количество комнат в квартире понятно из цвета кружка, а чем больше площадь квартиры, тем больше радиус кружка. С учётом сверхдорогих предложений график получился такой:



Если убрать дорогие предложения, то можно детальнее разглядеть цены в эконом-сегменте:



Корреляционный анализ показывает, что зависимость между стоимостью аренды квартиры и стоимостью её покупки близка к линейной.
Получилось такое соотношение между стоимостью годовой аренды квартиры и стоимостью приобретения квартиры (не забудем, что годовая стоимость это 12 месячных):
Количество комнат: Отношение стоимости годовой аренды квартиры к стоимости приобретения квартиры:
1-комнатные 5,11%
2-комнатные 4,80%
3-комнатные 4,94%
Всего 4,93%

Получили среднюю оценку в 4,93% годовых доходности от сдачи квартиры в аренду сверх инфляции. Также интересен момент, что дешёвые 1-комнатные квартиры сдавать в аренду немного выгоднее. Мы сравнивали цену предложения, которая в обоих случаях (аренды и покупки) немного завышена, поэтому корректировка не требуется. Однако требуются другие корректировки: сдаваемые в аренду квартиры нужно иногда хотя бы косметически ремонтировать, некоторое время занимает поиск арендатора и квартиры пустуют, иногда в цену аренды не заложены коммунальные платежи частично или полностью, также имеет место крайне незначительное обесценивание квартир с годами.
С учётом корректировок, от сдачи жилой недвижимости в аренду можно иметь доход до 4,5% годовых (сверх того, что сама недвижимость не обесценивается). Если такая доходность впечатляет, у Сбербанка есть множество предложений на ДомКлик.

Исторический анализ ставок по депозитам


Рублёвые депозиты в России в последние несколько лет в основном обыгрывают инфляцию. Но не на 4,5%, как сдаваемая недвижимость, а, в среднем, на 2%.
На графике ниже видим динамику сравнения ставок по депозитам и уровня инфляции.



Отмечу такой момент, что доход от депозитов обыгрывает инфляцию несколько сильнее, чем на вышеприведённом графике по следующим причинам:

  • Можно фиксировать ставку по пополняемым вкладам в благоприятное время на несколько месяцев вперёд
  • Ежемесячная капитализация, свойственная многим учтённым в этих усреднённых данных вкладам, за счёт сложных процентов добавляет прибыли
  • Выше были учтены ставки по топ-10 банкам по информации от Банка России, вне топ-10 можно найти ставки несколько выше

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

Исторический анализ рынка акций


Теперь посмотрим на более разнообразный и рискованный рынок российских акций. Доходность от вложения в акции не фиксирована и может сильно меняться. Однако, если диверсифицировать активы и заниматься инвестированием длительный период, то можно проследить среднюю годовую процентную ставку, характеризующую успешность инвестирования в портфель акций.
Для далёких от темы читателей скажу пару слов об индексах акций. В России есть индекс Мосбиржи, который показывает, динамику рублевой стоимости портфеля, состоящего из 50 крупнейших российских акций. Состав индекса и доля акций каждой компании зависит от объема торговых операций, объема бизнеса, количества находящихся в обращении акций. График ниже показывает, как рос индекс Мосбиржи (т.е. такой усреднённый портфель) в последние годы.



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

Поэтому нам будет интереснее индекс Мосбиржи полной доходности брутто (MCFTR), который учитывает полученные дивиденды и списанный с этих дивидендов налог. Покажем на графике ниже, как менялся этот индекс в последние годы. Кроме того, учтём инфляцию и посмотрим, как рос этот индекс в современных ценах:



Зелёный график это и есть реальная стоимость портфеля в современных ценах, если вложиться в индекс Мосбиржи, регулярно реинвестировать дивиденды и платить налоги.

Посмотрим, какой же был коэффициент роста индекса MCFTR за последние 1,2,3,,11 лет. Т.е. какова же была бы наша доходность, если бы мы купили акции в пропорциях этого индекса и регулярно реинвестировали бы полученные дивиденды в те же самые акции:
Лет Начало Конец MCFTR
нач. с
учётом
инфл.
MCFTR
кон. с
учётом
инфл.
Коэфф.
роста
Годовой
коэфф.
роста
1 30.07.2019 30.07.2020 4697,47 5095,54 1,084741 1,084741
2 30.07.2018 30.07.2020 3835,52 5095,54 1,328513 1,152612
3 30.07.2017 30.07.2020 3113,38 5095,54 1,636659 1,178472
4 30.07.2016 30.07.2020 3115,30 5095,54 1,635650 1,130896
5 30.07.2015 30.07.2020 2682,35 5095,54 1,899655 1,136933
6 30.07.2014 30.07.2020 2488,07 5095,54 2,047989 1,126907
7 30.07.2013 30.07.2020 2497,47 5095,54 2,040281 1,107239
8 30.07.2012 30.07.2020 2634,99 5095,54 1,933799 1,085929
9 30.07.2011 30.07.2020 3245,76 5095,54 1,569907 1,051390
10 30.07.2010 30.07.2020 2847,81 5095,54 1,789284 1,059907
11 30.07.2009 30.07.2020 2223,17 5095,54 2,292015 1,078318

Видим, что, вложившись любое количество лет назад, мы получили бы победу над инфляцией в 5-18% ежегодно в зависимости от удачности точки входа.

Составим еще одну табличку не прибыльность за каждые последние N лет, а прибыльность за каждый из последних N одногодовых периодов:
Год Начало Конец MCFTR
нач. с
учётом
инфл.
MCFTR
кон. с
учётом
инфл.
Годовой
коэфф.
роста
1 30.07.2019 30.07.2020 4697,47 5095,54 1,084741
2 30.07.2018 30.07.2019 3835,52 4697,47 1,224728
3 30.07.2017 30.07.2018 3113,38 3835,52 1,231947
4 30.07.2016 30.07.2017 3115,30 3113,38 0,999384
5 30.07.2015 30.07.2016 2682,35 3115,30 1,161407
6 30.07.2014 30.07.2015 2488,07 2682,35 1,078085
7 30.07.2013 30.07.2014 2497,47 2488,07 0,996236
8 30.07.2012 30.07.2013 2634,99 2497,47 0,947810
9 30.07.2011 30.07.2012 3245,76 2634,99 0,811825
10 30.07.2010 30.07.2011 2847,81 3245,76 1,139739
11 30.07.2009 30.07.2010 2223,17 2847,81 1,280968

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

Теперь, для лучшего понимания, давайте абстрагируемся от этого индекса и посмотрим на примере конкретной акции, какой бы получился результат, если вложиться в эту акцию 15 лет назад, повторно вкладывать дивиденды и платить налоги. Результат посмотрим с учётом инфляции, т.е. в современных ценах. Ниже показан пример обыкновенной акции Сбербанка. Зелёный график показывает динамику стоимости портфеля, изначально состоявшего из одной акции Сбербанка в современных ценах с учётом реинвестиции дивидендов. За 15 лет инфляция обесценила рубли в 3.014135 раз. Акция Сбербанка за эти годы подорожала с 21.861 руб. до 218.15 руб., т.е. цена выросла в 9.978958 раз без учёта инфляции. За эти годы владельцу одной акции было выплачено в разное время дивидендов за вычетом налогов в сумме 40.811613 руб. Суммы выплаченных дивидендов показаны на графике красными вертикальными палочками и не относятся к самому графику, в котором дивиденды и их реинвестиция также учтены. Если всякий раз на эти дивиденды вновь покупались акции Сбербанка, то в конце периода акционер уже владел не одной, а 1.309361 акциями. С учётом реинвестиции дивидендов и инфляции исходный портфель подорожал в 4.334927 раз за 15 лет, т.е. дорожал в 1.102721 раз ежегодно. Итого, обыкновенная акция Сбербанка приносила владельцу в среднем 10,27% годовых сверх инфляции каждый из 15 последних лет:



В качестве ещё одного примера приведём аналогичную картинку с динамикой по привилегированной акции Сбербанка. Привилегированная акция Сбербанка приносила владельцу в среднем ещё больше, 13,59% годовых сверх инфляции каждый из 15 последних лет:



Эти результаты на практике будут чуть ниже, потому что при покупке акций нужно платить небольшую брокерскую комиссию. В то же время результат может быть ещё улучшен, если использовать Индивидуальный Инвестиционный Счёт, позволяющий получить от государства налоговый вычет в некотором ограниченном объёме. Если вы не слышали о таком, предлагается поискать по аббревиатуре ИИС. Не забудем также упомянуть, что ИИС можно открыть в Сбербанке.

Итак, мы предварительно получили, что инвестировать в акции исторически выгоднее, чем в недвижимость и депозиты. Для развлечения приведём полученный в результате анализа данных хит-парад из 20 наилучших акций, которые торгуются на рынке более 10 лет. В последнем столбце видим, во сколько раз в среднем каждый год рос портфель из акций с учётом инфляции и реинвестиции дивидендов. Видим, что многие акции обыгрывали инфляцию более, чем на 10%:
Акция Начало Конец Коэфф. инфляции Нач. цена Кон. цена Рост
числа
акций
за счёт
реинве-
стиции
диви-
дендов,
раз
Итоговый
средне-
годовой
рост, раз
Лензолото 30.07.2010 30.07.2020 1,872601 1267,02 17290 2,307198 1,326066
НКНХ ап 30.07.2010 30.07.2020 1,872601 5,99 79,18 2,319298 1,322544
МГТС-4ап 30.07.2010 30.07.2020 1,872601 339,99 1980 3,188323 1,257858
Татнфт 3ап 30.07.2010 30.07.2020 1,872601 72,77 538,8 2,037894 1,232030
МГТС-5ао 30.07.2010 30.07.2020 1,872601 380,7 2275 2,487047 1,230166
Акрон 30.07.2010 30.07.2020 1,872601 809,88 5800 2,015074 1,226550
Лензол. ап 30.07.2010 30.07.2020 1,872601 845 5260 2,214068 1,220921
НКНХ ао 30.07.2010 30.07.2020 1,872601 14,117 92,45 1,896548 1,208282
Ленэнерг-п 30.07.2010 30.07.2020 1,872601 25,253 149,5 1,904568 1,196652
ГМКНорНик 30.07.2010 30.07.2020 1,872601 4970 19620 2,134809 1,162320
Сургнфгз-п 30.07.2010 30.07.2020 1,872601 13,799 37,49 2,480427 1,136619
ИРКУТ-3 30.07.2010 30.07.2020 1,872601 8,127 35,08 1,543182 1,135299
Татнфт 3ао 30.07.2010 30.07.2020 1,872601 146,94 558,4 1,612350 1,125854
Новатэк ао 30.07.2010 30.07.2020 1,872601 218,5 1080,8 1,195976 1,121908
СевСт-ао 30.07.2010 30.07.2020 1,872601 358 908,4 2,163834 1,113569
Красэсб ао 30.07.2010 30.07.2020 1,872601 3,25 7,07 2,255269 1,101105
ЧТПЗ ао 30.07.2010 30.07.2020 1,872601 55,7 209,5 1,304175 1,101088
Сбербанк-п 30.07.2010 30.07.2020 1,872601 56,85 203,33 1,368277 1,100829
ПИК ао 30.07.2010 30.07.2020 1,872601 108,26 489,5 1,079537 1,100545
ЛУКОЙЛ 30.07.2010 30.07.2020 1,872601 1720 5115 1,639864 1,100444

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

Задача. Найти акцию, стабильно дающую доход выше недвижимости (среднегодовой коэффициент роста 1.045 свыше инфляции) максимальное число раз по каждому из последних 10 одногодовых периодов, когда акция торговалась.
В этой и следующих задачах имеется в виду вышеописанная модель с реинвестицией дивидендов и учётом инфляции.
Вот победители в этой номинации согласно нашему анализу данных. Акции в верхней части таблицы из года в год стабильно показывают высокую доходность без провалов. Здесь Год 1 это 30.07.2019-30.07.2020, Год 2 это 30.07.2018-30.07.2019 и т.д.:
Акция Число
побед
над
недви-
жимо-
стью
за
после-
дние
10 лет
Год 1 Год 2 Год 3 Год 4 Год 5 Год 6 Год 7 Год 8 Год 9 Год 10
Татнфт 3ап 8 0,8573 1,4934 1,9461 1,6092 1,0470 1,1035 1,2909 1,0705 1,0039 1,2540
МГТС-4ап 8 1,1020 1,0608 1,8637 1,5106 1,7244 0,9339 1,1632 0,9216 1,0655 1,6380
ЧТПЗ ао 7 1,5532 1,2003 1,2495 1,5011 1,5453 1,2926 0,9477 0,9399 0,3081 1,3666
СевСт-ао 7 0,9532 1,1056 1,3463 1,1089 1,1955 2,0003 1,2501 0,6734 0,6637 1,3948
НКНХ ао 7 1,3285 1,5916 1,0821 0,8403 1,7407 1,3632 0,8729 0,8678 1,0716 1,7910
МГТС-5ао 7 1,1969 1,0688 1,8572 1,3789 2,0274 0,8394 1,1685 0,8364 1,0073 1,4460
Газпрнефть 7 0,8119 1,3200 1,6868 1,2051 1,1751 0,9197 1,1126 0,7484 1,1131 1,0641
Татнфт 3ао 7 0,7933 1,0807 1,9714 1,2109 1,0728 1,1725 1,0192 0,9815 1,0783 1,1785
Ленэнерг-п 7 1,3941 1,1865 1,7697 2,4403 2,2441 0,6250 1,2045 0,7784 0,4562 1,4051
НКНХ ап 7 1,3057 2,4022 1,2896 0,8209 1,2356 1,6278 0,7508 0,8449 1,5820 2,4428
Сургнфгз-п 7 1,1897 1,0456 1,2413 0,8395 0,9643 1,4957 1,2140 1,1280 1,4013 1,0031

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

Теперь сформулируем и решим такую задачу на анализ данных. Целесообразно ли немножко спекулировать, всякий раз покупая акции за M дней до даты выплаты дивидендов и продавая акции через N дней после даты выплаты дивидендов? Лучше ли собирать урожай с дивидендов и выходить из акции, чем сидеть в акции круглый год? Допустим, что нет потерь на комиссии от такого входа-выхода. И анализ данных поможет нам найти границы коридора M и N, который был исторически наиболее удачен в деле сбора урожая дивидендов вместо долгого владения акциями.

Приведём анекдот 2008 года.
Джон Смит, выпрыгнувший из окна 75-го этажа на Уолл Стрит, после удара о землю подпрыгнул на 10 метров, чем немного отыграл свое утреннее падение.

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

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

Наша модель просчитала все варианты ширины окрестности вокруг дат выплаты дивидендов за всю историю. Были приняты следующие ограничения: M<=30, N>=20. Дело в том, что далеко не всегда ранее, чем за 30 дней до выплаты дивидендов заранее известна дата и суммы выплаты. Также дивиденды приходят на счёт далеко не сразу, а с задержкой. Считаем, что нужно от 20 дней, чтобы гарантированно получить на счёт и реинвестировать дивиденды. С такими ограничениями модель выдала следующий ответ. Наиболее оптимально покупать акции за 34 дня до даты выплаты дивидендов и продавать их через 25 дней после даты выплаты дивидендов. При таком сценарии в среднем получался рост в 3,11% за этот период, что даёт 20,9% годовых. Т.е. при рассматриваемой модели инвестирования (с реинвестицией дивидендов и учётом инфляции) если покупать акцию за 34 дня до даты выплаты дивидендов и продавать её через 25 дней после даты выплаты дивидендов, то имеем 20,9% годовых свыше уровня инфляции. Это проверено усреднением по всем случаям выплаты дивидендов из нашей базы.

Например, по привилегированной акции Сбербанка такой сценарий входа-выхода давал бы 11,72% роста свыше уровня инфляции за каждый вход-выход в окрестности даты выплаты дивидендов. Это составляет аж 98,6% годовых свыше уровня инфляции. Но это, конечно, пример случайного везения.
Акция Вход Дата выплаты дивидендов Выход Коэфф. роста
Сбербанк-п 10.05.2019 13.06.2019 08.07.2019 1,112942978
Сбербанк-п 23.05.2018 26.06.2018 21.07.2018 0,936437635
Сбербанк-п 11.05.2017 14.06.2017 09.07.2017 1,017492563
Сбербанк-п 11.05.2016 14.06.2016 09.07.2016 1,101864592
Сбербанк-п 12.05.2015 15.06.2015 10.07.2015 0,995812419
Сбербанк-п 14.05.2014 17.06.2014 12.07.2014 1,042997818
Сбербанк-п 08.03.2013 11.04.2013 06.05.2013 0,997301095
Сбербанк-п 09.03.2012 12.04.2012 07.05.2012 0,924053861
Сбербанк-п 12.03.2011 15.04.2011 10.05.2011 1,010644958
Сбербанк-п 13.03.2010 16.04.2010 11.05.2010 0,796937418
Сбербанк-п 04.04.2009 08.05.2009 02.06.2009 2,893620094
Сбербанк-п 04.04.2008 08.05.2008 02.06.2008 1,073578067
Сбербанк-п 08.04.2007 12.05.2007 06.06.2007 0,877649005
Сбербанк-п 25.03.2006 28.04.2006 23.05.2006 0,958642001
Сбербанк-п 03.04.2005 07.05.2005 01.06.2005 1,059276282
Сбербанк-п 28.03.2004 01.05.2004 26.05.2004 1,049810801
Сбербанк-п 06.04.2003 10.05.2003 04.06.2003 1,161792898
Сбербанк-п 02.04.2002 06.05.2002 31.05.2002 1,099316569

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

Поставим нашей модели ещё одну задачу по анализу данных:

Задача. Найти акцию с наирегулярнейшей возможностью заработка на входе-выходе в окрестности даты выплаты дивидендов. Будем оценивать сколько случаев из случаев выплаты дивидендов дали возможность заработка более 10% в годовом исчислении выше уровня инфляции, если входить в акцию за 34 дня до и выходить через 25 дней после даты выплаты дивидендов.

Будем рассматривать акции, по которым было хотя бы 5 случаев выплаты дивидендов. Получившийся хит-парад приведён ниже. Отметим, что результат имеет ценность скорее всего только с точки зрения задачи на анализ данных, но не как практическое руководство к инвестированию.
Акция Количество
случаев выигрыша
более 10% годовых
сверх инфляции
Количество
случаев
выплаты
дивидендов
Доля
случаев
победы
Средний коэфф. роста
Лензолото 5 5 1 1,320779017
МРСК СЗ 6 7 0,8571 1,070324870
Роллман-п 6 7 0,8571 1,029644533
Россети ап 4 5 0,8 1,279877637
Кубанэнр 4 5 0,8 1,248634960
ЛСР ао 8 10 0,8 1,085474828
АЛРОСА ао 8 10 0,8 1,042920287
ФСК ЕЭС ао 6 8 0,75 1,087420610
НМТП ао 10 14 0,7143 1,166690777
КузбТК ао 5 7 0,7143 1,029743667

Из проведённого анализа рынка акций можно сделать такие выводы:
1) Проверено, что заявленная в материалах брокеров, инвестиционных компаний и прочих заинтересованных лиц доходность акций выше депозитов и инвестиционной недвижимости имеет место быть.
2) Волатильность рынка акций очень высокая, но на долгий срок с существенной диверсификацией портфеля вкладываться можно. Ради добавочных 13% налогового вычета при инвестиции на ИИС открывать для себя рынок акций вполне целесообразно и сделать это можно, в том числе, в Сбербанке.
3) Исходя из анализа результатов за прошлые периоды найдены лидеры по стабильной высокой доходности и по выгодности входа-выхода в окрестности даты выплаты дивидендов. Однако результаты не такие уж однозначные и руководствоваться только ими в своём инвестировании не стоит. Это были примеры задач на анализ данных.

Итого:
Полезно вести учет своего имущества, а также доходов и расходов. Это помогает в финансовом планировании. Если удаётся копить деньги, то есть возможности инвестировать их под ставку выше инфляции. Анализ данных из озера данных Сбербанка показал, что депозиты ежегодно приносят 2%, сдача квартир в аренду 4,5%, а российские акции около 10% свыше инфляции при наличии существенно больших рисков.

Автор: Михаил Гричик, эксперт профессионального сообщества Сбербанка SberProfi DWH/BigData.

Профессиональное сообщество SberProfi DWH/BigData отвечает за развитие компетенций в таких направлениях, как экосистема Hadoop, Teradata, Oracle DB, GreenPlum, а также BI инструментах Qlik, SAP BO, Tableau и др.
Подробнее..

Как настроить сбор данных с датчиков IoT и SCADA для Data Governance

09.11.2020 16:20:11 | Автор: admin
В этом году на форуме по управлению данными INFADAY 2020 было много интересных технических кейсов. Один из них настройка сбора потоковых данных с датчиков IoT и систем SCADA таким образом, чтобы эти данные сразу можно было включить в процессы стратегического управления данными в организации Data Governance.

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

Если вы работаете в крупной промышленной компании, скорее всего вы уже сталкивались с похожими задачами. А если не сталкивались, очень скоро столкнётесь. Особенно актуально это для нефтегазовых компаний. В таких компаниях широко применяют датчики SCADA для мониторинга инфраструктуры добычи и транспортировки нефтепродуктов. При этом управлять собранными потоковыми данными (повышать их качество, проводить их интеграцию, перемещать их в различные системы) нужно очень быстро. На показания датчиков важно реагировать незамедлительно, в реальном времени. Если вовремя среагировать на уведомление об инциденте, можно избежать очень больших проблем.

Ниже я расскажу, как можно настроить сбор данных с датчиков с учётом Data Governance на примере Tibbo Aggregate Network Manager и платформы Informatica. Если хотите посмотреть видеозапись демонстрации на форуме INFADAY 2020, это можно сделать на сайте мероприятия.


Собираем данные с датчиков в хранилище и Kafka

Давайте для примера соберём данные с коммутатора Ubiquity, обработаем их и передадим в хранилище данных и в Kafka.
Первичный сбор будем проводить с помощью решения AggreGate Network Manager (NM) компании Tibbo, которое прекрасно работает с разными типами датчиков и данных, которые с них собираются. Ниже вы можете видеть папку в разделе Devices Ubiquity Switch. Здесь теперь хранятся наши данные с коммутатора.



Так выглядят параметры работы коммутатора, данные о которых мы с вами собрали.



AggreGate NM стыкуется с Informatica через промежуточный MQTT-брокер. Network Manager отправляет данные IoT-протокола MQTT (Message Queuing Telemetry Transport), упакованные в формат JSON.
Заходим в раздел Модели, выбираем заранее созданный объект Informatica_MQTT_Sender и в закладке конструктора правил находим задание: упаковать таблицу интерфейсов ifXtable в формат JSON и послать на сервер брокера MQTT.



Открываем Data Engineering Streaming, в нём мы настраиваем два простых маппинга по захвату данных из брокера MQTT и перемещению в Kafka и в хранилище Hadoop.
В интерфейсе платформы Informatica маппинг по перемещению в хранилище будет выглядеть так.



Трансформация (string) нужна для разделения потока данных на отдельные строки с помощью символов #CRLF (Carriage Return, Line Feed).



Во втором случае посылаем те же самые данные в Kafka, используем ту же трансформацию.



А это уже интерфейс брокера Kafka с загруженными данными.



Если маршрутизация MQTT-трафика не создаёт существенной нагрузки, то тогда можно установить брокер на сервере Informatica. Это уберёт лишнее из вычислительной цепи и сократит задержки обработки данных.
Обратите внимание, консоль управления Kafka доступна на сборке кластере ArenaData, в сборке Hortonworks веб-интерфейс Kafka-брокеров отсутствует.

Не забываем включать данные с датчиков в процессы Data Governance

Если вы работали с платформой Informatica, знаете, что она умеет не только интегрировать данные и оптимально перемещать их между ИТ-системами, но и обеспечивает комплексные процессы Data Governance. В частности, перед отправкой данных из Data Engineering Streaming в корпоративное хранилище, вы могли бы проверить их качество внутри платформы Informatica c помощью Informatica Data Quality.
Подробнее..

Business Intelligence на очень больших данных опыт Yota

16.02.2021 16:13:42 | Автор: admin


Всем привет! Меня зовут Михаил Волошин, и я, как руководитель отдела инструментов бизнес-анализа, хочу верхнеуровнево рассказать о плюсах и особенностях BI-решения Yota.

200 Tb Vertica, 400 Tb Hadoop, кластер Tableau, специфичная организация процесса разработки и многое другое ждут вас под катом.

Внимательный читатель спросит: А причем тут Vertica и слоник Hadoop, технологии же разные? Да ни при чем это лишь КДПВ.

1. DWH: ода Вертике


Vertica. На ее базе построено корпоративное хранилище данных (data warehouse, DWH), являющееся ядром решения. Наша Vertica первая инсталляция в СНГ была развернута в 2012 году (я пришел лишь в 2016). 8 лет назад не было и половины зоопарка продуктов Apache, а выбор происходил между Netezza, Greenplum и, собственно, Vertica. Время показало, что выбор оказался верным: IBM прекратила техническую поддержку Netezza в 2019, Greenplum еще в 2015 стал opensource продуктом (т.к. никто не покупал шардированный Postgress). И к началу 2021 года в мире осталось 2 серьезных аналитических on-premise БД: Vertica и Teradata. Не хочу разводить холивар, но буду рад услышать об иных решениях, позволяющих обычным аналитикам в adhoc запросах оперировать >1 трлн строк за разумное время в минутах и без поддержки команды data engineer + dataops.

Итак, Vertica это колоночная MPP БД. Т.е. данные хранятся в колонках, что ускоряет доступ к ним и позволяет оптимизировать хранение. Запросы выполняются одновременно всеми нодами кластера, что также позитивно сказывается на скорости обработки данных (однако происходит высокая утилизация сети и дисков). При этом входной порог для доступа к терабайтам и петабайтам данных низок за счет ANSI SQL 99 с небольшими расширениями. 1-й Tb этого великолепия бесплатно. Важный момент все колоночные решения не соответствуют ACID, т.е. не могут заменить классических OLTP БД для условного биллинга, но отлично подходят для целей анализа данных. Более подробно об архитектуре Vertica здесь.

У нас 161 Tb на 34 rack нодах HP, каждая из которых имеет:

  • 2*CPU по 20 ядер
  • 256Gb RAM
  • 2*10G сеть
  • быстрые 10k SAS HDD RAID 10 (в 2017/18, когда мы планировали обновление и обновляли RAID массивы, SSD стоили как чугунный мост и были не такими надежными как сейчас)

Vertica может быть развернута на любом железе/виртуалках. Хоть на 3-х ноутбуках сыновей маминой подруги. Однако, важно помнить, что вендор явно рекомендует разворачивать кластер на гомогенном по типу оборудовании. Нас как раз в этом году ждет кейс смены вендора железа аж интересно, как все пройдет.

В целом продукт достаточно надежный: за все время, что я работаю в Yota (5-й год пошел), кластер ни разу не падал целиком. Были кейсы, когда 9 нод вываливались в течение 10 минут (диски, контроллеры рейдов, иные технические проблемы), и это приводило к просадкам производительности, но кластер не рассыпался, и после вывода сбоивших нод из кластера на горячую производительность восстанавливалась. Вывод необходим, т.к. кластер всегда работает со скоростью самой медленной ноды (вспоминаем рекомендацию вендора о гомогенности). Теоретически из строя может выйти до половины всех узлов кластера, но может хватить и 2 нод (при k-safety=1, параметр репликации данных со стандартным значением для большинства инсталляций в мире).

Еще одним фактом, касающимся надежности DWH, хотя и не красящим нас, является появление бэкапа: он у нас появился лишь в 2019 перед мажорным обновлением версии Vertica. И это при том, что до 2018 года наша Vertica была самой большой в СНГ (сейчас по объему вторая-третья, но по сложности самого хранилища, по-прежнему, первая).

Обновлялись мы, кстати, сразу на 2 версии (7 -> 8, 8 -> 9). Ну, как обновлялись: в 13:00 остановили кластер и запустили .py скрипт от вендора, а в 21:10 мы уже открывали пиво, после того как кластер начал подниматься. Никаких эксцессов не было. И тут вспомнилась статья на Хабре от коллег из телекома про обновление кластера Greenplum c 4-ой до 5-ой версии. Так они, насколько помню, потратили сотни дней разработчиков на costylmaking из-за несовместимости типов данных между мажорными версиями одного продукта.

Отчасти лукавлю, не рассказывая о сути стабильности нашего кластера, которая кроется в четкой настройке и управлении пулами ресурсов для оркестрации выполняющихся запросов. Это настоящее искусство, лежащее в основе DWH Vertica c uptime из множества девяток после запятой.

Anchor modeling, Datavault 2.0 всего этого у нас нет. Мы не фокусировались на жестком соблюдении какой-то одной изначально выбранной методологии, иначе сами себе устроили бы приключения. Почему? Хотя бы потому, что при разворачивании DWH, Yota была независимой компанией и крупнейшим оператором 4G, но предоставлявшим доступ в сеть только для модемных устройств. После покупки МегаФоном в Yota появились голосовые абоненты, а голос принципиально иной продукт, и мы бы просто не запустились в крайне сжатые сроки, если бы не определенная архитектурная свобода. У нас 37 схем, и архитектура внутри каждой не то, что схемы, но даже витрины, может отличаться от мейнстрима и выбирается в соответствии с решаемой задачей с учетом особенностей хранения в источниках.

И еще момент во внутренней команде нет ни архитектора, ни девопс-гуру. Они просто не нужны fulltime, т.к. Vertica не требует постоянного обслуживания. Эти роли у нас выполняются подрядчиком, а внутренняя команда сфокусирована на создании инструментов анализа бизнес-данных для всей компании и совместном с бизнесом улучшении продуктов. Как бы высокопарно это ни звучало, но Yota изначально data driven business. У нас под сотню персональных учеток для adhoc-запросов и широкого доступа к данным всем, кому он нужен.

В завершение разговора о Vertica хочется обсудить регулярно поднимающийся вопрос: Дорого же! Зачем оно надо?. По моему скромному мнению, в бизнесе нет понятий дорого/дешево, но есть понятие эффективно/не эффективно. Давным-давно я работал в складской логистике, так вот, строительство склада начинается с изучения характеристик будущих единиц хранения (SKU) и потоков движения этих SKU. При проектировании хранилища ситуация должна быть аналогичной: изучение данных, подразумеваемых для обработки внутри DWH, выбор наиболее оптимальной архитектуры с параллельными расчетами финансовой модели. Звучит просто, но это позволит избежать догматов: Делаем только на opensource или Наш потрясающий стартап может себе позволить Teradata в топ-комплектации. Пару месяцев назад создал модель Vertica total cost of ownership, и эффективность текущего решения Yota вышла оптимальной. Поделиться, к сожалению, по понятным причинам не смогу.

Hadoop. Их у нас целых 2 кластера (Cloudera 6.3), которые мы используем как дешевое хранилище некритичных для бизнеса данных. К данным, хранящимся в наших Hadoop, не требуется скорость доcтупа, предъявляемая к Вертике. Здесь стоит отметить подставу со стороны Cloudera: когда мы наши Хадупы планировали и разворачивали в 2018-2019, то существовавшая Comminity Edition нас вполне устраивала; однако в феврале 2020 пришла полярная лисичка в виде изменения политики лицензирования и, по сути, отмены т.н. free версий. Из-за этого вынуждены думать сейчас о редеплое кластера из 23 нод на CH 5.16 с потерей данных (ими можно пожертвовать). А на маленький кластер Hadoop вынуждены оформлять ненужную нам лицензию.

Oracle. Легаси-вишенкой на торте DWH выступает хранилище Oracle объемом всего 1.4 Tb. Его мы иногда используем для собственной обработки в ODS слое высокочастотных потоков малонасыщенных данных. Например, 100 000 файлов в сутки по несколько строк в каждом, конечно, можно писать в Вертику напрямую, но разумнее сначала в транзакционную БД, а уже затем часовыми батчами в DWH. Движемся дальше.

2. ETL


В нашем случае зачастую ELT, так как наше DWH позволяет перемалывать терабайты внутри себя без реализации стадии Transform на относительно слабых ETL-серверах.

Высоконагруженные потоки данных. У нас 9 пайплайнов по 2-8 ETL-джобов в каждом. Они редко меняются, и поскольку границы не выходят за staging слой, то мы отдали их нашим подрядчикам. Тем же, которые поддерживают Vertica. Коллеги написали свой Loader на Groovy 3, который сами и поддерживают. Loader вполне неплохо перемалывает свой 1 Tb в сутки, поступающий в Vertica, и до 10 Tb в большой Hadoop.

Из интересного стоит упомянуть используемый нами механизм CDC от Oracle Oracle Golden Gate. Kafka пока не используем, но, возможно, начнем, т.к. переезд на Oracle 19 имеет специфичную реализацию Oracle for BigData вместо старого доброго OGG. На текущий момент мы еще в процессе исследований, но как бы не пришлось свои костыли писать

Остальные потоки данных. Здесь кроется соль нашего решения формирование промежуточных и конечных витрин как на основе данных из п. 2.1, так и на собственных интеграциях примерно с 150 системами-источниками. Этим занимается исключительно внутренняя команда. Здесь примерно 1150 ETL-джобов. В основе стэка разработки: Talend Data Integration 7.1. Инструмент условно бесплатный. Условно, т.к. требует лицензии для использования среды выполнения и оркестрации. Я уже не застал того благостного времени, когда использовалась Talend Administration Console, но старшие товарищи рассказывали, что это был тот еще садомазочуланчик папаши Мюллера образцовый UI, привносящий незабываемый UX. Можно, конечно, деплоить джобы Talend в виде .zip пакетов сразу в .sh и оркестрировать в cron, а потом грепать логи. Но было решено еще в 2016 году, что деплоить джобы Talend будем сразу в Scheduler (рантайм с web UI для доступа к нему). Который, как уже понятно, написал под нас тот же самый подрядчик. Разумеется, и лицензия стоит дешевле чем TAC, UI оставляет более позитивный UX, и доработки под наши пожелания не затягиваются во времени.

Пара слов про Talend Data Integration. Это среда визуального программирования потоков интеграции. Сам инструмент не уступает Informatica PowerCenter по производительности. JVM под капотом у обоих. Максимум, что придется писать руками SQL для стадии Transform внутри некоторых компонентов, но его и нет смысла пытаться чем-то заменить. Чтобы не было сомнений в возможностях Talend и иных интеграционных комбайнов, 2 факта:

  • до появления Loader сотни Гб бинарников CDR парсились джобами Talend. Loader и появился из доработки джобов Talend, которые перестали справляться с нагрузкой;
  • внутренняя команда иногда переписывает за подрядчиком пайплайны, созданные в их Loader, и время на обработку данных уменьшается. Понятно, что ситуация разовая, и 1 Tb в сутки из бинарников вряд ли Talend распарсит, но факт есть факт.

3. Визуализация данных


Используем следующие инструменты: MS Analysis Services, Tableau, есть у нас и любимое легаси в виде SAP BO.

MS Analysis Services. Исторически аналитические кубы были значимым инструментом. В проде у нас всего 16 кубов весом от 6 Mb до 144 Gb, а через пару месяцев после доработок и до 200 Gb. В 2020 году возникла идея о возможном переносе кубов в Tableau, но там уже при экстракте в 5 Gb дэшборд стал люто тормозить. В нашем случае платформа оказалась безальтернативной. Кстати, используем последний free version MS AS 11. Не PowerBI, конечно, но нулевые траты на лицензии нас вполне устраивают.

Tableau. На конец 2020 у нас было 277 дэшбордов, и бизнесу они адски заходят. Одна из целей 2021 максимальная автоматизация ручной отчетности аналитиков. И тут мы споткнулись, т.к. наши аналитики, как и любые нормальные аналитики, для прототипирования используют Excel. Без шуток.

Есть у этих самых аналитиков любовь к типам диаграмм 'водопад':

Прошу прощения за низкое качество изображения, но суть передана верно

Очень круто выглядит и нравится топ-менеджменту. Как бывший аналитик данных, сам кайфую, когда вижу такую красоту. Но чтобы реализовать такой водопад в Tableau, нужно сделать 5 графиков, обеспечить синхронизацию фильтров между ними Ок, пару накликать можно. А если в дэшборде их 171? Ну, вы поняли. На одной стороне весов 12 человеко-часов аналитиков на ежемесячный сбор презентации. На другой полгода разработки сеньором + 100% гарантия превращения дэшборда в недвижимость. Недавно был тяжелый разговор с аналитиками, где мы зафиксировали, что такой красоты может быть не больше 2-3 графиков на весь дэшборд. Но продолжаем искать пути автоматизации именно этого типа визуализации в юзкейсах наших аналитиков адская идея скриптами powershell повторить ручные действия в Excel (там их пилят при помощи платной надстройки ThinkCell) пока не отпала. Офтопом стоит отметить сам факт повторения многостраничных презентаций в Tableau, где на самом деле однотипные данные намертво распечатаны в .pdf во всех возможных измерениях имеющихся в дэшборде. Конечно же, подход спорный, но мы очень клиентоориентированы по отношению к внутренним заказчикам, и мысли об изменении в сторону сторителлинга аккуратно и потихоньку продвигаем в жизнь.

Sap BO. Очевидная legacy система визуализации устаревшая чуть более, чем полностью. Аккуратно уходим от нее в сторону более современных и гибких решений, т.к. она прекрасна для point-to-point повторения отчетов (именно тут необходимо собирать большие и однотипные презентации аналитиков, но трудозатраты будут еще выше, да и такие водопады вообще не факт, что реализуемы в SAP BO), но не позволяет создавать интерактивные дэшборды. Следует отметить, что сам подход реализации point-to-point больших презентаций актуален для большого российского бизнеса, например, из сферы добычи сырья. В 2к21 это, на мой взгляд, выглядит морально устаревшим, особенно для Yota, средних размеров data driven business. Поэтому нам не имеет смысла заниматься реализацией намертво прибитых по брендбуку отчетов на миллион вкладок/страниц.

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

4. Data science (machine learning)


Кроме классического BI в отделе есть команда DS и, надеюсь, в этом году здесь появится ссылка на статью о Data Science в Yota, написанную профессионалом. Я таковым не являюсь, т.к. вырос из разработчиков классического BI. Извините, если кто-то зашел сюда только ради этого :-)

5. Agile? Нет У нас своё


Самый спорный пункт повествования. Рассказываю исключительно с целью поделиться тем, как у нас все работает, и почему не планируем менять на что-то более привычное и непротиворечивое.

В направлении классического BI 6 инженеров и тимлид. При этом нет ни выделенных архитекторов, ни аналитиков, ни тестировщиков, ни релиз-менеджеров. Каждый инженер = BI-фулстэк, реализует задачу под ключ, напрямую общаясь с бизнес-заказчиком и лично ответственен за конечный результат. Кодеров по ТЗ/токсичных рок-звезд нет и не будет от слова совсем. Но у всей команды изначально хорошие софт-скилы вдобавок к хард. В теории взаимозаменяемы, но жизнь вносит свои коррективы, и кто-то оказывается более сильным в ETL, а кому-то интереснее визуализация в Tableau пожелания в развитии каждого учитываются и мной, и тимлидом.

Работа по заявке идет с упором на 2 показателя: time-to-market (TTM) и customer satisfaction index (CSI). Причем сразу на проде, если речь об ETL-задачах в DWH. Тестовая зона, конечно же, есть, но подготовка данных на наших объемах занимает сильно больше времени, чем сама разработка. Важный момент: сообщения в чате наподобие ой, я оттранкейтил справочник... встречаются не чаще 1-2 раз в год и исправляются за 5-10 минут. Потерь невосстановимых, критичных для компании данных я не помню. В этом плане интереснее обращения от коллег из систем-источников на 100% реплицируемых в DWH с просьбой выслать из нашего бэкапа какую-нибудь таблицу фактов, которую массово проапдейтили, но что-то пошло не так За последний год такое было 2 раза.

Вы спросите, почему все так необычно устроено?

Кроме самого исчерпывающего объяснения

Так повелось в этом нашем лесу (с)

Есть очевидные минусы, с которыми мы умеем жить:

  • Быстроменяющаяся архитектура BI-решения является черным ящиком для всех смежных команд. Эта статья первый из шагов по расшариванию знаний о нашем решении.
  • Отток в команде потенциально критичен. Однако стабильность команды и преемственность нивелируют этот минус.

и плюсы:

  • Высокий TTM и высокая пропускная способность команды в целом. Весь проектный портфель компании (почти во всех проектах есть фичи на отдел) составляет 15-20% общего объема разработки отдела. Остальное прямые пожелания конечных бизнес-заказчиков, реализуемые с минимумом бюрократии.
  • Стабильно высокий CSI, демонстрирующий правильность выбранного подхода в организации разработки. Один раз в квартал мы проводим опрос среди бизнес-заказчиков. В 4Q20 из 43 респондентов ответили 21. По итогу получили 4,89 из 5. Это упавший CSI, хотя я предполагал падение до 4,5. Стандартно у нас ближе к 5. Объясняется это гибкостью в подходе к реализациям задумок бизнес-заказчиков и скоростью появления конечного результата с максимально эффективным использованием имеющихся инструментов/технологий.

В опросе CSI также можно оставить комментарий, например такой


Понимая все особенности подхода в BI-разработке, мы не планируем что-либо менять в обозримой перспективе.

Пользуясь случаем хочу поблагодарить #BI_TEAM за стабильно высокие результаты: ребята вы крутые, мне повезло работать со всеми вами! Спасибо.

6. Заключение


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

По идее здесь должны быть вакансии отдела, но извините full house) И даже есть небольшой лист ожидания Однако в соседних, не менее интересных, командах еще требуются люди. Буду рад комментариям нам есть куда расти.
Подробнее..

DB amp DWH Online Meetup 1509

07.09.2020 12:20:38 | Автор: admin
Первая онлайн-встреча сообщества DB & DWH Райффайзенбанка пройдет 15 сентября. Присоединяйтесь к нам, чтобы узнать про автоматизированное тестирование методом черного ящика и про переход на ETL-as-Service при помощи Informatica Power Center.



О чем будем говорить


Автоматизированное тестирование методом черного ящика в хранилище данных

Панюшкина Юлия и Колесников Дмитрий, Райффайзенбанк

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

Перевод банковских процессов на ETL-as-Service при помощи Informatica Power Center

Александр Попов, ОТП-банк

Спикер расскажет о том, как планируется из монолитной платформы Informatica PowerCenter сделать полноценное общебанковское средство разработки ETL.

>>> Начнем митап в 16:00 (МСК).

Регистрируйтесь, чтобы получить ссылку на трансляцию: письмо со ссылкой придет вам на почту. Мы вас ждем, до встречи online!
Подробнее..

Apache Airflow делаем ETL проще

27.07.2020 12:16:39 | Автор: admin

Привет, я Дмитрий Логвиненко Data Engineer отдела аналитики группы компаний Везёт.


Я расскажу вам о замечательном инструменте для разработки ETL-процессов Apache Airflow. Но Airflow настолько универсален и многогранен, что вам стоит присмотреться к нему даже если вы не занимаетесь потоками данных, а имеете потребность периодически запускать какие-либо процессы и следить за их выполнением.


И да, я буду не только рассказывать, но и показывать: в программе много кода, скриншотов и рекомендаций.



Что обычно видишь, когда гуглишь слово Airflow / Wikimedia Commons


Введение


Apache Airflow он прямо как Django:


  • написан на Python,
  • есть отличная админка,
  • неограниченно расширяем,

только лучше, да и сделан совсем для других целей, а именно (как написано до ката):


  • запуск и мониторинг задач на неограниченном количестве машин (сколько вам позволит Celery/Kubernetes и ваша совесть)
  • с динамической генерацией workflow из очень легкого для написания и восприятия Python-кода
  • и возможностью связывать друг с друг любые базы данных и API с помощью как готовых компонентов, так и самодельных плагинов (что делается чрезвычайно просто).

Мы используем Apache Airflow так:


  • собираем данные из различных источников (множество инстансов SQL Server и PostgreSQL, различные API с метриками приложений, даже 1С) в DWH и ODS (у нас это Vertica и Clickhouse).
  • как продвинутый cron, который запускает процессы консолидации данных на ODS, а также следит за их обслуживанием.

До недавнего времени наши потребности покрывал один небольшой сервер на 32 ядрах и 50 GB оперативки. В Airflow при этом работает:


  • более 200 дагов (собственно workflows, в которые мы набили задачки),
  • в каждом в среднем по 70 тасков,
  • запускается это добро (тоже в среднем) раз в час.

А о том, как мы расширялись, я напишу ниже, а сейчас давайте определим ber-задачу, которую мы будем решать:


Есть три исходных SQL Serverа, на каждом по 50 баз данных инстансов одного проекта, соответственно, структура у них одинаковая (почти везде, муа-ха-ха), а значит в каждой есть таблица Orders (благо таблицу с таким названием можно затолкать в любой бизнес). Мы забираем данные, добавляя служебные поля (сервер-источник, база-источник, идентификатор ETL-задачи) и наивным образом бросим их в, скажем, Vertica.

Поехали!


Часть основная, практическая (и немного теоретическая)


Зачем оно нам (и вам)


Когда деревья были большими, а я был простым SQL-щиком в одном российском ритейле, мы шпарили ETL-процессы aka потоки данных с помощью двух доступных нам средств:


  • Informatica Power Center крайне развесистая система, чрезвычайно производительная, со своими железками, собственным версионированием. Использовал я дай бог 1% её возможностей. Почему? Ну, во-первых, этот интерфейс где-то из нулевых психически давил на нас. Во-вторых, эта штуковина заточена под чрезвычайно навороченные процессы, яростное переиспользование компонентов и другие очень-важные-энтерпрайз-фишечки. Про то что стоит она, как крыло Airbus A380/год, мы промолчим.


    Осторожно, скриншот может сделать людям младше 30 немного больно




  • SQL Server Integration Server этим товарищем мы пользовались в своих внутрипроектных потоках. Ну а в самом деле: SQL Server мы уже используем, и не юзать его ETL-тулзы было бы как-то неразумно. Всё в нём в хорошо: и интерфейс красивый, и отчётики выполнения Но не за это мы любим программные продукты, ох не за это. Версионировать его dtsx (который представляет собой XML с перемешивающимися при сохранении нодами) мы можем, а толку? А сделать пакет тасков, который перетащит сотню таблиц с одного сервера на другой? Да что сотню, у вас от двадцати штук отвалится указательный палец, щёлкающий по мышиной кнопке. Но выглядит он, определенно, более модно:




Мы безусловно искали выходы. Дело даже почти дошло до самописного генератора SSIS-пакетов...


а потом меня нашла новая работа. А на ней меня настиг Apache Airflow.


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


Собираем кластер


Давайте не устраивать совсем уж детский сад, и не говорить тут о совершенно очевидных вещах, вроде установки Airflow, выбранной вами БД, Celery и других дел, описанных в доках.


Чтобы мы могли сразу приступить к экспериментам, я набросал docker-compose.yml в котором:


  • Поднимем собственно Airflow: Scheduler, Webserver. Там же будет крутится Flower для мониторинга Celery-задач (потому что его уже затолкали в apache/airflow:1.10.10-python3.7, а мы и не против);
  • PostgreSQL, в который Airflow будет писать свою служебную информацию (данные планировщика, статистика выполнения и т. д.), а Celery отмечать завершенные таски;
  • Redis, который будет выступать брокером задач для Celery;
  • Celery worker, который и займется непосредственным выполнением задачек.
  • В папку ./dags мы будет складывать наши файлы с описанием дагов. Они будут подхватываться на лету, поэтому передёргивать весь стек после каждого чиха не нужно.

Кое-где код в примерах приведен не полностью (чтобы не загромождать текст), а где-то он модифицируется в процессе. Цельные работающие примеры кода можно посмотреть в репозитории https://github.com/dm-logv/airflow-tutorial.

docker-compose.yml
version: '3.4'x-airflow-config: &airflow-config  AIRFLOW__CORE__DAGS_FOLDER: /dags  AIRFLOW__CORE__EXECUTOR: CeleryExecutor  AIRFLOW__CORE__FERNET_KEY: MJNz36Q8222VOQhBOmBROFrmeSxNOgTCMaVp2_HOtE0=  AIRFLOW__CORE__HOSTNAME_CALLABLE: airflow.utils.net:get_host_ip_address  AIRFLOW__CORE__SQL_ALCHEMY_CONN: postgres+psycopg2://airflow:airflow@airflow-db:5432/airflow  AIRFLOW__CORE__PARALLELISM: 128  AIRFLOW__CORE__DAG_CONCURRENCY: 16  AIRFLOW__CORE__MAX_ACTIVE_RUNS_PER_DAG: 4  AIRFLOW__CORE__LOAD_EXAMPLES: 'False'  AIRFLOW__CORE__LOAD_DEFAULT_CONNECTIONS: 'False'  AIRFLOW__EMAIL__DEFAULT_EMAIL_ON_RETRY: 'False'  AIRFLOW__EMAIL__DEFAULT_EMAIL_ON_FAILURE: 'False'  AIRFLOW__CELERY__BROKER_URL: redis://broker:6379/0  AIRFLOW__CELERY__RESULT_BACKEND: db+postgresql://airflow:airflow@airflow-db/airflowx-airflow-base: &airflow-base  image: apache/airflow:1.10.10-python3.7  entrypoint: /bin/bash  restart: always  volumes:    - ./dags:/dags    - ./requirements.txt:/requirements.txtservices:  # Redis as a Celery broker  broker:    image: redis:6.0.5-alpine  # DB for the Airflow metadata  airflow-db:    image: postgres:10.13-alpine    environment:      - POSTGRES_USER=airflow      - POSTGRES_PASSWORD=airflow      - POSTGRES_DB=airflow    volumes:      - ./db:/var/lib/postgresql/data  # Main container with Airflow Webserver, Scheduler, Celery Flower  airflow:    <<: *airflow-base    environment:      <<: *airflow-config      AIRFLOW__SCHEDULER__DAG_DIR_LIST_INTERVAL: 30      AIRFLOW__SCHEDULER__CATCHUP_BY_DEFAULT: 'False'      AIRFLOW__SCHEDULER__MAX_THREADS: 8      AIRFLOW__WEBSERVER__LOG_FETCH_TIMEOUT_SEC: 10    depends_on:      - airflow-db      - broker    command: >      -c " sleep 10 &&           pip install --user -r /requirements.txt &&           /entrypoint initdb &&          (/entrypoint webserver &) &&          (/entrypoint flower &) &&           /entrypoint scheduler"    ports:      # Celery Flower      - 5555:5555      # Airflow Webserver      - 8080:8080  # Celery worker, will be scaled using `--scale=n`  worker:    <<: *airflow-base    environment:      <<: *airflow-config    command: >      -c " sleep 10 &&           pip install --user -r /requirements.txt &&           /entrypoint worker"    depends_on:      - airflow      - airflow-db      - broker

Примечания:


  • В сборке композа я во многом опирался на известный образ puckel/docker-airflow обязательно посмотрите. Может, вам в жизни больше ничего и не понадобится.
  • Все настройки Airflow доступны не только через airflow.cfg, но и через переменные среды (слава разработчикам), чем я злостно воспользовался.
  • Естественно, он не production-ready: я намеренно не ставил heartbeats на контейнеры, не заморачивался с безопасностью. Но минимум, подходящий для наших экспериментиков я сделал.
  • Обратите внимание, что:
    • Папка с дагами должна быть доступна как планировщику, так и воркерам.
    • То же самое касается и всех сторонних библиотек они все должны быть установлены на машины с шедулером и воркерами.

Ну а теперь просто:


$ docker-compose up --scale worker=3

После того, как всё поднимется, можно смотреть на веб-интерфейсы:



Основные понятия


Если вы ничего не поняли во всех этих дагах, то вот краткий словарик:


  • Scheduler самый главный дядька в Airflow, контролирующий, чтобы вкалывали роботы, а не человек: следит за расписанием, обновляет даги, запускает таски.


    Вообще, в старых версиях, у него были проблемы с памятью (нет, не амнезия, а утечки) и в конфигах даже остался легаси-параметр run_duration интервал его перезапуска. Но сейчас всё хорошо.


  • DAG (он же даг) направленный ацикличный граф, но такое определение мало кому что скажет, а по сути это контейнер для взаимодействующих друг с другом тасков (см. ниже) или аналог Package в SSIS и Workflow в Informatica.


    Помимо дагов еще могут быть сабдаги, но мы до них скорее всего не доберёмся.


  • DAG Run инициализированный даг, которому присвоен свой execution_date. Даграны одного дага могут вполне работать параллельно (если вы, конечно, сделали свои таски идемпотентными).


  • Operator это кусочки кода, ответственные за выполнение какого-либо конкретного действия. Есть три типа операторов:


    • action, как например наш любимый PythonOperator, который в силах выполнить любой (валидный) Python-код;
    • transfer, которые перевозят данные с места на место, скажем, MsSqlToHiveTransfer;
    • sensor же позволит реагировать или притормозить дальнейшее выполнение дага до наступления какого-либо события. HttpSensor может дергать указанный эндпойнт, и когда дождется нужный ответ, запустить трансфер GoogleCloudStorageToS3Operator. Пытливый ум спросит: зачем? Ведь можно делать повторы прямо в операторе! А затем, чтобы не забивать пул тасков подвисшими операторами. Сенсор запускается, проверяет и умирает до следующей попытки.

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


  • Task instance когда генерал-планировщик решил, что таски пора отправлять в бой на исполнители-воркеры (прямо на месте, если мы используем LocalExecutor или на удалённую ноду в случае с CeleryExecutor), он назначает им контекст (т. е. комплект переменных параметров выполнения), разворачивает шаблоны команд или запросов и складывает их в пул.



Генерируем таски


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


Итак, в простейшем виде подобный даг будет выглядеть так:


from datetime import timedelta, datetimefrom airflow import DAGfrom airflow.operators.python_operator import PythonOperatorfrom commons.datasources import sql_server_dsdag = DAG('orders',          schedule_interval=timedelta(hours=6),          start_date=datetime(2020, 7, 8, 0))def workflow(**context):    print(context)for conn_id, schema in sql_server_ds:    PythonOperator(        task_id=schema,        python_callable=workflow,        provide_context=True,        dag=dag)

Давайте разбираться:


  • Сперва импортируем нужные либы и кое что ещё;
  • sql_server_ds это List[namedtuple[str, str]] с именами коннектов из Airflow Connections и базами данных из которых мы будем забирать нашу табличку;
  • dag объявление нашего дага, которое обязательно должно лежать в globals(), иначе Airflow его не найдет. Дагу также нужно сказать:
    • что его зовут orders это имя потом будет маячить в веб-интерфейсе,
    • что работать он будет, начиная с полуночи восьмого июля,
    • а запускать он должен, примерно каждые 6 часов (для крутых парней здесь вместо timedelta() допустима cron-строка 0 0 0/6 ? * * *, для менее крутых выражение вроде @daily);
  • workflow() будет делать основную работу, но не сейчас. Сейчас мы просто высыпем наш контекст в лог.
  • А теперь простая магия создания тасков:
    • пробегаем по нашим источникам;
    • инициализируем PythonOperator, который будет выполнять нашу пустышку workflow(). Не забывайте указывать уникальное (в рамках дага) имя таска и подвязывать сам даг. Флаг provide_context в свою очередь насыпет в функцию дополнительных аргументов, которые мы бережно соберём с помощью **context.

Пока на этом всё. Что мы получили:


  • новый даг в веб-интерфейсе,
  • полторы сотни тасков, которые будут выполняться параллельно (если то позволят настройки Airflow, Celery и мощности серверов).

Ну, почти получили.



Зависимости кто будет ставить?


Чтобы всё это дело упростить я вкорячил в docker-compose.yml обработку requirements.txt на всех нодах.


Вот теперь понеслась:



Серые квадратики task instances, обработанные планировщиком.


Немного ждем, задачи расхватывают воркеры:



Зеленые, понятное дело, успешно отработавшие. Красные не очень успешно.


Кстати, на нашем проде никакой папки ./dags, синхронизирующейся между машинами нет всё даги лежат в git на нашем Gitlab, а Gitlab CI раскладывает обновления на машины при мёрдже в master.

Немного о Flower


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


Самая первая страничка с суммарной информацией по нодам-воркерам:



Самая насыщенная страничка с задачами, отправившимися в работу:



Самая скучная страничка с состоянием нашего брокера:



Самая яркая страничка с графиками состояния тасков и их временем выполнения:



Догружаем недогруженное


Итак, все таски отработали, можно уносить раненых.



А раненых оказалось немало по тем или иным причинами. В случае правильного использования Airflow вот эти самые квадраты говорят о том, что данные определенно не доехали.


Нужно смотреть лог и перезапускать упавшие task instances.


Жмякнув на любой квадрат, увидим доступные нам действия:



Можно взять, и сделать Clear упавшему. То есть, мы забываем о том, что там что-то завалилось, и тот же самый инстанс таска уйдет планировщику.



Понятно, что делать так мышкой со всеми красными квадратами не очень гуманно не этого мы ждем от Airflow. Естественно, у нас есть оружие массового поражения: Browse/Task Instances



Выберем всё разом и обнулим нажмем правильный пункт:



После очистки наши такси выглядят так (они уже ждут не дождутся, когда шедулер их запланирует):



Соединения, хуки и прочие переменные


Самое время посмотреть на следующий DAG, update_reports.py:


from collections import namedtuplefrom datetime import datetime, timedeltafrom textwrap import dedentfrom airflow import DAGfrom airflow.contrib.operators.vertica_operator import VerticaOperatorfrom airflow.operators.email_operator import EmailOperatorfrom airflow.utils.trigger_rule import TriggerRulefrom commons.operators import TelegramBotSendMessagedag = DAG('update_reports',          start_date=datetime(2020, 6, 7, 6),          schedule_interval=timedelta(days=1),          default_args={'retries': 3, 'retry_delay': timedelta(seconds=10)})Report = namedtuple('Report', 'source target')reports = [Report(f'{table}_view', table) for table in [    'reports.city_orders',    'reports.client_calls',    'reports.client_rates',    'reports.daily_orders',    'reports.order_duration']]email = EmailOperator(    task_id='email_success', dag=dag,    to='{{ var.value.all_the_kings_men }}',    subject='DWH Reports updated',    html_content=dedent("""Господа хорошие, отчеты обновлены"""),    trigger_rule=TriggerRule.ALL_SUCCESS)tg = TelegramBotSendMessage(    task_id='telegram_fail', dag=dag,    tg_bot_conn_id='tg_main',    chat_id='{{ var.value.failures_chat }}',    message=dedent("""\         Наташ, просыпайся, мы {{ dag.dag_id }} уронили        """),    trigger_rule=TriggerRule.ONE_FAILED)for source, target in reports:    queries = [f"TRUNCATE TABLE {target}",               f"INSERT INTO {target} SELECT * FROM {source}"]    report_update = VerticaOperator(        task_id=target.replace('reports.', ''),        sql=queries, vertica_conn_id='dwh',        task_concurrency=1, dag=dag)    report_update >> [email, tg]

Все ведь когда-нибудь делали обновлялку отчетов? Это снова она: есть список источников, откуда забрать данные; есть список, куда положить; не забываем посигналить, когда всё случилось или сломалось (ну это не про нас, нет).


Давайте снова пройдемся по файлу и посмотрим на новые непонятные штуки:


  • from commons.operators import TelegramBotSendMessage нам ничто не мешает делать свои операторы, чем мы и воспользовались, сделав небольшую обёрточку для отправки сообщений в Разблокированный. (Об этом операторе мы еще поговорим ниже);
  • default_args={} даг может раздавать одни и те же аргументы всем своим операторам;
  • to='{{ var.value.all_the_kings_men }}' поле to у нас будет не захардкоженным, а формируемым динамически с помощью Jinja и переменной со списком email-ов, которую я заботливо положил в Admin/Variables;
  • trigger_rule=TriggerRule.ALL_SUCCESS условие запуска оператора. В нашем случае, письмо полетит боссам только если все зависимости отработали успешно;
  • tg_bot_conn_id='tg_main' аргументы conn_id принимают в себя идентификаторы соединений, которые мы создаем в Admin/Connections;
  • trigger_rule=TriggerRule.ONE_FAILED сообщения в Telegram улетят только при наличии упавших тасков;
  • task_concurrency=1 запрещаем одновременный запуск нескольких task instances одного таска. В противном случае, мы получим одновременный запуск нескольких VerticaOperator (смотрящих на одну таблицу);
  • report_update >> [email, tg] все VerticaOperator сойдутся в отправке письма и сообщения, вот так:


    Но так как у операторов-нотификаторов стоят разные условия запуска, работать будет только один. В Tree View всё выглядит несколько менее наглядно:



Скажу пару слов о макросах и их друзьях переменных.


Макросы это Jinja-плейсхолдеры, которые могут подставлять разную полезную информацию в аргументы операторов. Например, так:


SELECT    id,    payment_dtm,    payment_type,    client_idFROM orders.paymentsWHERE    payment_dtm::DATE = '{{ ds }}'::DATE

{{ ds }} развернется в содержимое переменной контекста execution_date в формате YYYY-MM-DD: 2020-07-14. Самое приятное, что переменные контекста прибиваются гвоздями к определенному инстансу таска (квадратику в Tree View), и при перезапуске плейсхолдеры раскроются в те же самые значения.


Присвоенные значения можно смотреть с помощью кнопки Rendered на каждом таск-инстансе. Вот так у таска с отправкой письма:



А так у таски с отправкой сообщения:



Полный список встроенных макросов для последней доступной версии доступен здесь: Macros Reference


Более того, с помощью плагинов, мы можем объявлять собственные макросы, но это уже совсем другая история.


Помимо предопределенных штук, мы можем подставлять значения своих переменных (выше в коде я уже этим воспользовался). Создадим в Admin/Variables пару штук:



Всё, можно пользоваться:


TelegramBotSendMessage(chat_id='{{ var.value.failures_chat }}')

В значении может быть скаляр, а может лежать и JSON. В случае JSON-а:


bot_config{    "bot": {        "token": 881hskdfASDA16641,        "name": "Verter"    },    "service": "TG"}

просто используем путь к нужному ключу: {{ var.json.bot_config.bot.token }}.


Скажу буквально одно слово и покажу один скриншот про соединения. Тут всё элементарно: на странице Admin/Connections создаем соединение, складываем туда наши логины/пароли и более специфичные параметры. Вот так:



Пароли можно шифровать (более тщательно, чем в варианте по умолчанию), а можно не указывать тип соединения (как я сделал для tg_main) дело в том, что список типов зашит в моделях Airflow и расширению без влезания в исходники не поддается (если вдруг я чего-то не догуглил прошу меня поправить), но получить креды просто по имени нам ничто не помешает.


А еще можно сделать несколько соединений с одним именем: в таком случае метод BaseHook.get_connection(), который достает нам соединения по имени, будет отдавать случайного из нескольких тёзок (было бы логичнее сделать Round Robin, но оставим это на совести разработчиков Airflow).


Variables и Connections, безусловно, классные средства, но важно не потерять баланс: какие части ваших потоков вы храните собственно в коде, а какие отдаете на хранение Airflow. C одной стороны быстро поменять значение, например, ящик рассылки, может быть удобно через UI. А с другой это всё-таки возврат к мышеклику, от которого мы (я) хотели избавиться.

Работа с соединениями это одна из задач хуков. Вообще хуки Airflow это точки подключения его к сторонним сервисам и библиотекам. К примеру, JiraHook откроет для нас клиент для взаимодействия с Jira (можно задачки подвигать туда-сюда), а с помощью SambaHook можно запушить локальный файл на smb-точку.


Разбираем кастомный оператор


И мы вплотную подобрались к тому, чтобы посмотреть на то, как сделан TelegramBotSendMessage


Код commons/operators.py с собственно оператором:


from typing import Unionfrom airflow.operators import BaseOperatorfrom commons.hooks import TelegramBotHook, TelegramBotclass TelegramBotSendMessage(BaseOperator):    """Send message to chat_id using TelegramBotHook    Example:        >>> TelegramBotSendMessage(        ...     task_id='telegram_fail', dag=dag,        ...     tg_bot_conn_id='tg_bot_default',        ...     chat_id='{{ var.value.all_the_young_dudes_chat }}',        ...     message='{{ dag.dag_id }} failed :(',        ...     trigger_rule=TriggerRule.ONE_FAILED)    """    template_fields = ['chat_id', 'message']    def __init__(self,                 chat_id: Union[int, str],                 message: str,                 tg_bot_conn_id: str = 'tg_bot_default',                 *args, **kwargs):        super().__init__(*args, **kwargs)        self._hook = TelegramBotHook(tg_bot_conn_id)        self.client: TelegramBot = self._hook.client        self.chat_id = chat_id        self.message = message    def execute(self, context):        print(f'Send "{self.message}" to the chat {self.chat_id}')        self.client.send_message(chat_id=self.chat_id,                                 message=self.message)

Здесь, как и остальное в Airflow, всё очень просто:


  • Отнаследовались от BaseOperator, который реализует довольно много Airflow-специфичных штук (посмотрите на досуге)
  • Объявили поля template_fields, в которых Jinja будет искать макросы для обработки.
  • Организовали правильные аргументы для __init__(), расставили умолчания, где надо.
  • Об инициализации предка тоже не забыли.
  • Открыли соответствующий хук TelegramBotHook, получили от него объект-клиент.
  • Оверрайднули (переопределили) метод BaseOperator.execute(), который Airfow будет подергивать, когда наступит время запускать оператор в нем мы и реализуем основное действие, на забыв залогироваться. (Логируемся, кстати, прямо в stdout и stderr Airflow всё перехватит, красиво обернет, разложит, куда надо.)

Давайте смотреть, что у нас в commons/hooks.py. Первая часть файлика, с самим хуком:


from typing import Unionfrom airflow.hooks.base_hook import BaseHookfrom requests_toolbelt.sessions import BaseUrlSessionclass TelegramBotHook(BaseHook):    """Telegram Bot API hook    Note: add a connection with empty Conn Type and don't forget    to fill Extra:        {"bot_token": "YOuRAwEsomeBOtToKen"}    """    def __init__(self,                 tg_bot_conn_id='tg_bot_default'):        super().__init__(tg_bot_conn_id)        self.tg_bot_conn_id = tg_bot_conn_id        self.tg_bot_token = None        self.client = None        self.get_conn()    def get_conn(self):        extra = self.get_connection(self.tg_bot_conn_id).extra_dejson        self.tg_bot_token = extra['bot_token']        self.client = TelegramBot(self.tg_bot_token)        return self.client

Я даже не знаю, что тут можно объяснять, просто отмечу важные моменты:


  • Наследуемся, думаем над аргументами в большинстве случаев он будет один: conn_id;
  • Переопределяем стандартные методы: я ограничился get_conn(), в котором я получаю параметры соединения по имени и всего-навсего достаю секцию extra (это поле для JSON), в которую я (по своей же инструкции!) положил токен Telegram-бота: {"bot_token": "YOuRAwEsomeBOtToKen"}.
  • Создаю экземпляр нашего TelegramBot, отдавая ему уже конкретный токен.

Вот и всё. Получить клиент из хука можно c помощью TelegramBotHook().clent или TelegramBotHook().get_conn().


И вторая часть файлика, в котором я сделать микрообёрточку для Telegram REST API, чтобы не тащить тот же python-telegram-bot ради одного метода sendMessage.


class TelegramBot:    """Telegram Bot API wrapper    Examples:        >>> TelegramBot('YOuRAwEsomeBOtToKen', '@myprettydebugchat').send_message('Hi, darling')        >>> TelegramBot('YOuRAwEsomeBOtToKen').send_message('Hi, darling', chat_id=-1762374628374)    """    API_ENDPOINT = 'https://api.telegram.org/bot{}/'    def __init__(self, tg_bot_token: str, chat_id: Union[int, str] = None):        self._base_url = TelegramBot.API_ENDPOINT.format(tg_bot_token)        self.session = BaseUrlSession(self._base_url)        self.chat_id = chat_id    def send_message(self, message: str, chat_id: Union[int, str] = None):        method = 'sendMessage'        payload = {'chat_id': chat_id or self.chat_id,                   'text': message,                   'parse_mode': 'MarkdownV2'}        response = self.session.post(method, data=payload).json()        if not response.get('ok'):            raise TelegramBotException(response)class TelegramBotException(Exception):    def __init__(self, *args, **kwargs):        super().__init__((args, kwargs))

Правильный путь сложить всё это: TelegramBotSendMessage, TelegramBotHook, TelegramBot в плагин, положить в общедоступный репозиторий, и отдать в Open Source.

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



В нашем даге что-то сломалось! А ни этого ли мы ждали? Именно!


Наливать-то будешь?


Чувствуете, что-то я пропустил? Вроде бы обещал данные из SQL Server в Vertica переливать, и тут взял и съехал с темы, негодяй!


Злодеяние это было намеренным, я просто обязан был расшифровать вам кое-какую терминологию. Теперь можно ехать дальше.


План у нас был такой:


  1. Сделать даг
  2. Нагенерить таски
  3. Посмотреть, как всё красиво
  4. Присваивать заливкам номера сессий
  5. Забрать данные из SQL Server
  6. Положить данные в Vertica
  7. Собрать статистику

Итак, чтобы всё это запустить, я сделал маленькое дополнение к нашему docker-compose.yml:


docker-compose.db.yml
version: '3.4'x-mssql-base: &mssql-base  image: mcr.microsoft.com/mssql/server:2017-CU21-ubuntu-16.04  restart: always  environment:    ACCEPT_EULA: Y    MSSQL_PID: Express    SA_PASSWORD: SayThanksToSatiaAt2020    MSSQL_MEMORY_LIMIT_MB: 1024services:  dwh:    image: jbfavre/vertica:9.2.0-7_ubuntu-16.04  mssql_0:    <<: *mssql-base  mssql_1:    <<: *mssql-base  mssql_2:    <<: *mssql-base  mssql_init:    image: mio101/py3-sql-db-client-base    command: python3 ./mssql_init.py    depends_on:      - mssql_0      - mssql_1      - mssql_2    environment:      SA_PASSWORD: SayThanksToSatiaAt2020    volumes:      - ./mssql_init.py:/mssql_init.py      - ./dags/commons/datasources.py:/commons/datasources.py

Там мы поднимаем:


  • Vertica как хост dwh с самыми дефолтными настройками,
  • три экземпляра SQL Server,
  • наполняем базы в последних кое-какими данными (ни в коем случае не заглядывайте в mssql_init.py!)

Запускаем всё добро с помощью чуть более сложной, чем в прошлый раз, команды:


$ docker-compose -f docker-compose.yml -f docker-compose.db.yml up --scale worker=3

Что нагенерировал наш чудорандомайзер, можно, воспользовавшись пунктом Data Profiling/Ad Hoc Query:



Главное, не показывать это аналитикам


Подробно останавливаться на ETL-сессиях я не буду, там всё тривиально: делаем базу, в ней табличку, оборачиваем всё менеджером контекста, и теперь делаем так:


with Session(task_name) as session:    print('Load', session.id, 'started')    # Load worflow    ...    session.successful = True    session.loaded_rows = 15

session.py
from sys import stderrclass Session:    """ETL workflow session    Example:        with Session(task_name) as session:            print(session.id)            session.successful = True            session.loaded_rows = 15            session.comment = 'Well done'    """    def __init__(self, connection, task_name):        self.connection = connection        self.connection.autocommit = True        self._task_name = task_name        self._id = None        self.loaded_rows = None        self.successful = None        self.comment = None    def __enter__(self):        return self.open()    def __exit__(self, exc_type, exc_val, exc_tb):        if any(exc_type, exc_val, exc_tb):            self.successful = False            self.comment = f'{exc_type}: {exc_val}\n{exc_tb}'            print(exc_type, exc_val, exc_tb, file=stderr)        self.close()    def __repr__(self):        return (f'<{self.__class__.__name__} '                 f'id={self.id} '                 f'task_name="{self.task_name}">')    @property    def task_name(self):        return self._task_name    @property    def id(self):        return self._id    def _execute(self, query, *args):        with self.connection.cursor() as cursor:            cursor.execute(query, args)            return cursor.fetchone()[0]    def _create(self):        query = """            CREATE TABLE IF NOT EXISTS sessions (                id          SERIAL       NOT NULL PRIMARY KEY,                task_name   VARCHAR(200) NOT NULL,                started     TIMESTAMPTZ  NOT NULL DEFAULT current_timestamp,                finished    TIMESTAMPTZ           DEFAULT current_timestamp,                successful  BOOL,                loaded_rows INT,                comment     VARCHAR(500)            );            """        self._execute(query)    def open(self):        query = """            INSERT INTO sessions (task_name, finished)            VALUES (%s, NULL)            RETURNING id;            """        self._id = self._execute(query, self.task_name)        print(self, 'opened')        return self    def close(self):        if not self._id:            raise SessionClosedError('Session is not open')        query = """            UPDATE sessions            SET                finished    = DEFAULT,                successful  = %s,                loaded_rows = %s,                comment     = %s            WHERE                id = %s            RETURNING id;            """        self._execute(query, self.successful, self.loaded_rows,                      self.comment, self.id)        print(self, 'closed',              ', successful: ', self.successful,              ', Loaded: ', self.loaded_rows,              ', comment:', self.comment)class SessionError(Exception):    passclass SessionClosedError(SessionError):    pass

Настала пора забрать наши данные из наших полутора сотен таблиц. Сделаем это с помощью очень незатейливых строчек:


source_conn = MsSqlHook(mssql_conn_id=src_conn_id, schema=src_schema).get_conn()query = f"""    SELECT         id, start_time, end_time, type, data    FROM dbo.Orders    WHERE        CONVERT(DATE, start_time) = '{dt}'    """df = pd.read_sql_query(query, source_conn)

  1. С помощью хука получим из Airflow pymssql-коннект
  2. В запрос подставим ограничение в виде даты в функцию её подбросит шаблонизатор.
  3. Скармливаем наш запрос pandas, который достанет для нас DataFrame он нам пригодится в дальнейшем.

Я использую подстановку {dt} вместо параметра запроса %s не потому, что я злобный Буратино, а потому что pandas не может совладать с pymssql и подсовывает последнему params: List, хотя тот очень хочет tuple.
Также обратите внимание, что разработчик pymssql решил больше его не поддерживать, и самое время съехать на pyodbc.

Посмотрим, чем Airflow нашпиговал аргументы наших функций:



Если данных не оказалось, то продолжать смысла нет. Но считать заливку успешной тоже странно. Но это и не ошибка. А-а-а, что делать?! А вот что:


if df.empty:    raise AirflowSkipException('No rows to load')

AirflowSkipException скажет Airflow, что ошибки, собственно нет, а таск мы пропускаем. В интерфейсе будет не зеленый и не красный квадратик, а цвета pink.


Подбросим нашим данным несколько колонок:


df['etl_source'] = src_schemadf['etl_id'] = session.iddf['hash_id'] = hash_pandas_object(df[['etl_source', 'id']])

А именно:


  • БД, из которой мы забрали заказы,
  • Идентификатор нашей заливающей сессии (она будет разной на каждый таск),
  • Хэш от источника и идентификатора заказа чтобы в конечной базе (где всё ссыпется в одну таблицу) у нас был уникальный идентификатор заказа.

Остался предпоследний шаг: залить всё в Vertica. А, как ни странно, один из самых эффектных эффективных способов сделать это через CSV!


# Export data to CSV bufferbuffer = StringIO()df.to_csv(buffer,          index=False, sep='|', na_rep='NUL', quoting=csv.QUOTE_MINIMAL,          header=False, float_format='%.8f', doublequote=False, escapechar='\\')buffer.seek(0)# Push CSVtarget_conn = VerticaHook(vertica_conn_id=target_conn_id).get_conn()copy_stmt = f"""    COPY {target_table}({df.columns.to_list()})     FROM STDIN     DELIMITER '|'     ENCLOSED '"'     ABORT ON ERROR     NULL 'NUL'    """cursor = target_conn.cursor()cursor.copy(copy_stmt, buffer)

  1. Мы делаем спецприёмник StringIO.
  2. pandas любезно сложит в него наш DataFrame в виде CSV-строк.
  3. Откроем соединение к нашей любимой Vertica хуком.
  4. А теперь с помощью copy() отправим наши данные прямо в Вертику!

Из драйвера заберем, сколько строчек засыпалось, и скажем менеджеру сессии, что всё ОК:


session.loaded_rows = cursor.rowcountsession.successful = True

Вот и всё.


На проде мы создаем целевую табличку вручную. Здесь же я позволил себе небольшой автомат:

create_schema_query = f'CREATE SCHEMA IF NOT EXISTS {target_schema};'create_table_query = f"""    CREATE TABLE IF NOT EXISTS {target_schema}.{target_table} (         id         INT,         start_time TIMESTAMP,         end_time   TIMESTAMP,         type       INT,         data       VARCHAR(32),         etl_source VARCHAR(200),         etl_id     INT,         hash_id    INT PRIMARY KEY     );"""create_table = VerticaOperator(    task_id='create_target',    sql=[create_schema_query,         create_table_query],    vertica_conn_id=target_conn_id,    task_concurrency=1,    dag=dag)

Я с помощью VerticaOperator() создаю схему БД и таблицу (если их еще нет, естественно). Главное, правильно расставить зависимости:

for conn_id, schema in sql_server_ds:    load = PythonOperator(        task_id=schema,        python_callable=workflow,        op_kwargs={            'src_conn_id': conn_id,            'src_schema': schema,            'dt': '{{ ds }}',            'target_conn_id': target_conn_id,            'target_table': f'{target_schema}.{target_table}'},        dag=dag)    create_table >> load

Подводим итоги


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

Джулия Дональдсон, Груффало


Думаю, если бы мы с моими коллегами устроили соревнование: кто быстрее составит и запустит с нуля ETL-процесс: они со своими SSIS и мышкой и я с Airflow А потом бы мы еще сравнили удобство сопровождения Ух, думаю, вы согласитесь, что я обойду их по всем фронтам!


Если же чуть-чуть посерьезнее, то Apache Airflow за счет описания процессов в виде программного кода сделал мою работу гораздо удобнее и приятнее.


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


Часть заключительная, справочно-информационная


Грабли, которые мы собрали за вас


  • start_date. Да, это уже локальный мемасик. Через главный аргумент дага start_date проходят все. Кратко, если указать в start_date текущую дату, а в schedule_interval один день, то DAG запустится завтра не раньше.


    start_date = datetime(2020, 7, 7, 0, 1, 2)
    

    И больше никаких проблем.


    С ним же связана и еще одна ошибка выполнения: Task is missing the start_date parameter, которая чаще всего говорит о том, что вы забыли привязать к оператору даг.


  • Всё на одной машине. Да, и базы (самого Airflow и нашей обмазки), и веб-сервер, и планировщик, и воркеры. И оно даже работало. Но со временем количество задач у сервисов росло, и когда PostgreSQL стал отдавать ответ по индексу за 20 с вместо 5 мс, мы его взяли и унесли.


  • LocalExecutor. Да, мы сидим на нём до сих пор, и мы уже подошли к краю пропасти. LocalExecutorа нам до сих пор хватало, но сейчас пришла пора расшириться минимум одним воркером, и придется поднапрячься, чтобы переехать на CeleryExecutor. А ввиду того, что с ним можно работать и на одной машиной, то ничего не останавливает от использования Celery даже не сервере, который естественно, никогда не пойдет в прод, чесслово!


  • Неиспользование встроенных средств:


    • Connections для хранения учетных данных сервисов,
    • SLA Misses для реагирования на таски, которые не отработали вовремя,
    • XCom для обмена метаданными (я сказал метаданными!) между тасками дага.

  • Злоупотребление почтой. Ну что тут сказать? Были настроены оповещения на все повторы упавших тасков. Теперь в моём рабочем Gmail >90k писем от Airflow, и веб-морда почты отказывается брать и удалять больше чем по 100 штук за раз.



Больше подводных камней: Apache Airflow Pitfails

Средства ещё большей автоматизации


Для того чтобы нам еще больше работать головой, а не руками, Airflow заготовила для нас вот что:


  • REST API он до сих пор имеет статус Experimental, что не мешает ему работать. С его помощью можно не только получать информацию о дагах и тасках, но остановить/запустить даг, создать DAG Run или пул.


  • CLI через командную строку доступны многие средства, которые не просто неудобны в обращении через WebUI, а вообще отсутствуют. Например:


    • backfill нужен для повторного запуска инстансов тасков.
      Например, пришли аналитики, говорят: А у вас, товарищ, ерунда в данных с 1 по 13 января! Чини-чини-чини-чини!. А ты такой хоба:
      airflow backfill -s '2020-01-01' -e '2020-01-13' orders
      
    • Обслуживание базы: initdb, resetdb, upgradedb, checkdb.
    • run, который позволяет запустить один инстанс таска, да еще и забить на всё зависимости. Более того, можно запустить его через LocalExecutor, даже если у вас Celery-кластер.
    • Примерно то же самое делает test, только еще и в баз ничего не пишет.
    • connections позволяет массово создавать подключения из шелла.

  • Python API довольно хардкорный способ взаимодействия, который предназначен для плагинов, а не копошения в нём ручёнками. Но кто ж нам помешает пойти в /home/airflow/dags, запустить ipython и начать беспредельничать? Можно, например, экспортировать все подключения таком кодом:


    from airflow import settingsfrom airflow.models import Connectionfields = 'conn_id conn_type host port schema login password extra'.split()session = settings.Session()for conn in session.query(Connection).order_by(Connection.conn_id):  d = {field: getattr(conn, field) for field in fields}  print(conn.conn_id, '=', d)
    

  • Подключение к базе метаданных Airflow. Писать в неё я не рекомендую, а вот доставать состояния тасков для различных специфических метрик можно значительно быстрее и проще, чем через любой из API.


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


    Осторожно, SQL!
    WITH last_executions AS (SELECT    task_id,    dag_id,    execution_date,    state,        row_number()        OVER (            PARTITION BY task_id, dag_id            ORDER BY execution_date DESC) AS rnFROM public.task_instanceWHERE    execution_date > now() - INTERVAL '2' DAY),failed AS (    SELECT        task_id,        dag_id,        execution_date,        state,        CASE WHEN rn = row_number() OVER (            PARTITION BY task_id, dag_id            ORDER BY execution_date DESC)                 THEN TRUE END AS last_fail_seq    FROM last_executions    WHERE        state IN ('failed', 'up_for_retry'))SELECT    task_id,    dag_id,    count(last_fail_seq)                       AS unsuccessful,    count(CASE WHEN last_fail_seq        AND state = 'failed' THEN 1 END)       AS failed,    count(CASE WHEN last_fail_seq        AND state = 'up_for_retry' THEN 1 END) AS up_for_retryFROM failedGROUP BY    task_id,    dag_idHAVING    count(last_fail_seq) > 0
    



Ссылки


Ну и естественно первые десять ссылок из выдачи гугла содержимое папки Airflow из моих закладок.



И ссылки, задействованные в статье:


Подробнее..

Выжать максимум Cloud Composer как fully-managed решение для Airflow

29.01.2021 14:10:10 | Автор: admin

Привет,Хабр! Меня зовут Сергей, яLeadSoftwareEngineer/SreamLeadв ЕРАМ,сертифицированныйGoogleCloudинженер и архитектор. Уже более 10лет занимаюсь коммерческой разработкой для различныхглобальныхкомпаний,в основном с фокусом набэкенд.А еще яочень люблю делиться своими знаниями.Сегодня хочу рассказать проApacheAirflow, который, на мой взгляд, является хорошиминструментом для построениявашихпайплайнов.

Какой план?

  • В двух словахрасскажупроAirflow для тех, кто ещес нимне работал.Всеэтоболее подробноможно найти на просторах интернета,поэтому пройдусьлишьпобазовымконцепциям.

  • Посмотрим, что такоеGoogleCloudComposer, как он используетAirflowиупрощает разработкунареальных проектах.

  • Взглянем наdevelopmentиdeploymentпрактикив рамкахGoogleCloudComposer,а такжетрудностииограничения,с которыми можно столкнутьсяво времязапускаAirflowвCloudComposer.

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

Airflowв нескольких абзацах

Итак, этоинструментдля планирования, построения и мониторингапайплайнов, написанных наPython.Есть и другие готовые решения дляоркестровки процессов, напримерLuigi. Но сейчас поговорим о достоинствахAirflow:

  1. Действительнохорошдля построенияпайплайнов. В основе всего унегостоитнаправленныйацикличный граф, которыйпозволяет реализовыватьпоследовательноеилипараллельное исполнение задач, а еще управлятьихпорядкомизависимостями.

  2. Достаточно расширяем.Сегоднясуществуютмногоopen-sourseрешений либо библиотек, которые можнок немуподключить и использовать.

  3. Веб-интерфейс: простой, хороший и удобный.

  4. Идет в пачке сREST API, а значитзапускатьпайплайныили получатьинформациюоихсостояниимы можемспомощьюAPI.

РассмотримETL-pipelineиз живогопроекта. Предположим, у нас есть файлы разных типов в GCSбакете. Их необходимо скачать, прочитать содержимое, агрегировать все данные с разных файлов, а затем записать полученный результат. Кроме того, перед началом и в момент завершенияпайплайнанужно отправить запрос на обновление статуса сервису, который хранит информацию опайплайнах.

Разберемся состроительнымиблокамиAirflow.Первым идетDAG(DirectedAcyclicGraph)ацикличный направленныйграф,основаAirflow. В целом это коллекция задач, которые мы хотим запустить. Они организованы специальным образом,могутиметьмежду собой зависимости.

Tasksопределяют единицу работы DAG и выражены как его узлы.Если проще, то задачиобозначают,чтоконкретно нам нужносделать.АOperatorsв свою очередь описывают,какмы хотим это сделать, конкретизируют действияв рамках задачи.На картинкехорошовидно, что задачи это узлыDAG-а. Ониявляются сущностямиразных операторовDataLoadOperator,GoogleCloudStorageListOperatorиUpdateStatusOperatorкаждый из которыхописывает некую единицу логики, которую нужно будет сделать.Как видно, некоторые задачи, выстроенные вDAG-е, могутвыполняться параллельно. Всезависит оттипаисполнителязадачи количестваворкеров.

EщеестьDAGsRunиTasksInstances.DAGsRun это объект самогоDAGа,предназначенныйк запуску в конкретные логические время и дату.TasksInstance объект задачи,ассоциированныйс конкретнымDAGRun.А логические дата и время дляDAGRunи егоTasksInstances этоexecutiondate.

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

Плавно переходим ккомпонентам, которыеучаствуютв исполнениипайплайнавнутриAirflow:

  • Планировщик(Scheduler)мониторит всеDAG-и,задачи иотвечаетзаихсвоевременный запускиотправку на исполнение.

  • Исполнители(илиExecutor)сущность, котораяследитзаорганизациейисполнениязадач.Они бывают разные:SequentialExecutor,CeleryExecutorи др. А некоторыеиз исполнителей могут иметь дополнительные зависимости. Например,CeleryExecutorтребуетQueueBroker.

  • Воркеры(Workers)просто исполняют наши задачи(подобноворкерамCelery).

  • Веб-сервернужен, чтобы при помощи, например,тех жеHTTP-запросов, предоставлять информацию поDAG-амизадачам.Онвзаимодействует с базой,где хранятсяданныео статусе задач, переменныхAirflow,DAG-ах, соединениях ит. д.

  • Всяинформацияпроисполнениезадачпишется вLogs.Логированиепроисходитво время исполнения задач из самого кода,а такжепишутсясистемныелогиAirflow.Местомхранения логовмогут быть просто файлы, база данных либо какое-нибудь решение отсloud-провайдера. Например,можно писать вStackdriverиливGCSbucket.

  • С помощьюAdminPanelмыактивируем/деактивируемизапускаемDAG-и,устанавливаемпеременные,смотримлоги,находимпричиныпроблем(например, почемупайплайныупали).

Как это всеработает вместе?

Операционная частьAirflowпостроена поверх базыданных, которая хранит информацию о задачахиDAG-ах. Планировщикотправляетзадачи в очередь, аворкерыпо мере доступности берут ивыполняютих.Веб-серверобычнозапускаетсяна той жемашине, где и планировщик.Ониспользуетбазу данных длярендерасостояниязадач,полученияtaskexecutionлогов, атакже обслуживаетзапросыAdminPanel. Важно отметить, что каждый выделенный блок может работать в изоляции от других. То естьэто реальноразвертыватьихабсолютно на разных серверах или узлах вопределенномвиде, в зависимости от настройкиdeployment-конфигурации.

GoogleCloudComposerизнутри

GoogleCloudComposerилипростоComposer этоfullymanagedсервисоркестрации, который позволяет создавать, планировать и мониторитьвашипайплайн вcloud.

Сразунесколько примеров, почему это удобно и классно.Допустим,у насестьклиент, который хочет автоматизироватьежедневныйпереносданных изstorageA вstorageB.Для этого мы напишемAirflowDAG,задеплоимего вComposerибудемзапускатьнашпайплайнвконкретный интервал времени.Задачимогут исполняться синхронно,стартоватьв определенном порядкеили параллельно.Если вдруг что-то упадет, тоестьмеханизмretry, которыйподнимет задачу ипопробуетзановоее исполнить.В общем, весь процесс зависит от настроек задачи.Теперь представьте, что нам нужно было бы сделатьcronjobs, строя при этом 100 разныхпайплайнов,без помощиAirflowиComposerоперирование, запуск и отладка дались бы гораздо труднее.

И еще немного о плюсахComposer:

  • Во-первых,с нимлегко разрабатывать идеплоить.Composerпостроен поверхAirflowипредоставляетвнутриGoogleConsoleвесь необходимый UI из коробки, чтобыбылоудобно работать,создавать окруженияизагружать файлыDAG-овс легкостью. Тамужевсе есть, мы просто создаемфайлыDAG-ов,плагиновиоператоров, указываем дополнительныезависисмости, а после загружаем в заранее созданныйComposerbucketвGCS.Дальшефайлы из бакета будут исполняться вузлахворкеров.

  • Во-вторых,уComposerестьмеханизмдля управления окружениями, причемвсе делается через UI.То есть мы можем создаватьнужное количествоокруженийсо своими версиямииотдельнымиAirflow, которые никак не будут пересекаться.

  • В-третьих,Composerтесно интегрирован сsecurity-подходами и инструментами, которые есть вGoogleCloud. Например,PrivateIPs,Authorizationит. д.

  • В-четвертых,ComposerConsoleпредоставляетстраницу метрикзапущенныхокружений.

Давайтепосмотримна компонентыComposer.На возможноеЗачем, если этоfullymanagedсервис и в нем все можно накликать?сразу скажу,чтознаниемеханизмов, сильных и слабых сторонпомогаетсделать разработку решений более эффективной.Итак,компоненты:

Пройдемся по некоторым(держим в голове, чтоComposerвсе делает сам).Накартинке видимTenantProject это для того, чтобы унифицироватьIdentityAccessManagement. Просто реализован дополнительный уровень безопасности.AppEngineFlexibleиспользуетсядля веб-сервера,CloudSQLкак база данныхAirflow.УCloudSQLестьопределенные плюсы, например,он даетограниченный доступ для сервис-аккаунта, а еще каждый день автоматически делает бэкапы.ВнутриCloudStorageComposerсоздаетbucket, который становится единой точкой для того, чтобы автоматическизагружать/обновлятьDAG-и,плагины и зависимости к ним. Файлы избакетабудут использоваться всемиворкерами, поднятымивKubernetesкластере.Core-компоненты, такие какпланировщик,worker-нодыи, например,Redis, который используетсядляCeleryExecutor,поднимаются внутриGoogleKubernetesEngine.Все это тоже автоматизировано:вместе с окружениемсоздаютсяKubernetesкластер, необходимое количествонодов,отдельноподнимаютсяворкерыиRedis.Кстати,RedisобслуживаеточередьзадачвнутриAirflow, а также нуженкак персистентное хранилище между рестартами контейнеров внутриKubernetesEngine.Большой плюс, чтоComposerхорошо интегрирован соStackdriverдля мониторинга илоггинга можно вселогии метрикисобиратьв одном месте.Это очень удобно, особенно когда у нас 100 узлов в кластере, а то и больше.

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

Кстати, воткакпроисходитмасштабированиеокружения:

Веб-сервер,который находится вTenantProject, и база данныхвCloudSQLавтоматически масштабируются вертикально.Аворкерыгоризонтально.Вовремя создания илив настройкахуже работающегоокружения мыможем либоувеличить,либо уменьшить количествоворкеров, следовательноичислоузлов в кластере.

Deploymentиdevelopment, трудности и ограничения

В первую очередь хочетсясказатьпро структурукодапроектаона очень важна дляудобства вразработке,поддержкеирасширении.Приведу пример из практики:

Обратите внимание, что код разбит подиректориямвендоров либоподоменным областямпродукта. А ещеAirflowдобавляетпеременныеDAGS_FOLDERиPLUGINS_FOLDERвsys.pathи потом автоматическипроцесситфайлы, которые в них находятся.Тутпросто: вDAGS_FOLDERищемDAG-и, а вPLUGINS_FOLDERсущности плагинов дляAirflow.

Итак, мы разделяем все на папки вендоров, каждаяизкоторыхможет содержатьвнутриподпапку библиотекlibsилиutils. Все зависит от того,каквам будетудобнеедержатьвместе файлы.Подпапки внутриpluginsотносятся кAirflowPLUGINS,их структурасоответствует конвенции оформления плагинов вAirflow.Я имею в видуoperators,hooks,macrosвсе, чтоAirflowпредоставляет из коробки.

Теперь поговорим прозависимостивнутри проекта.Ярекомендуюиспользоватьpipиrequirements.txtфайлкак можно чаще.Еслиработаем вComposer,тоуказывать в настройках UI,какие пакеты и версиинеобходимоустановитьв каждом из контейнеров. Или,если вы управляете окружениями внутри ваших CI/CDпайплайнов, можно устанавливать зависимости используя утилитуgcloud.Установка зависимостей с помощьюpipхорошо описана в документацииComposer.

Так какAirflowдобавляетDAGS_FOLDERиPLUGINS_FOLDERвsys.path,необходимо,по возможности,держатьлокальные зависимости вDAGS_FOLDER, а не вPLUGINS_FOLDER.Все дело в том, как работаетAirflow,ивего механизмахпоиска, сканированияи исполненияэтихдиректорий.Например,вAirflowестьнастройка, которая позволяет указать, с какой периодичностьюресканироватьпапку сDAG-ами. АвPLUGINS_FOLDERвсегораздосложнее.Поэтому не стоитпомещать много исполняемого кодаилимодулей внутрьpluginsпри каждомресканеили запуске все файлы будут исполняться и загружаться.А это может нести за собой дополнительные накладные расходы.Вообще многие команды сталкиваются с тем, что перезагрузка окружения происходит так долго, что задачисильно подвисают ипросто не могут исполняться.

Стоитьсказатьпроограничения настроекAirflow:их не все можно переопределить.Composerоставляетза собойправопредоставлять некоторые настройки только в режимеread-only. Полный список защищенных настроек можно подсмотретьв документации.

Ябыещепосоветовал использовать.airflowignoreфайл. Он работает как.gitignoreфайл, который можно помещать в директории либо описывать в нем паттерны для того, чтобымеханизмамиAirflowисключать из сканирования эти директорииилитипы файлов внутри. Это достаточно удобно, когда у нас есть большая директория. Например, в PLUGINS_FOLDER очень много кода,анамнужно хранить зависимости рядом с оператором в той же директории.Благодаря.airflowignoreзависимостибудут доступны внутриPythonкода, носам сканер их проигнорирует.

Немного про плагины вAirflow.Я рекомендуюне использовать встроенныйвAirflowмеханизмплагинов, разве что толькодля UI-вещей, например для созданияView.А дляoperators,hooksилибольшой общей логики я бывзялпростые классы.За счет того, чтоAirflowавтоматически добавляетPLUGINS_FOLDER вsys.path, можнобез проблемприменятьобычный импорт, как например,fromvnd.operators.my_operator. Такмы небудемиспользоватьникакие сущности,динамическую загрузкуилинейминг, которые предоставляет намAirflow. Пример, как может быть организован импорт внутри модулей:

from vnd.operators.my_operator import MyOperatorfrom vnd.sensors.my_sensor import MySensor

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

Еще совет:избегать, на сколько это возможно,callable-кода внутри модулей, то есть использоватьlazyподход. Это в целом относится и к плагинам, и кDAG-ам. За счет того, чторесканидеткаждый раз c определенныминтервалом, код будет исполнятьсятоже каждый раз.Этоможетповлечьза собой определенные ошибки, задержкивмоментобновлениясамого окружения.Иногдавстречаются кейсы,когда внутри наглобальномуровне самого модуля разработчики написали такойкод,гдемы ходим в базу данных, достаем какие-то записи,процессими потом используем этизначения в качествепеременныхв другом месте. Эти запросы будутвыполнятьсякаждый раз:мы каждую секунду будемресканироватьнашу директорию сDAG-амииисполнять этот код.

CI/СD подходтут нет ничего сложного. Мы используем те же самые подходысlinters,isortsдляGitlabCI/CDпайплайнов.В целомможнобрать любой CI/СDинструментна ваше усмотрение, ведьвсе они работают примерно одинаково:Jenkins,Gitlabpipeline,Spinnaker.

Например,мыв своемпроектеиспользуемlinters,потомидет шагunitтестов,дальшеинтеграционных.Чтобы доставить это вComposer,вызываемgcloudrsync.

Так как весь код вComposerдеплоитсявбакет,то нам нужнопросто обновить файлы до последней версиипри помощиrsync,которые в последствии будут использованыворкерамиAirflow.Конечно, можновзять командуgcloudcomposer,сее помощью регистрировать новыеDAG-и,плагиныи из каких-то своих исходных файлов все будет автоматическидоставляться внужныйбакет. Но,намой взгляд, этовыглядиткак-то сложно. Поэтому хорошо бы иметь свойrsync, который будет складыватьлибообновленные файлы,либо все фалы проекта вбакетоднимобщим подходом/способом.

Инструменты

Как и любая полноценная популярная система,Airflowпоощряет и поддерживает разработкурасширений. Они легко интегрируются,пишутся,сегодняихестьдостаточное количество в плане операторов.Но, ксожалению,не так много инструментов относятся к самомуAirflow. Один из них я могу привести. Этоafctlутилита командной строкиилиCLI-инструмент, который позволяет управлятьAirflowпроектом из командной строки. У него естьсвоибойлерплейтыилишаблоны, на базе которых можно создавать модули,DAG-иидр. Причем структура, в которойafctlсоздает свои директории,очень похожана ту,что мы используем в своем проекте (ячуть вышеотмечалее как рекомендованную).

А ещеесть достаточно много готовых интеграций, рабочих и хороших,дляAirflow.Например,GoogleCloudPlatform,AWS,Azure, различныеконнекторы кбазамданных.Их можно найтив папкеprovidersвнутри самого пакетаAirflow.

Стоит такжезайти напроектAirflowPluginsнайдетевсевозможные плагины, коннекторы к CRMсистемам, социальным сетямит. д.Не забывайтезаглядывать в поискGitHub: популярностьAirflowисообществорастут,а значит постояннопоявляются новые решения, которые можно подсмотреть.


Суммируем и резюмируем:

Все, что относится кAirflowи что можносделатьвself-service, запускаетсявCloudComposer.

CloudComposer, к сожалению, не масштабируется в ноль(scalingtozero). Тоесть,создавокружение, мы будем платить за него деньги. Но за счет этого можноуправлятьComposerинвайронментом:строитьновый,проводитьA/B-тесты, презентовать заказчику иудалить его. Естественно,еслинет окруженийиподнятогоGKE кластера, томы ни за что не платим.

Стоит разделять окружения внутриComposer, например,Prod/Dev/Staging.Иеще держать их обновленными.ВComposerиспользуется версияAirflow,адаптированная под работу с ним,акомандаGoogleтожеактивно обновляет свои версииAirflow.Поэтому время от времениComposerпредлагаетапдейтнутьокружение:появляется кнопка,автоматическисоздаетсяновыйimage, контейнеры и вашинвайронментпереводитсявновую версиюAirflow.

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

Очень важнодержатьпроект вопределеннойсемантичнойструктуре, чтобы было понятно,чтоиоткуда. Разделяемкодмежду доменамиисущностями, вендорами иконтрибьютерамипроекта.

По возможности не используемAirflowPluginсущноститолько дляUI вещей, которыепредоставляетAirflow. Но для операторов я бы не рекомендовал.

Пользуемся.airflowignoreфайлом, чтобыпропускатьопределенныедиректории на моментработы сканераAirflow. Например,если мы поместим .airflowignoreфайл в директориюDAG-овсвнутренними зависимостями, тоAirflowнайдет все нашиDAG-ии DAG-файлы. Ноонне будет читать каждый, чтобы найти сущностьDAG-авнутри директории,где, например, очень много файлов.

МеханизмсериализацииDAG-овхорошо подходит для ускорения вашего окружения внутриCloudComposer. В данном случае все нашиDAG-исохраняются в базе данных всериализованномвиде. Теперьнам не нужно каждый раз перечитывать реальные файлыможнопросто достать ихизхранилища.

Присмотритесь к существующим решениям и интеграциям дляAirflow.Например,GoogleCloudStorageListOperatorпозволяет нам обходитьбакеты, искать нужные файлы, скачиватьизагружатьих.Еще естьразличныерасширения-коннекторыковсевозможнымхранилищам, базамданныхитак далее.

ИспользуйтеKubernetesPodOperatorдля запуска зависимостей и утилит, написанных не наPython. Так как у нас все окружение загружается и исполняется вкластереKubernetes,Airflowбудет инициироватьисполнение задачив отдельномPod-е. Для этого будут использованы дополнительные ноды кластера, созданные Composer-ом.

Помните проограничениеComposer, а именно проread-onlyнастройкиAirflow.Полный список защищенных настроек можно подсмотретьв документации.

Уверен, что вы сможете оптимизировать свои пайпланы, а Airflow и Composer станут в этом отличными помощниками :)

Подробнее..

Как я сделал веб-фреймворк без MVC Pipe Framework

23.02.2021 14:15:47 | Автор: admin

Проработав фулстек разработчиком около 10 лет, я заметил одну странность.
Я ни разу не встретил не MVC веб-фреймворк. Да, периодически встречались вариации, однако общая структура всегда сохранялась:


  • Codeigniter мой первый фреймворк, MVC
  • Kohana MVC
  • Laravel MVC
  • Django создатели слегка подменили термины, назвав контроллер View, а View Template'ом, но суть не изменилась
  • Flask микрофреймворк, по итогу все равно приходящий к MVC паттерну

Конечно, с моим мнением можно поспорить, можно продолжить перечислять, однако суть не в этом.


Меня беспокоило то, что за все время существования веб-разработки, MVC является, по сути, монополистом в проектировании приложений. Я не говорю что это плохо,
просто это казалось мне странным.

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


  1. REST (порой GraphQL или другие варианты) бэкенд, выполняющий роль провайдера данных.
  2. Frontend, написаный на каком-либо из фреймворков большой тройки.

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

В ходе этих размышлений, мой взгляд упал на ETL паттерн, и в определенный момент я понял, что он идеально подходит для всех задач, которые на данный момент стоят перед бэкендом.
Осознав это, я решил провести эксперимент, и результатом этого эксперимента стал Pipe Framework.


О фреймворке


В Pipe Framework (далее PF) нет понятий модель-представление-контроллер, но я буду использовать их для демонстрации его принципов.


Весь функционал PF строится с помощью "шагов" (далее Step).


Step это самодостаточная и изолированная единица, призванная выполнять только одну функцию, подчиняясь принципу единственной ответственности (single responsibility principle).


Более детально объясню на примере. Представим, у вас есть простая задача создать API ендпоинт для todo приложения.


При традиционном подходе, вам необходимо создать Todo модель, которая представляет собой таблицу в базе данных.
В контроллере, привязанном к роуту, вы будете использовать экземпляр модели, чтобы извлечь данные о todo тасках, трансформировать их в https ответ, и отправить пользователю.


Я выделил извлечь и трансформировать чтобы вы могли ассоциировать MVC концепты с концептами, которые я использую в PF.

То есть, мы можем провести аналогию между MVC (Модель-Представление-Контроллер) и ETL (Извлечение-Преобразование-Загрузка):


Model Extractor / Loader


Controller Transformer


View Loader


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


Как видите, я обозначил View как Loader. Позже станет понятно, почему я так поступил.

Первый роут


Давайте выполним поставленную задачу используя PF.


Первое, на что необходимо обратить внимание, это три типа шагов:


  • Extractor
  • Transformer
  • Loader

Как определиться с тем, какой тип использовать?


  1. Если вам надо извлечь данные из внешнего ресурса: extractor.
  2. Если вам надо передать данные за пределы фреймворка: loader.
  3. Если вам надо внести изменения в данные: transformer.

Именно поэтому я ассоциирую View с Loader'ом в примере выше. Вы можете воспринимать это как загрузку данных в браузер пользователя.

Любой шаг должен наследоваться от класса Step, но в зависимости от назначения реализовывать разные методы:


class ESomething(Step):    def extract(self, store):        ...class TSomething(Step):    def transform(self, store):        ...class LSomething(Step):    def load(self, store):        ...

Как вы можете заметить, названия шагов начинаются с заглавных E, T, L.
В PF вы работаете с экстракторами, трансформерами, и лоадерами, названия которых слишком длинные, если использовать их как в примере:


class ExtractTodoFromDatabase(Extractor):    pass

Именно поэтому, я сокращаю названия типа операции до первой буквы:


class ETodoFromDatabase(Extractor):    pass

E значит экстрактор, T трансформер, и L лоадер.
Однако, это просто договоренность и никаких ограничений со стороны фреймворка нет, так что можете использовать те имена, которые захотите :)


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


  1. Извлекаем данные из базы
  2. Преобразовываем данные в JSON
  3. Отправляем данные в браузер посредством HTTP.

Итак, нам нужен будет 1 экстратор, 1 трансформер, и 1 лоадер.
К счастью, в PF есть набор предопределенных шагов, и они полностью покрывают описаные выше операции. Но, тем не менее, нам все-таки придется создать экстрактор, потому что нужно будет прописать данные доступа к базе данных.


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


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


Для этих целей, в PF предусмотрен @configure декоратор. То есть, вы просто перечисляете настройки, которые хотите добавить в шаг, следующим образом:


DATABASES = {    'default': {        'driver': 'postgres',        'host': 'localhost',        'database': 'todolist',        'user': 'user',        'password': '',        'prefix': ''    }}DB_STEP_CONFIG = {    'connection_config': DATABASES}

и потом передаете как аргумент декоратору, примененному к классу:


@configure(DB_STEP_CONFIG)class EDatabase(EDBReadBase):    pass

Итак, давайте создадим корневую папку проекта:


pipe-sample/


Затем папку src внутри pipe-sample:


pipe-sample/    src/

Все шаги, связанные с базой данных, будут находится в db пакете, давайте создадим и его тоже:


pipe-sample/    src/        db/            __init__.py

Создайте config.py файл с настройками для базы данных:


pipe-sample/src/db/config.py


DATABASES = {    'default': {        'driver': 'postgres',        'host': 'localhost',        'database': 'todolist',        'user': 'user',        'password': '',        'prefix': ''    }}DB_STEP_CONFIG = {    'connection_config': DATABASES}

Затем, extract.py файл для сохранения нашего экстрактора и его концигурации:


pipe-sample/src/db/extract.py


from src.db.config import DB_STEP_CONFIG # наша конфигурация"""PF включает в себя несколько дженериков для базы данных,которые вы можете посмотреть в API документации"""from pipe.generics.db.orator_orm.extract import EDBReadBase@configure(DB_STEP_CONFIG) # применяем конфигурацию к шагу class EDatabase(EDBReadBase):    pass     # нам не надо ничего добавлять внутри класса    # вся логика уже имплементирована внутри EDBReadBase

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

Теперь мы готовы к созданию первого пайпа.


Добавьте app.py в корневую папку проекта. Затем скопируйте туда этот код:


pipe-sample/app.py


from pipe.server import HTTPPipe, appfrom src.db.extract import EDatabasefrom pipe.server.http.load import LJsonResponse from pipe.server.http.transform import TJsonResponseReady@app.route('/todo/') # декоратор сообщает WSGI приложению, что этот пайп обслуживает данный маршрутclass TodoResource(HTTPPipe):     """    мы расширяем HTTPPipe класс, который предоставляет возможность описывать схему пайпа с учетом типа HTTP запроса    """    """    pipe_schema это словарь с саб пайпами для каждого HTTP метода.     'in' и 'out' это направление внутри пайпа, когда пайп обрабатывает запрос,    он сначала проходит через 'in' и затем через 'out' пайпа.    В этом случае, нам ничего не надо обрабатывать перед получением ответа,     поэтому опишем только 'out'.    """    pipe_schema = {         'GET': {            'out': (                # в фреймворке нет каких либо ограничений на порядок шагов                # это может быть ETL, TEL, LLTEETL, как того требует задача                # в этом примере просто так совпало                EDatabase(table_name='todo-items'),                TJsonResponseReady(data_field='todo-items_list'), # при извлечении данных EDatabase всегда кладет результат запроса в поле {TABLE}_item для одного результата и {TABLE}_list для нескольких                LJsonResponse()            )        }    }"""Пайп фреймворк использует Werkzeug в качестве WSGI-сервера, так что аргументы должны быть знакомы тем кто работал, например, с Flask. Выделяется только 'use_inspection'. Inspection - это режим дебаггинга вашего пайпа.Если установить параметр в True до начала воспроизведения шага, фреймворк будет выводить название текущего шага и содержимое стор на этом этапе."""if __name__ == '__main__':    app.run(host='127.0.0.1', port=8080,            use_debugger=True,            use_reloader=True,            use_inspection=True            )

Теперь можно выполнить $ python app.py и перейти на http://localhost:8000/todo/.


Из примера выше довольно сложно понять как выглядит реализация шага, поэтому ниже я приведу пример из исходников:


class EQueryStringData(Step):    """    Generic extractor for data from query string which you can find after ? sign in URL    """    required_fields = {'+{request_field}': valideer.Type(PipeRequest)}    request_field = 'request'    def extract(self, store: frozendict):        request = store.get(self.request_field)        store = store.copy(**request.args)        return store

Стор


На данный момент, стор в PF это инстанс frozendict.
Изменить его нельзя, но можно создать новый инстанс используя frozendict().copy() метод.


Валидация


Мы помним, что шаги являются самостоятельными единицами функционала, но иногда они могут требовать наличия определенных данных в сторе для выполнения каких-либо операций (например id пользователя из URL). В этом случае, используйте поле required_fields в конфигурации шага.


PF использует Valideer для валидации. На данный момент, я рассматриваю альтернативы, однако в случае смены библиотеки принцип останется тот же.


Пример


Все, что нам надо сделать это написать dict с необходимыми полями в теле шага (здесь вы найдете больше информации о доступных валидаторах: Valideer).


class PrettyImportantTransformer(Step):    required_fields = {'+some_field': valideer.Type(dict)} # `+` значит обязательное поле

Динамическая валидация


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


class EUser(Step):    pk_field = 'id' # EUser будет обращаться к полю 'id' в сторе    required_fields = {'+{pk_field}': valideer.Type(dict)} # все остальное так же

Пайп фреймворк заменит это поле на значение pk_field автоматически, и затем валидирует его.


Объединение шагов


Вы можете объединить два или более шага в случае, если вам необходимо контролировать порядок выполнения.


В этом примере я использую оператор | (OR)


    pipe_schema = {        'GET': {            'out': (                # В случае если EDatabase бросает любое исключение                 # выполнится LNotFound, которому в сторе передастся информация об исключении                EDatabase(table_name='todo-items') | LNotFound(),                 TJsonResponseReady(data_field='todo-items_item'),                LJsonResponse()            )        },

Так же есть оператор & (AND)


    pipe_schema = {        'GET': {            'out': (                # В этом случае оба шага должны выполниться успешно, иначе стор без изменений перейдет к следующему шагу                 EDatabase(table_name='todo-items') & SomethingImportantAsWell(),                 TJsonResponseReady(data_field='todo-items_item'),                LJsonResponse()            )        },

Хуки


Чтобы выполнить какие-либо операции до начала выполнения пайпа, можно переопределить метод: before_pipe


class PipeIsAFunnyWord(HTTPPipe):    def before_pipe(self, store): # в аргументы передается initial store. В случае HTTPPipe там будет только объект PipeRequest        pass

Также есть хук after_pipe и я думаю нет смысла объяснять, для чего он нужен.


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


Пример использования из исходников фреймворка:


class HTTPPipe(BasePipe):    """Pipe structure for the `server` package."""    def interrupt(self, store) -> bool:        # If some step returned response, we should interrupt `pipe` execution        return issubclass(store.__class__, PipeResponse) or isinstance(store, PipeResponse)

Потенциальные преимущества


Разрабатывая Pipe Framework, я ничего от него не ожидал, однако в ходе работы я смог выделить довольно большое количество преимуществ такого подхода:


  1. Принудительная декомпозиция: разработчик вынужден разделять задачу на атомарные шаги. Это приводит к тому, что сначала надо подумать, а потом делать, что всегда лучше, чем наоборот.
  2. Абстрактность: фреймворк подразумевает написание шагов, которые можно применить в нескольких местах, что позволяет уменьшить количество кода.
  3. Прозрачность: любая, пусть даже и сложная логика, спрятанная в шагах, призвана выполнять понятные для любого человека задачи. Таким образом, гораздо проще объяснить даже нетехническому персоналу о том, что происходит внутри через преобразование данных.
  4. Самотестируемость: даже без написаных юнит тестов, фреймворк подскажет вам что именно и в каком месте сломалось за счет валидации шагов.
  5. Юнит-тестирование осуществляется гораздо проще, нужно только задать начальные данные для шага или пайпа и проверить, что получается на выходе.
  6. Разработка в команде тоже становится более гибкой. Декомпозировав задачу, можно легко распределить различные шаги между разработчиками, что практически невозможно сделать при традиционном подходе.
  7. Постановка задачи сводится к предоставлению начального набора данных и демонстрации необходимого набора данных на выходе.

Фреймворк на данный момент находится в альфа-тестировании, и я рекомендую экспериментировать с ним, предварительно склонировав с Github репозитория. Установка через pip так же доступна


pip install pipe-framework


Планы по развитию:


  1. Django Pipe: специальный тип Pipe, который можно использовать как Django View.
  2. Смена Orator ORM на SQL Alchemy для Database Generics (Orator ORM библиотека с приятным синтаксисом, но слабой поддержкой, парой багов, и недостаточным функционалом в стабильной версии).
  3. Асинхронность.
  4. Улучшеный Inspection Mode.
  5. Pipe Builder специальный веб-дашбоард, в котором можно составлять пайпы посредством визуальных инструментов.
  6. Функциональные шаги на данный момент шаги можно писать только в ООП стиле, в дальнейшем планируется добавить возможность использовать обычные функции

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


Хорошего дня!

Подробнее..

Мониторинг места в хранилищах

03.10.2020 12:04:36 | Автор: admin

Всем привет Хабровчане!!

Одной из проблем хранилищ данных, которая часто возникает в процессе работы - это постоянное увеличение их размеров. А добавление все новых и новых источников данных только ускоряет заполнение места на дисках.

Да, конечно же настройка чистки самых больших таблиц и периода историцируемости позволят сократить неконтролируемое увеличение места. Но если речь идет о хранилищах, которые бодро наполняются и добавляются всё новые "большие" таблицы, и количество их увеличивается то вопрос места в DWH всегда становится ребром. И возникает вопрос "А куда же ушло место?", "Что можно почистить?" или "Как обосновать руководству расширение хранилища?" Системы мониторинга на подобие ZABBIX позволяют только верхнеуровнево отследить увеличение дискового пространства на полке но не дают возможности отследить рост самих объектов в базе.

Сегодня хочу поделится своим маленьким лайфхаком как легко можно поставить на мониторинг размеры таблиц на примере MS SQL для дальнейшего анализа и оптимизации базы. Это маленькое решение которое может помочь сэкономить кучу времени чтобы проанализировать "Куда же ушло все место в хранилище?". Данный принцип можно применить и на других базах (Oracle, PostgreSQL и т.д.) с той лишь разницей, что названия системных таблиц будут другие.

Ниже описан небольшой план и набор скриптов MS SQL чтобы автоматизировать мониторинг места:

Это будет регламентное задание , которое собирает статистику ежедневно.

1) На первом шаге создаем таблицу для хранения истории и счетчик. В этой таблице будет сохранятся ежедневная история статистики для каждой таблицы.

CREATE SEQUENCE prm.sq_etl_log_1  AS bigint START WITH 1 INCREMENT BY 1  CREATE TABLE prm.dwh_size_of_tables(ddate date NULL,--Дата  на момент который смотрим статистику таблицыrun_id numeric(14, 0) NOT NULL,--ID Запуска сбора статистики, Счетчикdb_name varchar(20) NOT NULL,--База данныхschema_name sysname NOT NULL,--Схема таблицыtable_name sysname NOT NULL,--Название таблицыrow_count bigint NULL,--Количество строк в таблицеreserved_KB bigint NULL,--Ощий размер таблицы  вместе с индесамиdata_KB bigint NULL,--Размер самих данных в таблице index_size_KB bigint NULL,--Размер индексовunused_KB bigint NULL--неиспрользованное место) 

2) Далее необходимо создать процедуру которая будет ежедневно запускаться и собирать статистику по-таблично. Эту процедуру необходимо поставить на ежедневное задание для запуска. Она собирает срез размеров таблиц на текущий день.

Скрипт процедуры представлен ниже:

Скрипт процедуры
USE [LEMON]GOCREATE  PROCEDURE  [prm].[load_etl_log]ASdeclare @run_id intBEGIN--Если сегодня был запуск очищаем текущюую статистику и перезаливаемdelete from lemon.prm.dwh_size_of_tables where ddate = cast(getdate() as date);--Для страых периодов  храним только статистику только на начало и на середину месяцаdelete from  lemon.prm.dwh_size_of_tableswhere (DATEPART(day, ddate)not in (1,15) and ddate < dateadd(month ,-2, getdate())) DECLARE @SQL_text varchar(max),@SQL_text_final varchar(max); ;  set @SQL_text=   'USE {SCHEMA_FOR_REPLACE};insert into  lemon.prm.dwh_size_of_tablesSELECT cast(getdate() as date) date_time,'''+ convert(nvarchar , @run_id  ) +''' run_id ,''{SCHEMA_FOR_REPLACE}'' db_name,a3.name AS schema_name,--Схемаa2.name AS table_name,--Имя таблицыa1.rows AS row_count,--Число записей(a1.reserved + ISNULL(a4.reserved, 0)) * 8 AS reserved_KB,--Зарезервировано (КБ)a1.data * 8 AS data_KB,--Данные (КБ)(CASE WHEN (a1.used + ISNULL(a4.used, 0)) > a1.dataTHEN (a1.used + ISNULL(a4.used, 0)) - a1.dataELSE 0END) * 8 AS index_size_KB,--Индексы (КБ)(CASE WHEN (a1.reserved + ISNULL(a4.reserved, 0)) > a1.usedTHEN (a1.reserved + ISNULL(a4.reserved, 0)) - a1.usedELSE 0END) * 8 AS unused_KB --Не используется (КБ)FROM (SELECT ps.object_id,SUM(CASE WHEN (ps.index_id < 2)THEN row_countELSE 0END) AS [rows],SUM(ps.reserved_page_count) AS reserved,SUM(CASE WHEN (ps.index_id < 2)THEN (ps.in_row_data_page_count + ps.lob_used_page_count + ps.row_overflow_used_page_count)ELSE (ps.lob_used_page_count + ps.row_overflow_used_page_count)END) AS data,SUM(ps.used_page_count) AS usedFROM sys.dm_db_partition_stats psWHERE ps.object_id NOT IN (SELECT object_idFROM sys.tablesWHERE is_memory_optimized = 1)GROUP BY ps.object_id) AS a1LEFT OUTER JOIN (SELECT it.parent_id,SUM(ps.reserved_page_count) AS reserved,SUM(ps.used_page_count) AS usedFROM sys.dm_db_partition_stats psINNER JOIN sys.internal_tables it ON (it.object_id = ps.object_id)WHERE it.internal_type IN (202,204)GROUP BY it.parent_id) AS a4 ON (a4.parent_id = a1.object_id)INNER JOIN sys.all_objects a2 ON (a1.object_id = a2.object_id)INNER JOIN sys.schemas a3 ON (a2.schema_id = a3.schema_id)WHERE a2.type <> N''S''AND a2.type <> N''IT''';DECLARE @request_id nvarchar(36), @schema_for_replace nvarchar(100)DECLARE bki_cursor CURSOR FOR   SELECT name as schem    FROM    sys.databases--Здесь можно перечислить список баз по которым собираем статистику/*  where name  in ('DWH','DWH_copy','VN','VN_test') --and name ='DWH'*/OPEN bki_cursor  FETCH NEXT FROM bki_cursor INTO @schema_for_replaceWHILE @@FETCH_STATUS = 0  BEGINset @SQL_text_final = replace (@sql_text,'{SCHEMA_FOR_REPLACE}',@schema_for_replace);  execute (@SQL_text_final)FETCH NEXT FROM bki_cursor INTO @schema_for_replaceEND   CLOSE bki_cursor;  DEALLOCATE bki_cursor;END
Создать ежедневное задание

3) Теперь по мере наполнения таблицы dwh_size_of_tables можно смотреть статистику по-таблично и по базам. Для просмотра можно воспользоваться вот таким удобным скриптом ниже.

Статистика места в DWH по таблицам
--Статистика места  в DWH по таблицамselect top 10 ddate -- [Дата],run_id --,db_name --БД-,schema_name --Схема,table_name --Имя таблицы,row_count --Число записей,round(cast(reserved_KB as float) /1024/1024,2) as  reserved_GB --Зарезервировано (КБ),round(cast(data_KB as float) /1024/1024,2) as data_GB --Данные (КБ),round(cast(index_size_KB as float) /1024/1024,2) as index_size_GB --Индексы (КБ),round(cast(unused_KB as float) /1024/1024,2) as unused_GB--Не используется (КБ) from  lemon.prm.dwh_size_of_tableswhere ddate = cast(getdate() as date)-- and  db_name='DWH' order by reserved_GB desc
Статистика места в DWH по базам
--Статистика места  в DWH по  Базам select ddate -- [Дата],run_id --,db_name --БД-,round(cast(sum(reserved_KB) as float) /1024/1024,2) as  reserved_GB --Зарезервировано (КБ),round(cast(sum(data_KB) as float) /1024/1024,2) as data_GB --Данные (КБ),round(cast(sum(index_size_KB) as float) /1024/1024,2) as index_size_GB --Индексы (КБ),round(cast(sum(unused_KB) as float) /1024/1024,2) as unused_GB--Не используется (КБ),sum(row_count) row_count--Число записей from  lemon.prm.dwh_size_of_tableswhere ddate = cast(getdate() as date)-- and  db_name='DWH' group by   ddate,run_id,db_nameorder  by  ddate,run_id,sum(data_KB+index_size_KB) desc

4) Далее создаем еще 3 процедуры, которые позволят нам очень удобно просматривать историю по базам и по таблично. Эти процедуры используются не для сбора статистики а для показа этой статистики в красивом виде. Причем указав период за который хотим посмотреть статистику, она по-колоночно разбивает статистику.

Дневная статистика места по базам. Указываем период за который смотрим
USE [LEMON]GO/****** Object:  StoredProcedure [prm].[dwh_daily_size_statistics]    Script Date: 02.09.2020 18:35:02 ******/SET ANSI_NULLS ONGOSET QUOTED_IDENTIFIER ONGOCREATE  procedure [prm].[dwh_daily_size_statistics]   @sdate date, @edate dateASBEGIN--Собираем подневную статистикуdeclare   @str nvarchar(4000)set @str= stuff (  ( select  N','+ 'round(cast(sum(case when ddate =  cast('''+ cast( ddate as nvarchar)+'''as date)  thenreserved_KBend) as float) /1024/1024,0)  ['+ cast( ddate as nvarchar)+']'+char(10)from ( select distinct ddate from  lemon.prm.dwh_size_of_tableswhere ddate >=@sdate and  ddate<@edate) t order by t.ddate   for xml path('')  ,type  ).value('.','nvarchar(max)'),  1,0,'' )-- column_string--print @strexec (' select db_name --БД-'+@str+' from  lemon.prm.dwh_size_of_tables--where ddate = cast(getdate() as date) group by  db_name--order  by  db_name');end ;GO
Месячная статистика места по базам. Указываем период просмотра истории.
USE [LEMON]GO/****** Object:  StoredProcedure [prm].[dwh_monthly_size_statistics]    Script Date: 02.09.2020 18:35:09 ******/SET ANSI_NULLS ONGOSET QUOTED_IDENTIFIER ONGOCREATE procedure [prm].[dwh_monthly_size_statistics]   @sdate date, @edate dateASbegin --Собираем помесячую статистикуdeclare   @str2 nvarchar(4000)set @str2= stuff (  ( select  N','+ 'round(cast(sum(case when ddate =  cast('''+ cast( ddate as nvarchar)+'''as date)  thenreserved_KBend) as float) /1024/1024,0)  ['+ CAST(year( ddate) as nvarchar) +'_'+ CAST(month( ddate) as nvarchar)--cast( ddate as nvarchar)+']'+char(10)from ( select distinct ddate from  lemon.prm.dwh_size_of_tableswhere ddate >=@sdate and  ddate<@edate and day(ddate)=1) t order by t.ddate   for xml path('')  ,type  ).value('.','nvarchar(max)'),  1,0,'' )exec (' select db_name --БД---,table_name'+@str2+' from  lemon.prm.dwh_size_of_tables--where ddate = cast(getdate() as date) group by  db_name--,table_nameorder  by  db_name');end;GO
Процедура для просмотра истории размеров таблиц
USE [LEMON]GO/****** Object:  StoredProcedure [prm].[dwh_monthly_table_size_statistics]    Script Date: 02.09.2020 18:36:15 ******/SET ANSI_NULLS ONGOSET QUOTED_IDENTIFIER ONGOALTER procedure [prm].[dwh_monthly_table_size_statistics]   @sdate date, @edate date ,@db_name nvarchar(100)ASbegin --Собираем помесячую статистикуdeclare   @str2 nvarchar(4000)set @str2= stuff (  ( select  N','+ 'round(cast(sum(case when ddate =  cast('''+ cast( ddate as nvarchar)+'''as date)  thenreserved_KBend) as float) /1024/1024,0)  ['+ CAST(year( ddate) as nvarchar) +'_'+ CAST(month( ddate) as nvarchar)--cast( ddate as nvarchar)+']'+char(10)from ( select distinct ddate from  lemon.prm.dwh_size_of_tableswhere ddate >=@sdate and  ddate<@edate and day(ddate)=1   ) t order by t.ddate   for xml path('')  ,type  ).value('.','nvarchar(max)'),  1,0,'' ) declare @ORDER_DATE NVARCHAR(100) SET @ORDER_DATE= convert(nvarchar, year( @edate)  ) +'_'+  convert(nvarchar, month( @edate) ) SELECT  @ORDER_DATE = convert(nvarchar, year( DDATE)  ) +'_'+  convert(nvarchar, month( DDATE) ) FROM (select MAX( ddate ) DDATE from  lemon.prm.dwh_size_of_tableswhere ddate >=@sdate and  ddate<@edate and day(ddate)=1 ) tt  ;declare @ddb_name nvarchar(100)set @ddb_name =  case when @db_name is null then '' else  ' and '+ 'db_name= '''+@db_name + '''' end exec (' select db_name --БД-,table_name'+@str2+' from  lemon.prm.dwh_size_of_tableswhere 1=1  ' + @ddb_name  + '-- ddate = cast(getdate() as date) group by  db_name,table_name order by  db_name,['+ @ORDER_DATE +'] desc');end;

5) В итоге у нас получились 3 процедуры которые позволяют :

A) Смотреть историю увеличения/уменьшения БД подневно

B) Смотреть историю увеличения/уменьшения БД помесячно

C) Смотреть историю увеличения/уменьшения таблиц помесячно. Очень удобно когда нужно отследить по конкретной таблице когда по ней пошел рост.

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

--Дневная статистика места по базам указываем период  за который смотримexec  LEMON.prm.dwh_daily_size_statistics @sdate ='2020-08-01', @edate ='2020-09-01'--Месячная статистика места по базам указываем период  за который смотримexec  LEMON.prm.dwh_monthly_size_statistics @sdate ='2020-03-01', @edate ='2020-09-01'--Месячная статистика места по каждой таблицеexec  LEMON.prm.dwh_monthly_table_size_statistics   @sdate ='2020-02-01', @edate ='2020-08-01', @db_name ='DWH'--если указываем null то показывает все таблицы по всем базам

Как видно на картинке выше по ней очень удобно просмотреть какая база начала резко увеличиваться в размерах. Более того этими тремя процедурами очень быстро можно найти , например, таблицу или базу которая начала в какой-то момент сильно расти. Особенно удобно когда в хранилище уже созданы тысячи объектов, и ручной поиск уже не применим.

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

На этом я пожалуй закругляюсь и надеюсь что эта статья будет полезна кому-нибудь. Оставляйте свои комментарии у кого есть другие способы по анализу места в хранилищах. Буду рад любым отзывам.

P.S. Все скрипты выложены на GitHub по ссылке ниже:

https://github.com/michailo87/MSSQL

До скорых встреч !!

Подробнее..

Моделирование данных зачем оно нужно и какие преимущества дает бизнесу

14.04.2021 12:16:12 | Автор: admin

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

Что такое трансформация и моделирование данных?

Чтобы ответить на этот вопрос, разберемся с основными терминами, которые вы встретите в этой статье.

Модель данных это описание объектов и их свойств; связей между объектами и ссылок на источники данных. Связь между объектами реализуется через специальные свойства ключи.

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

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

Как выглядит модель данных пример для Ecommerce:

Трансформация данных это процесс преобразования данных из одной структуры в другую.

Моделирование данных это трансформация данных в структуру, соответствующую требованиям модели.

Какую проблему решает моделирование данных?

Если представить основные этапы работы с данными в виде графика...

...можно увидеть, что на между этапами Preparation Reporting (Подготовка данных Создание отчетов) и возникает больше всего проблем.

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

Когда аналитику нужно: приджойнить в готовый отчет дополнительные данные; обновить логику расчета определенной метрики; заменить источник данных на источник с другой структурой он тратит значительное время на понимание и рутинное изменение надцати SQL-запросов.

Чтобы понять, как именно моделирование данных решает эти проблемы, рассмотрим подробнее процесс подготовки данных. Его можно разбить на три этапа:

1. Моделирование. На этом этапе аналитик задается вопросами: Как собранные данные соотносятся с бизнес-сущностями?, Какие способы объединения данных допустимы?.

Моделирование достаточно универсальная задача. Например, когда маркетолог спрашивает у аналитика: А мы вообще сможем объединить данные из источников X и Y?. Это задача моделирования.

2. Виртуализация. На этом этапе решается вопрос: Какая структура и вариант объединения данных нужны для конкретного отчета?. Структура может быть получена с помощью разных вариантов объединения, но какой нам нужен в данном случае? Нам нужны источники последней сессии или всех? Одну и ту же структуру мы можем получить разными способами. Для виртуализации аналитик должен ответить на эти вопросы и создать такой SQL-запрос, который эти данные вернет.

Задача виртуализации данных, когда нам нужно получить их в каком-то новом срезе, используется регулярно. Потому что каждый раз, когда нам нужно что-то поменять в отчете, мы сталкиваемся именно с этой задачей.

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

Задача трансформации достаточно нетривиальна. Потому что у каждого бизнеса есть уникальные требования. Она в целом менее универсальная и используется как минимум на этапе запуска. То есть мы сделали какой-то отчет и один раз как минимум определили правила трансформации.

Маркетологи зависят от аналитиков, потому что у них нет готового продукта, который решал бы задачу виртуализации. Из-за этого для каждого нового отчета, когда нужно добавить колонку, аналитик трансформирует данные снова и снова. Чтобы этого избежать, нужно решение, которое позволит трансформировать данные и хранить их в структуре, пригодной для многократного использования. Таким решением может выступать связка dbt + Google BigQuery.

Какое преимущество дает бизнесу моделирование данных?

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

Как сейчас выглядит построение отчетов? Маркетолог рассказал аналитику, что нужно сделать. Тот написал SQL-запросы и построил на их результатах дашборд. Нашли расхождения, добавили условия в запросы, сделали пару итераций, получили обратную связь и таким образом довели отчет до ума. Но все решения были в контексте отчета. Это значит, что когда понадобится другой отчет с другими источниками, новыми колонками, несколькими срезами данных операцию проверки сходимости данных, валидацию нужно будет проходить заново.

С трансформацией и моделированием данных эта задача решается до того, как строится отчет. Когда мы еще не знаем, какие нам понадобятся отчеты. Но ответ на вопрос, что такое сессия, пользователь, расходы, мы сформулировали на уровне модели. То есть трансформация это преобразование данных в структуру, которая соответствует требованиям модели. Она позволяет строить отчеты, не тратя время на очередную обработку данных. При этом вы будете уверены, что отчеты построены на сходимых данных.

Способы трансформации данных для моделирования

Есть встроенный механизм регулярных запросов, которые выполняются в Google BigQuery, Scheduled Queries и AppScript. Их легко можно освоить, потому что это привычный SQL, но проводить отладку в Scheduled Queries практически нереально. Особенно, если это какой-то сложный запрос или каскад запросов.

Есть специализированные инструменты для управления SQL-запросами, например, dbt и Dataform.

dbt (data build tool) это фреймворк с открытым исходным кодом для выполнения, тестирования и документирования SQL-запросов, который позволяет привнести

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

Главная проблема, которую решают специализированные инструменты это уменьшение времени, необходимого на поддержку и обновление. Это достигается за счет удобства отладки.

Сравнение способов трансформации данных:

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

Подробнее..

Из песочницы Побег от скуки процессы ETL

28.06.2020 18:15:43 | Автор: admin

В конце зимы и начале весны, появилась возможность поработать с новым для меня инструментом потоковой доставки данных Apache NiFi. При изучении инструмента, все время не покидало ощущение, что помимо официальной документации, нелишним были бы материалы "for dummies", с практическими примерами.


После выполнении задачи, решил попробовать облегчить вхождение в мир NiFi.


Предыстория, почти не связанная со статьей


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


Система Apache NiFi была выбрана на удачу. Прототип был построен и сдан заказчику.


Первоначально заказчик хотел монолитное приложение, а использование NiFi рассматривал просто как инструмент прототипирование (где-то прочитал). Но после знакомства в вблизи NiFi остался в продукте.


А теперь собственно история


После сдачи работы заказчику, интерес к ETL не пропал. В апреле наступила самоизоляция с нерабочими днями и свободное время нужно было потратить с пользой.


Когда я начал разбираться с Apache NiFi выяснилось, что более-менее подробная информация есть только на сайте проекта. Русские статьи во многом просто переводы вводных частей официальной документации. Основной недостаток часто не понятно в каком формате параметры вводить. Гугл спасает, но не всегда.


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


И так, задача получать данные о распространении вируса, обрабатывать строить графики.
Источниками данных будет сайты стопкороновирус.рф и covid19.who.int. Первый сайт содержит данные по регионам России, сайт ВОЗ данные по странам мира.


Данные на сайте стопкороновирус.рф находятся прямо в html-коде страницы в виде почти готового json-массива. Нужно его только обработать. Но об этом в следующей статье.
Данные ВОЗ находятся в csv-файле, который не получилось автоматически скачивать (либо я был недостаточно настойчив). Поэтому, когда нужно было, файл сохранялся из браузера в специальную папку.


Если кратко сказать о NiFi система при выполнении процесса (pipeline), состоящего из процессоров, получает данные, модифицирует и куда-то сохраняет. Процессоры выполняют работу читают файлы, преобразовывают один формат в другой, обогащают данные из другой базы данных, загружают в хранилища и т.д. Процессоры между собой соединены очередями. У каждой очереди есть признак, по которому данные в нее загружаются из процессора. Например в одну очередь можно загрузить данные при удачной выполнении работы процессора, в другую при сбое. Это позволяет запустить данные по разным веткам процессов. Например, берем данные, ищем подстроку если нашли отправляем по ветке 1, у тех данных, в которых подстрока не найдена отправляем по ветке 2.


Каждый процессор может находиться в состоянии "Running" или "Stopped". Данные накапливаются в очереди перед остановленным процессором, после старта процессора, данные будут переданы в процессор. Очень удобно, можно изменять параметры процессора, без потери данных на работающем процессе.


Данные в системе называются FlowFile, потоковый файл. Процесс начинает работать с процессора, который умеет генерировать FlowFile. Не каждый процессор умеет генерировать потоковый файл в этом случае в начало ставится процессор "GenerateFlowFile", задача которого создать пустой потоковый файл и тем самым запустить процесс. Файл может создаваться по расписанию или событию. Файл может быть бинарным или текстовым потоком.


Файл состоит из контента и метаданных. Метаданные называются атрибутами. Например, атрибутом является имя файл, id-файла, имя схемы и т.д. Система позволяет добавлять собственные атрибуты, записывать значения. Значения атрибутов используются в работе процессоров.


Содержимое файла может быть бинарным или текстовым. Поток данных можно представить в виде записей. Для записей можно определить их структуру схему данных. Схема описывает понимается формат записи, в которой описываются имена и типы полей, валидные значения, значения по умолчанию и т.д.


В NiFi еще одна важная сущность контроллеры. Это объекты которые знают как сделать какую-то работу. Например, процессор хочет записать в базу данных, то контроллер будет знать, как найти и подключиться к базе данных, таблице.


Контролер, который знает как читать данные на входе в контроллер называется Reader. После обработки, на выходе процессора, данные форматируются согласно контроллеру RecordSetWriter.


Итак практическое применение вольного изложения документации чтение данных ВОЗ и сохранение в БД. Данные будем хранить в СУБД PostgreSQL.


Создаем таблицу:


CREATE TABLE public.who_outbreak (    dt timestamp NULL,    country_code varchar NULL,    country varchar NULL,    region_code varchar NULL,    died int4 NULL,    died_delta int4 NULL,    infected int4 NULL,    infected_delta int4 NULL);

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


Источником данных будет cvs-файл с сайта https://covid19.who.int/ по кнопке Download Map Data. Файл содержит информацию по заболевшим и погибшим по всем странам на каждый день примерно с конца января. Оперативная информация там задерживается на 1-2 дня. За это время в файле менялись наименования полей (были даже наименования с пробелами), менялся формат даты.


Файл сохраняется из браузера в определенный каталог, откуда NiFi забирает его на обработку.


image
Общий вид визуализации процесса в интерфейсе Apache NiFI


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


Обработка начинается с верхнего процесса, отходящие от процесса стрелочки показывают направление движения FlowFile (атрибутов и контента).


На визуализации процесса показывается его статус (работает или нет), данное имя процессу, его тип, количество и общий объем прошедших файлов на входе и на выходе за период времени, в данном случае за 5 мин.


Визуализация очереди показывает ее имя, и объем данных в очереди. По умолчанию имя очереди это тип связи success, failed и другие.


Тип первого процесса "GetFile". Этот процессор создает flowfile и запускает процесс. Контентом потокового файла будет содержимое файла если он будет найден. В настройках процессора на вкладке Scheduling указываем расписание запуска процесса 20 секунд.



Вкладка Scheduling запуск каждые 20 сек.


После старта процессора, каждые 20 секунд процессор будет запускаться. Если файл будет найден FlowFile будет создан и процесс запустится.


Как видно из рисунка, указываем каталог и имя файла. В имени файла можно использовать символы подстановки. Например *.csv приведет к обработке всех csv-файлов в каталоге. Указываем также, что после обработки файл можно удалить ("Keep Source File"). Также есть возможность указать максимальные и минимальные значения возраста и размера файла. Это позволяет обрабатывать, например, только не пустые файл, созданные за последний час.
На вкладке Settings указываются базовые параметры процесса, такие как имя процесса, максимальное время работы процесса, время между запусками, типы связей.


Результатом работы первого процесса "GetFile" с именем "Read WHO datafile" будет просто поток данных из файла. Поток будет передан в следующий процесс "ReplaceText".



Процессор поиска подстроки


В этом процессе обратим внимание сразу на вкладку параметров. Данный процессор ищет regex-выражение "Search Value" в входном потоке и заменяет на новое значение "Replacement Value". Обработка ведется построчно ("Evaluation Mode"). В данном случае идет замена в строке даты. Во входном файле, в какой-то момент дата формата YYYY-MM-DD стала указываться как YYYY-MM-DDThh:mm:ssZ, причем время было всегда 00:00:00, а временная зона не указывалась.
Простого способа преобразования в даты уже в записи не нашел, поэтому к проблеме подошел в лоб просто через процессор "ReplaceText" убрал символы T и Z. После этого строка стала конвертироваться в timestamp в avro-схеме без ошибок.


На выходе процессора будет поток текстовых данных, в которых уже поправили подстроку даты. Но пока это просто поток байтов без какой-то структуры.


Следующий процессор "Rename fields" читает поток уже как структурированные данные.



Переименование полей


Процессор содержит ссылку на Reader специальный объект-контроллер, который умеет читать из потока структурированные данные и в таком виде уже передает процессору на обработку. В данном случае "WHO CVS Reader просто читает поток и преобразует каждую строку cvs-файла в запись (record) которая содержит поля со значениями из строки. Имена полей берутся из заголовка cvs-файла.



Контроллер чтения записей из cvs-файла


Параметр "Schema Access Strategy" указывают, что структура записи формируется из заголовка cvs-файла. Если заголовков нет, то можно изменить стратегию доступа к схеме и в реестре схем данных создать схему, указать ее имя в параметре "Schema Name" или еще проще указать саму схему в параметре "Schema Text".


Но так как у нас есть заголовки в файле читаем по ним.


Итак, в процессоре данные уже структурированные их можно представить как таблицу базы данных поля и строки. И к этой таблице мы выполняем SQL запрос :


select    Date_reported dt,    Country_code country_code,    Country country,    WHO_region region_code,    New_deaths died_delta,    Cumulative_deaths died,    New_cases infected_delta,    Cumulative_cases infectedfrom FLOWFILE

Поля в запросе такие же как заголовки cvs-файла. Имя таблицы служебное FLOWFILE обозначает чтение структурированных данных их контента файла. Язык запроса SQL довольно гибкий, есть функции преобразований, агрегаций и т.д. В данном случае запрос выводит все данные, только имена полей результата будут другие они соответствуют полям таблицы who_outbreak в целевой БД.


Поток записей с новыми именами полей передается в контроллер RecordSetWriter, ссылка на который также указана в параметра контроллера "WHO AvroRecordSetWriter".



Контроллер RecordSetWriter


Контролер RecordSetWriter уже использует предопределенную схему данных. Схема находится в отдельном объекте регистре схем ("Schema Registry"). В контроллере есть только ссылка на реестр схем и имя схемы.



Регистр схем


Работать с регистром схем довольно просто. Добавляем новый параметр. Его имя будет именем схемы. Значение параметр определение схемы.


В регистре схем создана схема who_outbreak, определение схемы:


{"type" : "record","name" : "who_outbreak","fields": [  {"name": "dt", "type": { "type" : "long", "logicalType": "timestamp-millis"}},  {"name": "country_code", "type" : ["null", "string"], "default": "-"},  {"name": "country", "type" : ["null", "string"], "default": ""},  {"name": "region_code", "type" : ["null", "string"], "default": ""},  {"name" : "died", "type" : "int", "default": 0},  {"name" : "died_delta", "type" : "int", "default": 0},  {"name" : "infected", "type" : "int", "default": 0},  {"name" : "infected_delta", "type" : "int", "default": 0} ]}

Имена и типы атрибутов схемы соответствуют именам и типам полей записи, сформированной sql-запросом.


После выполнения контроллером sql-запроса и передачи данных на выход контроллера в формате схемы данных, структурированный поток передается в контроллер "Delete all records". Это контроллер типа "PutSQL", который может передавать на выполнение sql-команды.


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



delete from who_outbreak;


В параметрах контроллера указываем SQL Statement delete from who_outbreak; и ссылку на пул соединений "JDBC Connection Pool". Параметры JDBC стандартные. Пул содержит настройки подключения к конкретной БД, поэтому его можно использовать во всех контроллерах, которые будут работать с этой БД.


Данные или атрибуты FlowFile не обрабатываются в процессоре, поэтому вход и выход процессора идентичен.


Последний процессор "PutDatabaseRecord".



Запись в БД


В этом процессоре указываем Reader, в котором используется определенная схема who_outbreak. Так как мы удалили все записи в предыдущем процессоре, используем простой INSERT для добавления записей в таблицу. Указываем пул соединений DBCPConnectionPool, далее указываем БД и имя таблицы. Имена полей в схеме данных и БД совпадают, то больше никакой дополнительной настройки проводить не нужно.


Все процессоры, контроллеры и регистры схем нужно перевести в состояние Running (Start).
Процесс доставки данных готов. Если положить файл WHO-COVID-19-global-data.csv в каталог D:\input, то в течении 20 секунд он будет удален, а данные пройдя через процесс доставки данных будут сохранены в БД.


Сейчас вынашиваю планы на вторую часть статьи, в которой планирую описать второй процесс ETL чтение данных с сайта, обогащение, загрузка в БД, в файлы, расщепление потока на несколько потоков и сохранение в файлы. Если тема интересна пишите в комментарии, это мне прибавит стимула быстрее написать.


На рисунке изображение в интерфейсе Apache NiFi описанного процесса (справа) и, для затравки, процесса для второй статьи (слева).


Подробнее..
Категории: Big data , Bigdata , Apache nifi , Etl

Установка и настройка Airflow на Ubuntu Server 20

19.03.2021 16:09:40 | Автор: admin

Первый раз я установил Airflow по туториалам за 1 час. Он отлично открывался с сайта, красиво выглядел, но, к сожалению, не работал.

На дальнейшую переустановку и отладку у меня ушло ещё 10-15 часов.

Статью пишу по горячим следам, постараюсь отметить все проблемы, с которыми пришлось столкнуться. На некоторые вопросы ответы удалось найти только странице на 10-й английского гугла. Даже в английской версии мануалов по Airflow не по всем вопросам есть информация.

Для начала неочевидный факт: когда начинаешь устанавливать airflow, думаешь что это будет одна программа. На самом деле, всё совсем не так. Это 2 сервиса:

  • airflow-webserver - отвечает за ту часть, которую вы видите в web-интерфейсе

  • airflow-scheduler - отвечает за запуск DAGов и в целом за ETL часть

Соответственно и настраивать их нужно отдельно. Если что-то не работает, проблема может быть только в одном из этих сервисов, а может и в обоих одновременно. Правильная локализация ошибки поможет сократить время отладки вдвое. Чтобы понять, что упало, нужно смотреть их статус:

systemctl status airflow-webserversystemctl status airflow-scheduler

Ещё очень помогает системный лог: /var/log/syslog

Но это будем использовать на этапе отладки, а сначала нам нужно всё установить.

Airflow - система, написанная на питоне. И запускается она тоже на питоне. А по умолчанию на только что созданном сервере ubuntu питона и pip нет - нам нужно их установить.

Установка питона

Я ставил python 3. Ставьте самую актуальную стабильную версию на текущий момент.

Необходимо запустить последовательно:

apt updateapt install software-properties-commonadd-apt-repository ppa:deadsnakes/ppaapt install python3.8

Проверим, что питон установился нормально:

python3 version

Установим pip

apt install python3-pip

Установка Airflow

Перед установкой Airflow очень важно указать домашнюю директорию, куда будем ставить

export AIRFLOW_HOME=~/airflow/

Я работал из-под root, директорию не указал, что привело ко всем последующим проблемам, какие у меня были.

После указания директории, ставим сам Airflow:

pip3 install apache-airflow

Он установит все необходимые пакеты и создаст в указанной директории следующие файлы:

  • airflow-webserver.pid - файл нужен для запуска web-сервера, запомните где он лежит

  • airflow.cfg - настройки Airflow, запомните где он лежит - ещё пригодится

  • airflow.db - SQLite база данных - в дальнейшем перейдём на постгресс и она нам не пригодится.

  • unittests.cfg

  • webserver_config.py

Дальше, перейдём в домашний каталог Airflow с этими файлами и создадим в нём ещё одну директорию:

mkdir dags

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

Путь к папке с дагами можно настроить в файле airflow.cfg параметр dags_folder

Если вы всё сделали правильно - можете запускать веб-сервер и шедулер:

systemctl start airflow-webserversystemctl start airflow-scheduler

И возможно у вас всё будет работать как надо - будет открываться веб-интерфейс по 8080 порту ip вашего сервера.

Тогда вам останется только настроить подключение к нормальной БД, где у вас будут храниться пользователи и настройки, например к postgress:

Установка PostgresSQL и настройка подключения к Airflow

Установим PostgreSQL:

apt-get install postgresql

По умолчанию доступ к СУБД имеет пользовательpostgres. Заходим под ним:

sudo -u postgres psql

Создадим базу данных и пользователя к ней для Airflow:

postgres=# create database airflow_metadata;postgres=# CREATE USER airflow WITH password 'password';postgres=# grant all privileges on database airflow_metadata to airflow;

После этого необходимо настроить подключение Airflow к БД:

Окткрываем файл airflow.cfg

правим значение параметраsql_alchemy_connнаpostgresql+psycopg2://airflow:password@localhost/airflow_metadata

Подключение к базе будет происходить через psycopg2, у меня он был установлен в питоне по умолчанию, если у вас нет - то установите его себе, лучше из бинарников:

pip3 install psycopg2-binary

Инициализируем новую базу данных:

airflow initdb

Если в процессе на каком-то этапе не будут применяться изменения - перезапускайте сервисы:

systemctl restart airflow-webserversystemctl restart airflow-scheduler

После чего необходимо создать пользователя Airflow, с которого будем логиниться через web-интерфейс:

airflow users create --username AirflowAdmin --firstname name1 --lastname name2 --role Admin --email airflow@airflow.com

После чего терминал попросит ввести пароль для этого пользователя.

Отладка - та часть, которую не найдёшь в туториалах

Когда я устанавливал airflow и не указал домашнюю директорию, он всё поставил в home директорию пользователя root, что привело к дальнейшим большим проблемам - не делайте так.

Если же вы так сделали - создайте папку с нормальными правами доступа в корне и перенесите туда папку airflow.

Я пытался сначала настроить чтобы всё работало из root - даже выдал всем пользователям полный доступ до папок airflow - но видимо сыграло роль, что это был root пользователь - сервис всё-равно не мог получить доступа до нужных файлов и ругался мне в лог.

Дальше нужно будет в папке airflow найти все пути указанные к root, после чего вручную переделать их в нужный путь к домашней директории с airflow.

grep root ./*

Так же пришлось редактировать скрипты запуска сервисов - иначе тоже постоянно были ошибки в логе.

Для настроек перейдите в папку /usr/lib/systemd/system и отредактируйте фалы таким образом, чтобы они были максимально похожи на следующие:

airflow-webserver.service

[Unit]

Description=Airflow webserver daemon

After=network.target postgresql.service mysql.service redis.service rabbitmq-server.service

Wants=postgresql.service mysql.service redis.service rabbitmq-server.service


[Service]

EnvironmentFile=/etc/sysconfig/airflow

User=airflow

Group=airflow

Type=simple

ExecStart=/usr/local/bin/airflow webserver --pid /airflow/airflow-webserver.pid

Restart=on-failure

RestartSec=5s

PrivateTmp=true


[Install]

WantedBy=multi-user.target

--pid /airflow/airflow-webserver.pid должен указывать туда, где у вас лежит файл airflow-webserver.pid - мы его указывали выше в статье.

airflow-scheduler.service

[Unit]

Description=Airflow scheduler daemon

After=network.target postgresql.service mysql.service redis.service rabbitmq-server.service

Wants=postgresql.service mysql.service redis.service rabbitmq-server.service


[Service]

EnvironmentFile=/etc/sysconfig/airflow

User=airflow

Group=airflow

Type=simple

ExecStart=/usr/local/bin/airflow scheduler

Restart=always

RestartSec=10s


[Install]

WantedBy=multi-user.target

Что ещё может потребоваться настроить:

Перейти по в папку /etc/sysconfig/ и там отредактировать файл airflow - указать в нём правильный путь AIRFLOW_CONFIG и AIRFLOW_HOME

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

daemon-reloadsystemctl restart airflow-schedulersystemctl restart airflow-webserver

И последняя проблема: после создания пользователя пытался зайти в Airflow. Говорит "login failed".

Сначала я думал, что не создался пользователь - несколько раз его пересоздавал, проверял в БД - пользователь на месте, а с интерфейса не заходит.

Читаем логи (/var/log/syslog):

Видим, что веб сервер одновременно запущен и работает, и при этом постоянно пытается запуститься ещё раз.

Но если посмотреть его статус, то видим, что он упал с ошибкой:

systemctl status airflow-webserver

airflow-webserver.service - Airflow webserver daemon

Loaded: loaded (/lib/systemd/system/airflow-webserver.service; enabled; vendor preset: enabled)

Active: activating (auto-restart) (Result: exit-code) since Tue 2021-03-16 18:00:03 MSK; 2s ago

Process: 761523 ExecStart=/usr/local/bin/airflow webserver --pid /run/airflow/webserver.pid (code=exited, status=1/FAILURE)

Main PID: 761523 (code=exited, status=1/FAILURE)

Mar 16 18:00:03 digitalberd systemd[1]: airflow-webserver.service: Main process exited, code=exited, status=1/FAILURE

Mar 16 18:00:03 digitalberd systemd[1]: airflow-webserver.service: Failed with result 'exit-code'.

При всём при этом на сайте по 8080 порту Airflow отлично работает, только залогиниться не даёт.

Самое удивительное было, когда я его остановил принудительно: systemctl stop airflow-webserver: статус у него был, что он остановлен, но на сайте по 8080 порту он отлично показывал веб-интерфейс.

В чём же было дело и как удалось решить эту проблему? Посмотрим, что работает по этому порту:

lsof -i tcp:8080

выяснилось, что после остановки airflow-webserver остался запущенным gunicorn, который занимал 8080 порт и отрисовывал интерфейс.

После того как убил его по ID и перезапустил веб-сервер, всё наконец заработало нормально.

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

Подробнее..

Бесплатный удобный ETL инструмент с открытым кодом на основе Python фантастика или нет?

04.03.2021 12:16:30 | Автор: admin

Мы давно ищем идеальный ETL инструмент для наших проектов. Ни один из существующих инструментов нас полностью не удовлетворял, и мы попробовали собрать из open-source компонентов идеальный инструмент для извлечения и обработки данных. Кажется, у нас это получилось! По крайней мере, уже многие аналитики попробовали эту технологию и отзываются очень позитивно. Сборку мы назвали ViXtract и опубликовали на GitHub под BSD лицензией. Под катом рассуждения о том, каким должен быть идеальный ETL, рассказ о том, почему его лучше делать на Python (и почему это совсем не сложно) и примеры решения реальных задач на ViXtract. Приглашаю всех заинтересованных к дискуссии, обсуждению, использованию и развитию нового решения для старых проблем!

Визуализация результатов анализа это очень важно, но роль загрузки данных нельзя недооценивать. За последние 5 лет, которые я занимаюсь BI (как с технической, так и с бизнесовой стороны), я провел более 500 интервью с клиентами, на которых мы обсуждали задачи и потребности конкретных компаний. И в большинстве из них мои собеседники подчеркивали, что визуализация это очень нужная и полезная вещь, но самые большие проблемы и трудозатраты возникают при загрузке и очистке данных.

80% времени аналитика уходит на преобразование, очистку, выгрузку и сверку данных80% времени аналитика уходит на преобразование, очистку, выгрузку и сверку данных

Мы в Visiology в основном работаем с крупными предприятиями, промышленностью и госорганизациями, но в разговорах с коллегами я убедился, что проблемы везде одни и те же. Аналитики могут уделить анализу и визуализации только 20% своего времени, потому что 80% уходит на преобразование, очистку, выгрузку и сверку данных. Чтобы эффективно решать эту проблему, мы постоянно ищем новые методы и инструменты работы с данными, тестируем, пробуем на реальных задачах. Что же мы называем идеальным ETL инструментом?

Итак, вот 5 основных критериев, которым должен соответствовать идеальный ETL (Extract-Transform-Load) инструмент:

  1. ETL-инструмент должен быть простым в освоении. Речь не о том, что с ним должны уметь работать совсем неопытные люди. Просто специалист не должен тратить полжизни на изучение нового ПО, а просто взять и практически сразу начать работать с ним.

  2. В нём должно быть предусмотрено максимальное количество готовых коннекторов. Ведь в сущности, мы все пользуемся плюс-минус одними и теми же системами: от 1С до SAP, Oracle, AmoCRM, Google Analytics. И никто не хочет программировать коннекторы к ним с нуля.

  3. Инструмент должен быть универсальным и работать с разными BI системами. Это облегчает переход аналитиков и разработчиков из одной компании в другую если на прошлом месте работы, например, использовали QlikView, а на новом Visiology, желательно сохранить возможность пользоваться тем же ETL-инструментом.

  4. ETL не должен ограничивать развитие аналитики. Увы, очень у многих ETL-инструментов есть критическая проблема в них несложно реализовать простенькие вещи, но для более сложных задач приходится искать новый инструмент, который сможет расти вместе с тобой.

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

Что может предложить нам рынок?

В поиске ответа на этот вопрос для себя самих и для наших клиентов. мы отметили на диаграмме наиболее известные решения, подходящие для этой задачи. Они расположены от бесплатных к дорогим, от простых до корпоративных систем Enterprise-класса.

В категории наиболее сложных и дорогих систем доминируют Oracle и Informatica. Microsoft SSIS чуть более демократичный. Рядом с ними Apache Airflow. Это открытый продукт, не требующий оплаты, но зато кривая входа для него оказывается довольно крутой. Кроме этого существуют ETL-инструменты, встроенные или связанные с конкретными BI-системами. В их число входят, например, Tableau Prep или Power Query, который используется совместно с Power BI. В числе бесплатных и демократичных решений Pentaho Data Integration, бывший Kettle, и Loginom.

Но, увы, ни одна из этих систем не удовлетворяет перечисленным 5 критериям. Oracle и Informatica оказываются слишком дорогими и сложными. С Airflow не так уж просто сразу начать работать. EasyMorph не дотягивает по функциональности, а все инструменты, оказавшиеся в центре нашей диаграммы, прекрасно работают, но не являются универсальными. Фактически, я называл бы достаточно сбалансированными решениями Loginom и Pentaho, но тут возникает ещё один важный момент, о котором обязательно нужно поговорить.

Визуальный или скриптовый ETL?

Если копнуть глубже, то все эти (и другие) ETL-инструменты можно разбить на два больших класса визуальные и скриптовые. Визуальный ETL позволяет делать схемы из готовых блоков, а скриптовый позволяет задавать параметры на специальном языке программирования, уже оптимизированном для обработки данных.

Выбор между визуальным и скриптовым ETL это настоящий холивар, достойный противостояния Android vs iOS. Лично я отношусь к той категории, которая считает, что за скриптовыми ETL будущее. Конечно, визуальный ETL имеет свои преимущества это наглядность и простота, но только на первом этапе. Как только возникает потребность сделать что-то сложное, картинки становятся слишком запутанными, и мы все равно начинаем писать код. А поскольку в визуальных ETL нет отладчиков и других полезных примочек для кодинга, делать это приходится в откровенно неудобных условиях.

Pentaho и Loginom относятся к визуальному типу. Я считаю, что это очень хорошие системы для своих задач, и если вы сторонник визуальной ETL на них можно остановиться. Но я всё-таки всегда делаю выбор в пользу скриптового подхода, потому что он позволяет задавать параметры без графических ограничений, и с ним можно значительно ускорить работу когда ты уже в этом разобрался.

Конечно, стоит учитывать, что все ETL-инструменты тяготеют к смешанному варианту работы, когда либо визуальный дополняется кодом, либо код дополняется визуальными. Но всё равно в ДНК системы может быть что-то одно. И если мы хотим получить хороший скриптовый ETL, нужно ответить на вопрос каким он должен быть?

Скриптовый - значит, должен быть основан на Python!

Если мы хотим, чтобы ETL был открытым, бесплатным и уже с экосистемой, значит инструмент должен быть на Python. Почему? Потому что, во-первых, Python это простой язык, сейчас даже дети учатся программировать на Python чуть ли ни с первого класса. Например, в Алгоритмике начинают курс программирования именно с Python, а не с Basic или визуального языка Google. Так что подрастающее поколение разработчиков уже знакомо с ним. Во-вторых,огромная экосистема готовых технологий и библиотек уже создана: от каких-то банальных коннекторов до очень серьёзных вещей, связанных с Data Science и так далее. Можно начинать развиваться в этом направлении: здесь ограничений никаких нет.

Конечно, у Python есть и минусы. При столкновении с экосистемой один на один будет серьёзная кривая входа. Новичкам разбираться с темой оказывается достаточно сложно. Как минимум, нужно иметь компетенции по работе с Linux, и это для многих сразу становится стоп-фактором. Именно поэтому нам часто говорят: Нет, мы хотим что-нибудь простое, готовое, с Python мы разбираться не готовы.

Решение = JupyterHub + PETL + Cronicle

Но поскольку во всём остальном готовый инструмент на Python получается хорош, для решения проблемы входа мы подобрали набор технологий, которые помогают упростить работу с системой. Это уже доказавшие свою эффективность зрелые open-source решения, которые можно запросто объединить и использовать:

  1. JupyterHub интерактивная среда выполнения Python-кода. По сути, это среда разработки, которая позволяет работать с кодом в интерактивном режиме. Она очень удобна для тех, кто не является профессиональным разработчиком, не накопил готовых навыков программирования на уровне спинномозговых рефлексов. JupyterHub помогает, когда ты разбираешься с кодом, пробуешь что-то новое, экспериментируешь.

  2. Библиотека PETL была разработана на Python специально для обработки данных. Она берёт на себя огромное количество рутинных задач, например, разбор CSV файлов различных форматов или создание схемы в БД при выгрузке данных.

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

Чтобы всем этим было проще пользоваться, мы объединили три инструмента в ViXtract. Речь идет о сборке набора open-source технологий, которая позволяет легко установить решение одной командой и использовать ETL, не заморачиваясь по поводу Linux, по поводу прав, нюансов интеграций и других тонкостей.

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

И еще несколько слов о самой оболочке

Давайте посмотрим на интерфейс нашего инструмента. На стартовой странице находятся кнопки запуска редактора, планировщика и переходы на полезные ресурсы сайт, telegram-канал, сообщество и библиотека PETL. Это документация, в которой описаны все функции преобразования, загрузки файлов. Когда вы начинаете работать с ViXtract, эту страницу логично держать всё время под рукой. Сейчас документация на английском, но одно из направлений развития это перевод всего набора информации на русский язык.

В ViXtract имеется сразу несколько ядер (aka настроенных окружений). Например, одно из них можно использовать для разработки, а другое для продуктива. Таким образом, вы можете установить много различных библиотек в одном окружении, а для продуктива оставить только проверенные. Окружения можно легко добавлять и изменять, а если вам интересно узнать о самом процессе работы с данными через ViXTract, вы всегда можете задать вопрос в Telegram сообществе ViXtract.

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

Загрузка данных

PETLподдерживает множество источников данных, мы рассмотрим несколько типовых примеров. Эти же примеры доступны в виде готовых тетрадок на GitHub или в установленном ViXtract, там их можно попробовать.

  • Загрузка из xlsx-файла

  • Использование открытых источников через API

  • Работа с базой данных

Данные из xlsx-файла

Рассмотрим работу сpetlна наборе результатов летних олимпиад по странам. Нам понадобится файлdatasets/summer_olympics.xlsx, посмотрим на первые строки, пока не сохраняя таблицу в переменную.

etl.fromxlsx('datasets/summer_olympics.xlsx')

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

olympics = etl.fromxlsx('datasets/summer_olympics.xlsx').skip(1)

Теперь заголовки у столбцов корректные, однако не достаточно информативны, исправим это, задав заголовки вручную.

olympics2 = olympics.setheader(['country','games','gold','silver','bronze'])

Мы начали выстраивать цепочку преобразования таблицы, это удобно, так как можно просмотреть результат работы на каждом этапе. Иногда, наоборот, удобнее объединить цепочку сразу в одной команде. Например, совместим изменение заголовка с сортировкой по количеству золотых медалей.

olympics2 = olympics.setheader(['country','games','gold','silver','bronze']).sort('gold', reverse=True)

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

Мы также применим мощный инструмент Python -Анонимные функции.Анонимная функция(функция без имени) - это запись видаlambda x: <функция от x>. Читается как: "То, что было подано на вход этого выражения, будет положено вx, а результатом исполнения будет<функция от x>. В PETL это часто применяется, чтобы выполнить быстрое преобразования значения какого-либо из полей. Например, если нужно все значения таблицыtableв полеfieldумножить на два, это можно написать какtable.convert('field', lambda x : x * 2). В примере ниже функция применяется не к отдельным значениям, а к строке целиком.

olympics2.addfield('total', lambda row : row['gold'] + row['silver'] + row['bronze'])

Вместо того, чтобы получить суммы, мы просто склеили значения. Чтобы такого не происходило, будем преобразовывать формат данных в целочисленные. Выясним, какая страна смогла набрать наибольшее число медалей, отсортировав сразу таблицу по новому столбцу по убыванию, с помощью функцииsort. Также используем символ\, чтобы разбить команду на несколько строк для улучшения читаемости.

olympics3 = olympics2\    .addfield('total', lambda x: int(x['gold']) + int(x['silver']) + int(x['bronze']))\    .sort('total', reverse=True)

Видим, что в таблице есть сумма по всем странам, что нас не интересует в данной задаче. Можем выбрать из таблицы все строки, кроме строки со значениемcountry == Totals. Воспользуемся функциейselect.

Кроме того, дополнительно рассчитаем новый показатель - результативность страны, определив её как среднее число медалей за игру.

olympics4 = olympics3\    .select(lambda x: x.country != 'Totals')\    .addfield('effectiveness', lambda x: round(x['total'] / float(x['games']), 2))

Сохраним полученные результаты в новый xlsx-файл.

olympics4.toxlsx('olympics.xlsx')

Готово! Теперь обработанный файл можно скачать или загрузить в BI-систему.

Данные из открытого источника рынка акций

Рассмотрим немного более продвинутый пример - получение данных из веб-сервиса по API. Это также делается очень просто с использованием библиотекиrequests

response = requests.get('https://www.quandl.com/api/v3/datasets/WIKI/AAPL.json?start_date=2017-05-01&end_date=2017-07-01')

Посмотрим, что мы получили в ответ. Мы увидим данные в формате JSON, которые нужно будет промотать до конца

stock_prices_json = response.json()stock_prices_json

Видим, что в полученном JSON сама таблица с данными лежит в разделеdataset. Посмотрим, какие в ней есть поля.

stock_prices_json['dataset'].keys()

Нас интересуют два поля ответа:column_names, который мы будем использовать в качестве заголовков таблицы, иdata, содержащий все необходимые данные построчно. Для преобразования данных из объектаdictв таблицуpetlсделаем следующее:

  • Транспонируем содержимоеdata, чтобы превратить строки в столбцы

  • Используемcolumn_namesв качестве значения параметраheaderфункцииfromcolumns

stock_prices = etl.fromcolumns(stock_prices_json['dataset']['data']).skip(1)\    .transpose()\    .setheader(stock_prices_json['dataset']['column_names'])

Уберём часть столбцов, все, содержащие'Adj', переведём все значения в числа (где это возможно), вычислим разницу курса на определённую дату.

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

List comprehension- это запись вида(<функция от x> for x in <список> if <условие от x), которая читается как: "Возьми все элементы из<список>, отбери те их них, для которых истинно<условие от x>, выполни над каждым<функция от x>и верни результаты в виде списка. Например, есть массив чиселarrи нужно отобрать из него четные числа и разделить их на 4. Это можно записать как(x/4 for x in arr if x % 2 == 0)

stock_prices2 = stock_prices\    .cutout(*(x for x in stock_prices.fieldnames() if 'Adj' in x))\    .convertnumbers()\    .addfield('Difference', lambda row: round(row.Close - row.Open, 2))stock_prices2

Сохраним полученную табличку в csv-файл.

stock_prices2.tocsv('stock.csv')

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

Данные из БД (PostgreSQL)

В состав ViXtract входит предустановленная СУБД PostgreSQL, её удобно использовать как промежуточное хранилище данных, из которого их уже забирает BI-система. Похожие подходы могут быть использованы и с любой другой СУБД.

Рассмотрим следующий пример.
Доступны данные о состояниях различных типов транспортных средств. В базе есть 2 таблицы:

  • status_tsсодержит информацию о состояниях различных ТС

  • ts_typesсодержит наименования типов ТС

Необходимо подготовить таблицу, содержащую валидные данные по бульдозерам:

  • В данных не должно быть пропусков

  • Время указано в формате datetime

  • Кроме данных по бульдозерам других нет

  • Все состояния, кроме отсутствия данных

  • Для каждого состояния рассчитана продолжительность

statuses = etl.fromdb(connection, 'SELECT * FROM status_ts')ts_types = etl.fromdb(connection, 'SELECT * FROM ts_types')# Вспомогательные функции# Определяем фильтр для исключения строк с пустыми значениямиrow_without_nones = lambda x: all(x[field] != '' for field in statuses.fieldnames())# Перевод отметки времени в формат datetimeto_datetime = lambda x: dt.fromtimestamp(int(x))

Чтобы исключить строки с пропусками, используем функциюselectи определенный выше фильтрrow_without_nones

statuses.select(row_without_nones)

Переведём столбцы со временем в требуемый формат. Для этого необходимо воспользоваться функциейconvert. Сразу можем добавить расчёт продолжительности функциейaddfield.

statuses.\    convert('Начало', to_datetime).\    convert('Окончание', to_datetime).\    addfield('Продолжительность', lambda x: x['Окончание'] - x['Начало'])

Объединим обе таблицы и выберем данные только по бульдозерам, сразу уберём строки с состоянием "Отсутствие данных".

statuses.\    join(ts_types, lkey='id ТС', rkey='id').\    select(lambda x: 'Бульдозер' in x['Тип ТС'] and x['Состояние'] != 'Отсутствие данных')

Все перечисленные операции можно произвести за раз, сформируем цепочку функций. Заметим, что столбецid ТСуже не требуется, его можно убрать функциейcutout.

В дополнение ко всему отсортируем таблицу по времени начала состояний, применивsort.

result = statuses.\    join(ts_types, lkey='id ТС', rkey='id').\    select(lambda x: 'Бульдозер' in x['Тип ТС'] and x['Состояние'] != 'Отсутствие данных').\    select(row_without_nones).\    convert('Начало', to_datetime).\    convert('Окончание', to_datetime).\    addfield('Продолжительность', lambda x: x['Окончание'] - x['Начало']).\    convert('Начало', str).convert('Окончание', str).convert('Продолжительность', str).\    cutout('id ТС').\    sort('Начало')
# Импортируем библиотеку, позволяющую создавать таблицы в БДimport sqlalchemy as db# Подготовим подключение_user = 'demo'_pass = 'demo'_host = 'localhost'_port = 5432target_db = db.create_engine(f"postgres://{_user}:{_pass}@{_host}:{_port}/etl")# Пробуем пересоздать таблицу (удалить и создать заново). Если таблицы нет - просто создаем новую.try:    result.todb(target_db, 'status_cleaned', create=True, drop=True, sample=0)except:    result.todb(target_db, 'status_cleaned', create=True, sample=0)

Проверим, что таблица создалась. Обратите внимание, что схема таблицы (типы полей, их названия и так далее) была создана полностью автоматически.

etl.fromdb(connection, 'SELECT * FROM status_cleaned')

Так мы не зря выбрали Python?

Я по-прежнему часто слышу мнение: Python, вся эта экосистема это ужас какой-то, это что-то необъятное!. Но на самом деле для того, чтобы выгружать данные, требуется лишь небольшое подмножество этого Python, примерно такое же, как с любым другим ETL-инструментом. Когда вы разберетесь с теми функциями, которые действительно нужны, появляется возможность развиваться дальше, переходить к обработке больших данных, потому что все стеки Big Data уже имеют обёртки на Python качественные, нативные и удобные. А те технологии, которые используются в ViXtract, применяются и для обработки больших данных, за исключением, может быть, PETL, который ориентирован на средние объёмы информации.

Кстати, продвинутая аналитика и Data Science тоже строятся на экосистеме Python. И если что-то было предварительно создано на Python, результаты можно легко передать разработчику уже для внедрения в продуктив. Другими словами, проведенная в ViXtract работа на Python может быть дальше использована в AirFlow для развития в Enterprise-системе. Возможно, разработчику нужно будет переписать код в соответствии со стандартами продуктива, но затраты на коммуникации уменьшаются на порядок.

В ходе нашего Beta-тестирования ViXtract аналитики начали сами решать задачи по загрузке данных из разных источников и их очистке. Раньше эти люди предпочитали написать задачу и отдать её разработчикам мол, пусть готово будет через неделю, но зато без проблем. А сейчас они могут сами сделать все необходимое за полчаса. И мне хотелось бы, чтобы вы тоже оценили ViXtract, оставили свое мнение о нём, а может быть подключились к разработке этого инструмента. Так что если вам тоже интересна эта тема, подписывайтесь на наш блог и подключайтесь к обсуждению.

Сайт ViXtract, на котором можно посмотреть видео-демонстрацию и попробовать ViXtract без установки на свой сервер - https://vixtract.ru/

Ссылка на GitHub - https://github.com/visiologyofficial/vixtract

Telegram сообщество ViXtract - https://t.me/vixtract_ru

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru