Микросервисная архитектура популярна. Даже если речь идет о создании одного небольшого приложения, как правило его реализуют в виде пачки микросервисов, которые запущены отдельно и как-то реплицируются. Как они между собой будут взаимодействовать?
В этой статье поговорим о том, какие бывают способы общения в микросервисной среде. Расскажу на пальцах, какие обычно предъявляются требования к общению сервисов, почему большинство использует REST API, даже при том, что у него тоже хватает минусов, и при чем тут Kafka.
Рассчитываю на новичков, но если у вас есть интересный опыт в этих вопросах - добро пожаловать в комментарии.
Почему об этом вообще надо задумываться?
Мы разворачиваем сервисы в облаках. Как правило, это означает, что и общение микросервисов будет происходить по сети (разные экзотические и эзотерические способы мы в этой статье рассматривать не будем). А это тащит за собой сразу много проблем:
Проблема доступности. Мы не знаем, какой из микросервисов сейчас доступен для общения, а какой упал или потерял коннект.
Задержка передачи, потери, дублирование пакетов. Мы думаем, что отправили пакет, но получатель его не принял или получил в двойном экземпляре, и нам об этом неизвестно.
Хорошо иллюстрирует проблему так называемая задача двух генералов.
Представьте, что вы - генерал на поле боя. Второй генерал (союзник) отрезан от вас вражеской армией, но нужно координировать с ним начало атаки. Вы посылаете гонца. Он может спокойно доехать и передать сообщение или же погибнуть по дороге. Причем, его могут убить по пути обратно, так что он не сможет сообщить, что сообщение на самом деле доставлено. Можно ли в этой ситуации обеспечить себе уверенность в доставке или недоставке сообщения?
В общем случае - когда есть два сервиса, разделенных сетью - эта задача нерешаема
Нагрузка на трафик и память. Сервисы бывают нагруженные, поэтому общение приходится оптимизировать. От этого все становится сложнее. Если использовать какие-то асинхронные системы общения, придется хранить информацию какое-то время, а значит появляется вопрос к утилизации памяти или диска.
При этом хочется, чтобы общение было отказоустойчивым. Часто бывает, что сервисы падают каскадом - один упал, запросы не обрабатывает. Вслед за ним валятся другие сервисы, которые его вызывали - все от того, что они не могут получить ответ на свой запрос. Так по цепочке происходит полный коллапс.
А еще все это общение надо как-то поддерживать. Реализовать API один раз - не сложно, с этим справится каждый разработчик. А вот создать API так, чтобы было удобно вносить изменения, - это хитрая задача.
Зоопарк решений
С учетом противоречивых требований, описанных в прошлом разделе, нам доступен целый зоопарк разнородных решений. В разных комбинациях они дают ответы на перечисленные выше вопросы. При этом идеального варианта не существует.
Синхронные способы общения - мы делаем вызов и ждем получения ответа:
Синхронный REST-like и аналоги. В чистом виде REST встречается редко, но в целом он один из самых популярных. При желании через костыли его можно сделать “асинхронным”, но этот случай мы тут не будем рассматривать.
gRPC - RPC на бинарном формате сообщений поверх HTTP/2 от Google.
SOAP - RPC с форматом XML. Это решение очень любили использовать в энтерпрайзе, оно чаще встречается в более старых системах.
Асинхронные способы общения - мы отправляем сообщение, а ответ придет когда-нибудь потом или он в принципе не предусмотрен:
Месседжинг - RabbitMQ, ZeroMQ, ActiveMQ. Они все разные, и об этом мы далее поговорим.
Стриминг - Kafka. В принципе, Kafka похожа на мессендинговую платформу, но я выделил ее отдельно, т.к. отличия все-таки есть.
Ниже я пройдусь по основным инструментам, с которыми лично сталкивался в работе. Приглашаю продвинутых читателей в комментариях дополнить этот опыт.
REST API
По моим ощущениям, 90% сервисов работает на REST API. Это отправная точка, с которой все начинают. Его так любят, потому что:
Нужен минимум библиотек как клиенту, так и серверу. HTTP у нас и так работает, в JSON можно писать / читать string.
Легко вызвать API с фронта, легко прочитать ответ сервера из JS с помощью JSON.parse().
Легкий старт для потребления API - узнал endpoint, посмотрел пример запроса / ответа и можно начинать общение.
Текстовый формат - легко дебажить и логировать, видя, что у него внутри в сообщении.
Админы любят - можно заглядывать в данные HTTP-запроса и что-то с ними делать, например настраивать роутинг трафика на основе данных http-запросов (L7) или балансировку нагрузки по полю ID из запроса.
Легкая для понимания синхронная парадигма “запрос-ответ”. Здесь нет подводных камней. Ответ либо получен тут же, либо нет.
Но во всей этой простоте есть свои проблемы. Каждая из особенностей REST API, которая делает его столь простым и популярным, на самом деле имеет оборотную сторону:
Синхронный - если вызываемый сервис недоступен, нельзя отложить передачу или получение информации на более позднее время. Если какой-то простой batch должен передать сообщение и завершиться, но получатель лежит, будут проблемы.
Peer-to-peer - нужно обращаться напрямую к искомому сервису. Более того, здесь нет броадкастинга - если надо отправить одни и те же данные в пять разных сервисов, придется сделать пять запросов. Сервисы получаются более связанными и могут погибать в “волне отказов”.
Текстовый - в JSON много лишних данных (ключи - значения), которые порождают дополнительный трафик. Компрессия не позволяет сжать так, как хотелось бы, плюс потребляет процессор. Для экономии некоторые пытаются называть ключи компактнее, но глобально это не решает проблему.
Нет схемы данных - вместо нее используют OpenAPI (Swagger), но он внедряется не всегда и в целом это лишь свод рекомендаций, а не четкий закон. Он может не соответствовать действительности.
Концепция “все есть ресурс” может быть неудобна и непонятна части разработчиков. Лично мне не нравится, что не все задачи легко решаются в рамках этой концепции. Иногда не совсем понятно, как все уложить в понятие ресурса. Есть уже прижившиеся способы описывать разные проблемные случаи, вроде изменений статусов объектов. Но фактически это костыли.
REST API - неплохое решение, когда нет высоких нагрузок и получается хорошо контролировать доступность микросервисов. Взаимодействия по REST API проще отслеживать, чем асинхронный обмен, - сразу видно, что ответ не приходит или приходит не в том формате.
gRPC
Хорошая альтернатива REST - это gRPC. Использовал на проекте его всего один раз, и там было все довольно просто, поэтому я не ощутил основных преимуществ. Но в целом мне понравилось.
gRPC:
Использует бинарный формат Protobuf - утилизация трафика лучше, можно слать только значения, не передавая ключи. Конечно, ты не можешь прочитать в консоли сообщение глазами, как мог бы это сделать с JSON (приходится писать дополнительную утилиту, которая будет его парсить перед тем, как прочитать). Зато на больших нагрузках все это будет лучше работать.
Есть схема данных, по которой генерируется DTO на запрос / ответ. С жесткой схемой работать удобнее, Protobuf накладывает ограничения на изменение схемы данных, чтобы сохранялась обратная совместимость. Это и плюс, и иногда минус.
Можно передавать не один запрос, а слать объекты один за другим - стримить.
Есть встроенный механизм backpressure - если сообщения отправляются слишком быстро и получатель не может их “переварить”, этот механизм позволяет замедлить передачу.
Отправка запроса выглядит, как вызов метода в коде, - используется RPC-стиль.
Недостаток в том, что нужно подключать библиотеку gRPC для своего языка. Но кажется, она есть для всех популярных языков, в том числе для фронта на JS.
gRPC подходит, если у вас в облаке крутится целый комбайн из кучи микросервисов, которым надо между собой передавать много данных под высокой нагрузкой. Для внешних пользователей он, конечно, не так удобен, как REST, но для внутреннего общения это самое то, потому что здесь жестко фиксируется схема и потребляется меньше трафика. Лично я на высоких нагрузках с gRPC никогда не экспериментировал. Но кажется, что он оптимален там, где стоимость трафика выше, чем затраты на то, чтобы заставить разработчиков читать документацию Protobuf (чтобы они не модифицировали поля там, где нельзя этого делать).
Читал, что Google любит его использовать внутри. Есть ощущение, что на Западе его в целом применяют чаще, как раз потому, что Google пиарит эту тему.
SOAP
Лично я очень не люблю XML. Чуть что, он перестает нормально парситься, десериалайзер начинает ругаться по любому поводу.
В новых проектах его сейчас редко увидишь, кроме случаев, когда надо общаться с сервисом, написанным 15 лет назад.
SOAP придумали тогда, когда json ещё не был популярен, и много кто пользовался XML. Сам формат XML сложнее json, больше разных правил и получается многословнее. Плюс всё это обернуто схемой данных и клиентскими библиотеками для генерации RPC-запроса и ответа, что не очень легковесно.
Большого опыта с SOAP у меня нет, так что и познать все плюсы и минусы не успел.
Мессенджинг - RabbitMQ, ZeroMQ, ActiveMQ и Kafka
Все перечисленные инструменты занимаются отправкой сообщений. Они очень разные, но в целом верно следующее:
Они обычно асинхронные. Шлешь сообщение и оно доставится когда-то в будущем. Никакого “запрос-ответ”. Есть решения, где прямой очереди соответствует обратная очередь для отправки ответа на сообщение, но это нельзя назвать синхронной историей.
Бывают с брокером и без него. С моей точки зрения именно брокер дает слабую связанность общению, правда, бывают кейсы, где все это реализуется очень медленно. Поэтому брокер иногда считается оверхедом.
С хранилищем (persistence) и без него (in-memory). В случае отказа брокер с хранилищем (например, Kafka), который пишет все сразу на диск, ничего не потеряет. Правда, только при условии, что вы позаботились настроить кластер как следует. Так можно хранить логи, например за последние полгода. Но при чтении старых данных с диска все подтормаживает.
С балансировкой нагрузки и без.
Текстовые и бинарные.
Со схемой данных или без нее.
С одним получателем или с поддержкой броадкаста. Если честно, возможность броадкастинга мне нравится больше всего - ты можешь реализовать модель издатель-подписчик, когда тебе не интересно, кто подписался на тему. Твоя задача просто писать в нее сообщения.
С подтверждением получения и без него.
Выбор системы при реализации собственного проекта - это поиск ответов на частные вопросы, нужны ли тебе подтверждения доставки или запись на диск. И это очень важные ответы, поскольку они влияют не только на логику работы, но и на производительность.
Лично я использовал только Kafka, поэтому буду в рассуждениях отталкиваться от нее.
С одной стороны, асинхронные взаимодействия - это круто. С другой - они тоже не панацея.
На одном из проектов, где я работал, Kafka использовалась для того, чтобы передавать на бэк заявки с фронта, которые оформляют менеджеры по продажам. Кажется, что это хорошее решение - если бэк по каким-то причинам не отвечал, заявка не терялась, а попадала в очередь и обрабатывалась, когда бэк восстанавливался. С другой стороны, при такой схеме взаимодействия мы не могли отправить менеджерам ответ, может ли вообще выполниться клиентская заявка. Менеджер довольный закрывал окно, но на бэке выяснялось, что заявка не может быть выполнена. А у нас не было никакого канала обратной связи. Плюс мы постоянно сталкивались с повторно отправленными заявками или отменой уже отмененных, потому что сообщения применялись не мгновенно, а складывались в очередь, и иногда могло пройти несколько часов, пока сообщение обработается (например если сервис лежал).
В теории здесь можно было бы реализовать обратную очередь или вообще обойтись REST API. В этом случае менеджер бы видел, что заявка не обрабатывается, и предпринял бы какие-то действия.
Kafka и прочие перечисленные инструменты - это дополнительный слой абстракции, который обеспечивает асинхронность и отказоустойчивость. Он хорошо работает там, где все это действительно востребовано - в сложной архитектуре с большим количеством исполнителей, которых хочется подключать и отключать. Kafka умеет следить за тем, чтобы сообщения не терялись, правильно все распараллеливать и реплицировать, занимается дедупликацией при необходимости.
Оборотная сторона медали в том, что в асинхронном взаимодействии сложнее разобраться. Ответ не приходит сразу - он может не прийти вообще или появиться через несколько часов в ответном топике. У нашей команды множество историй про Kafka, в том числе и связанных с багами самой Kafka. Кажется, что у любого разработчика есть свои примеры странного поведения брокера.
Конкретно Kafka еще и очень сложно настраивать. Кажется, что мало кто знает, как это делать правильно. Там очень много нюансов, которые надо учесть. В свое время мы организовывали книжный клуб - каждую неделю в офлайне читали по главе из какой-нибудь полезной книги и собирались, чтобы обсудить прочитанное. Как раз так мы изучили книгу по Kafka, и это было мега-полезно. Пока я это не прочитал, не понимал по-настоящему. Но кажется и этих знаний недостаточно, чтобы применять Kafka направо и налево без дополнительного чтения документации.
Автор статьи: Дмитрий Литвин и коллектив Максилект.
P.S. Мы публикуем наши статьи на нескольких площадках Рунета. Подписывайтесь на нашу страницу в VK или на Telegram-канал, чтобы узнавать обо всех публикациях и других новостях компании Maxilect.
Комментарии (7)
Xop
15.07.2022 14:06+1Недавно был отличный опыт с комбинацией gRPC + golang, после этого если честно на REST даже смотреть не хочется. Часть положительных моментов уже расписана в статье, но хотелось бы еще добавить:
отличный тулинг для кодогенерации, как сервера, так и клиента, меньше рууами писать бойлерплейтов
очень удобный и гибкий API (в отличие от гошного net.http - правда это скорее камень в огород net.http), легко реализуются такие штуки как реконнекты, походы по разным эндпоинтам (если один отвалился), распределенная трассировка запросов
если прям очень надо именно HTTP - тулинг позволяет накодогенерить и сопутствующий HTTP сервак, причем реализацию контроллера переписывать не надо - она будет общей и для gRPC и для HTTP
Karchevskiy
15.07.2022 16:34Я бы накидал рекомендаций. Для того что вы описываете, как мессаджинг, отделяя от кафки, есть отличное слово AMQP-брокер. Посмотрите еще лекций как работает кафка,
Karchevskiy
15.07.2022 18:24чертов встроенный редактор хабра.) Я хотел сказать, что важно понимать как связаны слова kafka и лог с указателем) И если уже вы касаетесь древнего SOAP, то было бы круто взять более современный аналог с аналогичным подходом - GraphQl
rinat_crone
15.07.2022 17:04Про gRPC, кажись, одну из главных деталей не упомянули — отсутствие null в качестве значений полей (они всегда инициализируются дефолтными для типа значениями, как и структуры в Go, т.е. никакой вам троичной логики). Какой-нибудь аналог PATCH (когда необходимо обновить только часть полей объекта, в зависимости от того, какой набор полей в запросе пришел) устанете делать, прийдется флагами на каждое поле обвеситься.
кажется, что он оптимален там, где стоимость трафика выше, чем затраты на то, чтобы заставить разработчиков читать документацию Protobuf (чтобы они не модифицировали поля там, где нельзя этого делать).
Не расшифруете, что вы тут имеете в виду под «чтобы они не модифицировали поля там, где нельзя этого делать»?sgzmd
15.07.2022 18:19Про gRPC, кажись, одну из главных деталей не упомянули — отсутствие null в качестве значений полей
В 3.15.0 снова завезли optional fields, стало как в proto2
skymal4ik
Эх, ещё бы примеры кода на распространённых языках… но это я уже размечтался, при желании всё можно найти в интернетах :))
А за список спасибо, интересно было почитать. Добавил бы сюда ещё redis, и возможно, бд как средство общения между сервисами. Сам на одном проекте кладу данные в MySQL одним сервисом, а забираю и обрабатываю другим.
Ну и плюс в облаках свои сервисы для очередей, вроде aws sqs, тоже можно добавить в статью