Проектирование взаимодействия между службами для микрослужб

Azure DevOps

Обмен данными между микрослужбами должен быть эффективным и надежным. При взаимодействии множества небольших служб для выполнения одной бизнес-задачи это может стать проблемой. В этой статье мы рассмотрим компромиссы между асинхронным обменом сообщениями и синхронными API. Затем мы рассмотрим некоторые проблемы при проектировании устойчивого взаимодействия между службами.

Сложности

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

Устойчивость. Может быть десятки или даже сотни экземпляров любой данной микрослужбы. Инстанция может завершиться ошибкой по ряду причин. Может произойти сбой на уровне узла, например сбой оборудования или перезагрузка виртуальной машины. Экземпляр может аварийно завершить работу или быть перегружен запросами и не в состоянии обрабатывать новые запросы. Любое из этих событий может вызвать сбой сетевого вызова. Существует два конструктивных шаблона, которые могут повысить устойчивость сетевых вызовов между службами.

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

    Если операция не является идемпотентной, повторные попытки могут вызвать непредвиденные последствия. Начальный вызов может быть выполнен успешно, но вызывающий объект никогда не получит ответ. Если вызывающий объект повторяет попытку, операция может вызываться дважды. Как правило, небезопасно повторить методы POST или PATCH, так как эти операции не гарантированы идемпотентными. Дополнительные сведения см. в статье Обработка временных сбоев.

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

Балансировка нагрузки. Когда служба "A" вызывает службу "B", запрос должен достичь работающего экземпляра службы "B". В Kubernetes тип ресурса Service предоставляет стабильный IP-адрес для группы подов. Сетевой трафик к IP-адресу сервиса перенаправляется на pod с помощью правил Iptables. По умолчанию выбирается случайный pod. Сетка служб может предоставлять более интеллектуальные алгоритмы балансировки нагрузки на основе наблюдаемой задержки или других метрик.

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

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

Шифрование TLS и взаимная проверка подлинности TLS. По соображениям безопасности может потребоваться зашифровать трафик между службами с помощью TLS и использовать взаимную проверку подлинности TLS для проверки подлинности вызывающих пользователей.

Синхронный и асинхронный обмен сообщениями

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

  1. Синхронный обмен данными. В этом шаблоне служба вызывает API, который предоставляет другая служба, используя протокол, такой как HTTP или gRPC. Это шаблон синхронного обмена сообщениями, потому что отправитель ожидает ответа от получателя.

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

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

Каждый шаблон имеет свои недостатки. Запрос и ответ — это хорошо понятная парадигма, поэтому проектирование API может чувствовать себя более естественным, чем разработка системы обмена сообщениями. Однако асинхронное обмен сообщениями имеет некоторые преимущества, которые могут быть полезны в архитектуре микрослужб:

  • Уменьшенная связанность. Отправитель сообщения не должен знать о потребителе.

  • Несколько подписчиков. Используя модель pub/sub, несколько потребителей могут подписаться на получение событий. Дополнительные сведения см. в статье Стиль архитектуры, управляемой событиями.

  • Изоляция сбоев. Если объект-получатель вышел из строя, отправитель все равно может отправлять сообщения. Сообщения собираются, когда потребитель восстанавливается. Эта возможность особенно полезна в архитектуре микрослужб, так как каждая служба имеет свой собственный жизненный цикл. В любой момент времени служба может стать недоступной или быть заменена более новой версией. Асинхронный обмен сообщениями может справляться с периодическими простоями. С другой стороны, в синхронных API нижестоящая служба должна быть доступна, иначе операция завершится ошибкой.

  • Скорость реагирования. Вышестоящей службе можно быстрее ответить, если она не ожидает подчиненных служб. Это особенно удобно использовать в архитектуре микрослужб. Если существует цепочка зависимостей службы (например, служба A вызывает службу B, которая вызывает службу C), ожидание синхронных вызовов может добавить неприемлемое время задержки.

  • Выравнивание нагрузки. Очередь может выступать в качестве буфера для выравнивания рабочей нагрузки, поэтому получатели могут обрабатывать сообщения с нужной им скоростью.

  • Рабочие процессы. Очереди можно использовать для управления рабочим процессом, фиксируя сообщение после каждого его этапа.

Однако существуют некоторые проблемы с эффективностью использования асинхронного обмена сообщениями.

  • Взаимозависимость с инфраструктурой обмена сообщениями. Использование определенной инфраструктуры обмена сообщениями может привести к жесткой связи с этой инфраструктурой. Позже может быть трудно переключиться на другую инфраструктуру обмена сообщениями.

  • Задержка. Сквозная задержка для операции может стать высокой, если очереди сообщений заполняются.

  • Стоимость. При высокой пропускной способности инфраструктура обмена сообщениями может быть дорогостоящей.

  • Сложность. Обработка асинхронного обмена сообщениями не является тривиальной задачей. Например, нужно обрабатывать дублирующиеся сообщения, удаляя дубликаты либо делая операции идемпотентными. При использовании асинхронного обмена сообщениями также трудно реализовать семантику "запрос — ответ". Для отправки ответа потребуется другая очередь, а также способ коррелировать запросы и ответные сообщения.

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

Доставка с помощью дронов: выбор шаблонов обмена сообщениями

В этом решении используется пример доставки дронов. Это идеально подходит для аэрокосмической и авиационной промышленности.

Учитывая эти рекомендации, команда разработчиков сделала следующие варианты проектирования для приложения доставки дронов:

  • Служба приема предоставляет открытый интерфейс REST API, который клиентские приложения используют для планирования, обновления или отмены поставок.

  • Служба приема использует Центры событий для отправки асинхронных сообщений в службу планировщика. Асинхронные сообщения необходимы для реализации выравнивания нагрузки, которая необходима для приема данных.

  • Службы учетной записи, доставки, посылок, дронов и транспортных услуг третьих лиц предоставляют внутренние интерфейсы REST API. Служба планировщика вызывает эти API для выполнения запроса пользователя. Одной из причин использования синхронных API является то, что планировщику необходимо получить ответ от каждой из нижестоящих служб. Сбой в любой из нижестоящих служб означает, что вся операция завершилась неудачно. Тем не менее потенциальная проблема — это объем задержки, которая возникает при вызове серверных служб.

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

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

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

  • При изменении состояния доставки служба доставки отправляет событие состояния доставки, например DeliveryCreated или DeliveryCompleted. Любая служба может подписаться на эти события. В текущей структуре служба журнала доставки является единственным подписчиком, но может быть и другие подписчики позже. Например, события могут попадать в службу аналитики в режиме реального времени. Так как планировщик не должен ожидать ответа, добавление большего количества подписчиков не влияет на путь основного рабочего процесса.

Схема обмена данными между дронами

Обратите внимание, что события состояния доставки являются производными от событий расположения дронов. Например, когда дрон достигает места доставки и выгружает груз, служба доставки преобразует это в событие DeliveryCompleted (доставка завершена). Это пример подхода с учетом модели предметной области. Как описано выше, управление дронами принадлежит к отдельному ограниченному контексту. События дронов передают физическое расположение дрона. С другой стороны, события доставки представляют собой изменения состояния доставки, что является другой бизнес-сущностью.

Использование слоя взаимодействия между службами

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

Примечание.

Сетка служб является примером шаблона "Посол" — вспомогательной службой, которая отправляет сетевые запросы от имени приложения.

Сейчас основными вариантами для сетки службы в Kubernetes являются Linkerd и Istio. Обе эти технологии быстро развиваются. Однако некоторые функции, которые общи для Linkerd и Istio, включают:

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

  • Маршрутизация уровня 7 на основе URL-адреса, заголовка узла, версии API или других правил уровня приложения.

  • Повторение невыполненных запросов. Слой взаимодействия между службами распознает коды ошибок HTTP и может автоматически повторять невыполненные запросы. Можно настроить максимальное количество попыток, а также время ожидания, чтобы ограничить максимальную задержку.

  • Прерывание цепи. Если экземпляр постоянно не выполняет запросы, сервисная сетка временно помечает его как недоступное. После периода ожидания он снова пытается выполнить попытку экземпляра. Можно настроить автоматический выключатель на основе различных критериев, таких как количество последовательных сбоев.

  • Слой взаимодействия между службами фиксирует показатели вызовов между службами, такие как объем запроса, задержка, уровень ошибок и успешности, а также размеры ответов. Слой взаимодействия между службами также позволяет распределенную трассировку путем добавления информации о корреляции для каждого прыжка в запросе.

  • Взаимная проверка подлинности TLS для вызовов между службами.

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

Распределенные транзакции

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

Здесь необходимо рассмотреть два случая:

  • Служба может столкнуться с временным сбоем, таким как таймаут ожидания сети. Эти ошибки часто можно устранить, повторив вызов. Если операция по-прежнему завершается неудачей после определенного количества попыток, она считается непреходящей ошибкой.

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

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

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

Схема микрослужбы

Служба планировщика может выйти из строя (например, из-за сбоя узла). В этом случае новый экземпляр может запуститься и перенять управление. Однако любые транзакции, которые уже начали выполняться, должны быть возобновлены.

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

Другой вариант – проектировать все операции как идемпотентные. Операция является идемпотентной, если ее можно вызвать несколько раз, не создавая дополнительных побочных эффектов после первого вызова. По сути, нижестоящей службе следует игнорировать повторяющиеся вызовы, что означает, что служба должна иметь возможность обнаруживать повторяющиеся вызовы. Не всегда просто реализовать идемпотентные методы. Дополнительные сведения см. в разделе "Идемпотентные операции".

Следующие шаги

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