Вот уже 24 года мы используем сложный для понимания, завязанный на особенности HTTP REST-протокол. Не пора ли переходить к чему-то более современному?

Привет! Меня зовут Игорь Алексеев и я работаю бэкенд-разработчиком в компании Garage Eight. Некоторое время назад я внедрил gRPC для части своих сервисов. В этой статье поговорим о том, что такое gRPC, сравним его с возможными альтернативами, рассмотрим преимущества gRPC и пробежимся по проблемам, которые возникают почти в каждом проекте, где этот протокол приходится внедрять.

Уточню, я говорю только про gRPC в связке с Protobuf, поскольку GSON в отсутствие типизации, кодогенерации и других фич нивелирует большинство преимуществ.

Что такое gRPC? 

gRPC — это протокол вызова удаленных процедур, когда мы обращаемся к сетевому сервису как будто к локальному методу. 

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

На транспортном уровне gRPC использует HTTP2 over TCP. Важным моментом является автоматическая, удобная генерация стабов для любых языков из описания контракта в protobuf файле, в кратком и удобном формате. 

Далее сравним gRPC с другими популярными протоколами – jsonRPC2 и GraphQL.

Сравнение gRPC с jsonRPC2 и GraphQL

jsonRPC2 — ещё один популярный выбор для RPC. Он легок, понятен, имеет маленькое описание и удобен в отладке, но jsonRPC2 расходует относительно много трафика, а это накладные расходы на Unmarshalling. 

GraphQL предназначен для данных в произвольной форме. Он текстовый, но объемный, и парсинг запроса может быть очень тяжелый. Так же, как jsonRPC2, GraphQL может работать по HTTP/1 и легко вписывается в любую инфраструктуру.

И наконец наш gRPC — это предопределенный формат запроса, сложности с интеграцией и бинарный формат, что ограничивает нас в дебаге и тестировании привычными средствами. 

Какие преимущества у gRPC и зачем его использовать? 

Первый плюс — это легкое распространение контрактов Protobuf. Контракты можно распространять любыми способами, о нескольких из этих способов я расскажу чуть дальше. Удобный формат шаблона. Если вас напрягает многословность OpenAPI и Swagger, то этот формат сам по себе может стать аргументом в пользу gRPC. 

Второй плюс — gRPC передает данные в бинарной форме, выигрыш по объему передаваемых данных 50-80% экономии трафика, что может быть существенно важно для средств с высокой нагрузкой.

Третий плюс — благодаря соединению по HTTP/2 и благодаря мультиплексированию и переиспользованию единственного сетевого соединения этот протокол позволяет сократить сетевые задержки в 2-10 раз, что тоже особенно важно для нагруженных систем. Это связано как с экономией трафика, так и с отсутствием накладных расходов на переподключение.

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

Проблемы при использовании gRPC в реальной инфраструктуре

Проблема 1. Распространение protobuf модели

Исторически, как только вы начинаете связывать два или более сервисов через этот протокол, происходит распространение protobuf модели. 

Варианты решений

Решение 1. Тут можно пойти традиционным путём и взять репозиторий GitHub модуля веб-артефакта, опубликовать его на Artifactory, Nexus, S3 и потом по мере необходимости стягивать. Это неплохой вариант, потому что он задействует существующую структуру.

Решение 2. Второй вариант для более продвинутой инфраструктуры — monorepo, когда все наши связанные сервисы находятся в одном Git репозитории. В отдельную директорию можно положить все контракты, упорядочить их, сложить рядом с документацией. 

Так делают в Google, Microsoft и во многих других крупных компаниях. Такое решение может быть удобным, а может принести огромные расходы и потребовать специализированного отдела для поддержки репозитория.

Решение 3. Перекладывать руками. Если вы в начале пути, связанных сервисов немного, а обновления происходит редко, то лучшее решение — скопировать руками.

Решение 4. Также есть специальные утилиты и даже выделенные репозитории. Например, существует сервис buf.build с разнообразными возможностями по тестированию, версионированию и даже автодокументированию изменений в описаниях ваших API. 

Если кто-то пользовался — отпишитесь в комментариях. Очень интересно узнать, как работает их модель тарификации по $0.5/месяц за один тип в сообщении.

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

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

Проблема 2. Интеграция через Ingress-nginx

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

Traefik, Envoy, Istio. Всё пройдёт легко и удобно, если у вас современный проект с Traefik/Envoy/Istio — с ними все прекрасно, в них gRPC работает из коробки. Вот только они медленные и в проде их можно встретить в основном на хипстерских проектах.

Nginx. Если же сеть построена на Nginx, то есть две новости: gRPC настроить в нем можно, но потребуется определенная настойчивость. 

Во-первых, Nginx не поддерживает HTTP/2 без TLS ввиду своей архитектуры, то есть вам придется озаботиться выпуском сертификатов, даже если оба сервиса сосуществуют на одной и той же машине.

Следующая проблема тоже связана с Nginx — это grpc_proxy_module. Он немного отличается от http_proxy_module. Немного, но достаточно, чтобы запутаться, где set_header, а где add_header.

Проблема 3. gRPC-web: Интеграция бека и фронта

Как только мы настроили Nginx, может возникнуть мысль: почему бы не использовать gRPS-web и не связать фронт через тот же самый протокол?

И это третья проблема — интеграция бека и фронта. Идея не плохая, она используется, но по сути оказалось, что gRPC-web развит недостаточно. 

Я пошел сложным путем и взял gRPC-web для коммуникации с Web клиентом. Неожиданно оказалось, что добиться генерации пригодного для Web TypeScript от компилятора protoc не так уж и легко. Фактически возможность оказалась экспериментальной и найти рабочий вариант генерации вебстабов оказалось невозможно. Потребовалось несколько часов для написания мастер строки параметров. Эту строку вы можете найти в репозитории с примерами на моем гитхабе. Ссылка будет в конце статьи.

Проблема 4. Ручная верификация СSRF

Четвёртая проблема так же связана с использованием gRPC-Web. 

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

CSRF позволяет пользователю браузера быть уверенным, что на запрос через Интернет отвечает авторизованный сервер с того же URI.

Проблема заключается в том, что gRPC не позволяет использовать CSRF из коробки. Поддержка в принципе не реализована. Таким образом для использования в открытом вебе потребуется как минимум реализовать ответ OPTIONS Acces-Control-Allow-*, а как максимум — расширение с подписанием запросов токеном. 

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

Советы по реализации gRPC

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

Совет 1. Метод Ping или healthcheck 

Проверка соединения становится абсолютно обязательной. Простой пинг или healthcheck на этапе старта даст отличную подсказку, если что-то идет не так.

Совет 2. Переключение между режимами работы сети 

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

Совет 3. Тестовый клиент

Поскольку протокол у нас бинарный, традиционными утилитами вроде CURL потестировать ничего не получится. 

Postman, Insomnia или родной grpc_cli имеют на самом деле неудобную и ограниченную поддержку. Например, в Postman все еще нельзя загрузить proto файл и получить предзаполненные поля. Так что специальный, написанный под конкретный случай тестовый клиент — абсолютный маст-хэв.

Заключение

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

Однако теперь у нас есть участок с gRPС, и он нам нравится. Мы получили значимую выгоду, которая перевешивает все минусы, и сумели преодолеть проблемы с интеграцией.

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

Пишите в телеграм, скачивайте репозиторий с примерами, оттуда можно свободно взять все перечисленные мной решения и использовать в своем проекте! 

Полезные ссылки: 

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


  1. minaevmike
    16.09.2024 16:32

    Мне кажется что есть более простой вариант для "Распространение protobuf модели". Каждый сервис хранит модель у себя. Клиенты ее импортируют (тут конечно зависит от ЯП, ка реализовать импорт). И за использовать тулу чтобы не ломать бек совместимость proto. Монорепа в крупных компаниях делается не для этого и никто не хранит там все протофоайлы в одной директории.


    1. ptr128
      16.09.2024 16:32

      Проблема в том, что из proto файла protoc генерирует исходный код для клиента и сервера. То есть, модель требуется во время компиляции клиента, а во время выполнения proto файл не нужен ни клиенту, ни серверу. Чтобы воспользоваться proto во время выполнения, клиент должен уметь перекомпилировать свой код. На C# в некоторых случаях мы так и делаем. Но это всё же скорее исключение, чем правило. Например, когда сервер, обнаружив изменение метаданных во внешнем источнике, обновляет свой proto файл, публикует его и перекомпилирует изменившиеся классы.


    1. igor-fa Автор
      16.09.2024 16:32

      Когда все на одном ЯП, можно сделать пакет. Монорепа подходит в том числе и для контрактов, удобный паттерн


      1. ptr128
        16.09.2024 16:32

        Это только если речь о внутренней разработке. Если gRPC используется в B2B сценарии, то монорепа уже решением быть не может. Тут удобней, когда сервис может сам предоставить схему proto по запросу.


  1. STAR
    16.09.2024 16:32

    grpc-web не поддерживает подписку на сообщения через websocket и выжидают пока все браузеры будут поддерживать webtransport. Есть какое то готовое решение для подписок через websocket или только придумывать свой протокол\реализацию обмена?


    1. Heggi
      16.09.2024 16:32

      grpc-web нормально поддерживает service-side streams (нужно только с реконнектами разобраться, из коробки не работает и нужно самому накостылить обработчик).

      Для задач веб-интерфейса этого обычно достаточно.


      1. STAR
        16.09.2024 16:32

        Через long polling?


        1. Heggi
          16.09.2024 16:32

          Если fetch, не умирающий по таймауту, можно назвать long polling, то он.

          Но long polling, емнип, подразумевает, что коннект через некоторое время рвется, а тут дисконнект только если со связью проблема.


        1. Heggi
          16.09.2024 16:32

          Примерно вот такое. Мы раз в 2 минуты кидаем пустое сообщение на всякий случай. А так оно в принципе и без "пингов" висит и не отваливается. Но на nginx/envoy надо правильно таймауты накрутить.


  1. Heggi
    16.09.2024 16:32

    Для js/ts и grpc-web в частности лучше использовать библиотеку https://github.com/timostamm/protobuf-ts

    Она гораздо более удобна для работы и генерит натуральный ts код (а не js с тайпингами)


  1. Heggi
    16.09.2024 16:32

    Про nginx и http2 чушь написана. Нормально он несекурный http2 переваривает.


    1. igor-fa Автор
      16.09.2024 16:32

      Можно пример с connection upgrade, чтобы не получить "Received HTTP/0.9 when not allowed"?


      1. Heggi
        16.09.2024 16:32

        location / {
            grpc_pass grpc://127.0.0.1:10011;
            grpc_set_header Host $server_name;
            grpc_set_header X-Real-IP $remote_addr;
            grpc_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        
            grpc_read_timeout 7d;
            grpc_send_timeout 7d;
        }
        


  1. Abrazacs
    16.09.2024 16:32

    Например, в Postman все еще нельзя загрузить proto файл и получить предзаполненные поля. 
    Сейчас это не так. В postman можно загрузить proto файл
    https://learning.postman.com/docs/sending-requests/grpc/using-service-definition/


  1. positroid
    16.09.2024 16:32
    +1

    gRPC передает данные в бинарной форме, выигрыш по объему передаваемых данных 50-80% экономии трафика

    Учтено ли здесь, что практически весь plaintext трафик rest/graphql/etc проходит через gzip? Сколько останется процентов, если его учесть?


    1. ptr128
      16.09.2024 16:32

      А кто запрещает использовать компрессию и для HTTP/2 траффика? На gRPC не проверял, а на Kafka при lz4 компрессии protobuf выигрывает у JSON на небольших пакетах более чем в два раза. А вот если вломить пакет из 10 тыс. записей, то выигрыш сразу падает до 20%.


      1. positroid
        16.09.2024 16:32

        Никто не запрещает, но бинарные данные сжимаются сильно хуже текста, так что выигрыш в 50-80% из статьи это сравнение plaintext и сжатых данных. Выигрыш в объеме передаваемых данных у gRPC и аналогичных протоколов все же есть, но не такой драматический, как пишут в абсолютно всех статьях по gRPC при его сравнении с json


        1. ptr128
          16.09.2024 16:32

          Вы точно прочитали, то что я написал? Или ограничились только первым предложением?


          1. positroid
            16.09.2024 16:32

            Прочитал, да, в принципе все верно, вопрос тут больше в типе пакета и объеме данных, когда мы говорим про выигрыш в 2+ раза - это скорее небольшой по размеру пакет (или очень разнородный пакет, т.е. данные, которые слабо поддаются сжатию)?

            Для проверки с o1-preview написал скриптик, который гоняет разное количество разных по формату пакетов через protobuf и json с lz4 компрессией и получил в качестве результата те же 10-50% выигрыша у protobuf (меньше пакетов - больше выигрыш у бинарного формата, разумеется).

            Не спец в python и тем более в реализации protobuf, но изъянов на беглый взгляд не обнаружил, можете посмотреть репозиторий.

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


            1. ptr128
              16.09.2024 16:32

              выигрыш в 2+ раза - это скорее небольшой по размеру пакет

              А я писал: "на небольших пакетах более чем в два раза". Вы это повторно не увидели?

              Не спец в python

              Но на proto то можно было посмотреть, раз именно его обсуждаем? Бинарный protobuf экономит место для чисел, но уж никак не для строк. А в репозитории по ссылке его старательно грузят преимущественно строками, на которых по определению заметной разницы между бинарным и текстовым форматом быть не может.

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

              В контексте web-разработки

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

              Даже в web-разработке для меня странно гонять десятки килобайт в одном запросе. Это же на один-два порядка хуже время реакции на одно сообщение в сотню байт. Для примера, в Kafka по умолчанию max.in.flight.requests.per.connection = 5. То есть сжимаемый пакет содержит не более пяти сообщений.


              1. positroid
                16.09.2024 16:32
                +1

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

                В моей сфере (интернет-магазины) типовое сообщение это на 90% текстовая информация и даже небольшой блок на сайте с картинками вполне может весить больше килобайта, а страница каталога с двумя десятками товаров больше 10 килобайт - возможно, у меня здесь профдеформация и для других систем (биржи акций, например) действительно можно слать очень небольшие пакеты из чисел.

                Я ни в коем случае не спорю с тем, какого сжатия с protobuf можно добиться на определенных данных, но безаппеляционно заявлять про 50-80% экономии трафика на gRPC по сравнению с REST - это большое лукавство.


                1. ptr128
                  16.09.2024 16:32
                  +1

                  безаппеляционно заявлять про 50-80% экономии трафика на gRPC по сравнению с REST - это большое лукавство

                  Бритва Хэнлона. Это, скорее, ошибка, чем лукавство, так как не указано было, что речь идет исключительно о несжатых данных. В случае компрессии выигрыш 10-60%, в зависимости от количества бинарных данных и размеров пакета. Что, впрочем, тоже не мало.

                  gRPC по сравнению с REST

                  Protobuf по сравнению с JSON. Нужно отделять мух от котлет. gRPC не обязан использовать именно protobuf. Все же потоковый gRPC (HTTP/2) дает заметные преимущества перед REST в некоторых сценариях использования. Например, сервис прогнозируемых курсов валют на активном двунаправленном потоковом gRCP в локальной сети вполне может давать ответ на запрос за 1 мс. На REST такого достигнуть мало реально.


  1. p__k
    16.09.2024 16:32

    У нас контракты каждого сервиса живут в отдельном проекте. При сборке собираются пакеты: npm для front-end, nuget для backend, py для тестов. Плюс генерируется документация на контракт (html). Всё через ptotoc, в рамках ci. И всё это добро пушится в nexus. Front-end общается с бэком по gRPC. Полёт нормальный.


  1. rabotun
    16.09.2024 16:32

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