Курс Essential Architecture #Data

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

Рис. “Титульный слайд лекции”

План этой лекции достаточно насыщен и представлен ниже.

Рис. “План беседы”

Начнем мы с того, что вспомним про один из принципов 12 факторного приложения, а именно про принцип номер 6. Суть в том, чтобы делать stateless приложения, то есть не хранить состояние внутри него. Если получается так спроектировать приложение, то вопросы отказоустойчивости и масштабирования решаются гораздо проще, чем в случае stateful приложений.

Рис. “12 factor app и фактор про stateless приложение”

Представим, что мы именно так проектируем свои приложения, но возникает вопрос — а где тогда хранить состояние?

Рис. “А где хранить данные”

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

Рис. “Файловые системы”

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

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

Хранение информации в виде файлов и каталогов было всем хорошо, но было 2 проблемы:

  • формат хранения варьировался от задачи к задаче и требовалось писать отдельно логику того, как сложить данные и как их считать с устройства хранения — в итоге, часто смешивалась бизнес-логика приложения и логика работы с data layer
  • приложения становились многопользовательскими, поэтому было сложно реализовывать параллельную работу с данными внутри файлов

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

Рис. “Реляционные базы данных”

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

  • отношение — таблица
  • кортеж — строка
  • атрибут — колонка
Рис. “Реляционные базы данных — introduction”

Давайте отдельно обсудим основные характеристики реляционных баз данных, которые сделали их такими популярными в использовании. И начнем мы с нормализации, которая влияет напрямую на проектирование моделей предметной области. Суть в том, чтобы таблицы (отношения), спроектированные в рамках реляционной модели соответствовали определенным критериям, которые называются нормальными формами. На рисунке ниже они представлены снизу вверх по мере ужесточения “критериев нормальности”:)

Рис. “Реляционные базы данных — нормализация”

Интересно для примера рассмотреть первую нормальную форму ( 1NF), определение которой звучит так

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

В итоге, если мы начинаем складывать в атрибуты целые json-документы, то мы тем самым нарушаем уже первую нормальную форму. Теперь стоит пойти ко второй характеристики, а именно к SQL.

Рис. “Реляционные базы данных — SQL”

Реляционные базы данных принесли с собой декларативный язык программирования SQL, который разделил вопросы “что” и “как”. Это был большой шаг вперед по сравнению с файлами, где эти вопросы часто были смешаны. SQL позволял прикладным программистам написать запрос, в котором они указывали какие данные им нужны, а дальше его выполнение было за СУБД (системой управления базами данных). Если интересно узнать про внутренности баз данных, то рекомендую почитать книгу “Database Internals”, в которой есть мясо для техногиков:) А мы идем дальше в сторону того, что позволяет СУБД быстро выбирать данные… правда, не все, а те, которым помогают индексы.

Рис. “Реляционные базы данных — индексы”

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

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

Рис. “Реляционные базы данных — транзакции”

Транзакции — это группа последовательных операций с базой данных, которые логически атомарны. Но интересно не просто определение транзакций, а гарантии (ACID), которые дают СУБД и почему эти гарантии так нравятся прикладным разработчикам.

Рис. “Реляционные базы данных — ACID — atomicity”

Первое свойство транзакций atomicity прямо следует из определения транзакций. Второе свойство consistency более интересно.

Рис. “Реляционные базы данных — ACID — consistency”

По-факту, СУБД обещает поддерживать не абсолютную целостность модели предметной области, а то, что она контролирует на своем уровне — а именно foreign keys и constraints.

Следующее свойство isolation позволяет организовать относительно просто многопользовательскую работу с данными.

Рис. “Реляционные базы данных — ACID — isolation”

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

Последний элемент акронима ACID относится к durability. Это свойство транзакций позволяет клиентам, получив подтверждение о прохождении транзакции, дальше не беспокоиться о сбое или отказе. Теоретически СУБД после восстановления от сбоя сможет восстановить данные, даже если они еще не были полностью обработаны — главное, что уже прошел fsync и они оказались на диске в write ahead log (как делается в Postgres).

Рис. “Реляционные базы данных — ACID —durability”

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

Рис. “Реляционные базы данных — replication”

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

Например, мы можем настроить primarysecondary репликацию для повышения отказоустойчивости нашей схемы и для масштабирования потока чтений. В первом случае мы можем во время сбоя на primary ноде нашей базы данных выбрать одну из secondary нод и промотировать ее до главной. Эту возможность надо настраивать очень аккуратно. Если это сделать не слишком хорошо, то понять поведение нашей системы во время моргания сети между нодами нашей primarysecondary инсталляции будет достаточно нетривиальной задачей. Второй вариант с масштабированием потока чтений тоже имеет свои особенности — важно, чтобы приложение умело читать и писать данные в разные инстансы. Еще есть особенность с тем, что репликации бывают синхронные и асинхронные. Первые сильно замедляют запись, так как мы ждем подтверждения от всех реплик, а вторые — просто немного отстают от primary ноды и асинхронно ее догоняют. В случае асинхронной репликации нам надо сделать так, чтобы чтения только что записанных данных были минимизированы и четко определены — их нам придется отправлять на primary ноду.

Последний пункт повестки — это обсуждение шардирования данных.

Рис. “Реляционные базы данных —sharding”

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

Рис. “OLAP”

OLAP ( online analytical processing) — технология обработки данных, заключающаяся в подготовке суммарной (агрегированной) информации на основе больших массивов данных, структурированных по многомерному принципу. В 1993 году Кодд предложил 12 правил, которым должна удовлетворять система аналитической обработки по аналогии с 12 правилами для реляционных баз данных. В итоге, для OLTP (online transactional processing) нагрузок предполагалось использовать реляционные базы данных, а для аналитических и Business Intelligence задач — OLAP системы, которые используют OLAP-структуры, созданные из рабочих данных, называемые OLAP-кубами. Куб создаётся из соединения таблиц с применением схемы звезды или схемы снежинки. В центре схемы звезды находится таблица фактов, которая содержит ключевые факты, по которым делаются запросы. Множественные таблицы с измерениями присоединены к таблице фактов. OLAP-куб содержит базовые данные и информацию об измерениях (агрегаты).

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

А теперь перейдем к вопросам BigData и апостолу ее Hadoop.

Рис. “BigData”

Hadoop считается одной из основополагающих технологий «больших данных». Вокруг Hadoop образовалась целая экосистема из связанных проектов и технологий. Разработка стартовала в 2005 году. Он используется для реализации поисковых и контекстных механизмов многих высоконагруженных веб-сайтов. Разработан на Java в рамках вычислительной парадигмы MapReduce, которую чуть раньше описали в своей статье специалисты Google. Согласно этой парадигме приложение разделяется на большое количество одинаковых элементарных заданий, выполнимых на узлах кластера и естественным образом сводимых в конечный результат. Одной из основных целей Hadoop изначально было обеспечение горизонтальной масштабируемости кластера посредством добавления недорогих узлов.

На самом деле эта экосистема очень интересна, но как и история с OLAP подходам она обычно нужна тем, кто решает много задач с большими данными, например, отгружая логи в HDFS, выбирая данные при помощи HBase или Hive и выполняя стриминг задачи при помощи Apache Spark. А в этой статье мы затронули BigData краем и пошли дальше к Object Storage.

Рис. “Объектное хранилище”

Объектное хранилище — это способ хранения данных без иерархии, который обычно используется в облачной среде. В отличие от остальных способов хранения данных, объектное хранилище не использует дерево каталогов. Отдельные единицы данных (объекты) сосуществуют в пуле данных на одном уровне. Каждый объект имеет уникальный идентификатор, используемый приложением для обращения к нему. Кроме того, каждый объект может содержать метаданные, получаемые вместе с ним. Самое примечательное хранилище, API которого стало стандартом де–факто, было запущено AWS в 2006 и называлось Simple Storage Service или S3. Дальше объектные хранилища стали стандартным кубиком для построения систем и почти все из них обладают S3-совместим интерфейсом.

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

Рис. “NoSQL”

Всего мы рассмотрим 4 варианта NoSQL решений:

  • ключ-значение (key-value)
  • семейство столбцов (column-family)
  • документо-ориентированная (document-oriented)
  • графовая (graph)

Все они представлены ниже

Теперь, когда мы знаем какие NoSQL решения бывают, стоит поговорить о тех гарантиях, которые они обеспечивают. И эти гарантии далеко не ACID, а BASE. Но, перед тем как перейти к их рассмотрению, стоит рассмотреть CAP-теорему, предложенную Эриком Брюером, которая говорит про связь согласованности, доступности и устойчивости к разделению в распределенных системах.

Рис. “NoSQL — CAP теорема и BASE”

После рассмотрения CAP-теоремы, мы готовы обсудить гарантии BASE, которые говорят о следующем.

Рис. “NoSQL—BASE”

Теперь, зная гарантии ACID реляционных баз данных, гарантии BASE NoSQL решений и CAP-теорему, мы можем подумать о том, как нам правильно применять NoSQL решение в нашем конкретном случае.

Рис. “NoSQL — BASE — применение с учетом гарантий”

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

Рис. “Как обмениваться данными”

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

Рис. “Варианты интеграции — file transfer”

Следующий вариант — это общая база данных (shared database). Этот вариант был достаточно популярен в прошлом и до сих пор существует в legacy системах. Его проблема в том, что в общей базе данных нет нормальных интерфейсов и инкапсуляции логики за ними (если не считать поддержку views). В итоге, у нас возникает high coupling между частями системы, что приводит к феерическому переплетению систем и общему замедлению разработки до полной остановки. Не даром, что в современных подходах проповедуют концепции shared nothing между сервисами, имея в виду как раз базы данных

Рис. “Варианты интеграции — shared database”

Следующие два варианта интеграции, remote procedure call (PRC) и messaging, которые мы рассмотрим в следующей лекции, так как они больше относятся к проектированию распределенных систем.

Рис. “Варианты интеграции — все виды”

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

Рис. “А что по аналитике использования продукта”

Для этого есть достаточно давно педалируемый подход с отгрузкой данных из operational data plane в data lake на благо дата сатанистам и аналитикам по данным. Обычно это делают data инженеры, которые берут данные прямо из oltp систем и настраивают etl процессы.

Рис. “Отчетность и аналитика (Data Lake)”

У этого подхода есть проблемы с тем, что мы опять получаем high coupling между системами, которые используют oltp базы и нашими витринами, которые работают поверх data lake. Суть в том, что любые доработки внутри сервисов потенциально могут повлиять на качество отгружаемых через etl процессы данных. В крупных компаниях контролировать такое очень сложно.

Но на помощь приходит новый подход, называемый Data Mesh. Основные принципы данного подхода представлены на слайде ниже.

Рис. “Отчетность и аналитика (Data Mesh) — принципы”

А вот в чем их суть

  1. Доменно-ориентированная децентрализация владения и архитектуры помогает экосистеме создания и потребления данных масштабироваться при увеличении числа источников данных, сценариев использования и увеличении многообразия способов доступа к данным — ноды сети становятся более автономными.
  2. Принцип подхода к данным как продукту (data products) позволяет потребителям данных просто найти, понять и использовать данные высокого качества, которые распределены по многим доменам предметной области.
  3. Self-service платформа для данных позволяет доменным командам создавать и потреблять данные (data products) автономно с использованием платформенных абстракций, скрывая сложность создания, наполнения и поддержки безопасности этих интероперабельных data products.
  4. Federated computational governance позволяет пользователям данные получить эффект от агрегации и корреляции независимых data products. Data Mesh выступает как экосистема, которая следует общим стандартам интероперабельности (interoperability), которые встроены в self service платформу.

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

Рис. “Как проектировать доменную модель”

И на этот вопрос нам дает ответ подход, называемый Domain Driven Design или просто DDD. Его основные моменты изложены ниже

Рис. “Domain Driven Design”

В итоге, мы пробежались галопом по Европам по темам

  • Где хранить состояние приложения
    - Файловые системы
    - Реляционные базы данных
    - Объектное хранилище
    - OLAP
    - BigData
    - NoSQL
  • Как обмениваться данными (варианты интеграции)
  • Как организовывать работу с аналитическими данными
    (Data Lake vs Data Mesh)
  • Как проектировать доменную модель

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

Источники

Director of digital ecosystem development department at Tinkoff. Bachelor at applied math, Master at system analysis, Postgraduate studies at economics.

Love podcasts or audiobooks? Learn on the go with our new app.