Как тестировать не-REST-бэкенд. Часть третья, gRPC

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Итак, мы с вами добрались до третьей, самой «хардовой» части цикла. Сегодня поговорим про gRPC.

Что такое gRPC? 

Сам RPC — удалённый вызов процедур (иногда вызов удалённых процедур; RPC от англ. remote procedure call) — класс технологий, позволяющих программам вызывать функции или процедуры других программ, делая это так, как если бы они находились в одном адресном пространстве. Буква g в названии — это гугловая реализация этих технологий.

Разберем это все на примере.

Допустим, что вы — программист и сидите в монолитной репе. У вас одно приложение. Сам проект открыт в IDE и вы в нем работаете. В репе реализован определенный класс (например, на Kotlin), у которого есть метод, возвращающий вам данные по пользователю.

fun getUserInfo(id: String) {
   return //some data
}

Теперь в другом месте проекта, в другом классе, вы хотите вызвать этот метод, чтобы поработать с данными о пользователе. Что вы тогда делаете? Импортируете класс и просто делаете обычный вызов метода.

var userData = getUserInfo("userID")
// Continue work with data

Он выполнится, вы получите все нужные данные. Всё здорово и всё работает.

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

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

Есть сервис A, который хочет данные от пользователя. Есть сервис B, у которого есть публичный API, через который эти данные можно получить.

Что делать сервису A?

Ему нужно сделать get-запрос на endpoint, который предоставит сервис B, затем получить обратно HTTP-ответ (там будет JSON), извлечь данные и работать с ними.

Что предлагает gRPC?

А предлагает он вот что — пусть сервис B заведет некий протофайл с расширением .proto, в котором опишет те методы и функции, которые можно будет вызывать у него всем остальным. 

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

И на своем уровне поднимает RPC-службу.

Эта служба обрабатывает все запросы, которые будут приходить.

Окей, сервисом В разобрались. Что теперь делать на уровне сервиса А, чтобы получить данные о пользователе?

Мы определенным образом забираем себе модели, описанные в .proto file сервиса В. Например, подключаем в gradle через dependency этот пакет, в котором есть все описанные модели.

 

И дальше реализуем над ними обёртку со своей логикой и вызовом методов сервиса В.

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

При этом стоит заметить, что в gRPC все запросы используют Protocol  Buffers — это специальный бинарный протокол сериализации данных, разработанный в компании Google. То есть сервис А предоставит сервису В свой запрос в бинарном виде. Сервис В поймёт, какой метод у него вызывают, выполнит его и вернет результат назад. Причем ответ будет также проходить через RPC службу и будет в двоичном виде. Сервис А уже получит такой ответ и продолжит работать дальше, как ни в чем не бывало.

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

  1. На клиентской машине мы вызываем метод. Например, нам нужна информация о пользователях, и вызываем его так, как будто находимся на самом деле в сервисе B.

  2. При этом мы делаем обращение именно в реализованную ранее обёртку.

  3. Затем уже подрубаются специальные библиотеки, которые наш запрос транслируют определенным образом в двоичный запрос. Проводят сериализацию, передают его уже дальше в RPC-службу, и далее через protocol buffers всё попадает на сервер B.

  1. Он в свою очередь обратно десериализует эту двоичную последовательность в виде имени процедуры, параметров и значения. Выполняет ее, получает данные, а затем обратно проделывает всю эту трансформацию. 

  2. Данные запаковывает в бинарную последовательность и отправляет ее обратно по сети. 

  3. Мы на стороне клиента ее получаем, распаковываем данные, возвращаем обратно нашей программе и продолжаем работать дальше. 

Такой алгоритм, если в двух словах.

Честно скажу, когда я изучал эту технологию, первое, о чем я подумал — что за жесть вообще? Зачем так усложнять всё, можно же проще все делать. Есть же REST HTTP, он понятен, там есть endpoint-ы, нормальные запросы и ответы, которые можно увидеть и прочитать, увидеть данные которые передаем и получаем, их можно осмыслить. Зачем вообще все эти бинарные вещи?

На самом деле, тут есть три очень важные штуки.

Во-первых, и это по сути главная киллер-фича — скорость. Сам Google заявлял, что прирост по скорости будет в 3—10 раз, энтузиасты же, которые все это протестировали, вывели свою цифру — в 7 раз. В 7 раз быстрее происходит сериализация данных при работе с этим бинарным форматом, чем при работе с JSON.

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

Во-вторых, помните ситуацию при работе с REST HTTP, когда ребята сделали endpoint, а документацию забыли? А тестировать-то как-то надо. Поэтому в этой случае документацией станет сам разработчик.

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

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

На практике

Давайте перейдем к практике. Для начала я выкачиваем проект отсюда. Собираем и запускаем его согласно документации

Далее заходим с вами в Postman -> NEW и выбираем gRPC.

Прежде чем начнем работать, надо объяснить Postman, с каким сервисом мы будем иметь дело. Как это делается? Правильно! Надо подгрузить в него протофайл сервиса.

Переходим во вкладку Service definition.

Кнопка “Import .proto file” так и просится быть нажатой.

Нажимаем ее и далее выбираем файл.

Нужный нам протофайл будет лежать в папке со скачанным проектом, подпапка proto. Имя GrpcExampleService.proto

Выбираем его и жмем “Next”.

Postman увидел наш файл и понял, что мы будем работать с некоторым сервисом, поэтому предложит импортировать его. Я не против, поэтому жмем “Import as API”.

После успешного импорта мы увидим, что наш созданный пустой gRPC Request использует наш новый API.

Что после этого изменилось?

Вы можете нажать на селектор выбора метода.

Ого! Тут нас уже ждут все методы, с которыми умеет работать сервис.

Прикольно, а можно как-то проверить, что тут реально все методы?

Можно, заходим в левом меню Postman в раздел APIs, далее выбираем наш New API — Definition и кликаем на протофайл.

Для любознательных — можете изучить его весь, а я покажу только первые 10 строк, где у нас лежит информация о том, какие методы “открыты наружу”. Смотрим блок service.

Действительно, 3 метода — Postman не обманул, но не протестировать его я не мог

Источник: https://habr.com/ru/companies/qiwi/articles/753778/


Интересные статьи

Интересные статьи

Всем привет!Меня зовут Игорь Гулькин, и я Unity разработчик. За свои 5 лет накопилось много опыта, поэтому в этой статье хотел бы поделиться принципами и подходами, с помощью которых можно реализовать...
Когда-то я проходил серию собеседований на Backend-Java-разработчика и записывал вопросы себе на будущее, чтобы потом можно было пробежаться и освежить память. Подумалось, что, вероятно...
В прошлой части цикла «Введение в SSD» мы рассказали про историю появления дисков. Вторая часть расскажет про интерфейсы взаимодействия с накопителями. Общение между процессором и перифери...
Основное событие года, это, конечно, Windows 95. Пока ещё не очевидная победа — по прежнему сильны DOS (не только MS) и Windows 3.xx, IBM выкатывает русскую OS/2 Warp, где-то ещё шевелится Alpha ...
C 2014 года, когда в Python появилась поддержка аннотаций типов, программисты работают над их внедрением в свой код. Автор материала, первую часть перевода которого мы публикуем сегодня, говорит,...