Итак, Вы – руководитель разработки (главный инженер, архитектор и т.п.) большой системы. После здравых размышлений Вы (обосновано) выбираете для системы микросервисную архитектуру. Далее Вы (и опять обоснованно) разделяете систему на микросервисы, продумываете API, рисуете стрелочки и диаграммы и можно программировать.
Можно? Наверное, но лучше сначала рассмотреть принципы для организации доступа к данным.
Принципы
Мы будем рассматривать и сравнивать между собой два принципа:
Данными владеет только владелец.
Локальность данных.
Данными владеет только владелец
Принцип «данными владеет только владелец» означает, что тот, кто создает данные, тот ими и владеет и если кто-то хочет эти данные получить, он должен вызвать владельца (например, через web API) и получить то, что ему нужно. По существу, это доведенное до предела правило «один источник правды». Это очень важное и ценное правило.
Данный принцип выглядит естественно, и позволяет строить красивые диаграммы микросервисной системы.
Однако, этот принцип в пределе приводит к построению хрупких систем. Представьте себе, что Ваш веб-сервер вызывает сервис А для получения необходимых данных. Сервис А владеет некоторой информацией (своей информацией), но, чтобы выполнить запрос BFF, ему надо информацию обогатить. Он делает запрос к сервису Б для обогащения. Цикл может повториться вовлечением сервиса В (Б -> В) и так далее. Это приводит к замедлениям из-за сетевых вызовов и хрупкости – достаточно выйти из строя сервису Ё, чтобы веб-сервер не получил свои данные и не смог выполнить запрос пользователя.
Но это крайне абсурдно, так как одним из преимуществ микросервисной архитектуры заявляется бОльшая, чем у монолита, устойчивость, которой здесь и не пахнет. Самое страшное то, что цепочки вызовов появляются не сразу, поначалу все цепочки короткие, но через N лет разработки вдруг неожиданно оказывается, что сервис Ё нужен всегда и не имеет права выйти из строя, так как это положит почти всю систему. А это уже свойство монолита…
Локальность данных
Как показано выше, принцип владения в абсолюте непрактичен. Принцип владения должен пониматься как принцип «новую правду генерирует только один компонент – ее владелец», но потреблять правду (копировать себе) имеют право все, кому эта правда нужна. Таким образом, источник правды генерирует её, сохраняет себе и публикует её где-то (например, в топике Kafka), а все потребители подписываются на топик и копируют (возможно, с фильтрацией и обработкой) к себе, например, в свои базы данных.
Если это мысленно довести до предела, никакого API в сервисе быть не должно, так как есть информация, подписывайся и обрабатывай. Контракт тут – структура json в топике, а не вызовы API.
Любое API создает риск того, что получится цепочка вызовов и соотв. цепочка отказа. Это особенно опасно, так как вызвать API гораздо проще, чем заморачиваться с подпиской и сохранением копии правды, Ваши программисты наверняка поддадутся этому соблазну, и Вы получите хрупкую систему. Нужен будет перманентный глаз-да-глаз за ними, на протяжении лет, что малопрактично.
Разумеется, на практике довести до предела эту концепцию невозможно, и API всё равно существуют вместе с «топиками». Такая гибридная схема (немножко API + топик с объявленным контрактом) намного лучше и гибче, чем чистый API без топика.
Дублирование кода
Представим себе, что у нас работает сервис по схеме «нет никакого API, только топик и контракт на него». Добавим в эту картину два других сервиса-потребителя (и две команды разработки оных), которые успешно подписаны на топик, сохраняют данные к себе и обрабатывают их каким-то образом. Допустим, что обработка в каждом потребителе многоэтапная, и, так получилось, что один или несколько этапов обработки одинаковый.
Зачастую, одинаковая обработка данных вполне обыденная ситуация, и для каждого вида информации существуют «естественные обработки». Тривиальный (но плохой) пример – фильтрация по какому-либо статусу или полю. Бывают и более «жирные» случаи, которые как раз нас и интересуют.
В наивном случае, эти одинаковые алгоритмы в разных сервисах будут реализованы независимо обеими командами. Тут кроется неэффективность: появляются двойные затраты на разработку одного и того же.
Выхода можно предложить два:
Давайте просто создадим API в сервисе-источнике, напишем там код один раз, и пусть обе команды вызывают этот API, получают обработанные данные и уже их складывают себе.
Давайте просто заведем второй топик в сервисе-источнике, куда будут складываться обработанные данные и пусть обе команды подписываются уже на него, вместо топика «сырой» правды.
Первый вариант возвращает риски и недостатки цепочки вызовов. Второй вариант получше, но, кто знает, не придется ли Вам наблюдать десятки топиков через N лет. Тут есть большой риск комбинаторного взрыва, когда одним потребителям нужна обработка А, вторым – обработка Б, третьим - обработка В, а четвертым, пятым и т.д. – смешанные обработки АБ, БВ, АВ и АБВ и все эти топики должны поставляться источником правды.
Также есть философская проблема дизайна: почему модифицируется поставщик, если появляется потребитель с новыми потребностями обработки? Может лучше модифицировать потребителя (и вернуться к риску дублирования кода)?
Ответ кроется в dev time.
Поставка данных и кода
Итак, мы имеем сервис-источник правды с топиком «правды». Мы имеем несколько потребителей, код которых содержит какую-то «естественную» обработку правды и эта обработка одна и та же в разных потребителях, но код написан свой, так как сервисы независимы. Также, наш контракт на топик уже менялся, то есть часть сообщений в топике в старом формате v1, а те, что посвежее – в формате v2. Оба потребителя вынуждены были независимо делать поддержку и v1 и v2. Что можно тут оптимизировать?
Идея заключается в том, что команда-разработчик сервиса «правды» должна не только создавать топик, публиковать контракт на него, но и распространять библиотеку (например, nuget пакет для .NET) для работы с этим топиком. Можно назвать эту библиотеку – SDK.
Что же можно упаковать в эту библиотеку:
Документацию на топик, контракт и любые другие описания.
Код, который позволяет подписаться на топик и сохранять данные из него в БД сервиса-потребителя.
Код, который знаком и умеет работать с разными версиями контракта данных (v1, v2…).
Общие (естес��венные) алгоритмы обработки информации из топика.
Как выглядит работа с точки зрения программиста команды-потребителя:
Осознана необходимость получить информацию («правду») из сервиса-источника.
Подключается библиотека общего кода от команды-источника, настраиваются реквизиты, задействуются необходимые естественные алгоритмы.
При запуске сервиса-потребителя, выполняется созданием структур в целевой БД, подписка на топик, и копирование информации в БД одновременно с обработкой естественными алгоритмами.
По мере выхода новых версий библиотеки он обновляется, чтобы уметь обрабатывать новые версии контракта.
Этот подход позволяет сэкономить:
На бойлерплейте типа «создать структуры в целевой БД», «подписаться», «скопировать в целевую БД».
На написании естественных алгоритмов обработки.
На когнитивной нагрузке команды-потребителя от версионирования контракта и на миграции структур в целевой БД по мере изменения контракта.
Окончательный вид
В окончательном виде Вы будете иметь сервис-источник правды в следующем виде:
Сам код сервиса, его логика, его БД.
Минимальное API сервиса (в пределе – отсутствующее).
Что-то, позволяющее подписываться на новую правду (топик Kafka, RabbitMQ, таблица в БД и т.п.). Здесь же – описание контракта данных.
Общая библиотека с описанием, подпиской, обработкой (версий) контракта и естественными алгоритмами обработки правды, которые могут быть задействованы потребителями.
Недостатки
Метод, разумеется, не идеальный. Сходу можно сформировать следующие недостатки:
Сложность. Вместо одного АПИ, появляются топики, библиотеки, естественные обработки (которые надо выявить) и т.п.
Лаги в процессе передачи данных. Пока в топик сохранится, пока прочитается, сохранится в целевую БД и станет доступна для бизнес-логики потребителя.
Сложность – это большой недостаток, и надо оценивать pro и cons. Что важнее – отсутствие цепочек вызовов (даже в отдаленном будущем) или простота системы?
Лаги тоже неприятная проблема, особенно, если учесть, что подписки, как и вызовы API, тоже выстраиваются в цепочки! Сервис А опубликовал данные, сервис Б принял их, обработал, обогатил, опубликовал, сервис В принял их и ситуация повторяется и т.п. Так стоила ли овчинка выделки? Вернемся к простому API?
Нет, тут другая задержка: не задержка обработки запроса пользователя, а задержка доступа до новых(!) данных. Это – очень разные вещи. Очень много систем на практике толерантны к задержке новых данных. При этом очень мало систем толерантны к тормозам и хрупкости в обработке запросов от пользователя.
В любом случае, в этой заметке описана идея, и цель статьи – просто навести Вас на размышления на эту тему. Вариантов не два, вариантов всегда бесконечно много и, возвращаясь к вопросу «можно программировать?», отвечаем – программировать можно, но лучше сначала заметку прочесть и обдумать.
Проектируйте сбалансировано, друзья.
