Введение
Доброго времени суток, коллеги. Я go разработчик, по-этому примеры будут преимущественно на нём. Хочу порассуждать о методах взаимодействия сервисов. Тема очень обширна. Зачастую мы пользуемся реализациями, которые не всегда подходят, т.к. не знаем куда применить ту или иную технологию. Я хочу попытаться начать закрывать этот пробел как у себя, так и у людей. Любые комментарии и конструктивные исправления приветствуются.
В данной статье хочу разобрать как работает gRPC, что он может, а так же когда и зачем его использовать.
Основные методы взаимодействия сервисов
REST (Representational State Transfer)
Архитектурный стиль, основанный на принципах, описанных в диссертации Роя Филдинга.
Использует стандартные методы HTTP (GET, POST, PUT, DELETE) для взаимодействия с ресурсами.
Обмен данных часто происходит в формате JSON или XML.
RPC (Remote Procedure Call):
Механизм, позволяющий вызывать функции или процедуры на удаленном сервере, как если бы они были локальными.
Примеры: gRPC, SOAP, JSON-RPC.
Message Brokers:
Асинхронный метод обмена сообщениями между различными компонентами системы.
Примеры: RabbitMQ, Apache Kafka, Apache ActiveMQ.
Что такое RPC?
Remote Procedure Call (RPC) – это протокол взаимодействия между клиентом и сервером, который позволяет клиенту вызывать процедуры (функции, методы) на удаленном сервере, как если бы они были локальными. Это обеспечивает абстракцию взаимодействия по сети и позволяет программам работать в распределенной среде, скрывая сложности передачи данных и выполнения удаленных операций.
Основные компоненты RPC
Клиент: Программа или компонент, инициирующий вызов удаленной процедуры.
Сервер: Программа или компонент, предоставляющий методы, которые могут быть вызваны удаленно.
Прокси: Клиент использует прокси-объект для вызова удаленных процедур на сервере, как если бы они были локальными.
Сериализация: процесс преобразования данных и параметров процедур в формат, который может быть передан через сеть (например, в формат JSON, бинарный формат и т. д.).
Транспорт: механизм передачи сериализованных данных между клиентом и сервером, например, http.
IDL (Interface Definition Language): Язык определения интерфейса, который определяет структуру и сигнатуры удаленных процедур.
Протокол RPC позволяет разработчикам вызывать функции или методы на удаленных серверах таким образом, что код клиента выглядит так, как будто вызовы происходят локально. Процессы сериализации и десериализации обеспечивают преобразование данных между форматами, понятными клиенту и серверу.
Преимущества RPC включают простоту использования, удобство абстракции и возможность вызова удаленных процедур, не заботясь о деталях взаимодействия по сети.
Когда применять?
В чём основное отличие монолитного приложение от распределенного?
Монолитное приложение может так же иметь множество модулей-сервисов, у которых своя логика. Допустим, один модуль отвечает за пользователей, другой за товары, третий за рассылку каких-то сообщений, четвертый за логически сложный расчёт бонусов и скидок.
У нас одна программа, но в ней выделено множество отдельно связанных функциональностей. Все они обращаются за какими-то “просьбами” друг к другу. Т.к. у нас всё в одной программе, мы выделяем какие-то подходящие абстракции, связи между ними, которые отделяют реализацию и типизируют вызовы. Мы реализовали интерфейс, условно имплементировали его к себе в модуль и начинаем общаться через эти абстракции. По факту всё лежит в куче (иногда попадает в стек) в оперативной памяти, мы тянем функции по адресам, но в коде мы отделили реализации. Разработчик выдохнул, можно разрабатывать поверх продуманной архитектуры.
Однако, что если эти модули вынести в отдельные программы-сервисы? Это будут разные программы. Один сервис не может получить доступ к реализации другого сервиса напрямую, через его виртуальную оперативную память. Для связи нам нужно задействовать транспортные протоколы связи. Необходимо послать “просьбу” на сервис. По просьбе сервиса-клиента, этот сервис-сервер сам у себя активирует нужный участок памяти и вызовет функцию.
Методы связи разные, но нам по-прежнему надо писать человекоудобный код. А еще система остается одна, просто состоит из множества программ, а не из одной.
В итоге мы сели, рисовали и думали о плюсах и минусах подходов. Под наши нужды была выбрана распределенная микросервисная архитектура. Но как лучше связать связать сервисы? Хочется упростить написание, делать сразу читаемый и чистый код. Мы хотим как в монолите сделать абстракцию-интерфейс. С помощью абстракции мы могли бы вызывать нужные нам методы напрямую. С нужным интерфейсом мы бы точно знали реализацию, а не просто посылали бы запросы по HTTP. Звучит как RPC. Можно реализовать интерфейс и так же тянуть методы напрямую.
Почему gRPC?
И так, мы выбрали RPC подход. Но как нам лучше его реализовать, ведь выбор есть. Какой инструмент выбрать? Ситуации бывают разные, реализации бывают разные так же. Нужно знать и понимать какие инструменты бывают в принципе. Приведу некоторые для примера:
gRPC (gRPC Remote Procedure Calls) - это фреймворк, разработанный компанией Google, который предоставляет мощный и эффективный механизм для реализации RPC в различных языках программирования, включая Go. Он использует Protocol Buffers для определения структуры данных и HTTP/2 для передачи данных. Хотя Protobuf не единственный способ представления данных, но самый распиаренный. Появляются такие инструменты, например, как FlatBuffers, которые пытаются ускорить сериализацию данных. В gRPC также поддерживаются различные языки, что делает его очень гибким.
JSON-RPC - это простой протокол для обмена данными в формате JSON между сервером и клиентом. В Go есть библиотеки, такие как "github.com/gorilla/rpc/jsonrpc" или "github.com/ybbus/jsonrpc", которые могут использоваться для реализации JSON-RPC в Go.
Пакет net/rpc в стандартной библиотеке Go предоставляет базовую поддержку RPC. Он использует формат Gob (Binary JSON) для сериализации данных. Вместе с пакетом net/rpc/jsonrpc, он может обеспечивать JSON-RPC.
Twirp - это фреймворк для создания простых и эффективных API с использованием протокола RPC. Он также основан на Protocol Buffers.
Допустим, у меня несколько сервисов на go с общими моделями, и единственное что может произойти - добавиться новый сервис также на go. При том запросы будут максимально простые, вида: отправь сообщение, дай список сообщений. Тогда я подумаю о выборе стандартного пакета net/rpc.
Если потребуется что-то сложнее, то мне придётся писать дополнительные “обвязки” на net/rpc, а если добавятся сервисы на других языках, то нужно будет реализовывать совершенно другие интерфейсы для работы с ними.
По таким причинам обычно берут самый популярный, удовлетворяющий производительностью, хорошо документированный, мультиязычный и функциональный инструмент. В европейских регионах это gRPC. В европейских, потому что Азия живет в “своём мире”, со своими решениями и перекликаются они с западом далеко не всегда.
Что такое gRPC?
gRPC - это фреймворк, т.е. достаточно комплексное решение. На данный момент он отправляет данные по протоколу http 2. На просторах github я находил попытки в http 3, но эти решения не пользуются большой популярностью.
gRPC реализует своё виденье http 2. Обычно поверх него не добавляются шифрование в виде протокола TLS, однако при необходимости его можно добавить.
Передача данных идет в бинарном формате. Данных получается меньше, они более оптимизированы, чем тот же json (весь json объект будет приведен в символы ASCII для передачи, что влечёт за собой передача неоптимальных и ненужных байтов). Сериализация проходит в бинарную систему для более оптимальной передачи.
Для примера, я хочу создать пользователя с именем 321 (такой себе Оруэлл, людей называем по номерам=)). Тогда мне надо вызвать метод, с его данными.
Данные в protobuf:
syntax = "proto3";
message User {
string name = 1;
}
Запрос:
POST /path/to/resource/CreateUser HTTP/2
Host: example.com
Content-Type: application/grpc
User-Agent: grpc-go/1.42.0
Content-Length: 5
0a033231
Разбор по строкам:
1: Указывает метод запроса и версию HTTP (HTTP/2.0).
2: Заголовок Host указывает на адрес сервера.
3: Указывает, что контент запроса представляет собой gRPC.
4: Информация о пользователе, указывающая на использование gRPC на сервере go 1.42.0 версии.
5: Указывает на длину бинарного тела запроса в байтах.
6: Тело запроса, представленное в виде 16-ричной системы для более удобной читаемости. Оно содержит бинарное представление protobuf-сообщения.
Разбор тела запроса:
0a: Тег и тип поля. В данном случае, 0a указывает на поле с тегом 1 (поле name) и типом данных "байтовая строка" (string).
03: Длина следующей строки в байтах. Здесь 03 означает, что следующая строка имеет длину 3 байта.
3231: Это ASCII-кодированное представление строки "321" в шестнадцатеричной системе счисления.
Дополнительно заголовки шифруются по принципам http 2 с помощью алгоритма HPACK.
На чём реализуется клиент и сервер не так важно, т.к. поддержка идёт множества популярных языков. Можно с помощью общей схемы protobuf реализовать go сервер, а клиент на python без трудностей, используя уже готовые и расписанные решения.
Возможности gRPC
Хотелось бы кратко структурировать возможности gRPC, которые хорошо используют функционал http 2.
Протокол-независимая сериализация:
По умолчанию используется Protocol Buffers (ProtoBuf) для сериализации данных.
Возможна поддержка других форматов сериализации, таких как JSON.
Поддержка множества языков:
gRPC поддерживает множество языков программирования, включая C++, Java, Python, Go, Ruby, C#, Node.js, и многие другие.
Одновременные запросы (Multiplexing):
Позволяет отправлять несколько запросов и получать несколько ответов одновременно на одном соединении.
Streaming:
Поддерживает как однонаправленный, так и двунаправленный поток данных.
Клиенты и серверы могут отправлять последовательности сообщений.
Автоматическая генерация кода:
Генерация клиентского и серверного кода на основе описания API в proto файле (Protocol Buffers).
Сжатие данных:
Возможность сжимать данные для уменьшения объема трафика.
Поддержка SSL/TLS:
Возможность обеспечивать безопасную передачу данных с использованием SSL/TLS.
Библиотека метаданных:
Возможность отправлять и получать метаданные в заголовках запросов.
Отмена запросов:
Клиенты могут отменять отправленные запросы, что особенно полезно в асинхронных сценариях.
gRPC-заголовки:
Поддержка кастомизированных заголовков для передачи дополнительной информации.
Дополнительные аутентификационные механизмы:
Поддержка аутентификации с использованием токенов и других механизмов.
gRPC Web:
Возможность использовать gRPC в веб-браузерах с использованием gRPC Web.
Средства мониторинга и трассировки:
Интеграция с инструментами мониторинга, такими как Prometheus, и трассировки, такими как Jaeger.
Встроенная поддержка статусов:
Поддержка стандартных и пользовательских кодов статуса.
В репозитории github есть примеры <- использования gRPC.
Пару слов о gateway
gRPC можно использовать и для генерации REST API, с помощью grpc-gateway. Останавливаться не буду, дам пример и объяснение смысла этого. Grpc-gateway генерирует интерфейсы под RPC и REST. Таким образом к серверу одновременно можно подключаться и по RPC, и по REST. Например, браузер посылает запрос по REST для получения пользователей, а какой-нибудь промежуточный сервис будет вызывать методы RPC для создания этих пользователей.
Я находил не так много примеров реализации gateway, так что хотел бы добавить свой пример <-. В Makefile можно найти пример генерации интерфейсов.
Заключение
Я считаю, что надо чаще поднимать темы проектирования. Вопросы о применении технологий, решений и архитектуры должны решаться верно сразу, т.к. ошибки в фундаментальных выборах влекут за собой большие потери и тех. долг. Стоит ли применять gRPC, RPC и вообще распределенную архитектуру зависит только от потребностей.
Я буду корректировать и дополнять информацию статьи и свои знания, если будет конструктивная критика. Прошу Вас о ней, буду благодарен. Хорошего дня. =)