Как создать более полную структуру RPC вручную

Java задняя часть RPC
Как создать более полную структуру RPC вручную

источник

Недавно делился раздачей RPC в компании, поэтому подведу итоги.

Концепции

Что такое РПЦ?

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

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

Базовая архитектура RPC Framework

Давайте поговорим об базовой архитектуре фреймворка RPC через картинкуimage.pngПлатформа RPC состоит из трех наиболее важных компонентов, а именно клиента, сервера и реестра. В процессе вызова RPC эти три компонента взаимодействуют следующим образом:

  • После запуска сервера он опубликует в реестре список предоставляемых им услуг, а клиент подпишется в реестре на адрес службы;

  • Клиент будет вызывать сервер через локальный прокси-модуль Proxy, а модуль Proxy получает и отвечает за преобразование данных, таких как методы и параметры, в сетевые потоки байтов;

  • Клиент выбирает один из адресов службы из списка услуг и отправляет данные на сервер по сети;

  • После получения данных сервер декодирует их для получения информации о запросе;

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

Процесс коммуникации в рамках RPC и задействованные роли

image.pngИз приведенного выше рисунка видно, что структура RPC обычно имеет следующие компоненты: управление службами (обнаружение регистрации), балансировка нагрузки, отказоустойчивость, сериализация/десериализация, кодек, сетевая передача, пул потоков, динамический прокси и, конечно же, другие роли. В некоторой инфраструктуре RPC также будут роли, такие как пул соединений, ведение журнала и безопасность.

Конкретный процесс вызова

image.png

  1. Потребитель услуги (клиент) вызывает услугу в режиме локального вызова

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

  3. Клиентская заглушка кодирует сообщение и отправляет его на сервер

  4. Заглушка сервера декодирует сообщение после получения сообщения

  5. Заглушка сервера вызывает локальную службу в соответствии с результатом декодирования.

  6. Локальная служба выполняется и возвращает результат серверной заглушке.

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

  8. Клиентская заглушка получает сообщение и декодирует его.

  9. Потребитель услуги (клиент) получает результат

Протокол сообщений RPC

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

Настоящий бой

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

  • Как пользователи могут использовать нашу инфраструктуру RPC с минимально возможной настройкой
  • Как зарегистрировать сервис на ЗК (тут центр регистрации выбирает ЗК) и сделать так, чтобы пользователь не знал
  • Как вызвать прозрачного (насколько пользователь не может воспринимать) звонящего поставщика услуг
  • Как включить динамическую балансировку нагрузки для нескольких поставщиков услуг
  • Как платформа позволяет пользователям настраивать компоненты расширения (например, расширять пользовательские стратегии балансировки нагрузки)
  • Как определить протокол сообщения и кодек
  • ...и т.д

Вышеуказанные проблемы будут решены при разработке этой структуры RPC.

Технический отбор

  • Регистрационный центр В настоящее время к зрелым центрам регистрации относятся Zookeeper, Nacos, Consul и Eureka, здесь в качестве центра регистрации используется ZK, а функции переключения и пользовательских центров регистрации не предусмотрены.

  • IO Communication Framework Эта реализация использует Netty в качестве основных структур связи, поскольку Netty - это высокопроизводительная структура, управляемая событиями, не блокирующими IO (NIO). Он не предоставляет другим реализациям и не поддерживает пользовательские структуры связи.

  • протокол сообщения В этой реализации используется собственный протокол сообщений, который будет объяснен позже.

Общая структура проекта

image.pngИз этой структуры можно узнать, что модули, начинающиеся с rpc, являются модулями фреймворка rpc и содержимым фреймворка RPC данного проекта, при этом потребитель — потребитель сервиса, провайдер — провайдер сервиса, а провайдер -api — это открытый API службы.

общая зависимость

image.png

Введение в реализацию проекта

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

Зачем разрабатывать два пусковых устройства (клиент-стартер/сервер-стартер)?

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

Почему он разработан как стартер?

На основе механизма автоматической сборки spring boot файл spring.factories в стартере будет загружен, а в файле будет настроен следующий код.Здесь класс конфигурации нашего стартера вступит в силу, и настроим некоторые необходимые бины в класс конфигурации.

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.rrtv.rpc.client.config.RpcClientAutoConfiguration

Публикация и потребление услуг

  • Для издательских услуг
    Поставщику услуг необходимо добавить аннотацию @RpcService к предоставляемой службе. Эта пользовательская аннотация основана на @service. Это составная аннотация, имеющая функцию аннотации @service. Интерфейс службы и версия службы указываются в @service. RpcService аннотация, и сервис публикуется.На ZK он будет зарегистрирован по этим двум метаданнымimage.png

    • Принцип издательского сервиса:
      После запуска сервис-провайдера, согласно механизму автоматической сборки spring boot, вступает в силу конфигурационный класс server-starter, в постпроцессоре бина (RpcServerProvider) получается бин, украшенный аннотацией @RpcService, и метаданные аннотации зарегистрирована.к ЗК.
      image.png
  • Для бытовых услуг
    Потребительские службы должны быть идентифицированы пользовательской аннотацией @RpcAutowired, которая представляет собой составную аннотацию, основанную на @Autowired.image.png

    • Принципы потребительских услуг
      Для того, чтобы клиент незаметно вызывал поставщика услуг, необходимо использовать динамический прокси, как было показано выше, HelloWordService не имеет класса реализации, ему нужно назначить класс прокси и инициировать вызов запроса в классе прокси. На основе автоматической сборки весенней загрузки запускается потребитель службы и начинает работать постпроцессор компонента RpcClientProcessor.В основном он проходит по всем компонентам, чтобы определить, изменены ли свойства в каждом компоненте аннотацией @RpcAutowired, и если да, то сделать свойство dynamic Назначьте прокси-класс, который вызовет метод вызова прокси-класса при повторном вызове.
      image.png

      Метод вызова прокси-класса получает метаданные сервера посредством обнаружения службы, инкапсулирует запрос и инициирует вызов через netty.image.png

Регистрационный центр

Центр регистрации этого проекта использует ZK, потому что центр регистрации используется как потребителями услуг, так и поставщиками услуг. Так что поставьте ZK в модуль rpc-core.image.pngМодуль rpc-core показан на рисунке выше, и все основные функции находятся в этом модуле. Услуги регистрируются под пакетом реестра.

Интерфейс регистрации сервиса, конкретная реализация реализована с помощью ZK.image.png

стратегия балансировки нагрузки

Балансировка нагрузки определяется в rpc-core, который в настоящее время поддерживает циклический (FullRoundBalance) и случайный (RandomBalance) и по умолчанию использует стратегию случайного выбора. Задается rpc-client-spring-boot-starter.image.png

При обнаружении через службу ZK будет найдено несколько экземпляров, а затем будет получен один экземпляр с помощью стратегии балансировки нагрузки.image.png

Можно настроить в потребителеrpc.client.balance=fullRoundBalanceВ качестве альтернативы вы также можете настроить стратегию балансировки нагрузки, реализовав интерфейс LoadBalance и добавив созданный класс в IOC-контейнер. Поскольку мы настраиваем @ConditionalOnMissingBean, пользовательские компоненты будут загружены первыми.
image.png

Пользовательский протокол сообщений, кодек

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

  • собственный протокол сообщенийimage.png

    • Магическое число: Магическое число — это секретный код, согласованный обеими сторонами при общении, обычно представленный фиксированным числом байтов. Роль магического числа состоит в том, чтобы предотвратить отправку данных на порт сервера без разбора. Например, магическое число 0xCAFEBABE хранится в начале файла класса java.При загрузке файла класса в первую очередь проверяется правильность магического числа.

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

    • Алгоритм сериализации. Поле алгоритма сериализации указывает, какой метод должен использовать отправитель данных для преобразования запрошенного объекта в двоичный и как преобразовать двоичный файл в объект, например JSON, Hessian и собственную сериализацию Java.

    • Тип пакета: в разных бизнес-сценариях могут быть разные типы пакетов. В структуре RPC есть запросы, ответы, тактовые импульсы и другие типы сообщений.

    • Состояние: Поле состояния используется для определения того, является ли запрос нормальным (УСПЕХ, НЕУДАЧА).

    • Идентификатор сообщения: уникальный идентификатор запроса, с которым связывается ответ, и ссылку также можно отследить по идентификатору запроса.

    • Длина данных: укажите длину данных, которая используется для определения того, является ли это полным пакетом данных.

    • Содержимое данных: содержимое тела запроса

  • Кодек
    Кодек реализован вrpc-coreмодуль, в упаковкеcom.rrtv.rpc.core.codecВниз.

    Пользовательский кодировщик, наследующий от nettyMessageToByteEncoder<MessageProtocol<T>>Класс реализует кодировку сообщений.image.png

    Пользовательские декодеры наследуются от nettyByteToMessageDecoderКласс реализует декодирование сообщений.

    image.png image.png

При расшифровке нужно обратить внимание на проблемы залипания TCP и распаковки

Что такое залипание и распаковка TCP

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

В процессе сетевой связи размер пакетов данных, которые могут быть отправлены каждый раз, ограничивается различными факторами, такими как размер единицы передачи MTU, скользящее окно и т. д.
Таким образом, если размер данных сетевого пакета, передаваемых за один раз, превышает размер единицы передачи, то наши данные могут быть разделены на несколько пакетов данных и отправлены. Если данные сетевых пакетов каждого запроса очень малы, например, всего 10 000 запросов, TCP не будет отправлять 10 000 раз по отдельности. Для этого оптимизирован алгоритм Nagle (пакетная отправка, в основном используемый для решения проблемы перегрузки сети, вызванной частой отправкой небольших пакетов данных), принятый TCP.

Итак, передача по сети будет выглядеть так:
tcp_package.png

  1. Сервер прочитал два полных пакета A и B, и проблем с распаковкой/склеиванием не было;
  2. Сервер получает пакеты данных, которые A и B склеены вместе, и серверу необходимо разобрать A и B;
  3. Сервер получает полные части A и B пакета данных B-1, серверу необходимо проанализировать полный A и дождаться, чтобы прочитать полный пакет данных B;
  4. Сервер получает часть пакета данных A-1 от A, и в это время ему необходимо дождаться получения полного пакета данных A;
  5. Пакет данных A большой, и серверу требуется несколько раз, чтобы получить пакет данных A.

Как решить проблему залипания и распаковки TCP

Фундаментальное средство решения задачи: найти границу сообщения:

  • фиксированная длина сообщения
    Каждая дейтаграмма требует фиксированной длины. Когда получатель кумулятивно считывает сообщения фиксированной длины, он считает, что получено полное сообщение. Когда данные отправителя меньше фиксированной длины, их необходимо заполнить пробелами.
    Метод фиксированной длины сообщения очень прост в использовании, но недостатки также весьма очевидны. Невозможно очень хорошо установить значение фиксированной длины. Если длина слишком велика, это приведет к потере байтов и если длина слишком мала, это повлияет на передачу сообщения.Поэтому, как правило, сообщение имеет фиксированную длину.Закон не будет принят.
  • конкретный разделитель
    Добавляя специальный разделитель в конце каждого отправленного сообщения, получатель может разделить сообщение в соответствии со специальным разделителем. Следует избегать выбора разделителя, чтобы он совпадал с символами в теле сообщения, чтобы избежать конфликтов. В противном случае может произойти некорректное разделение сообщения. Рекомендуемая практика заключается в кодировании сообщения, например, кодировке base64, а затем вы можете выбрать символ, отличный от 64 закодированных символов, в качестве определенного разделителя.
  • длина сообщения + содержание сообщения
    Длина сообщения + содержание сообщения — наиболее часто используемый протокол при разработке проектов.Приемник считывает содержимое сообщения в соответствии с длиной сообщения.

В этом проекте используется метод «длина сообщения + содержимое сообщения» для решения проблемы залипания и распаковки TCP. Поэтому при декодировании необходимо судить о том, достаточно ли длинны данные для чтения.Если недостаточно, значит, данные не готовы.Продолжить чтение данных и декодировать их.Здесь один полный пакет данных может получиться таким образом.image.png

Сериализация и десериализация

Сериализация и десериализация вrpc-coreмодульcom.rrtv.rpc.core.serializationпакет, при условииHessianSerializationиJsonSerializationСериализация.
Использовать по умолчаниюHessianSerializationСериализация. Пользователь не может настроить.

Производительность сериализации:

  • пространственно

serialization_space.png

  • во время

serialization_time.png

Передача по сети с использованием netty

Код netty исправлен.Стоит отметить, что порядок обработчиков не может быть ошибочным.В качестве примера сервера кодирование является исходящей операцией (может быть размещено после входящей), а декодирование и получение ответа являются одновременно въездные операции.image.png

Метод вызова клиентского RPC

Зрелые платформы RPC обычно предоставляют четыре метода вызова, а именно синхронную синхронизацию, асинхронное будущее, обратный вызов обратного вызова и односторонний односторонний вызов.

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

  • Будущий асинхронный вызов
    После того, как клиент инициирует вызов, он не будет блокироваться и ждать, а получит объект Future, возвращаемый инфраструктурой RPC.Результат вызова будет кэширован сервером, и клиент сам решит, когда получить возвращаемый результат в будущем. . Когда клиент активно получает результат, процесс блокируется и ждетfuture.png

  • Обратный звонок Обратный звонок Когда клиент инициирует вызов, он передает объект обратного вызова в инфраструктуру RPC и возвращает результат напрямую, не дожидаясь синхронного результата возврата. Когда получен результат ответа сервера или исключение тайм-аута, выполняется обратный вызов обратного вызова, зарегистрированный пользователем.callback.png

  • Односторонний односторонний вызов После того, как клиент инициирует запрос, он возвращается напрямую, игнорируя возвращаемый результат.
    oneway.png

Здесь используется первый: синхронные вызовы на стороне клиента, остальные не реализованы. Логика находится в RpcFuture, используя CountDownLatch для реализации ожидания блокировки (ожидания тайм-аута).image.png

Общая архитектура и процесс

image.png

Процесс разделен на три части: процесс запуска поставщика услуг, запуск потребителя услуги и процесс вызова.

  • запуск поставщика услуг
    1. Поставщик услуг будет зависеть от rpc-server-spring-boot-starter.
    2. Запускается ProviderApplication, по механизму автоматической сборки springboot вступает в силу автоматическая настройка RpcServerAutoConfiguration
    3. RpcServerProvider — постпроцессор компонента, который публикует сервисы и регистрирует метаданные сервисов в ZK.
    4. Метод RpcServerProvider.run запустит службу netty.
  • запуск потребителя услуг
    1. Потребители услуг будут зависеть от rpc-client-spring-boot-starter
    2. Запускается ConsumerApplication, согласно механизму автоматической сборки springboot вступает в силу автоматическая настройка RpcClientAutoConfiguration
    3. Добавьте обнаружение службы, балансировку нагрузки, прокси и другие компоненты в контейнер IOC.
    4. Постпроцессор RpcClientProcessor будет сканировать bean-компонент и динамически назначать свойства, измененные @RpcAutowired, прокси-объекту.
  • процедура вызова
    1. Потребитель услуги инициирует запросhttp://localhost:9090/hello/world?name=hello
    2. Потребитель службы вызывает метод helloWordService.sayHello(), которому будет делегировано выполнение метода ClientStubInvocationHandler.invoke().
    3. Потребитель службы получает метаданные службы через обнаружение службы ZK, и ошибка 404 не обнаружена.
    4. Потребитель услуг Пользовательский протокол, инкапсулирующий заголовок и тело запроса
    5. Потребитель службы кодирует сообщение с помощью пользовательского кодировщика RpcEncoder.
    6. Потребитель услуг получает IP-адрес и порт поставщика услуг посредством обнаружения услуг и инициирует вызов через сетевой транспортный уровень Netty.
    7. Потребитель услуги вводит возвращаемый результат (тайм-аут) через RpcFuture и ждет
    8. Поставщик услуг получает запрос потребителя
    9. Поставщик услуг декодирует сообщение с помощью пользовательского декодера RpcDecoder.
    10. Поставщик услуг Декодированные данные отправляются в RpcRequestHandler для обработки, а локальный метод на стороне сервера выполняется через вызовы отражения и получается результат
    11. Поставщик услуг выполнит результат кодирования сообщения через кодировщик RpcEncoder. (Поскольку протокол запроса и ответа одинаков, кодировщик и декодер могут использовать один набор)
    12. Потребитель службы декодирует сообщение с помощью пользовательского декодера RpcDecoder.
    13. Потребитель службы записывает сообщение в пул запросов и ответов через RpcResponseHandler и устанавливает результат ответа RpcFuture.
    14. Потребители услуг получают результаты

Описанный выше процесс можно совместить с анализом кода, который будет приведен ниже в коде.

Строительство окружающей среды

  • Операционная система: Windows
  • Интегрированные средства разработки: IntelliJ IDEA
  • Стек технологий проекта: SpringBoot 2.5.2 + JDK 1.8 + Netty 4.1.42.Final
  • Инструмент управления зависимостями проекта: Maven 4.0.0
  • Центр регистрации: Zookeeper 3.7.0

испытание проекта

  • Запустите сервер Zookeeper: bin/zkServer.cmd
  • Запустите модуль провайдера ProviderApplication
  • Запустите потребительский модуль ConsumerApplication
  • Тест: ввод через браузерhttp://localhost:9090/hello/world?name=hello, успешно возвращеноПривет: привет, вызов rpc прошел успешно

Код проекта

git ee.com/listen_me/character…