Замечание от Чеда: ? Привет, меня зовут Чед Сандерсон, я пишу о продуктах для работы с данными, контрактах данных, моделировании данных, а также о будущем инженерии данных и архитектур данных. Сегодняшняя статья — особенная. Дело в том, что в последнее время вспыхнула активная дискуссия о контрактах данных.

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

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

-Чед

Предисловие

Контракты данных — это основанные на API соглашения между Разработчиками, владеющими сервисами, и Потребителями Данных, понимающими, как устроен бизнес. Контракты помогают генерировать хорошо смоделированные, высококачественные и авторитетные данные. Если впервые видите термин контракты данных и не вполне понимаете, чем они полезны — почитайте пост Rise of Data Contracts, в котором подробно объяснено, что они собой представляют, а потом переходите к этому руководству по их реализации.

Здесь рассмотрены контракты данных на основе объектов, реализованные на базе CDC. Рассказано, каково определение контракта, как он выполняется, и как для него предписывается схема.

Контракты, сервисы и захват изменения данных (CDC)

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

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

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

События уровня приложения — это неизменяемые события «из реального мира», а также их свойства, публикуемые на основе кода приложения. Код приложения служит «мгновенным снимком» реальности, который схватывается по итогам события (оно же «глагол»). Например, ’ canceled’ может быть значением свойства shipment_status, в таком случае событие shipment_cancelled будет выдаваться именно из той точки кода, в которой данное событие было записано.

Так может быть, «контракты данных» — это просто вычурный синоним «API»? Если коротко — да. Развёрнутый ответ заключается в том, что термин «контракты данных» помогает ввести следующую концепцию: должно существовать официальное соглашение между теми, кто производит данные, и теми, кто их потребляет, причём, ранее таких соглашений, в сущности, не было.

Разработчики ПО привыкли размышлять о том, как системы взаимодействуют с другими системами (через API), но редко продумывают в такой же степени те данные, которые производятся в их системах. Зачастую так бывает потому, что команды, занимающиеся приложениями, и команды, занимающиеся данными, работают по разные стороны от инфраструктуры ELT/ETL, и разработчики слабо представляют, как именно потребляются данные. С точки зрения производителей данных, платформа с данными — это непроницаемый чёрный ящик. С точки зрения потребителей данных — до производителей данных не достучаться, так как они работают на слишком высоком уровне, чтобы отдавать приоритет конкретному практическому случаю. Популяризуя термин «контракты данных», мы повышаем внимание к этой проблеме, пусть даже реализация и примерно согласуется с тем, что разработчики уже успели сделать на уровне своих API.

Захват изменения данных (CDC)

Контракты данных применимы как к объектам, так и к событиям, происходящим на уровне приложения. Тогда как концептуально контракты работают одинаково как с первыми, так и со вторыми, детали реализации в этих случаях отличаются. В основе реализации объектных контрактов — так называемый захват изменения данных, превратившийся в популярный метод синхронизации данных из продакшен-систем с данными из хранилища. Работа CDC заключается в перехвате всех изменений, происходящих в базе данных на уровне строк. Здесь открывается несколько важных свойств, благодаря которым этот механизм действительно оказывается наилучшей основой для объектных контрактов:

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

  • Если контракт реализован правильно, то гарантируется как минимум однократная доставка, поэтому вы никогда не потеряете обновления объекта. CDC предоставит полный аудиторский след всех изменений, накопившихся со временем.

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

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

Использовать события CDC напрямую — плохая инженерная практика

Требования

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

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

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

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

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

4. Контракты данных охватывают семантику. При проектировании API любое изменение поведения API считается коренным, даже, если сигнатура API при этом не меняется. Аналогично, если изменить базовые значения производимых данных, это должно нарушать контракт данных. Допустим, у вас есть объект, длина и ширина которого задаются в числовых полях. Если взять и просто пересчитать хранимые в них значения из дюймов в сантиметры, то такое изменение будет коренным. На практике это означает, что в контрактах кроме схемы должны содержаться и дополнительные метаданные, в том числе, описания, ограничения значений и так далее.

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

6. Контракты данных не должны тормозить итерации и в работе дата-сайентистов. Многие потоки задач в области исследования и анализа данных по определению являются экспериментальными и предполагают множество итераций. Зачастую вы не знаете, какие именно данные вам нужны, пока не сможете поэкспериментировать с уже имеющимися у вас данными. Доступ к сырым продакшен-данным (не охваченным контрактами) должен предоставляться в функционально ограниченной «песочнице», в которой удобно исследовать и прототипировать. Однако, старайтесь не толкать прототипы напрямую в продакшен. Вновь отмечу, что внедрение контрактов данных помогает замедлить накопление технического долга и предотвращает изоляцию знаний в пределах отдельных команд, что оказывает общий положительный эффект на ускорение итераций.

Реализация: определение, обязывание, выполнение и мониторинг

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

Определение контракта данных

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

Контракты для объектов выполняются при помощи устоявшихся опенсорсных проектов для сериализации и десериализации структурированных данных. Два наиболее известных таких проекта — Protocol Buffers (protobuf) от Google и Apache Avro. В рамках обоих предоставляется свой IDL (язык определения интерфейса), на котором можно описать схему событий (контракт) так, чтобы это не зависело от языка. Затем на основе этой информации генерируется код, необходимый для сериализации полезной нагрузки события с последующей публикацией в Kafka. В качестве примера ниже рассмотрен простой контракт заказа (Order), определённый при помощи protobuf:

В обоих проектах предусмотрены возможности дополнять определения контрактов метаданными. В частности, это означает, что контракт можно аннотировать информацией о владении и описательными метаданными или указывать ограничения значений в конкретных полях. Также в Protobuf поддерживаются пользовательские опции. В свою очередь, в спецификации Avro по поводу определения схемы сказано следующее: “Атрибуты, не определённые в этом документе, допускаются в виде метаданных, но они не должны влиять на формат сериализованных данных.”

Обязательное исполнение контрактов данных

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

Интеграционные тесты

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

Этот процесс опирается на Docker Compose (или другую систему оркестрации контейнеров, которой вы пользуетесь), которая позволяет вам поднять тестовый экземпляр, конвейер CDC, Kafka и реестр схем, а также задания для потоковой обработки, соответствующие вашим контрактам по работе с объектами. Подробнее о таких заданиях мы поговорим в разделе о выполнении контрактов данных. Если применить к объекту задание потоковой обработки, то нужно убедиться, что объект реализован правильно. Также здесь предоставляется шанс сравнить схему, выдаваемую на выходе из задания (которая затем поступает в реестр тестовых схем), со схемой объектного контракта, которую вы определили выше. Поскольку этот процесс одинаков для всех объектных контрактов, тест автоматически проверит каждый из них. Это избавит вас от необходимости что-либо вручную обновлять в этих тестах при развитии контракта путём добавления или изменения данных.

Замечание: в зависимости от того, каким именно технологическим стеком вы пользуетесь, вы, возможно, захотите упростить вышеприведённую реализацию. Например, если вы используете базу данных, совместимую с JDBC, и kSQL, то можно пропустить этап подъёма Kafka + ваш конвейер CDC в Docker и просто выполнять запросы kSQL непосредственно к тестовой базе данных с использованием плагина JDBC Source Connector plugin

Совместимость схемы

На втором этапе воспользуемся схемой реестра от Confluent (Kafka). Как только мы убедимся, что код сервиса будет корректно выполнять определённые контракты, берём схемы объектных контрактов и пользуемся реестром схем, используемым в продакшене, чтобы проверять, не возникло ли обратно несовместимых изменений.

Поскольку такой подход к контрактам данных, где основное внимание уделяется продьюсеру (сначала обновляется продьюсер), то режим совместимости реестра схем нужно установить в FORWARD. Это может показаться нелогичным, но означает следующее: «данные, произведённые по новой схеме могут читать потребители, использующие последнюю актуальную схему, но при этом не смогут использовать все возможности новой схемы». Вот так затейливо можно сказать, что вы вправе добавлять новые поля и удалять опциональные поля. Здесь подробнее рассказано о режимах совместимости и об эволюции схемы.

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

Выполнение контрактов данных

Как упоминалось выше, напрямую использовать необработанные события CDC — плохо с инженерной точки зрения. Чтобы ввести дополнительный уровень абстракции между моделью данных сервиса и окончательным контрактом объекта, пользуемся потоковой обработкой. В процессе такой обработки удаётся брать и преобразовывать сырые события CDC из одной или нескольких таблиц сервиса так, чтобы они соответствовали контракту объекта. В сервисах определяются задания для потоковой обработки, слушающие собственные сырые CDC-события сервиса и преобразующие эти разрозненные события в одно событие обновления объекта, выполняющее условия контракта.

При использовании фреймворка для потоковой обработки с диалектом SQL (kSQL, Apache Flink, Materialize) разработчики могут обращаться с CDC-событиями как с таблицами из базы данных своего сервиса и писать простые инструкции на SQL, выходная схема которых и определяет объектный контракт данных. Как правило, такие задания скрывают внутренние детали, выбирая (SELECT) лишь некоторое подмножество столбцов из таблицы и объединяя (JOIN) потоки из множества нормализованных таблиц баз данных, используемых в продакшене. В результате удаётся представить объект в денормализованном виде.

Ранее я написал статью с глубоким разбором того, как механизм CDC работает в проекте Debezium, но просто кратко напомню: Debezium — это проект коннектора для Kafka Connect, который слушает лог транзакций, происходящих в базе данных и производит по событию на каждое изменение, происходящее на уровне строк. Развёрнутое таким образом задание для потоковой обработки потребляет CDC-события и выдаёт событие обновления объекта, поступающее в топик публичного контракта объекта. Всё это делается на основе SQL-инструкции, лежащей в основе обрабатывающего задания. Чтобы у работающих в продакшене продьюсеров не возникало прямых зависимостей от CDC-событий, кластер Kafka сегментируется на «публичные» топики контрактов, доступные всем потенциальным потребителям компании и «внутренние» CDC-топики, как правило, недоступные для других потребителей. Отнесение топиков к публичным или внутренним управляется на основе списков контроля доступа (ACL) в Kafka.

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

Мониторинг контрактов данных

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

Заключительные мысли

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

-Адрианк

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

Контракты данных – действительно нововведение культурного характера. Но и как любые инновации, повлиявшие на культуру, от непрерывной интеграции и доставки до электрокаров, любая простая в использовании технология, при помощи которой легче достигать желаемого результата, не может не вызывать интереса. В нашем случае, контракты – это поверхность, через которую можно начинать налаживать обсуждение важных вопросов, связанных с данными. Эти обсуждения позволяет увереннее судить, как именно будут использоваться внизу данные, поступающие сверху, подчёркивают важность данных и влияют на бизнес. Предоставляя «окно возможностей» для развития контрактов, начинающееся с минимально возможного стимула для разработчиков и доходящее до кульминации в виде полностью самостоятельных событий, обслуживающих аналитику, мы выстраиваем кривую созревания для контрактов данных. Чем дольше вы с ними работаете, тем осмысленнее и полезнее они становятся.

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

- Чед


Книгу, написанную авторами этой статьи, уже можно приобрести на нашем сайте.

При покупке бумажного издания на сайте piter.com электронная книга высылается в подарок на ваш e-mail.

Для Хаброжителей действует скидка 25% по промокоду — DATACONTRACTS

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