Как денормализация данных работает с шаблоном Microservice?

Я только что прочитал статью о Microservices и PaaS Architecture. В этой статье около трети пути вниз автор утверждает (под Denormalize like Crazy):

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

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

Итак, я спрашиваю: Учитывая базу данных, которая полностью состоит из связанных таблиц, как ее можно денормализовать на более мелкие фрагменты (группы таблиц), чтобы фрагменты могли управляться отдельными микросервисами?

Например, учитывая следующую (довольно небольшую, но примерную) базу данных:

[users] table
=============
user_id
user_first_name
user_last_name
user_email

[products] table
================
product_id
product_name
product_description
product_unit_price

[orders] table
==============
order_id
order_datetime
user_id

[products_x_orders] table (for line items in the order)
=======================================================
products_x_orders_id
product_id
order_id
quantity_ordered

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

  • UserService - для пользователей CRUDding в системе; должен в конечном итоге управлять таблицей [users]; и
  • ProductService - для продуктов CRUDding в системе; должен в конечном счете управлять таблицей [products]; и
  • OrderService - для заказов CRUDding в системе; должен в конечном итоге управлять таблицами [orders] и [products_x_orders]

Однако все эти таблицы имеют отношения внешнего ключа друг с другом. Если мы денормализуем их и относимся к ним как к монолитам, они теряют все свое смысловое значение:

[users] table
=============
user_id
user_first_name
user_last_name
user_email

[products] table
================
product_id
product_name
product_description
product_unit_price

[orders] table
==============
order_id
order_datetime

[products_x_orders] table (for line items in the order)
=======================================================
products_x_orders_id
quantity_ordered

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

Итак, эта статья типична для академического hullabaloo, или есть реальная практичность в этом денормализационном подходе, и если да, то каково это (бонусные баллы за использование моего примера в ответе)?

Ответ 1

Это субъективно, но для меня, моей команды и нашей команды БД работало следующее решение.

  • На уровне приложения Microservices разлагаются на семантическую функцию.
    • например. Служба Contact может иметь контакты CRUD (метаданные о контактах: имена, номера телефонов, контактные данные и т.д.).
    • например. Служба User может использовать CRUD-пользователей с учетными данными для входа, ролями авторизации и т.д.
    • например. Служба Payment может выполнять платежи CRUD и работать под капотом с помощью стороннего PCI-совместимого сервиса, такого как Stripe и т.д.
  • На уровне DB таблицы могут быть организованы, однако разработчики /DB/devops хотят, чтобы организованные таблицы

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

interface PaymentService {
    PaymentInfo makePayment(User user, Payment payment);
}

Создайте его так:

interface PaymentService {
    PaymentInfo makePayment(Long userId, Payment payment);
}

Таким образом, объекты, принадлежащие другим микросервисам, ссылаются только на конкретную службу по идентификатору, а не по ссылке на объект. Это позволяет таблицам DB иметь внешние ключи повсюду, но на уровне приложения "внешние" объекты (то есть объекты, живущие в других службах) доступны через ID. Это останавливает каскадирование объекта от выхолащивания и четкое разграничение границ обслуживания.

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

User user = paymentService.getUserForPayment(payment);

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

Long userId = paymentService.getPayment(payment).getUserId();
User user = userService.getUserById(userId);

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

Ответ 2

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


Чтобы реорганизовать это на микросервисы, мы можем использовать несколько стратегий:

Api Join

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

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

Только для чтения

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

Высокопроизводительное чтение

Достижение высокой производительности можно получить, введя такие решения, как redis/memcached поверх решения read only view. Обе стороны соединения должны быть скопированы в плоскую структуру, оптимизированную для чтения. Вы можете ввести совершенно новую микросервисную систему без состояния, которая может использоваться для чтения из этого хранилища. Хотя кажется, что много хлопот, стоит отметить, что он будет иметь более высокую производительность, чем монолитное решение поверх реляционной базы данных.


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

Ответ 3

Я понимаю, что это, возможно, не очень хороший ответ, но что за черт. Ваш вопрос:

Учитывая базу данных, которая полностью состоит из связанных таблиц, один денормализовать это на более мелкие фрагменты (группы таблиц)

WRT дизайн базы данных, я бы сказал , вы не можете без удаления внешних ключей.

То есть люди, которые подталкивают Microservices со строгим общим правилом БД, просят разработчиков баз данных отказаться от внешних ключей (и они делают это неявно или явно). Когда они явно не указывают на потерю FK, вы задумываетесь, действительно ли они знают и распознают значение внешних ключей (потому что это часто не упоминается вообще).

Я видел большие системы, разбитые на группы таблиц. В этих случаях может быть либо A), либо нет FK между группами или B) одна специальная группа, которая содержит "основные" таблицы, на которые FK может ссылаться на таблицы в других группах.

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

Для меня другая проблема, связанная с подходом Microservice к разделению БД, - это влияние, которое у него есть на отчетность, вопрос о том, как все данные объединяются для отчетности и/или загрузки в хранилище данных.

В некоторой степени связана и тенденция игнорировать встроенные функции репликации БД в пользу обмена сообщениями (и как репликация базовых таблиц на основе базы данных /DDD ) влияет на дизайн.

EDIT: (стоимость JOIN через вызовы REST) ​​

Когда мы разделяем БД, как это было предложено микросервисами и удаляем FK, мы не только теряем принудительное декларативное бизнес-правило (FK), но также теряем способность БД выполнять объединение (-и) через эти границы.

В значениях OLTP FK обычно не "UX Friendly", и мы часто хотим присоединиться к ним.

В примере, если мы получаем последние 100 заказов, мы, вероятно, не хотим показывать значения идентификатора клиента в UX. Вместо этого нам нужно сделать второй звонок клиенту, чтобы получить свое имя. Однако, если нам также нужны строки заказа, нам также нужно сделать еще один вызов службе продуктов, чтобы показать имя продукта, sku и т.д., А не идентификатор продукта.

В общем, мы можем обнаружить, что, разбивая дизайн БД таким образом, нам нужно сделать много вызовов "JOIN via REST" . Итак, какова относительная стоимость этого?

Фактическая история: пример затрат для "JOIN via REST" и DB Joins

Есть 4 микросервиса, и они задействуют много "JOIN via REST" . Базовая нагрузка для этих 4 сервисов составляет ~ 15 минут. Эти 4 микросервиса, преобразованные в 1 сервис с 4 модулями против общей БД (которая допускает объединения), выполняют одну и ту же нагрузку в ~ 20 секунд.

Это, к сожалению, не является прямым явлением для сравнения яблок для объединений БД против "JOIN via REST" , так как в этом случае мы также изменились с базы данных NoSQL на Postgres.

Неудивительно, что "JOIN via REST" работает относительно плохо по сравнению с БД, у которой есть оптимизатор с затратами и т.д.

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

Ответ 4

Я бы рассматривал каждую микросервис как объект, и, как и любой ORM, вы используете эти объекты для вытягивания данных, а затем создаете объединения внутри ваших коллекций кода и запросов, Microservices должны обрабатываться аналогичным образом. Разница только здесь будет заключаться в том, что каждый Microservice будет представлять один объект за раз, чем полное дерево объектов. Уровень API должен потреблять эти службы и моделировать данные таким образом, чтобы они были представлены или сохранены.

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

@ccit-spence, мне понравился подход служб пересечения, но как его можно спроектировать и использовать другими службами? Я считаю, что это создаст своего рода зависимость для других сервисов.

Любые комментарии, пожалуйста?