Как на самом деле работает RPC
Что такое RPC? Разбираемся в удалённых вызовах процедур для микросервисов.
Как сотни внутренних сервисов таких компаний, как Netflix или Google, взаимодействуют друг с другом? И как это происходит миллионы раз в минуту?
Стандартный REST API отлично подходит для публичного использования, но часто создаёт слишком много накладных расходов для внутренней высокоскоростной передачи данных. Это связано с тем, что обычно используются текстовые форматы, такие как JSON. Хотя они легко читаются, они неэффективны для сервисов, которым приходится обрабатывать миллионы запросов в день.
Вместо этого эти сервисы должны использовать язык, который невероятно компактен и быстр в обработке для компьютеров. Кроме того, им нужен заранее согласованный набор правил, своего рода общий план, который гарантирует, что обе стороны точно знают, как взаимодействовать друг с другом без ошибок.
И самое главное, он должен обеспечивать удобство для разработчиков без ущерба для производительности.
Современные приложения используют множество распределённых сервисов. Однако это создаёт серьёзную проблему:
Как добиться максимальной эффективности взаимодействия между машинами? Вызов функции через сеть должен быть таким же простым, как и локальный вызов.
Ответ: удалённый вызов процедуры (RPC). Это протокол, разработанный для высокопроизводительной внутренней коммуникации.
Я хочу представить Ашутоша в качестве приглашённого автора.
Он работает инженером-программистом в YouTube, ранее работал в Google Search и Microsoft Azure.
Что такое удалённый вызов процедуры
RPC — это протокол, который позволяет вызывать функции из адресного пространства одной программы в адресном пространстве другой. Это освобождает разработчика от необходимости вручную управлять точками подключения к сети (сокетами) или преобразовывать данные в формат, пригодный для отправки (сериализацию).
Когда ваш код вызывает такую функцию, как math.sqrt(9), вас не волнует сложный алгоритм, работающий внутри математической библиотеки. Вы просто доверяете её общедоступному API и знаете сигнатуру функции.
RPC распространяет эту концепцию на всю сеть.
Клиентское приложение получает объект-заглушку — локальный прокси-сервер, который отражает API удалённого сервиса. Но его методы содержат только логику для переадресации вызова по сети. Когда вы вызываете функцию на этом объекте-заглушке, он выполняет всю сложную сетевую коммуникацию в фоновом режиме, чтобы код выполнялся на сервере.
Таким образом, вызов, подобный result = paymentService.charge(userId, amount), не обращается напрямую к платежному механизму. Вместо этого он вызывает локальный прокси-метод, который затем выполняет удаленную операцию. Благодаря этому удаленный вызов выглядит как простой вызов локальной библиотеки.
Прежде чем двигаться дальше, давайте разберемся в следующем:
Как работает Удаленный вызов процедуры
1. Клиентская заглушка (местный представитель)
Ваш код не обращается напрямую к удалённому сервису. Вместо этого он взаимодействует с заглушкой — локальным представителем, единственная задача которого — замещать удалённый сервис.
У него есть те же методы, которые вы хотите использовать, например, «.charge()”. Но его настоящая цель — запустить процесс удалённой связи.
2. Маршалинг (упаковка сообщения)
Заглушка принимает указанные вами параметры, например userId, amount, и упорядочивает их.
Маршалинг — это красивое слово, обозначающее сериализацию. Оно означает преобразование данных из формата языка программирования в стандартизированный формат, чтобы их можно было отправить по сети.
Представьте, что вы аккуратно упаковываете свои инструкции в универсальный контейнер для доставки, который может открыть любой сервер в мире. Этот формат часто представляет собой JSON или более компактный и быстрый формат, например буферы протокола.
3. Сетевое путешествие
Затем клиент отправляет это упакованное сообщение по сети на сервер, используя эффективные протоколы, такие как HTTP/2. Это «удаленная» часть вызова.
4. Скелет сервера
На сервере прослушивается «скелет». Это аналог клиентской заглушки.
Его задача — получить входящее сообщение и распаковать (или десериализовать) его. Представьте, что вы распаковываете транспортировочный контейнер и возвращаете данные в формат, понятный серверу.
Затем скелет вызывает фактический метод сервиса с этими распакованными данными.
5. Обратный путь
После того как сервер завершает свою работу, весь процесс повторяется в обратном порядке.
Скелет маршалирует возвращаемое значение, например, {success: true}. Затем отправляет его обратно по сети, а клиентская заглушка демаршалирует его.
После этого строка кода, в которой был выполнен исходный вызов, получает результат.
Далее я расскажу вам о различных сценариях сбоев RPC и способах их устранения.
Обработка ошибок при вызове удалённых процедур
В распределённой системе сеть может быть ненадёжной.
Соединения обрываются. Службы работают медленно. Происходят сбои.
Надежная реализация RPC подразумевает не только успешные вызовы, но и корректную обработку ошибок. Вот несколько стратегий:
1. Тайм-ауты (не ждите вечно)
Тайм-аут — это простой срок ожидания.
Каждый раз, когда клиент отправляет запрос, запускается таймер. Если клиент не получает ответ до истечения времени, отведённого на выполнение запроса, он прекращает попытку.
Почему это важно: без тайм-аутов ваше приложение может бесконечно ждать ответа от медленного или недоступного сервиса. Это приведет к нехватке ресурсов, таких как потоки или память, и в конечном итоге может привести к зависанию всего приложения. Тайм-ауты — это первая линия защиты клиента.
2. Повторные попытки (если первый вызов не увенчался успехом)
Многие сетевые ошибки носят временный характер: это сбой, который быстро устраняется.
Иногда сервер может отвечать слишком долго или сетевой пакет может потеряться при передаче. В таких случаях имеет смысл автоматически повторить неудачный запрос.
Важное предостережение (идемпотентность): повторные попытки могут быть опасны, если один и тот же запрос вызывает побочные эффекты при каждом выполнении. Операция является идемпотентной, если её многократное выполнение даёт тот же результат, что и однократное.
- Безопасно повторять (идемпотентно): получение баланса аккаунта пользователя. Повторное выполнение не изменит состояние системы.
- Повторное выполнение (не идемпотентное) может привести к списанию средств с кредитной карты пользователя. Повторное выполнение может привести к дублированию платежей.
Поэтому разработчики должны проектировать свои системы с учётом идемпотентности, например с использованием уникальных идентификаторов транзакций, чтобы можно было безопасно повторять попытки.
3. Автоматические выключатели (защита системы)
Если сервис не работает или работает медленно, отправка дополнительных запросов только усугубит ситуацию.
Автоматический выключатель — это интеллектуальное устройство, которое предотвращает подобные ситуации.
Если клиент замечает, что многие обращения к сервису завершаются неудачей, автоматический выключатель «срабатывает» и на короткое время прекращает отправку запросов к этому сервису. В этот период любые новые обращения завершаются мгновенным сбоем, а не отправляются по сети.
Почему это важно: Автоматические выключатели предотвращают каскадные сбои. Они дают сбойной службе время на восстановление. И не позволяют клиенту тратить время и ресурсы на вызовы, которые, скорее всего, завершатся неудачей.
4. Продление сроков (не начинайте работу, которую не сможете закончить)
Один пользовательский запрос часто запускает цепочку вызовов между несколькими сервисами. Например, сервис A вызывает сервис B, а сервис B вызывает сервис C.
Если общий тайм-аут исходного запроса составляет 500 мс, а служба A уже использует 300 мс. Тогда службе C нет смысла запускать задачу, которая займёт ещё 400 мс, потому что клиент уже перестанет ждать.
Распространение крайнего срокаисправляет эту проблему, передавая оставшийся срок выполнения при каждом вызове. Таким образом, каждый сервис знает, сколько времени у него осталось, и может решить, успеет ли он выполнить свою часть работы до истечения срока.
Почему это важно: Такой подход позволяет избежать лишней работы и обеспечивает быстродействие и отзывчивость системы. Сервисы могут «быстро завершать работу» вместо того, чтобы выполнять длительные операции для запроса, который уже не имеет значения.
Как и любая технология, RPC не идеальна. У неё есть свои сильные стороны, но есть и недостатки, на которые следует обратить внимание. Продолжим!
RPC: хорошее, плохое и ужасное
Преимущества
- Простая модель программирования: вы можете вызывать удалённые функции так же, как и локальные, что делает ваш код чище и проще.
- Высокая производительность: RPC использует компактные двоичные форматы, которые быстро обрабатываются компьютерами.
- Строгая типизация: поскольку обе стороны соблюдают строгие правила, ошибки часто выявляются на ранней стадии, до того как они приведут к проблемам.
- Языковая совместимость: клиент и сервер могут использовать разные языки программирования и при этом беспрепятственно взаимодействовать.
Недостатки
- Тесная связь: при изменении API сервера обычно требуется обновление и клиента, что может замедлить процесс разработки.
- Менее доступны для обнаружения, чем REST: Вы не сможете легко протестировать или просмотреть RPC API без специальных файлов контрактов.
- Требуется специализированное оборудование: для генерации кода для клиента и сервера нужны специальные инструменты.
- Абстракция скрывает сетевые реалии. Поскольку удалённые вызовы выглядят как локальные, разработчики могут забыть, что имеют дело с сетью, и не обработать должным образом тайм-ауты или ошибки.
Теперь давайте рассмотрим, как на самом деле работает RPC в реальных системах и с какими трудностями можно столкнуться при его масштабировании.
RPC в реальном мире
Использование RPC в реальных системах сопряжено с рядом практических трудностей. Вот четыре наиболее распространённые из них, с которыми приходится сталкиваться разработчикам:
1. Обнаружение сервисов (как сервисы находят друг друга?)
В современных системах серверы постоянно меняются: их добавляют, удаляют или перезапускают, часто присваивая им новые IP-адреса.
Но вы не можете просто указать местоположение сервера в своём коде. Так как же клиент узнаёт, куда отправлять запрос?
Вот тут-то и вступает в игру обнаружение сервисов.
Аналогия: Представьте, что это живой, самообновляющийся список контактов. Вам не нужно запоминать точный домашний адрес вашего друга. Вместо этого вы просто ищете его имя в списке контактов, чтобы узнать его последний адрес.
Как это работает: Центральный реестр (например, Consul или Zookeeper) отслеживает все работоспособные сервисы и их местоположение. Поэтому, когда клиент хочет связаться с платёжным сервисом, он спрашивает у реестра: «Где я могу его найти?»
Затем реестр возвращает список работоспособных серверов, и клиент выбирает один из них для отправки RPC-вызова.
2. Эволюция API (как обновлять сервисы, не нарушая работу всего остального?)
Когда сервис добавляет новую функцию, может потребоваться изменить параметры функции. Если клиенты не обновят систему сразу, это может привести к сбоям из-за тесной связи.
Решение заключается в обратной совместимости.
Аналогия: Представьте, что вы разослали форму для опроса. Вы можете спокойно добавить новый необязательный вопрос; старые формы по-прежнему будут работать. Но если вы удалите вопрос или сделаете новый вопрос обязательным, все старые формы станут недействительными.
Как это работает: RPCфреймворки, такие как gRPC, следуют строгим правилам обновления API. Например, вы можете спокойно добавлять необязательные поля, но не должны удалять или изменять существующие. Это позволяет новым версиям сервиса работать без сбоев, а старым клиентам — продолжать работать.
3. Потоковая передача (больше, чем просто запрос и ответ)
RPC не ограничивается моделью «один запрос — один ответ».
Современные фреймворки, такие как gRPC, поддерживают потоковую передачу, при которой данные непрерывно передаются между клиентом и сервером.
Аналогия: Обычный RPC-вызов похож на отправку текстового сообщения и получение одного ответа. Потоковый RPC-вызов похож на телефонный звонок или видеопоток: обе стороны могут обмениваться информацией в режиме реального времени.
- Потоковая передача с сервера: клиент отправляет один запрос и получает в ответ поток данных (например, при подписке на обновления в реальном времени).
- Клиентская потоковая передача: клиент отправляет поток сообщений и получает один ответ (например, при загрузке большого файла).
- Двунаправленная потоковая передача: и клиент, и сервер могут отправлять сообщения в любое время (например, в чате в реальном времени).
4. Обработка ошибок и коды состояния (сообщение о том, что пошло не так)
Когда что-то идёт не так, клиенту нужно больше информации, чем просто сообщение «что-то пошло не так». Тогда он сможет предпринять необходимые шаги.
Вот тут-то и появляются коды состояния.
Как и в случае с кодами состояния HTTP (404 Not Found, 403 Forbidden, 500 Internal Server Error), в RPC-фреймворках есть свои стандартизированные коды:
Как это работает: Возвращая определённый код ошибки, сервер сообщает клиенту, что делать дальше.
- Повторите попытку, если услуга была временно недоступна.
- Сообщите об ошибке ввода данных пользователем, если аргумент был недопустимым.
- Или прекратите попытки, если проблему невозможно решить автоматически.
Теперь, когда мы увидели, как работает RPC в реальных системах, давайте сравним его с другими моделями взаимодействия.
Выбор правильного шаблона общения
- RPC (gRPC, Thrift): лучше всего подходит для высокопроизводительных внутренних микросервисов.
- REST: идеально подходит для общедоступных API и операций, ориентированных на ресурсы.
- GraphQL: отлично подходит для получения данных, специфичных для конкретного клиента.
- Очереди сообщений: идеально подходят для асинхронных, автономных рабочих процессов.
Итог
RPC скрывает большую часть сетевых деталей, но не устраняет их полностью. Вам всё равно нужно предусмотреть возможность сбоев.
В то же время gRPC с буферами протокола стал новым стандартом для быстрой и надёжной связи между внутренними сервисами.
Помните, что успешная настройка RPC зависит от поддерживающих систем. Поэтому для бесперебойной работы необходимы обнаружение сервисов, трассировка и мониторинг.
Перевод: https://newsletter.systemdesign.one/p/how-rpc-works