В предыдущей статье все закончилось на том, что мы выделили отдельные сервисы из единого монолита.

Проектируем обмен данными.

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

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


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

Попробуем посмотреть на нашем примере.

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

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

Проблемы при построении микросервисов.

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

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

Проблема масштабирования исторических данных.

Ярким примером могут быть процессы, в работе которых есть историчность данных.
Небольшой пример из жизни: есть некоторое хранилище, содержащее интересующую нас таблицу на 8 миллионов записей. Раз в сутки данные в этой таблице обновляются и так же раз в сутки нам надо их получить и обработать. Сходить просто так напрямую в это хранилище мы не можем, поэтому взаимодействие с данными происходит через цепочку прокси сервисов. То есть раз в сутки запускается определенная задача, которая вычитывает данные из этой таблицы и кидает в Kafka. В начале кидает сообщение о том, что данные начались и в конце, что данные закончились. Кажется все хорошо, но этой прокси цепочкой хотят пользоваться многие и поэтому в ней возникает несколько задач на вычитывание разных таблиц. Так как данных должно будет передаваться достаточно много, то топик логично сделать партиционированным. Все выглядит хорошо: данные равномерно размазываются, нагрузка равномерно распределяется. Но как их теперь читать? Если сделать равномерное размазывание данных по партициям, то неизбежно возникает проблема, что можно прочитать сообщение об окончании данных раньше, чем последний кусок данных. Это может произойти, если начать читать несколькими потребителями.

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

  • Надо ли отправить специальное сообщение в очередь?

  • Надо ли продолжать получать следующую страницу?

  • Что делать тем, кто прочтет сообщение об ошибке из очереди?



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

Еще более красочный пример - история платежных транзакций. В этом случае сохранение историчности принципиально важно. Перестановка сообщений местами может запретить вполне легальную транзакцию. Думаю, любой понимает ситуацию, когда на счете есть 10 рублей, идут транзакции списать 5 рублей, потом начислить 2 рубля и потом списать 6 рублей. В итоге состояние счета 1 рубль. А вот если переставить местами 2 последних транзакции, то все разрушится. Достаточно ясно можно видеть, что в данном случае должен быть один топик с одной партицией и тогда Kafka обеспечит четкую очередность сообщений. Но вот вопрос, увеличилось количество сообщений как в данном случае масштабировать систему под нагрузку? Тут так же нет однозначного ответа. В общем случае масштабировать вертикально (увеличивать мощность сервера, ядра, память и прочее). Либо разводить транзакции по партициям на основе каких-то знаний о клиенте.
Тут необходимо понимать, что большинство решений будет предназначено для общих случаев. Но конкретно для вашей задачи его придется адаптировать под условия в которых находитесь именно вы.

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

  1. Является ли он решением всех проблем разработки? Нет.

  2. Стоит ли его попробовать применить в своей работе? Да. Но не забывать про вопрос номер один.

Комментарии (2)


  1. LeshaRB
    25.07.2022 12:54
    +1

    К предыдущей статье доступ закрыт...