За последние два года я много работал с удаленными вызовами процедур (RPC), применяя этот подход для взаимодействия между нашими микро-сервисами. В подобных ситуациях RPC определенно может приносить пользу. Однако иногда стоит поискать другое решение.

Я продемонстрирую реализацию RPC и расскажу о некоторых проблемах, с которыми мы столкнулись в работе.

Реализация RPC

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

Есть несколько вариантов реализации RPC. К примеру, через шаблон “запрос-ответ”. Этот шаблон реализуется с помощью брокера сообщений, например RabbitMQ.

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

На следующем рисунке показано, как будет выглядеть такая реализация:

Пример реализации RPC с применением запроса-ответа

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

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

Когда клиент получит сообщение в очереди ответов, то с помощью CorrelationId сопоставит его с одним из своих ожидающих запросов.

Последнее, что придется сделать,  —  это убедиться, что сгенерированные очереди будут удалены, когда сервис завершит работу.

Обработка отказов

Теперь, когда мы разобрались с реализацией RPC, пришло время рассмотреть пограничные случаи.

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

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

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

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

Вот некоторые преимущества такой реализации:

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

Когда не следует применять RPC 

Давайте рассмотрим несколько случаев, когда RPC лучше заменить иным подходом.

Долго выполняющиеся задачи

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

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

Вот почему лучше добавить RPC тайм-аут через пару секунд. Это позволит убедиться, что экземпляр может корректно завершить работу при развертывании новой версии. 

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

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

Цепочки RPC

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

Примеры проблем, которые могут возникнуть

Эти проблемы не относятся исключительно к цепочкам RPC, однако объединение нескольких служб в цепочку увеличивает вероятность возникновения указанных проблем, особенно если эти службы не отвечают в течение нескольких миллисекунд. Убедитесь, что при выполнении RPC через несколько служб такие случаи покрыты автоматическими повторными попытками.

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

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

Тем не менее, если все случаи сбоев обрабатываются правильно, то, безусловно, можно связать несколько сервисов в цепочку с RPC.

Действия, не подлежащие повторению

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

Почему?

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

Пример повторяющихся сообщений в очереди сообщений

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

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

Послесловие

RPC  —  хороший подход до тех пор, пока все случаи сбоев обрабатываются правильно. Мне лично очень нравится следующее утверждение из учебника RabbitMQ:

«Если сомневаетесь, избегайте RPC».

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

Спасибо за чтение!

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи: Stein Janssen, “Remote Procedure Calls With Request-Response”

Предыдущая статьяPython: публикация ваших пакетов в PyPi
Следующая статья25 наборов аудиоданных для исследований