Проблема

Когда мы разрабатываем приложение, которое разделено на независимые автономные компоненты, мы говорим о микросервисной архитектуре. Для взаимодействия компонентов используется API. Самый популярным API является REST. Это обусловлено его гибкостью, эффективностью (в большинстве сценариев) и тем, что он легко масштабируется. 

Большая часть реализаций REST использует стандарт JSON для обмена сообщениями. Обычно это удобно - сама по себе такая структура легко читается людьми и предоставляет независимость от языка программирования.

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

Зачем нужен gRPC

gRPC (Remote Procedure Calls) — это система удалённого вызова процедур (RPC) с открытым исходным кодом, первоначально разработанная в Google. В качестве основного протокола передачи применяется HTTP/2, для описания процедур применяется “Protocol Buffers”. Это в свою очередь приносит дополнительные преимущества: сжатие HTTP-заголовков и мультиплексирование запросов.

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

Типичная реализация gRPC
Типичная реализация gRPC

Когда использовать gRPC

Статистика говорит о том, что gRPC используется гораздо реже, чем REST. К сожалению, часто только из-за этого разработчики выбирают REST. И если в небольших проектах это оправдано, тогда напрашивается вопрос: в каких случаях правильным выбором всё же будет gRPC?

Стоит взглянуть на таблицу их основных отличий.

Сравнение gRPC и REST

Технология

gRPC

REST

Протокол

http 2.0

http 1.1

Формат обмена данными

protobuf

JSON

Избыточность

Нет

Да

Декларативность

Единый .proto файл

Нет единого соглашения

Хотя REST подход также может использовать HTTP 2 для обмена данными, тем не менее он будет ограничен моделью запрос-ответ, при которой не применяется поддержка двусторонней потоковой передачи данных возможной для HTTP При этом, конечно же сохраняется возможность унарных взаимодействий, которые реализует HTTP 1.1 (один запрос - один ответ).

В конечном итоге мы получаем более высокую скорость ответа в разных сценариях.

Скорость ответа REST и gRPC
Скорость ответа REST и gRPC

При этом в gRPC наименования полей, ожидаемых запросов и возвращаемых ответов определяется и описывается в одном месте - в файле .proto. И если вы хотя бы раз разрешали конфликты в наименовании полей вида ObjectID и object_id, то вам определенно понравится работать с gRPC.

Пример .proto файла:

syntax = "proto3";

/* Наименование пакета данных */

message Calculator {

    int32 xxx = 1;

    int32 yyy = 2;

}

message CalculatorRequest {

    Calculator calculator = 1;

}

message CalculatorResponse {

    int32 result = 1;

}

Когда не использовать gRPC

gRPC при всех своих достоинствах не лишен недостатков. Вот парочка:

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

  • Более сложная отладка по сравнению с REST из-за бинарного обмена сообщениями

Реальный кейс

Как упоминалось выше, для работы с gRPC достаточно одного .proto файла.

Мы делали реализацию API на Python, где использовали дуплексное (двунаправленное соединение) для сервиса потокового распознавания речи в текст.

Дуплексное соединение gRPC на Python:

def stream_recorder(self) -> Union[RecognitionRequest, str]:

    """Потоковый захват речи"""

    for _ in range(0, int(self.rate / self.chunk * self.seconds)):

        data = self.stream.read(self.chunk, exception_on_overflow=False)

        yield RecognitionRequest(audio=data)

def stream_recognize(self) -> str:

    """Потоковое распознавание речи"""

    credentials = grpc.ssl_channel_credentials()

    with grpc.secure_channel(f'{settings.SPEECH_SERVER}:{settings.SPEECH_PORT}',

                            credentials=credentials) as chennel:

        stub = VoiceAssistantServiceStub(chennel)

        metadata = [('authorization', f'Bearer {settings.SPEECH_API_TOKEN}'), ('audioformat', 'lpcm')]

        for response in stub.RecognizeAudio(self.stream_recorder(), metadata=metadata):

            if response.chunks[0].final:

                return response.chunks[0].text.strip()

Эти декларативные преимущества gRPC работают не только в момент написания кода. Особенно удобно делать отладку соединения в Postman, который предоставляет поддержку протокола. 

Даже не имея доступа к удаленному серверу мы можем имитировать запросы с моковыми данными и эмулировать ответ нажатием одной кнопки.

Поддержка gRPC в Postman
Поддержка gRPC в Postman

Выводы

В отличие от REST, gRPC - решение, которое не зависит от платформы и языка. И благодаря HTTP/2 и протоколу Protobuf решает проблемы, связанные со скоростью передачи данных, их весом, в результате повышая эффективность обмена сообщениями.

Комментарии (30)


  1. BugM
    17.12.2022 17:22
    +3

    И благодаря HTTP/2 и протоколу Protobuf решает проблемы, связанные со скоростью передачи данных, их весом, в результате повышая эффективность обмена сообщениями.

    А эти проблемы они есть? Допустим для типичного довольно нагруженного сайта с парой тысяч РПС они точно есть?


    1. alixplisov Автор
      17.12.2022 17:39
      -7

      Да, http 2 дает реальный прирост в скорости загрузки сайта, причем независимо от количества пользователей в секунду. Если говорить не только о потоковой передаче, то можно увидеть разницу даже в контексте одного единственного пользователя: здесь роль играет тот факт, что HTTP/2 позволяет эффективней обрабатывать раздельные запросы большой коллекции ресурсов. Но следует сделать ремарку, что разница в производительности несущественна для простых схем раздельной отдачи ресурсов от методов. Источник


      1. BugM
        17.12.2022 17:49
        +10

        Да вроде все освоили выкладывание статики на CDNки какие-то. От nginx в своей сети, до всемирной CDN в зависимости от. У всех приличных людей с аппликейшен серверов только джейсоны отдаются давно уже.

        Судя по вашей картинке разница между джейсон+gzip+keep_alive и grpc на уровне погрешности. Так ради чего усложнять все? Джейсон человекочитаемый и дебажить его заметно проще. Сервер и клиент для джейсона тоже заметно проще.


  1. angly
    17.12.2022 18:16
    +13

    Использование gRPC на в мобильных клиентах может значительно увеличивать размер приложений. На моей практике был случай, когда внедрение gRPC привело к раздутию приложений на 10-20 МБ на обеих платформах. И если мобильного пользователя подобным не удивить…

    … то на вебе подобное может дорого стоить. Библиотека протобафа вместе с самим вебовым API могла занимать в бандле до нескольких мегабайт.

    Сложности при этом могут быть как в отладке, так и в поддержке необходимого дополнительного кода, который все это обрабатывает. На каждой из платформ.

    Так что крайне желательно, чтоб плюсы перехода на gRPC на клиентах перевешивали все сопутствующие минусы.


  1. AlanFrost
    17.12.2022 19:09
    +4

    Не знаю на счёт веба, но gRPC очень хорошо себя показывают в высоконагруженных сетевых играх с разными системами и интерфейсами. Например вот выступление товарища из ЕVE Online на эту тему на GDC)


    1. alex_smite
      19.12.2022 00:30
      -1

      Как-то делали нагрузочное тестирование на gatling сравнивали http, http2 и grpc. Так вот, судя по результатам, grpc очень хорошо держит нагрузку. Http2 разваливается при большом количестве соединений


      1. BugM
        19.12.2022 00:41

        А что ваш тест тестирует? По тексту статьи это вообще непонятно.

        Я вам могу точно сказать что сериализация/сжатие/отправка сообщений размером в единицы килобайт на 1000 rps ни в одном сценарии ни даст никакой значимой нагрузки. Если у вас это не так, то стоит перепроверить свой тест.


        1. alex_smite
          19.12.2022 12:55

          Тестировался сервис, обмен данными с которым осуществлялся по трем rpc http, http2, grpc (3 разных теста) с небольшими возможностями инфраструктуры в кубере нагрузка давалась разная примерно до 5к одновременно работающих пользователей. Grpc нагрузку выдержал, http нет (c keep_alive не игрались)


          1. BugM
            19.12.2022 22:40

            Что такое одновременно работающие пользователи? Они ведь как-то конвертируются в rps?

            Я как и в прошлом сообщении говорю вам абсолютно точно, опираясь на реальные цифры, что 1000rps это совершенно незаметная нагрузка для типичного rest сервиса. Если не брать вашу бизнес логику, а только сериализацию со сжатием. У вас точно есть какая-то проблема в тесте, если он показал что-то другое.


  1. Heggi
    17.12.2022 19:09

    Для веб-интерфейсов есть grpc-web, который, в принципе, поддерживает почти все нужные плюшки grpc (со стримингом только шляпа, поддерживается только серверный и автореконнекта в клиенте нет, нужно колхозить свой велосипед).
    Объем библиотеки не так уж и велик, у нас в проекте админки vuetify больше весит.
    Кодогенерация на основе proto-файлов устраняет много ручной работы, что однозначно перевешивает немногие минусы.

    Но на беке к реализации grpc для некоторых языков есть вопросы (не буду показывать пальцем в Python)


  1. onegreyonewhite
    17.12.2022 19:11
    +6

    Протокол

    Ему вообще чихать на протокол. Он спокойно http/3 может использовать. Вопрос будет ли лучше от протокола. Понимаете ли вы как они устроены и в каких случаях реально дают преимущества?

    Формат обмена данными

    ...а ещё можно использовать msgpack (который без сжатия более эффективный, кстати), yaml, xml или любой другой (хоть свой изобретайте). И внезапно интегрироваться проще, больше нативной поддержки, как уже сказали выше, уменьшает размер бандла.

    Избыточность

    Вот опять же, странный маркетинговый булшит или как выкинуть непонятную метрику. Размер бадла или приложения говорит, что grpc избыточен. Более того, он более эффективно справляется в межсерверном взаимодействии, чем для клиента (а собственно кто сказал, что для конкретного PWA оно будет лучше?). Вся статья из домыслов состоит и передёргиваний.

    Декларативность

    Прям единственная правда. Единого стандарта и правда нет, потому что где-то удобнее выдать swagger 2.0, но часто лучше OpenAPI 3.0, но в некоторых случаях достаточно удобно HATEOAS. Притом каждый очень удобно подбирается под задачу, отлично жмётся br или gzip.

    Больше половины преимуществ вообще звучат в стиле "это лучше потому что новое и крутое". Как, например, HTTP/2 преподносится как крутизна, но какую конкретно проблему он решает? Если это браузер, то можно сказать никакую, потому что в 1.1 есть keep-alived и браузер поднимает 8 параллельных соединений. На самом деле, довольно много кейсов, в которых это гораздо эффективнее. Но почему тогда не HTTP/3? Он вообще ж на udp и там круто решена проблема шифрования. REST API могут в него, а grpc? Я, кстати, не шучу, мне и правда интересно может ли.

    Кстати, в http2 есть push-апи, от которого решили вообще отказаться. Так что это не односторонняя связь. Обычно, когда хотят двустороннюю связь поднимают websocket и его родственников.

    У меня всё.


    1. Heggi
      18.12.2022 06:10
      -2

      grpc умеет в http3, в последнем Net7.0 добавили поддержку, как в остальных языках - не знаю, не интересовался.


  1. virrus
    17.12.2022 19:29
    +2

    Главное - не пытайтесь использовать gRPC в Win приложениях. Скомпилировать код в dll из коробки - нельзя, авторы неправильно расставили dllexport по коду. Попытка отгрузить dll с gRPC - приводит к утечке памяти. В общем, как с любым другим гугловым кодом - убедитесь, что ваш сценарий использования совпадает с Гуглом, иначе ходить вам по минному полю.


    1. gdt
      18.12.2022 00:22

      Где можно об этом почитать? Используем у себя gRPC под Windows на C#, не замечал таких проблем


      1. mentin
        18.12.2022 03:17
        +2

        Скорее всего проблем и не будет. Те C# библиотеки что я видел - реализованы независимо, а не поверх гугловского с++ кода, так что скорее всего эти проблемы вас не коснутся.


      1. virrus
        18.12.2022 09:32
        +1

        Тут уже ответили, что C# библиотека отличается от плюсовой, мне надо было уточнить это в комментарии. Мы использовали gRPC для коммуникации между плюсовым и шарповым кодом и со стороны шарпов проблем, действительно, не было. Где это можно почитать - не знаю, у нас все шишки набиты самостоятельно и судя по коду gRPC - наш сценарий был уникальным и никем не е предусмотренным. Если бы мы использовали gRPC как статическую lib, со всеми влинковаными зависимостями, то, наверное, тоже было бы все хорошо. Но мы хотели все разделяемые библиотеки иметь динамическими (сейчас только zlib вспомню, но их было с десяток), и это резко всё усложнило.


  1. AngryEvilCookie
    17.12.2022 22:52
    -3

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


    1. BugM
      18.12.2022 00:03
      +2

      Слово "тяжелый" вы готовы подтвердить бенчмарками? В таких вопросах принято измерять, а не верить словам.


      1. AngryEvilCookie
        19.12.2022 14:03

        Жаль не ставятся комменты к минусам, чего минусят не понятно.. Да тяжелый, это же очевидно из протокола, rest с json избыточен, хоть как его сжимай бротли/зипом/дефлейтом. Т.е. по-простому: куча сжатого хоть и повторяющегося текста всегда будет хуже заранее предопределенного бинарного пакета. Измерял уже дотнетовский грпс и не я один. Если можете сервер в с++, то задержки еще меньше будут. Да размер клиента будет побольше, но не 20мбайт никак, 667 кб весит протобаф и грпс для wpf, например. Да где-то не работает хттп 2, но доступен хттп 1.1 и это все равно лучше чем рест получается, quick (хттп 3) в вин11 есть и можно в никсах подключить через бубен (не тестировал, но по судя по изменениям протокола еще быстрее работать будет). Конечно для каждого проекта нужно тестировать отдельно все, но сделать клиент и сервер на шарпе для пары унарных методов проще простого. У всех свои причины перейти на грпс, я это делал для уменьшения трафика и задержек в мобильном и впф клиенте. Если кто тестил грпс в хттп3 киньте результаты сравнения с хттп 2, интересно.


        1. BugM
          19.12.2022 22:42

          Talk is cheap. Show me the code.

          Нет ничего очевидного в вопросах производительности. Нет ничего очевидного в том какая итоговая нагрузка получится. Все надо мерять, а вы опять слова какие-то пишите. Может измерения свои покажете? Вместе с тем как вы это намеряли.


  1. IRS
    18.12.2022 00:22

    Размер сообщения? Лол. Http серверы давно умеют отдавать со сжатием данные. Что само по себе нивелирует необходимость заморачиваться с форматами, а пользовать тот, который удобнее в конкретной ситуации. И да в случае с протобуф будут заморочки с изменением структур api. В случае с json этих проблем нет - обратно-совместимые изменения делать легко. Если идти дальше то уходить от http на rsocket какой


    1. mentin
      18.12.2022 03:31

      В протобуферах довольно хорошо продуман вопрос изменения API, добавления и удаления полей, совместимость при некоторых изменениях (скажем optional в repeated). Проблем скорее всего будет не больше чем с json, а может и меньше, поскольку изменения формализованы. По-моему это одно из главных его достоинств (есть еще всякие возможности мультиплексирования - но они появляются в бекэнде, а в общении клиента с одним сервером не помогают).


  1. caballero
    18.12.2022 02:41

    JSОN-RPC - золотая середина


  1. igrishaev
    18.12.2022 09:00

    Мои пять копеек.

    1) Remote Procedure Calls как конценпция разработана не в Google. Уже лет 20 существуют XML-RPC и JSON-RPC со своими спецификациями.

    2) как видно из графика, обычный HTTP+Keep-Alive почти не уступает по скорости gRPC. Позволить Keep-Alive себе может каждый.

    3) Как справедливо заметили выше, gRPC дорого обходится в вебе. Схема с JSON-RPC + websockets/keep-alive наоборот, работает прозрачно и открыто.

    4) любой RPC (JSON, XML, Googe) выигрывает у REST просто за счет того, что есть единая точка входа в API. Не нужно иметь 30 урлов с вариациями GET/PUT/ETC. В RPC достаточно один раз все наладить, а потом расширять определения.

    5) Синтаксис: что такое калькулятор это понятно: класс с двумя числами. Но как объяснить эту запись:

    ```

    Calculator calculator = 1

    ```

    мы что, присвоили экземпляру калькулятора единицу?

    Вместо итога: gRPC, может быть и хорош, но как правило хватает JSON-RPC + gzip + websocket/keep-alive.


    1. igrishaev
      18.12.2022 09:22

      Вдогонку 6) JSON-RPC не обязывает использовать именно JSON. Можно различать формат по заголовку и для веба использовать JSON, а для бека MessagePack, что уменьшит трафик на треть и повысит скорость записи/чтения.


    1. Des96
      18.12.2022 10:26
      +2

      1 - это нумерация поля, в grpc в передаваемом сообщении нет названий полей, а только номер поля, емнип


  1. Daimonn
    18.12.2022 10:28
    +8

    Автор совершенно не раскрыл тему gRPC.

    аутентификации, потоковой передачи данных в любую сторону, управление потоками, отмену и time-out запросов

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

    В отличие от REST, gRPC - решение, которое не зависит от платформы и языка

    REST тоже не зависит от платформы и языка

    в gRPC наименования полей, ожидаемых запросов и возвращаемых ответов определяется и описывается в одном месте - в файле .proto

    На практике разницы практически никакой, посмотреть в документации, какую структуру создать перед кодированием в JSON или посмотреть в proto файл, какую структуру создать перед отправкой по gRPC.

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

    gRPC отлично себя показывает в бэкенде при взаимодействии микросервисов друг с другом под высокой нагрузкой.

    1. Тот самый protobuf, автор так и не сказал, в чем его преимущество, а оно вот в чем: перед отправкой данные запаковываются в бинарный вид и сжимаются. Эта операция легче, чем кодирование/декодирование JSON. Тут мы получаем во-первых уменьшение утилизации CPU, т.к. отсутствует кодирование/декодирование JSON, во-вторых отказываемся от передачи (пусть и сжатых в gzip) избыточных данных (всякие кавычки, двоеточия, названия параметров и т.д.), что уменьшает объем передаваемых данных, что в свою очередь уменьшает задержку и снова нагрузку на CPU.

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

    3. Общий таймаут на всю цепочку сервисов. Например, у нас есть сервис А который принимает внешний HTTP запрос и отправляет его дальше в обработку в сервис B. "B" в свою очередь в С и т.д. Получается подобная цепочка: A->B->C->D->E, Сервис E генерирует окончательный результат и он возвращается по цепочке обратно в A. Допустим, сервис A должен вернуть ответ клиенту максимум за 1 секунду. Получается, если, например, сервис C затупил, на нем уже истекла эта секунда, сервис А прекращает ждать ответ по таймауту и возвращает клиенту ошибку, все остальные все-равно продолжают работать, сервис С, а затем D и E не знают ничего о том, что сервис А ответ уже не ждет и их работа бесполезна.
      В gRPC можно установить общий таймаут на всю цепочку сервисов. Если сервис А прекращает ожидание ответа, об этом узнает и сервис C и прекращает обработку запроса и не отправляет его дальше по цепочке в D и C

    4. У gRPC клиентов из коробки доступна балансировка запросов по Round Robin. При настройке коннекта передаем несколько хостов или хост DNS, который возвращает несколько ip. В результате, клиент начинает их чередовать, поочередно отправляет запросы на разные сервера. Это позволяет не писать самостоятельно подобную балансировку на каждом клиенте (сервисе) и не ставить балансировщики перед каждым сервисом.

    5. gRPC так же имеет в своем арсенале трассировку запросов, по ней подробно ничего не могу сказать, не использовал, т.к. привык уже работать с Jaeger, но такая возможность есть.


    1. aml
      18.12.2022 11:46
      +3

      Зашёл написать такой же комментарий, а он тут уже есть! Поэтому просто добавлю чуть-чуть в копилочку плюсов:

      1. gRPC это не только протокол, но и целый фреймворк с возможностью расширения во все стороны: перехват (и опционально модификация) запросов, перехват (и модификация) ответов, возможность написания своих сериализаторов и десериализаторов (чем-то не нравится бинарный protobuf? можно в json. Хотите какую-нибудь экзотическую компрессию? Пожалуйста), хотите сервис-дискавери эндпоинтов через какой-нибудь zookeeper (если его ещё кто-то использует), чтобы в рантайме устанавливались новые соединения, плавно вводились в использование, а когда они пропадают из дискавери, плавно снимать с них нагрузку и закрывать.

      2. gRPC поддерживает побочные каналы, которые позволяют вместе с запросами отправлять любые метаданные. Скажем, трассировку, аутентификационные данные пользователя или любую другую служебную информацию. Менять при этом описание структур запроса и ответа не надо - API сервисов остаётся чистым.


    1. Borz
      18.12.2022 13:28

      На практике разницы практически никакой, посмотреть в документации,
      какую структуру создать перед кодированием в JSON или посмотреть в proto
      файл, какую структуру создать перед отправкой по gRPC.

      Нюанс в том, что если в proto просто переименовать поле, не меняя его тип и индекс, то клиенту будет всё равно на это. В REST же потребуется поменять имя поля и в клиенте тоже


      1. Alexerator
        18.12.2022 23:28

        В случае использования FieldMask-ов это уже не будет обратно совместимым изменением, т.к. клиент в маске передаёт серверу именно названия полей, а не номера.