Понимание концепций gRPC, сценариев использования и лучших практик

По мере прогресса в разработке приложений, среди множества вещей, есть одна основная вещь, которой мы меньше беспокоимся: вычислительная мощность. Поскольку появились поставщики облачных услуг, мы меньше беспокоимся о управлении дата-центрами. Всё доступно в течение секунд по требованию. Это приводит к увеличению размера данных. Большие данные генерируются и передаются с использованием различных сред в одном запросе.

С увеличением размера данных к этому добавляются такие действия, как сериализация, десериализация и транспортные расходы. Хотя мы не беспокоимся о вычислительных ресурсах, латентность становится накладными расходами. Нам нужно сократить транспортные расходы. В прошлом было разработано множество протоколов обмена сообщениями для решения этой проблемы. SOAP был громоздким, а REST – урезанной версией, но нам нужен еще более эффективный фреймворк. Именно здесь появляются Удаленные Процедурные Вызовы — RPC.

В этой статье мы рассмотрим, что такое RPC, различные реализации RPC, с фокусом на gRPC, который является реализацией RPC от Google. Мы также сравним REST с RPC и рассмотрим различные аспекты gRPC, включая безопасность, инструментарий и многое другое.

Что такое RPC?

RPC расшифровывается как Удаленные Процедурные Вызовы. Определение уже в названии. Процедурные вызовы просто означают вызов функции/метода; именно слово “Удаленный” делает всё отличие. А что, если мы можем вызывать функцию удаленно?

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

Этапы, связанные с вызовом RPC:

Последовательность потока RPC

Вот как обычно выглядит процесс REST:

RPC сводит процесс к следующему:

Это потому, что все осложнения, связанные с созданием запроса, теперь абстрагированы от нас (мы обсудим это в генерации кода). Все, о чем нам нужно беспокоиться, это данные и логика.

gRPC: Что, Зачем и Как это работает

До сих пор мы обсуждали RPC, который по сути означает вызов функций/методов удаленно — тем самым предоставляя нам преимущества, такие как “строгое определение контракта,” “абстрагирование передачи и преобразования данных,” “снижение задержек” и так далее, о чем мы будем говорить, продолжая эту публикацию. То, в чем мы действительно хотели бы углубиться, — это одна из реализаций RPC. RPC — это концепция, а gRPC — это фреймворк на ее основе.

Существуют различные реализации RPC. Вот они:

  • gRPC (Google)

  • Thrift (Facebook)

  • Finalge (Twitter)

Версия RPC от Google называется gRPC. Она была представлена в 2015 году и с тех пор набирает популярность. Это один из наиболее предпочитаемых механизмов коммуникации в архитектуре микросервисов.

gRPC использует протоколы буферов (открытый формат сообщений) в качестве стандартного метода коммуникации между клиентом и сервером. Также, gRPC использует HTTP/2 в качестве стандартного протокола. Существует четыре типа коммуникации, которые поддерживает gRPC:

Переходя к формату сообщения, широко используемому в gRPC — протоколам буферов, также известным как протобуферы. Сообщение протобуфера выглядит примерно следующим образом:

message Person {
string name = 1;
string id = 2;
string email = 3;
}

Здесь «Person» — это сообщение, которое мы хотим передать (в рамках запроса/ответа), которое имеет поля «name» (тип string), «id» (тип string) и «email» (тип string). Числа 1,2,3 обозначают позицию данных (например, «name», «id» и «has_ponycopter») при их сериализованном преобразовании в двоичный формат.

Как только разработчик создал файл(ы) Protocol Buffer со всеми сообщениями, мы можем использовать компилятор протокола (бинарный файл), чтобы скомпилировать написанный файл протокола, который сгенерирует все вспомогательные классы и методы, необходимые для работы с сообщением. Например, как показано здесь, сгенерированный код (в зависимости от выбранного языка) будет выглядеть как это.

Как Определить Сервисы?

Нам нужно определить сервисы, которые используют вышеупомянутые сообщения для отправки/приема.

После написания необходимых типов запроса и ответа, следующим шагом является написание самого сервиса.

gRPC сервисы также определяются в Protocol Buffers и используют ключевые слова “service” и “RPC” для определения сервиса.

Взгляните на содержимое нижеследующего файла proto:

message HelloRequest {
string name = 1;
string description = 2;
int32 id = 3;
}

message HelloResponse {
string processedMessage = 1;
}

service HelloService {
rpc SayHello (HelloRequest) returns (HelloResponse);
}

Здесь HelloRequest и HelloResponse — это сообщения, а HelloService предоставляет один унарный RPC под названием SayHello, который принимает HelloRequest в качестве входных данных и возвращает HelloResponse в качестве выходных.

Как упоминалось, HelloService в настоящее время содержит единственный унарный RPC. Но он может содержать более одного RPC. Также он может включать различные типы RPC (унарные/потоковые клиента/потоковые сервера/двунаправленные).

Для определения потокового RPC необходимо просто добавить префикс ‘stream ’ перед аргументом запроса/ответа, определения прототипов потоковых RPC и сгенерированный код.

В ссылке на код:

gRPC против REST

Мы довольно много говорили о gRPC. Также упоминался REST. Пропущенным было обсуждение различий. Я имею в виду, когда у нас есть хорошо установленный, легковесный коммуникационный фреймворк в виде REST, зачем было искать другой коммуникационный фреймворк? Давайте узнаем больше о gRPC по сравнению с REST, а также о преимуществах и недостатках каждого из них.

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

  • Формат сообщения: протокольные буферы против JSON

    • Скорость сериализации и десериализации намного выше в случае протокольных буферов для всех размеров данных (малых/средних/больших). Результаты тестовых бенчмарков

    • После сериализации JSON читаем для человека, в то время как protobuf (в бинарном формате) нечитаемы. Не уверен, является ли это недостатком, потому что иногда хочется видеть детали запроса в инструментах разработчиков Google или темах Kafka, и в случае protobuf ничего нельзя разобрать. 

  • Протокол связи: HTTP 1.1 vs. HTTP/2T

    • REST основан на HTTP 1.1; коммуникация между клиентом и сервером REST требует установленного TCP-соединения, которое, в свою очередь, включает в себя трехэтапное рукопожатие. Когда мы получаем ответ от сервера после отправки запроса с клиента, TCP-соединение после этого не существует. Для обработки другого запроса необходимо создать новое TCP-соединение. Установка TCP-соединения на каждый запрос увеличивает задержку.

    • Таким образом, gRPC, основанный на HTTP 2, столкнулся с этой проблемой, используя постоянное соединение. Следует помнить, что постоянные соединения в HTTP 2 отличаются от соединений в веб-сокетах, где TCP-соединение перехватывается и передача данных не контролируется. В соединении gRPC, как только TCP-соединение установлено, оно используется для нескольких запросов. Все запросы от одного и того же клиента и сервера мультиплексируются на одно и то же TCP-соединение.

  • Просто беспокоиться о данных и логике: Генерация кода как первоклассное лицо

    • Особенности генерации кода встроены в gRPC благодаря его встроенному компилятору protoc. С REST API необходимо использовать сторонний инструмент, такой как Swagger, для автоматической генерации кода для вызовов API на различных языках.

    • В случае gRPC процесс маршалинг/уммаршалинг, настройки соединения и отправки/приема сообщений абстрагирован; все, о чем нам нужно беспокоиться, это данные, которые мы хотим отправить или получить, и логика.

  • Скорость передачи

    • Поскольку двоичный формат значительно легче формата JSON, скорость передачи в случае gRPC в 7-10 раз выше, чем у REST.

Особенность

REST

gRPC

Протокол связи

Следует модели запрос-ответ. Может работать с любой версией HTTP, но обычно используется с HTTP 1.1

Следует модели клиент-ответ и основан на HTTP 2. Некоторые серверы имеют обходные пути для работы с HTTP 1.1 (через шлюзы REST)

Поддержка браузера

Работает везде

Ограниченная поддержка. Необходимо использовать gRPC-Web, который является расширением для веб-приложений и основан на HTTP 1.1

Структура данных пакета

В основном использует JSON и XML-основанные пакеты для передачи данных

Использует протоколы буферов по умолчанию для передачи пакетов

Генерация кода

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

gRPC имеет встроенную поддержку генерации кода для различных языков

Кеширование запросов

Легко кешировать запросы на стороне клиента и сервера. Большинство клиентов/серверов поддерживают это встроенно (например, через cookies)

По умолчанию не поддерживает кеширование запросов/ответов

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

Теперь, когда мы сравнили оба фреймворка: gRPC и REST, когда и какой из них использовать?

  • В архитектуре микросервисов с множеством легковесных микросервисов, где эффективность передачи данных является ключевой, gRPC будет идеальным выбором.

  • Если требуется генератор кода с поддержкой нескольких языков, gRPC должен быть основным фреймворком.

  • С возможностями потоковой передачи gRPC, реальное время приложений, таких как торговля или OTT, извлекут выгоду из этого, а не из опроса с использованием REST.

  • Если ограничен пропускной способностью, gRPC обеспечит гораздо более низкую задержку и пропускную способность.

  • Если требуется более быстрое развитие и высокоскоростное итерационное проектирование, REST должен быть основным вариантом.

Концепции gRPC

Балансировка нагрузки

Хотя постоянная связь решает проблему задержки, она создает другую проблему в виде балансировки нагрузки. Так как gRPC (или HTTP2) создает постоянные соединения, даже при наличии балансира нагрузки, клиент формирует постоянное соединение с сервером, который находится за балансировщиком нагрузки. Это аналогично липкой сессии.

Мы можем понять проблему или вызов через демонстрацию. При этом код и файлы развертывания находятся по адресу: https://github.com/infracloudio/grpc-blog/tree/master/grpc-loadbalancing.

Из вышеупомянутого кода базы демонстрации мы можем узнать, что ответственность за балансировку нагрузки ложится на клиента. Это приводит к тому, что преимущество gRPC, а именно постоянная соединение, в этом изменении не существует. Но gRPC все еще можно использовать из-за других своих преимуществ.

Подробнее о балансировке нагрузки в gRPC.

В вышеупомянутом коде базы демонстрации используется/демонстрируется только стратегия балансировки нагрузки по кругу. Но gRPC также поддерживает другую стратегию балансировки нагрузки на стороне клиента OOB, называемую “pick-first”.

Более того, поддерживается также настраиваемая клиентская сторона балансировки нагрузки.

Чистый контракт

В REST контракт между клиентом и сервером документирован, но не строг. Если мы вернемся еще дальше к SOAP, контракты были представлены через файлы wsdl. В REST мы предоставляем контракты через Swagger и другие положения. Но строгость отсутствует, мы не можем быть уверены, изменился ли контракт на стороне сервера во время разработки клиентского кода.

При использовании gRPC контракт либо через прото файлы, либо сгенерированные стержни из прото файлов, делится между клиентом и сервером. Это похоже на вызов функции, но удаленно. И поскольку мы вызываем функцию, мы точно знаем, что должны отправить и что ожидаем в качестве ответа. Сложности установления соединений с клиентом, заботы о безопасности, сериализации-десериализации и т.д. абстрагированы. Все, о чем мы заботимся, – это данные.

Рассмотрим следующую базу кода:

https://github.com/infracloudio/grpc-blog/tree/master/greet_app     

Клиент использует стержень (сгенерированный код из прото файла) для создания объекта клиента и вызова удаленной функции: 

 

```sh

import greetpb "github.com/infracloudio/grpc-blog/greet_app/internal/pkg/proto"



cc, err := grpc.Dial(“<server-address>”, opts)

if err != nil {

    log.Fatalf("could not connect: %v", err)

}



c := greetpb.NewGreetServiceClient(cc)



res, err := c.Greet(context.Background(), req)

if err != nil {

    log.Fatalf("error while calling greet rpc : %v", err)

}

```

Точно так же сервер также использует тот же стержень (сгенерированный код из прото файла) для приема объекта запроса и создания объекта ответа: 

 

```sh

import greetpb "github.com/infracloudio/grpc-blog/greet_app/internal/pkg/proto"



func (*server) Greet(_ context.Context, req *greetpb.GreetingRequest) (*greetpb.GreetingResponse, error) {

 

  // сделай что-то с 'req'

 

   return &greetpb.GreetingResponse{

    Result: result,

      }, nil

}

```

Оба они используют один и тот же стержень, сгенерированный из прото файла, находящегося здесь.

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

 

```sh

protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative internal/pkg/proto/*.proto

```

Безопасность

Аутентификация и авторизация gRPC работает на двух уровнях:

  • Аутентификация/авторизация на уровне вызова обычно обрабатывается через токены, которые применяются в метаданных при совершении вызова. Пример аутентификации на основе токенов.

  • Аутентификация на уровне канала использует клиентский сертификат, который применяется на уровне подключения. Он также может включать учетные данные аутентификации/авторизации на уровне вызова для автоматического применения ко всем вызовам на канале. Пример аутентификации на основе сертификата.

Любая или обе эти механизмы могут быть использованы для обеспечения безопасности сервисов.

Middleware

В REST мы используем middleware для различных целей, таких как:

  • Ограничение скорости

  • Проверка до/после запроса/ответа

  • Обработка угроз безопасности

Мы можем достичь того же с помощью gRPC. Терминология в gRPC отличается — их называют interceptors, но они выполняют аналогичные действия.

В ветке middleware в кодовой базе ‘greet_app’ мы интегрировали логгер и interceptors Prometheus.

Посмотрите, как настроены interceptors для использования пакетов Prometheus и логирования здесь.

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

Поддерживаемые middleware фреймворка gRPC.

Упаковка, Версионирование и Код Практики Proto Files

Упаковка

Давайте пойдем по ветке упаковки.

Во-первых, начните с файла ‘Taskfile.yaml’, где задача ‘gen-pkg’ указывает ‘protoc –proto_path=packaging packaging/*.proto –go_out=packaging’. Это означает, что ‘protoc’ (компилятор) преобразует все файлы в ‘packaging/*.proto’ в соответствующие файлы ‘go’, как указано флагом ‘–go_out=packaging’, прямо в директории ‘packaging’.

Во-вторых, в файле ‘processor.proto’ определены 2 сообщения: ‘CPU’ и ‘GPU’. При этом CPU представляет собой простую структуру с 3 полями встроенных типов данных, а GPU, с другой стороны, имеет пользовательский тип данных под названием ‘Memory’. ‘Memory’ является отдельным сообщением и определено в другом файле.

Так как же использовать сообщение ‘Memory’ в файле ‘processor.proto’? Используя импорт.

Даже если вы попытаетесь сгенерировать файл proto, выполнив задачу ‘gen-pkg’ после упоминания импорта, это вызовет ошибку. Поскольку по умолчанию ‘protoc’ предполагает, что оба файла ‘memory.proto’ и ‘processor.proto’ находятся в разных пакетах. Поэтому необходимо указать одинаковое имя пакета в обоих файлах.

Опциональный ‘go_package’ указывает компилятору создать имя пакета как ‘pb’ для файлов go. Если должны быть созданы файлы proto для любого другого языка, имя пакета будет ‘laptop_pkg’.

Версионирование

  • В gRPC могут быть два вида изменений: критические и некритические.

  • Некритические изменения включают добавление нового сервиса, добавление нового метода к сервису, добавление поля в прото-запрос или ответ, и добавление значения в перечисление

  • Критические изменения, такие как переименование поля, изменение типа данных поля, номера поля, переименование или удаление пакета, сервиса или методов требуют версионирования сервисов

  • Необязательно упаковка.

Практики кодирования 

  • Сообщение запроса должно заканчиваться на запрос `CreateUserRequest`

  • Сообщение ответа должно заканчиваться на запрос `CreateUserResponse`

  • Если сообщение ответа пустое, вы можете использовать пустой объект `CreateUserResponse` или использовать `google.protobuf.Empty`

  • Имя пакета должно иметь смысл и быть версионированным, например: пакет `com.ic.internal_api.service1.v1`

Инструменты

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

  • protoc-gen-grpc-gateway — плагин для создания шлюза REST API для gRPC. Он позволяет использовать конечные точки gRPC как конечные точки REST API и выполняет преобразование из JSON в прото. В основном, вы определяете сервис gRPC с некоторыми пользовательскими аннотациями, и он делает эти методы gRPC доступными через REST с использованием запросов JSON.

  • protoc-gen-swagger — спутниковый плагин для grpc-gateway. Он способен генерировать swagger.json на основе пользовательских аннотаций, необходимых для шлюза gRPC. Затем вы можете импортировать этот файл в свой выбранный клиент REST (например, Postman) и выполнять вызовы REST API к методам, которые вы раскрыли.

  • protoc-gen-grpc-web — плагин, который позволяет нашему фронтенду общаться с бэкэндом с помощью вызовов gRPC. Отдельный блог о этом появится в будущем.

  • protoc-gen-go-validators — плагин, который позволяет определять правила валидации для полей сообщений прото. Он генерирует метод Validate() error для сообщений прото, который можно вызывать в GoLang для проверки, соответствует ли сообщение вашим предварительно определенным ожиданиям.

  • https://github.com/yoheimuta/protolint — плагин для добавления правил линтинга в файлы прото

Тестирование с использованием POSTMAN

В отличие от тестирования REST API с помощью Postman или любых эквивалентных инструментов, таких как Insomnia, тестирование gRPC сервисов не совсем удобно.

Примечание: gRPC-сервисы также можно протестировать из CLI с использованием инструментов, таких как evans-cli. Но для этого необходима поддержка отражения (если она не включена, то требуется путь к файлу прото обязателен). Для включения отражения в gRPC-серверах необходимо внести изменения и узнать, как войти в режим REPL evans-cli. После входа в режим REPL evans-cli, gRPC-сервисы можно тестировать из CLI, и процесс описан на странице GitHub evans-cli.

Postman имеет бета-версию для тестирования gRPC-сервисов.

Вот шаги, как это можно сделать:

  1. Откройте Postman

  2. Перейдите в ‘APIs’ в левой боковой панели

    

  1. Нажмите на знак ‘+’ для создания нового API: 

    

  1. В появившемся окне введите ‘Имя’, ‘Версия’, и ‘Детали схемы’ и нажмите на создание [если только вы не хотите импортировать из источников, таких как github/bitbucket]. Этот шаг актуален, если вы хотите вставить контракт прото.

5. Ваш API создается, как показано ниже. Нажмите на версию ‘1.0.0’, перейдите в определение и введите ваш контракт proto.

  1. Помните, что импорт здесь не работает, поэтому лучше хранить все зависимые протосы в одном месте.

  2. Вышеуказанные шаги помогут сохранить контракты для будущего использования.

  3. Затем нажмите на ‘Новый’ и выберите ‘gRPC запрос’:

  1. Введите URI и выберите прото из списка сохраненных API:

    

  1. Введите ваш запрос сообщения и ‘Вызовите’:

В вышеуказанных шагах мы разобрались с процессом тестирования наших gRPC API через POSTMAN. Процесс тестирования конечных точек gRPC отличается от тестирования конечных точек REST с использованием POSTMAN. Важно помнить, что при создании и сохранении контракта proto, как в #5, все определения сообщений proto и сервисов должны находиться в одном месте. Поскольку в POSTMAN не предусмотрена возможность доступа к сообщениям proto между версиями.

Вывод

В этом посте мы разработали представление о RPC, провели параллели с REST, а также обсудили их различия, затем перешли к обсуждению реализации RPC, а именно gRPC, разработанного Google. 

gRPC как фреймворк может быть критически важным, особенно для архитектуры на основе микросервисов для внутреннего взаимодействия. Его также можно использовать для внешнего взаимодействия, но потребуется шлюз REST. gRPC является обязательным для потоковых и реальных приложений. 

Так как Golang продолжает проявлять себя как язык сценариев для серверной стороны, gRPC продолжает проявлять себя как де-факто фреймворк для коммуникации.

Source:
https://dzone.com/articles/understanding-grpc-concepts-use-cases-amp-best-pra