Варианты синхронизации данных между БД разных микросервисов?
Пусть имеем абстрактный проект на микросервисах.
Сервис А хранит список юзеров (id, ФИО), сервис Б обрабатывает транзакции и хранит список транзакций с привязкой к user_id.
БД (PostgreSQL) микросервисов разные (разные схемы, сервер может быть как один, так и разные)
Еще есть вебинтерфейс, который показывает список транзакций в табличном виде (id, user_id, ФИО, ...) с пагинацией и поиском по ФИО. Список формирует сервис Б, которому для этого нужна информация о ФИО.
Глобально вариантов решения я смог придумать фактически 3:
1. При создании транзакции в таблице делаем поле ФИО и туда сохраняем текущее значение. Его и используем в дальнейшем отобрадении и поиске.
Плюсы: дернуть сервис А во время создания транзакции просто и быстро.
Минусы: пример абстрактный, это может быть не ФИО, а что-то другое, что в принципе может менять значение в системе (да и ФИО может измениться), и если оно изменится в БД сервиса А, то должно измениться и при отображении транзакций.
2. Каким-то образом таблицу user из А забираем в БД сервиса Б (только нужные нам поля).
Плюсы: Банальным JOIN двух таблиц получаем всю нужную информацию. Возможность поиска по ФИО.
Минусы: в зависимости от реализации разные и будут рассмотрены отдельно.
3. На каждый запрос напрягаем сервис А через API, запрашиваем ФИО по каждому пользователю, попавшему в выборку, результат, возможно, помещаем в кеш.
Плюсы: У нас всегда актуальная информация о ФИО юзера.
Минусы: Сильно напрягаем сервис А. Что делать с поиском? Делать отдельный эндпоинт в API, который по строке будет возвращать список подходящих user_id? Выглядит монструозно и долго.
При взвешивании этих всех вариантов, выбор пал на вариант 2. Синхронизация данных нужных таблиц в нужные сервисы.
Первый вариант хорош для систем, где исходные данные никогда не меняются или на момент создания записи в БД нам нужно сохранить текущие ФИО и другие данные и они не должны никогда меняться в дальнейшем. Но это редкое явление и не подходит для большинства операций в нашем абстрактном проекте.
Поэтому вариант 2.
Нам каким-то образом необходимо передать кучу данных (миллионы записей) из сервиса А в сервис Б и поддерживать данные в таблице сервиса Б актуальными. К тому же надо придумать механизм первоначальной синхронизации для сервиса В, который будет развернут когда-нибудь потом, но ему тоже нужен будет список юзеров.
Варианты решения:
1. Самое простое - это сделать view через dblink. Сработает как в пределах одного сервера, так и если серверов несколько.
Плюсы: отсутствует избыточное хранение данных, мы можем пользоваться JOIN, актуальность данных поддерживается механизмами СУБД. Нет никаких затрат на первоначальную синхронизацию.
Минусы: Завязаны жестко на структуру таблицы-донора (поле не переименовать, тип данных не поменять. Такое редко когда нужно, но все же). Не сработает для СУБД разных типов. При перемещении БД с таблицами-донорами на другой сервер вызовет необходимость перестраивать все view.
Тщательное гугление в интернетах не нашло даже упоминания такого подхода, не говоря уже о реализациях. Может плохо гуглил?
2. Синхронизация данных через шину данных, типа RabbitMQ, Kafka.
Плюсы: полная независимость от типа СУБД.
Минусы: Сама СУБД в шину данных данные писать не умеет, а значит это должен делать сам сервис. Аналогично и с приемом данных из шины - сервис должен слушать шину и нужные ему данные сохранять в таблицу в своей БД. Это достаточно много когда, часть которого можно вынести в библиотеки, но все-равно он достаточно специфичен из проекта в проект.
Сейчас в нашем реальном проекте используется именно такой подход.
В сервисах на сохранение сущностей в таблицу стоят обработчики и сохраненные данные отправляются в RabbitMQ. У каждого сервиса/сущности есть свой уникальный route_key, на который сервисы могут подписываться, и к ним в очередь будут поступать нужные им данные, которые уже сам сервис должен сохранять в таблицу.
Так же предусмотрен и способ первоначальной синхронизации. Сервис при старте (при необходимости), кидает запрос в RabbitMQ в определеном формате, где указывается имя сервиса, имя сущности и имя очереди. Соответствующий сервис отправляет всю таблицу в указанную очередь.
И в целом это все работает хорошо, стабильно и в процессе эксплуатации (уже порядка 2-х лет) никаких проблем не вызывало.
НО!
Таблицы подросли и первоначальная синхронизация занимает уже больше часа (что весьма долго). Причем она весьма значительно нагружает как кролика, так и БД (да и сам сервис жрет CPU как не в себя).
Поэтому главные вопросы:
А можно ли лучше?
Как такие вопросы решаются в других проектах?
Возможно существуют какие-то готовые опенсурс системы синхронизации данных между БД?
Использую Event Driven Architecture и при таком подходе будет.
А. Сервис "Пользователей," со всей необходимой информацией по пользователю и со всеми действиями касающимися пользователя. Тут информация по пользователю - выступает "Источником правды".
В. Сервис "Транзакций", который содержит всю информацию по транзакциям и минимальную иформацию по пользователям. Может просто хранить только user id (для проверки целостности данных)
С. Сервис "Поиска". Хранит всю информацию необходимую для поисковых запросов пользователя. Может содержать только Elastic или что-то подобное и не иметь реляционной или обьектной БД.
D. Сервис "Message broker"
Сервис "А" по любому событию над пользователем (CRUD) посылает всем подписанным сервисам, через "D" собщения об изменении информации по пользователю. Сервисы "В" из сообщений берут нужный минимум информации из сообщений и сохраняют у себя. Сервис "С" агрегирует всю нужную информацию для пользовательских запросов. Все подписанные на "D" сервисы поддерживают дублирование сообщений.
При возникновении проблем сервис являющийся "Источником правды" просто заново отсылает все сообщения с данными, а подписанные сервисы обновляют свое состояние.
Ну в целом все тоже самое, только у вас еще и сервис поиска (зачем?), в котором кроме списка пользователей, видимо, еще придется продублировать и список транзакций.
В сервисе транзакций продублирован список пользователей (как минимум id), а значит сервису при первом старте все-равно надо получить все миллионы записей.
В сервисе поиска надо не только миллионы пользователей продублировать, но еще и сотни миллионов транзакций.
Т.е. подход не решает основную проблему дублирования данных (что в целом вроде как и не проблема), так же не решает проблему долгой начальной синхронизации (это вроде тоже не проблема, но хочется лучше)
Heggi, Сервис поиска для сложных поисковых запросов и поиск по агрегированным данным.
Пример: Сервис "пользовательских действий" (лайки, просмотры, комментарии) и сервис "статей", а посковый сервис уже просто отдает статьи из индекса, плюс содержит количество лайков, просмотров на статью. Детальная иформация остается только в сервисе "пользовательских действий" .
"еще придется продублировать и список транзакций." для чего? Пользователям реально требуются к показу эти миллионы?
"так же не решает проблему долгой начальной синхронизации" так это начальная синхроницация, она розовая и она может быть отделена на время от всей системы. И при постоянной работе больше синхронизация не нужна. И снова нужны ли тут эти миллионы транзакций?
Петр, Если нам нужно искать по транзакциям (например фильтрануть по дате, по сумме, по пользователю), то как иначе? В каком-то, возможно урезанном виде, но придется тащить эти транзакции в сервис поиска.
Плюс это не решает вопрос подробного отображения данных по транзакции (а там свыше 20 полей, 5 из которых - id на справочники - это тоже все тащить в сервис поиска?).
Я так не заморачивался, у меня сервис транзакций одновременно выступает и сервисом поиска. Благо никакого хитрого поиска с морфологией бизнесу не нужно (Elastik не нужОн) и постгресс вполне справляется.
Я же ищу варианты как не дублировать данные в принципе, или минимизировать это.
Но судя по всему никак.
Heggi, У вас взаимодействие пользователя с сервисами должно идти через Gateway, а он может иметь коннект сразу и к сервису "Поиска" и сервису "Транзакций". Так что детализацию получить не проблема без дублирования.
Использование Gateway в качестве "объединятора" данных с разных сервисов в один ответ мне кажется вообще антипаттерном.
Т.е. у нас условно идет запрос по данным транзакции, ее надо обогатить сторонними данными типа ФИО, это значит надо будет еще дернуть сервис пользователей. А если таких сторонних поставщиков много - то ради одного запроса напрягаем 100500 сервисов. Ну такое.
У нас вообще gateway нет. Был, но убрали, т.к. смысла в нем отсутствует, только усложняет разработку и развертывание.
Проверять роли? Каждый сервис у нас и так проверяет роли, т.к. у нас не только по ролям ограничения, но еще и по подразделениям и группам. Кто лучше сервиса транзакций знает к какой группе и подразделению относится конкретная транзакция? Никто. Соответственно ему и проверять доступ.
Объединять несколько запросов в один? Смысл? Фронту проще сделать несколько запросов. Про обогащение уже писал.
Heggi, "Использование Gateway в качестве "объединятора" данных" - об этом не говорилось. Конечное разные запросы и никакой логики по обьединению в gateway нет.
"Проверять роли? Каждый сервис у нас и так проверяет роли". Gateway проверяет права доступа к данным. А сервис только отдает нужные данные в соответсвии с условиями, в том числе с условиями по доступу (группа, подразделение и т.д.). Еще gateway отсекает важные сервисы от внешнего мира, чтобы у вас не торчали сервисы наружу.
"Объединять несколько запросов в один? Смысл? Фронту проще сделать несколько запросов. Про обогащение уже писал."
Об этом не говорилось. Каждый сервис выполняет свои запросы, никаких обьединений данных от разных сервисов gateway не делает. Если нужны обьединения, то это доп.сервис "Поисковый", который содержит нужные копии данных.
Петр,
"Gateway проверяет права доступа к данным."
Вот тут я логику немного не понимаю. Есть две ситуации:
1. Пользователь запрашивает последние 30 транзакций. Сервис транзакций отдает список, фильтранув его по доступным группам/подразделениям. Если фильтрацию отдать на сторону Gateway, то получим не 30 записей, а меньше. Следовательно сервис транзакций так или иначе должен обрабатывать содержимое JWT-токена или сходить в сервис авторизации и запросить что этому пользователю позволено.
2. Пользователь запрашивает транзакию с id=7152522. Откуда сервис gateway узнает можно ли этому пользователю получить данные по этой транзакции? Только запросив сервис транзакций. Т.е. запрос в любом случае уйдет в сервис транзакций и будет запрос в БД. Так сервис транзакций у нас уже обрабатывает JWT токен (чтобы корректно работать для ситуации 1), а значит права доступа можно проверить в самом сервисе транзакций.
Зачем тут gateway?
Heggi, JWT по факту должен содержать только UserID.
Gateway на себя берет валидацию токена, авторизацию пользователя и роутинг запросов к сервисам.
GW для админки будет иметь одни роутинги, для Dashboard другие.
Для запроса "Пользователь запрашивает транзакцию с id=7152522" GW проверяет имеет ли пользователь права на это действие и просто отправляет запрос в сервис "Транзакций" или "Поиска". Зависит от бизнес логики проекта (В GW нет БЛ).
"Если фильтрацию отдать на сторону Gateway, то получим не 30 записей"
GW не должен содержать бизнес логики, я тут выше описал для чего он.
"Следовательно сервис транзакций так или иначе должен обрабатывать содержимое JWT-токена или сходить в сервис авторизации и запросить что этому пользователю позволено." Сервис транзакций не должен знать об наличии сервиса "Авторизации". GW имеет прямой доступ к сервису Авторизации для получения "разрешений". А информация об группах и подразделениях приходит только через Message Broker.
Петр,
JWT может содержать что угодно, лишь бы не дергать сервис авторизации лишний раз (зачем его дергать, если в токене будет всё? Минус одна зависимость в пайлайне обработки запроса)
У нас в токене есть список пермишенов (пусть для просмотра транзакции у нас пермишн trans_view), список групп и подразделений к которым пользователь имеет доступ.
Впрочем не суть, в токене хранятся эти данные или gateway на каждый запрос лазит в сервис авторизации.
Gateway проверяет, что в токене есть пермишн trans_view, а дальше что? Как ограничить доступ к конкретной транзакции по принадлежности к группе и подразделению?
Heggi, "JWT может содержать что угодно, лишь бы не дергать сервис авторизации лишний раз (зачем его дергать, если в токене будет всё? Минус одна зависимость в пайлайне обработки запроса)"
Может. Но и GW может кешировать нужные данные по пользователю. Он может оперативно реагировать на состояние пользователя и оперативно блокировать токен или чать пользовательских пермишенов.
Поместив все в токен, вы увеличиваете сильно его размер и тем увеличивает лишний трафик, ведь большинство данных в токене может никогда и не пригодиться. Не можете оперативно прекратить время жизни токена.
"в токене хранятся эти данные или gateway на каждый запрос лазит в сервис авторизации" GW проводит валидацию токена и проверяет пользовательские пермишены. Зачем ему ваши группы и подразделения? Это все будет в сервисе.
" Как ограничить доступ к конкретной транзакции по принадлежности к группе и подразделению?" это уровень сервиса "Транзакций". Т.е. банальный фильтр.
EDA подразумевает, что описание групп, подразделений, правила на их CRUD будут в сервисе "Авторизаций", он источник правды по этим данным. А сервис "Транзакций" подписан на обновление состояний данных обьектов и просто обновляет минимальную копию этих данных, нужных для правильнй фильтрации ваших запросов. Для примера наменование группы/подразделений хранить не нужно если эти обьекты используются только для фильтрации и никогда не отдаются в GW