Означает ли использование этого важного шаблона проектирования, что мы не можем выполнять поиск в наших данных?

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

Ответ категорически отрицательный.

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

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

Вкратце, Агрегат - это группа связанных сущностей, рассматриваемая как единая атомарная единица. По определению Агрегат состоит из:

  • Некоторое количество связанных сущностей
  • Граница, которая четко определяет сущности, которые содержатся в Агрегате (а также те, которые не включены)
  • Единственная корневая сущность, которая является единственной сущностью в Агрегате, к которой можно получить прямой доступ из внешнего мира.

Одним из ярких примеров агрегирования может быть объект User. Хотя конкретные варианты использования могут отличаться, мы можем легко представить себе объектную модель, в которой у нас есть основной User объект (с такими полями, как FirstName, LastName, DateOfBirth , NationalId и т. д.). Мы можем прикреплять к этому объекту User наборы контактной информации, такие как Address, Phone или Email.

Очевидно, что сам объект User будет корнем User Aggregate. Никакой другой объект за пределами Агрегата не должен иметь доступ к Пользовательскому агрегату через какие-либо другие объекты в Агрегате.

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

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


Агрегаты и микросервисы

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

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

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

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

Обратите внимание, что в наших примерах мы будем изображать API-интерфейсы ReST в иллюстративных целях. Но те же принципы применимы к Thrift API, gRPC API и т. Д. База данных может быть СУБД, такой как MySQL, но с тем же успехом может быть хранилищем данных документов, например MongoDB.

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

  • предоставляет API GET, который принимает GUID заказа
  • извлекает агрегат заказа, представленный этим GUID
  • извлекает GUID покупателя и продавца (которые оба являются пользователями)
  • извлекает пользовательские агрегаты, представляющие покупателя и продавца
  • упаковывает все эти агрегаты в высокоуровневую модель деталей заказа и возвращает ее вызывающей стороне

Звучит здорово. Но как насчет запросов к нашим агрегатам?

Одно из требований импорта агрегатов, которое стоит повторить, заключается в том, что весь доступ к агрегатам должен осуществляться через корневой объект. Таким образом, любой URL-адрес запроса должен начинаться с чего-то вроде
/aggregate/{aggregate-identifier}
Таким образом, если мы предоставим API чтения для нашей службы данных пользователей, они будут выглядеть примерно так:

GET  /users/{guid}
GET  /users/{guid}/phones
GET  /users/{guid}/phones/{phoneId}

Это означает, что мы не можем предоставлять API, подобные следующим:

GET /users/phones/{phoneId}

Проницательные читатели могли догадаться, что мы также не можем предоставлять такие «поисковые» API, как следующие:

GET /users/[email protected]

На первый взгляд это выглядит большой проблемой.

Что, если нам d o нужно иметь возможность искать пользователей - не по их GUID, а по их адресам электронной почты?

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

В более широком смысле, что, если нам нужно найти any Aggregate по чему-то другому, кроме его GUID? Вряд ли это необычная потребность или необоснованная просьба. Но исключает ли это архитектура, ориентированная на агрегаты?

Решение: внедрить отдельную службу индексирования

Решение этой проблемы обманчиво простое. Мы реализуем отдельный сервис, который оптимизирован для индексации поисковых запросов. Все «поиски», то есть все запросы «GET», которые запрашивают что-либо, кроме экземпляра нашего агрегата, указанного его идентификатором, будут выполняться в отношении этой службы индексирования.

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

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

Чтобы поддержать это, мы представим службу индексирования заказов. Эта служба будет уведомлена службой данных заказов о любых дополнениях (или обновлениях, если мы разрешаем обновление существующих заказов). Затем он будет поддерживать индекс идентификаторов GUID покупателей и продавцов, которые сопоставляются с их связанными заказами. Затем служба оркестрации запросит службу индексирования заказов.

Что вернет Служба индексирования заказов, если она найдет соответствующие заказы? У нас есть несколько вариантов.

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

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

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

Резюме

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

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

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

Получите доступ к экспертному обзору - Подпишитесь на DDI Intel