Привет, Хаброжители! Мы недавно сдали в типографию книгу Криса Ричардсона, цель которой — научить успешно разрабатывать приложения с использованием микросервисной архитектуры. В книге обсуждаются не только преимущества, но и недостатки микросервисов. Вы узнаете, в каких ситуациях имеет смысл применять их, а когда лучше подумать о монолитном подходе.
Основное внимание в книге уделяется архитектуре и разработке. Она рассчитана на любого, в чьи обязанности входят написание и доставка программного обеспечения, в том числе на разработчиков, архитекторов, технических директоров и начальников отделов по разработке.
Ниже представлен отрывок из книги «Использование асинхронного обмена сообщениями»
Использование асинхронного обмена сообщениями для улучшения доступности
Как вы видели, разнообразные механизмы IPC подталкивают вас к различным компромиссам. Один из них связан с тем, как механизм IPC влияет на доступность. В этом разделе вы узнаете, что синхронное взаимодействие с другими сервисами в рамках обработки запросов снижает степень доступности приложения. В связи с этим при проектировании своих сервисов вы должны по возможности использовать асинхронный обмен сообщениями.
Сначала посмотрим, какие проблемы создает синхронное взаимодействие и как это сказывается на доступности.
3.4.1. Синхронное взаимодействие снижает степень доступности
REST — это чрезвычайно популярный механизм IPC. У вас может возникнуть соблазн использовать его для межсервисного взаимодействия. Но проблема REST заключается в том, что это синхронный протокол: HTTP-клиенту приходится ждать, пока сервис не вернет ответ. Каждый раз, когда сервисы общаются между собой по синхронному протоколу, это снижает доступность приложения.
Чтобы понять, почему так происходит, рассмотрим сценарий, представленный на рис. 3.15. У сервиса Order есть интерфейс REST API для создания заказов. Для проверки заказа он обращается к сервисам Consumer и Restaurant, которые тоже имеют REST API.
Создание заказа состоит из такой последовательности шагов.
- Клиент делает HTTP-запрос POST /orders к сервису Order.
- Сервис Order извлекает информацию о заказчике, выполняя HTTP-запрос GET /consumers/id к сервису Consumer.
- Сервис Order извлекает информацию о ресторане, выполняя HTTP-запрос GET /restaurant/id к сервису Restaurant.
- Order Taking проверяет запрос, задействуя информацию о заказчике и ресторане.
- Order Taking создает заказ.
- Order Taking отправляет HTTP-ответ клиенту.
Поскольку эти сервисы используют HTTP, все они должны быть доступны, чтобы приложение FTGO смогло обработать запрос CreateOrder. Оно не сможет создать заказ, если хотя бы один из сервисов недоступен. С математической точки зрения доступность системной операции является произведением доступности сервисов, которые в нее вовлечены. Если сервис Order и те два сервиса, которые он вызывает, имеют доступность 99,5 %, то их общая доступность будет 99,5 %3 = 98,5 %, что намного ниже. Каждый последующий сервис, участвующий в запросе, делает операцию менее доступной.
Эта проблема не уникальна для взаимодействия на основе REST. Доступность снижается всякий раз, когда для ответа клиенту сервис должен получить ответы от других сервисов. Здесь не поможет даже переход к стилю взаимодействия «запрос/ответ» поверх асинхронных сообщений. Например, если сервис Order пошлет сервису Consumer сообщение через брокер и примется ждать ответа, его доступность ухудшится.
Если вы хотите максимально повысить уровень доступности, минимизируйте объем синхронного взаимодействия. Посмотрим, как это сделать.
3.4.2. Избавление от синхронного взаимодействия
Существует несколько способов уменьшения объема синхронного взаимодействия с другими сервисами при обработке синхронных запросов. Во-первых, чтобы полностью избежать этой проблемы, все сервисы можно снабдить исключительно асинхронными API. Но это не всегда возможно. Например, публичные API обычно придерживаются стандарта REST. Поэтому некоторые сервисы обязаны иметь синхронные API.
К счастью, чтобы обрабатывать синхронные запросы, вовсе не обязательно выполнять их самому. Поговорим о таких вариантах.
Использование асинхронных стилей взаимодействия
В идеале все взаимодействие должно происходить в асинхронном стиле, описанном ранее в этой главе. Представьте, к примеру, что клиент приложения FTGO применяет для создания заказов асинхронный стиль взаимодействия вида «запрос/асинхронный ответ». Чтобы создать заказ, он отправляет сообщение с запросом сервису Order. Затем этот сервис асинхронно обменивается сообщениями с другими сервисами и в итоге возвращает клиенту ответ (рис. 3.16).
Клиент и сервис общаются асинхронно, отправляя сообщения через каналы. Ни один из участников этого взаимодействия не блокируется в ожидании ответа.
Такая архитектура была бы чрезвычайно устойчивой, потому что брокер буферизирует сообщения до тех пор, пока их потребление не станет возможным. Но проблема в том, что у сервисов часто есть внешний API, который использует синхронный протокол вроде REST и, как следствие, обязан немедленно отвечать на запросы.
Если у сервиса есть синхронный API, доступность можно улучшить за счет репликации данных. Посмотрим, как это работает.
Репликация данных
Одним из способов минимизации синхронного взаимодействия во время обработки запросов является репликация данных. Сервис хранит копию (реплику) данных, которые ему нужны для обработки запросов. Чтобы поддерживать реплику в актуальном состоянии, он подписывается на события, публикуемые сервисами, которым эти данные принадлежат. Например, сервис Order может хранить копию данных, принадлежащих сервисам Consumer и Restaurant. Это позволит ему обрабатывать запросы на создание заказов, не обращаясь к этим сервисам. Такая архитектура показана на рис. 3.17.
Сервисы Consumer и Restaurant публикуют события всякий раз, когда их данные меняются. Сервис Order подписывается на эти события и обновляет свою реплику.
В некоторых случаях репликация данных — это хорошее решение. Например, в главе 5 описывается, как сервис Order реплицирует данные сервиса Restaurant, чтобы иметь возможность проверять элементы меню. Один из недостатков этого подхода связан с тем, что иногда он требует копирования больших объемов данных, что неэффективно. Например, если у нас много заказчиков, хранить реплику данных, принадлежащих сервису Consumer, может оказаться непрактично. Еще один недостаток репликации кроется в том, что она не решает проблему обновления данных, принадлежащих другим сервисам.
Чтобы решить эту проблему, сервис может отсрочить взаимодействие с другими сервисами до тех пор, пока он не ответит своему клиенту. Речь об этом пойдет далее.
Завершение обработки после возвращения ответа
Еще один способ устранения синхронного взаимодействия во время обработки запросов состоит в том, чтобы выполнять эту обработку в виде следующих этапов.
- Сервис проверяет запрос только с помощью данных, доступных локально.
- Он обновляет свою базу данных, в том числе добавляет сообщения в таблицу OUTBOX.
- Возвращает ответ своему клиенту.
Во время обработки запроса сервис не обращается синхронно ни к каким другим сервисам. Вместо этого он шлет им асинхронные сообщения. Данный подход обеспечивает слабую связанность сервисов. Как вы увидите в следующей главе, этот процесс часто реализуется в виде повествования.
Представьте, что сервис Order действует таким образом. Он создает заказ с состоянием PENDING и затем проверяет его, обмениваясь асинхронными сообщениями с другими сервисами. На рис. 3.18 показано, что происходит при вызове операции createOrder(). Цепочка событий выглядит так.
- Сервис Order создает заказ с состоянием PENDING.
- Сервис Order возвращает своему клиенту ответ с ID заказа.
- Сервис Order шлет сообщение ValidateConsumerInfo сервису Consumer.
- Сервис Order шлет сообщение ValidateOrderDetails сервису Restaurant.
- Сервис Consumer получает сообщение ValidateConsumerInfo, проверяет, может ли заказчик размещать заказ, и отправляет сообщение ConsumerValidated сервису Order.
- Сервис Restaurant получает сообщение ValidateOrderDetails, проверяет корректность элементов меню и способность ресторана доставить заказ по заданному адресу и отправляет сообщение OrderDetailsValidated сервису Order.
- Сервис Order получает сообщения ConsumerValidated и OrderDetailsValidated и меняет состояние заказа на VALIDATED.
И так далее…
Сервис Order может получить сообщения ConsumerValidated и OrderDetailsValidated в любом порядке. Чтобы знать, какое из них он получил первым, он меняет состояние заказа. Если первым пришло сообщение ConsumerValidated, состояние заказа меняется на CONSUMER_VALIDATED, а если OrderDetailsValidated — на ORDER_DETAILS_VALIDATED. Получив второе сообщение, сервис Order присваивает заказу состояние VALIDATED.
После проверки заказа сервис Order выполняет оставшиеся шаги по его созданию, о которых мы поговорим в следующей главе. Замечательной стороной этого подхода является то, что сервис Order сможет создать заказ и ответить клиенту, даже если сервис Consumer окажется недоступным. Рано или поздно сервис Consumer восстановится и обработает все отложенные сообщения, что позволит завершить проверку заказов.
Недостаток возвращения ответа до полной обработки запроса связан с тем, что это делает клиент более сложным. Например, когда сервис Order возвращает ответ, он дает минимальные гарантии по поводу состояния только что созданного заказа. Он отвечает немедленно, еще до проверки заказа и авторизации банковской карты клиента. Таким образом, чтобы узнать о том, успешно ли создан заказ, клиент должен периодически запрашивать информацию или же сервис Order должен послать ему уведомительное сообщение. Несмотря на всю сложность этого подхода, во многих случаях стоит предпочесть его, особенно из-за того, что он учитывает проблемы с управлением распределенными транзакциями, которые мы обсудим в главе 4. В главах 4 и 5 я продемонстрирую эту методику на примере сервиса Order.
Резюме
- Микросервисная архитектура является распределенной, поэтому межпроцессное взаимодействие играет в ней ключевую роль.
- К развитию API сервиса необходимо подходить тщательно и осторожно. Легче всего вносить обратно совместимые изменения, поскольку они не влияют на работу клиентов. При внесении ломающих изменений в API сервиса обычно приходится поддерживать как старую, так и новую версию, пока клиенты не обновятся.
- Существует множество технологий IPC, каждая со своими достоинствами и недостатками. Ключевое решение на стадии проектирования — выбор между синхронным удаленным вызовом процедур и асинхронными сообщениями. Самыми простыми в использовании являются синхронные протоколы вроде REST, основанные на вызове удаленных процедур. Но в идеале, чтобы повысить уровень доступности, сервисы должны взаимодействовать с помощью асинхронного обмена сообщениями.
- Чтобы предотвратить лавинообразное накопление сбоев в системе, клиент, использующий синхронный протокол, должен быть способен справиться с частичными отказами — тем, что вызываемый сервис либо недоступен, либо проявляет высокую латентность. В частности, при выполнении запросов следует отсчитывать время ожидания, ограничивать количество просроченных запросов и применять шаблон «Предохранитель», чтобы избежать обращений к неисправному сервису.
- Архитектура, использующая синхронные протоколы, должна содержать механизм обнаружения, чтобы клиенты могли определить сетевое местонахождение экземпляров сервиса. Проще всего остановиться на механизме обнаружения, который предоставляет платформа развертывания: на шаблонах «Обнаружение на стороне сервера» и «Сторонняя регистрация». Альтернативный подход — реализация обнаружения сервисов на уровне приложения: шаблоны «Обнаружение на стороне клиента» и «Саморегистрация». Этот способ требует бо'льших усилий, но подходит для ситуаций, когда сервисы выполняются на нескольких платформах развертывания.
- Модель сообщений и каналов инкапсулирует детали реализации системы обмена сообщениями и становится хорошим выбором при проектировании архитектуры этого вида. Позже вы сможете привязать свою архитектуру к конкретной инфраструктуре обмена сообщениями, в которой обычно используется брокер.
- Ключевая трудность при обмене сообщениями связана с их публикацией и обновлением базы данных. Удачным решением является применение шаблона «Публикация событий»: сообщение в самом начале записывается в базу данных в рамках транзакции. Затем отдельный процесс извлекает сообщение из базы данных, используя шаблон «Опрашивающий издатель» или «Отслеживание транзакционного журнала», и передает его брокеру.
» Более подробно с книгой можно ознакомиться на сайте издательства
» Оглавление
» Отрывок
Для Хаброжителей скидка 30% на предзаказ книги по купону — Микросервисы
Автор: ph_piter