В последние годы мы видим, как инженерия данных всё больше сливается с индустрией DevOps. В обоих этих направлениях для доставки надёжных цифровых продуктов клиентам используется облачная инфраструктура, контейнеризация, CI/CD и GitOps. Это схождение в плане использования одного набора инструментов заставило многих думать, что инженерия данных не имеет значительных отличий от инженерии программного обеспечения. Как следствие, первая оказывается «несовершенной», поскольку дата-инженеры отстают с внедрением эффективных практик разработки ПО.
Но такая оценка ошибочна. Несмотря на то что в обработке данных и разработке ПО используется много общих инструментов и практик, между ними есть ряд существенных отличий. Игнорирование этих отличий и управление командой дата-инженеров по аналогии с командой разработки ПО является ошибкой. Так что цель данной статьи – подчеркнуть некоторые уникальные проблемы в инженерии данных и пояснить, почему в этой области иногда требуется особый подход.
Конвейеры данных – это не приложения
Разработка ПО подразумевает создание приложений. В контексте этой статьи термин приложение будет иметь очень широкий смысл, подразумевая как сайт, так и десктопное приложение, API, игру, микросервис, библиотеку и прочее. Объединяет же все эти виды приложений то, что они:
- Предоставляют пользователям ценность, предлагая новую модель взаимодействия. В игру можно играть, сайт можно просматривать, API можно использовать в другом ПО.
- Обладают рядом преимущественно независимых возможностей. У сайта может расти количество страниц, в игре может увеличиваться число уровней или доступных персонажей, в API могут добавляться новые конечные точки. В итоге получается, что приложение никогда не является поистине законченным.
- Мало работают с создаваемыми ими состояниями. Состояние предназначено для поддержки приложения, и управление им обычно возлагается на внешнюю систему. Смысл в том, чтобы основная часть ПО работала без использования состояний. Веб-приложение можно в любой момент закрыть и запустить заново — его состоянием управляет база данных, выполняющаяся в отдельном процессе.
- Слабо связаны с другим ПО и сервисами. Хорошее ПО должно функционировать независимо в любой среде, что объясняет популярность микросервисов и контейнеров.
Если же говорить о дата-инженерах, то они занимаются построением конвейеров данных. Такие конвейеры получают данные от их создателя, трансформируют эти данные и передают потребителю. Обычно стоит цель автоматизировать работу этих конвейеров по расписанию, чтобы датасеты периодически обновлялись новыми данными. Подобно приложениям — конвейеры обычно являются программами, но отличаются от них следующим:
- Не обеспечивают непосредственной ценности. У конвейера нет пользователей. Конечным потребителям нужны только производимые ими датасеты. Если эти данные попадут в место назначения по какой-то замысловатой схеме перекопирования, то потребителя такой вариант вполне устроит.
- Имеют для потребителя всего один аспект значимости, а именно создают запрошенный датасет. Следовательно, существует явная точка завершения, хотя сам конвейер требует непрерывного обслуживания ввиду изменений пользовательских требований в вышестоящей системе.
- Управляют большим количеством состояний. Конвейер предназначен для обработки существующего, не контролируемого им состояния другого ПО и его преобразования в контролируемое. Многие конвейеры выстраивают датасеты поэтапно, с каждым шагом внося в них новые данные. В этом смысле их можно рассматривать как очень долгие процессы, непрерывно создающие всё больше состояний.
- Неизбежно обладают сильным зацеплением. Цель конвейера состоит в привязке к источнику данных. В итоге его стабильность и надёжность всегда будут зависеть от стабильности и надёжности этого источника.
Эти фундаментальные отличия ставят перед дата-инженерами уникальные вызовы, которые зачастую недостаточно понимают владельцы бизнесов, IT-руководство и даже разработчики ПО.
Далее об этих вызовах мы и поговорим.
Конвейер либо завершён, либо бесполезен
Нынче многие организации управляют своими командами разработчиков ПО с помощью одной из схем Agile. Основная философия этой методологии заключается в максимизации скорости доставки ценности потребителю за счёт создания и релиза элементов ПО в краткосрочных циклах. Задача – максимально быстро поставить клиентам минимально жизнеспособный продукт (MVP) и обеспечить быструю обратную связь, которая бы позволила разработчикам непрерывно трудиться над функционалом, имеющим наивысший приоритет.
Но к инженерии данных эти принципы уже неприменимы.
Конвейер данных нельзя разрабатывать малыми итерациями с возрастающей потребительской ценностью. У конвейера нет MVP-эквивалента — он либо создаёт необходимый потребителю датасет, либо нет.
Следовательно, разработка конвейера данных не вписывается в схемы Agile. Сложный конвейер соответствует одной пользовательской истории, но обычно требует для завершения нескольких спринтов. Руководство, не обладающее технической подготовкой, редко учитывает этот тонкий нюанс и пытается всё равно впихнуть дата-инженеров в scrum команды. В результате пользовательские истории заменяются задачами, например, «создать API-коннектор» и «построить логику потребления», что неизбежно превращает доску Scrum в инструмент микроменеджмента.
Непонимание руководителями основ того, чем они управляют, зачастую ведёт к принятию неэффективных и даже нерабочих решений. Как-то раз разозлённый медленным прогрессом создания конвейера менеджер потребовал от инженера из нашей команды создавать датасет итеративно, столбец за столбцом, чтобы у клиента «были хоть какие-то данные, с которыми можно начать работать». Инженеры, имеющие практический опыт работы со сложными конвейерами, и аналитики данных, которым доводилось получать бесполезный датасет, поймут смехотворность этой ситуации. Для тех же читателей, кто подобного опыта не имеет, приведу три причины, по которым такой подход не сработает:
▍ 1. Ценность частичного датасета не вычисляется пропорционально
Если датасет содержит 9 столбцов из 10, значит ли это, что он полезен на 90%? Всё зависит от того, какого столбца не хватает. Если аналитик собирается построить на основе этих данных предиктивную модель, но отсутствующий столбец представляет метки или прогнозируемые значения, то польза датасета составит 0%. Если же этот столбец содержит какие-то случайные метаданные, не связанные с метками, то такой датасет может оказаться полезен и на все 100%.
Чаще всего столбец представляет поле, которое может как быть, так и не быть связанным с метками. Выяснение того, связан ли он с ними в действительности, и является целью экспериментов аналитика данных. Именно поэтому ему нужен максимально полноценный датасет, на основе которого он сможет начать изучать данные и оптимизировать модель. В результате же предоставления частичного датасета эксперименты и оптимизацию придётся выполнять заново по мере появления остальных полей.
▍ Время разработки конвейера не соотносится с размером датасета
Даже если клиент будет рад и половине датасета, создание этой половины займёт не вдвое меньше времени, как можно предположить. Конвейер данных состоит не из независимых задач, каждая из которых создавала бы столбец. Несколько столбцов могут быть связаны, если происходят из одного источника, в случае чего включение в конечный датасет хоть одного, хоть нескольких составит одинаковый объём работы.
Однако логика для слияния этих столбцов со столбцами в другой таблице может представлять как простое объединение, так и требовать сложной серии оконных функций. Кроме того, конвейеру для вывода обработанных данных может потребоваться написание большого объёма шаблонного кода, например — клиента для доступа к API или парсера для обработки неструктурированных данных.
После написания такого компонента расширить логику для обработки дополнительных полей будет, скорее всего, несложно. Поэтому количество столбцов в итоговом датасете совершенно не подходит в качестве метрики для оценки его сложности, равно как количество строк написанного кода не подходит для оценки продуктивности.
Размер датасета также может относиться к количеству строк/записей. Грамотно построенный конвейер должен уметь обработать любое число записей, поэтому на времени разработки этот критерий не скажется. Хотя в её процессе могут происходить «скачки», вызванные конкретными условиями вроде:
- Как часто набор данных нужно обновлять? То есть нужно реализовать пакетное или потоковое обновление?
- Поступления какого объёма данных мы ожидаем и с какой скоростью?
- Умещаются ли эти данные в ОЗУ?
Все эти моменты необходимо знать заранее, так как они повлияют на всю структуру конвейера.
▍ 3. Затраты времени и ресурсов коррелируют с размером датасета
Чем больше в датасете строк и столбцов, тем больше времени потребуется на его построение и обновление. Редактирование одной записи в огромной базе данных производится легко и быстро, но в датасетах для аналитики такие случаи, как правило, не встречаются. Изменение подобного датасета обычно подразумевает добавление/корректировку целых столбцов (что приводит к изменению всех записей) или обновление тысяч/миллионов строк. Корректировку данных можно обработать двумя способами, ни один из которых не обходится дешёво.
С позиции разработчика изменить датасет проще всего путём обновления конвейера и его повторного выполнения. Однако это оказывается наиболее накладным вариантом в плане вычислительных затрат и времени, необходимого для обновления датасета. Проектирование конвейера так, чтобы он корректно переписывал состояние прошлых выполнений (был идемпотентным) также не всегда оказывается простой задачей и часто требует подобающего предварительного планирования.
В качестве альтернативы логику для обновления датасета можно закодировать в отдельном конвейере, который будет получать на входе старый датасет. Этот подход окажется более экономичным в плане вычислений и быстродействия, но потребует больше времени на разработку и добавит лишнюю умственную нагрузку. Конвейеры, которые применяют дельты, идемпотентными не являются, поэтому при выполнении конкретных операций также важно отслеживать текущее состояние. И даже в этом случае для внесения изменений в новые версии старые конвейеры необходимо обновлять.
Как бы то ни было, задача получается сложной, поскольку датасетам свойственна инерция – чем больше их размер, тем больше для внесения изменений требуется времени, усилий и средств.
▍ Вывод: развёртывать частичный конвейер в продакшене бессмысленно
Развёртывание частично готового конвейера в продакшене не принесёт пользы клиенту, зато приведёт к трате вычислительных ресурсов и усложнит жизнь инженеров, поскольку им придётся дополнительно работать со старым состоянием. Слепое привнесение в сферу разработки конвейеров данных принципов DevOps и Agile, где приветствуются постепенные изменения и частые деплои, просто означает игнорирование описанной инерции данных.
Любой инженер предпочёл бы «сделать всё правильно с первого раза» и минимизировать число деплоев в продакшен. Частое развёртывание конвейера говорит либо о том, что клиент не знает чего хочет, либо о том, что источник данных очень нестабилен, и конвейер необходимо постоянно обновлять. В противоположность stateless-приложениям, где обновление по сложности обычно сопоставимо с удалением пары контейнеров и запуском двух новых, обновление датасета не равнозначно повторному развёртыванию кода конвейера. И упаковывание этого кода в контейнер с последующим его запуском в Kubernetes данный пробел не компенсирует.
Циклы обратной связи в разработке конвейеров очень длительны
Для того чтобы быстро создать новый функционал или исправить в ПО баги, разработчикам требуется обратная связь, которая позволит убедиться, что написанный ими код ведёт программу в нужном направлении.
В разработке ПО такая связь обычно достигается посредством модульных тестов, которые программист выполняет локально для проверки правильной работы каждого компонента. Модульные тесты должны быть быстрыми, не связанными ни с какими внешними системами и не зависящими от каких-либо состояний. Они должны тестировать функции, методы и классы независимо. В таком случае программист получает быструю обратную связь во время разработки и может быть уверен, что на момент пул-реквеста его код будет работать должным образом. Если же есть необходимость протестировать взаимодействие с другими системами, конвейер CI также может включать в себя более медленные интеграционные тесты.
Поделюсь секретом дата-инженеров: конвейеры данных редко прогоняют через модульные тесты (сюрприз!). Обычно их тестируют уже посредством развёртывания – как правило, сначала в среде разработки. Для этого требуется этап сборки и деплоя, после которого инженер должен какое-то время помониторить конвейер, убедившись в его правильной работе. Если же конвейер не идемпотентный, то повторный деплой сначала может потребовать ручного вмешательства для сброса состояния, оставленного прежним деплоем. Такой цикл обратной связи очень медленный, если сравнивать его с модульным тестированием.
Так почему бы просто не писать модульные тесты?
▍ 1. Конвейеры данных уязвимы в местах, которые нельзя проверить модульными тестами
Самодостаточная логика, которую можно проверить модульным тестированием, обычно в конвейерах данных ограничена. Бо́льшая часть их кода представляет собой «связующие компоненты и костыли», поэтому почти все сбои возникают в корявых интерфейсах между системами или при попадании в конвейер неожиданных данных.
Интерфейсы между системами нельзя проверить модульными тестами, поскольку изолированно эти тесты не выполняются. Внешние системы можно сымитировать, но это подтвердит лишь правильность работу конвейера с системой, действующей так, как это себе представляет инженер. В реальности же инженер редко знает все детали.
Возьмём пример из жизни: для предотвращения DDOS-атак публичный API может иметь скрытый лимит допустимого числа запросов от одного IP за определённый промежуток времени. Макет такого API вряд ли будет учитывать этот нюанс, но факт его существования в реальной системе может сломать конвейер в продакшене. К тому же внешние системы редко отличаются стабильностью. На деле конвейеры данных обычно создаются, потому что люди хотят переместить данные из неустойчивых систем в более надёжные. Макет же не сможет отразить возможное изменение системы, ведущее к сбою работы конвейера.
При этом хорошо известно, что редко какой поставщик данных сможет предоставлять их постоянно в качественном виде. В конвейере обязательно должно учитываться то, какие ему могут быть переданы данные. Неожиданное содержимое или структура в лучшем случае приведут к неверному результату, а в худшем к сбою его работы. Обычно для защиты от нестабильных источников данных используется проверка схем при считывании.
Но это не оградит от ошибочного содержимого данных и тонких «багов». К примеру, правильно ли во временном ряду учитывается период экономии света? Есть ли в столбце строки, не вписывающиеся в ожидаемый паттерн? Имеют ли физический смысл значения в числовом столбце единиц измерения? Ни один из этих примеров не связан с логикой конвейера, которую можно проверить модульными тестами.
▍ 2. Модульные тесты сложнее логики конвейера
Модульные тесты, необходимые для проверки ограниченной, самостоятельной логики преобразования, сложнее самого кода, поскольку от разработчика требуется создать репрезентативные тестовые данные, а также ожидаемые выходные данные. Это много работы, которая не повысит значительно уверенность в правильном функционировании конвейера. Кроме того, это сменяет вопрос «Работает ли данная функция должным образом?» на «Адекватно ли эти тестовые данные представляют реальные данные?» Модульные тесты идеально охватывают доброе подмножество комбинаций входных параметров, но в функциях, которые преобразуют датасет, к примеру, в датафрейм, сам аргумент датасета представляет чуть ли не бесконечное пространство параметров.
▍ Вывод: конвейеры разрабатываются медленно
Лучший способ получения надёжной обратной связи от конвейера данных – это его развёртывание и запуск. Такой вариант всегда будет медленнее локального выполнения тестов, а значит, для получения обратной связи потребуется намного больше времени. В результате разработка конвейеров, особенно на стадии отладки, движется раздражающе медленно.
Здесь можно рассмотреть интеграционные тесты, которые выполняются быстрее, чем весь конвейер. Однако обычно их нельзя запустить на машине разработчика ввиду отсутствия прямого доступа к релевантным системам-источникам данных. В связи с этим такие тесты можно выполнять только в той же среде, что и конвейер – а это, опять же, требует развёртывания. Выходит, что смысл писать тесты для получения быстрой обратной связи по большому счёту — пропадает.
Сейчас в качестве средства борьбы с ненадёжными поставщиками данных модно использовать «контракты данных». Уверенность в получаемых конвейерами данных исключила бы из процесса разработки значительную долю неуверенности и сделала их менее хрупкими. Однако есть проблема с утверждением таких контрактов, поскольку поставщики данных не заинтересованы следовать утверждаемым ими условиям. Кроме того, организация также захочет использовать данные, полученные из внешних источников вроде публичных API. Можете представить себе ещё и необходимость обсуждения контракта с этими сторонними участниками?
Разработку конвейера невозможно распараллелить
Мы выяснили, что конвейеры данных – это одна пользовательская история, и разрабатываются они медленно ввиду продолжительного цикла обратной связи. Но поскольку конвейер состоит из нескольких задач, некоторые менеджеры в попытке ускорить процесс пытаются распределить их среди нескольких разработчиков.
К сожалению, такой подход не работает. Задачи по обработке данных в конвейере последовательны. Для того чтобы построить второй шаг, необходимо добиться стабильного вывода первого. И наоборот, выводы, полученные в ходе разработки второго шага, позволяют внести улучшения в первый. В таком контексте конвейер в целом должен рассматриваться как функционал, который разработчик итеративно дорабатывает.
Некоторые менеджеры считают, что это просто означает недостаточную изначальную спланированность конвейера. Есть данные в начале и вполне ясно, какие данные должны получиться в конце. Разве тогда не очевидно, что необходимо выстроить в середине? Парадоксально, но те же менеджеры, которые так рассуждают, будут активно защищать преимущества методов Agile.
Планирование всего конвейера не работает до тех пор, пока источник данных не будет должным образом охарактеризован. При отсутствии контрактов и документации дата-инженер вынужден блуждать в догадках, пытаясь обнаружить частные особенности данных. И этот процесс поиска задаёт архитектуру конвейера. В некотором смысле он получается гибким. Просто такой вариант не устраивает стейкхолдеров.
Общие выводы и рекомендации
Конвейеры данных относятся к программному обеспечению, но не являются программными продуктами. Они представляют собой фабрику, которая лишь собирает запрошенную клиентом машину. Это лишь средства достижения цели, способ автоматизации для создания легко потребляемого датасета на основе громоздких источников данных. Конвейеры – это костыль для связывания систем, в которых не предусмотрено взаимодействие друг с другом. Они являются неуклюжими, хрупкими и дорогостоящими решениями для проблемы «последней мили» в сфере данных. Их единственная задача – управлять состоянием, что делает их разработку медленной, непредсказуемой и зачастую самым узким местом в проектах по аналитике данных.
Что бы ни утверждали авторитеты в сфере данных, навязывая какой-то очередной инструмент, сколько бы слоёв абстракции ни нанизывалось поверх друг друга, данные фундаментально отличаются от ПО. Отказ признавать эти отличия и попытка насаждать процессы Agile в команде по обработке данных лишь потому, что они работают в команде разработки ПО, даст только обратный эффект.
Что же сделать для повышения успешности и продуктивности дата-инженеров?
- Признать, что лёгкая форма каскадной модели (Waterfall, в разработке ПО обычно считается плохим подходом) в проектах по созданию конвейеров данных неизбежна. Прежде чем начинать разработку, необходимо тщательно проговорить с клиентами требования к нужному им датасету, а также обсудить с поставщиками данных передачу информации о недокументированных API и подключение к системе-источнику. Не тратьте много времени на разработку, пока не достигнете соглашения с клиентами и поставщиками данных. Признайте, что после запуска конвейера в продакшен любые его дальнейшие изменения будут затратны.
- Позволить дата-инженерам поэкспериментировать с источниками данных. Признать, что все примерные оценки сроков готовности датасета будут ошибочны.
- Не разделять работу над конвейером между несколькими разработчиками. Вместо этого — позвольте двум или более специалистам трудиться над ним совместно. Максимальной продуктивности позволят добиться техники парного/экстремального/группового программирования в рамках основной ветви, поскольку такой подход исключает неразбериху в git-ветвлении, пул-реквесты и ревью кода. Регулярное инспектирование кода в две пары глаз отлично помогает обнаруживать в нём проблемы, что особенно ценно для конвейеров с очень медленными циклами обратной связи.
Telegram-канал с полезностями и уютный чат