Всем привет, меня зовут Олег, я старший бэкенд разработчик, а также по совместительству ментор бэкенда в команде Sapphire в Битве пет-проектов. Вот уже на протяжении 10 лет Python является моим основным языком программирования.

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

Что такое gRPC (из вики)

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

Только правильное питание для сервисов!
Только правильное питание для сервисов!

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

  1. Для начала надо освоить protocol buffers и составить корректный .proto файл, чтобы описать интерфейс будущего gRPC сервиса.

  2. Потом с помощью Python библиотеки Protobuf нужно на основе .proto файлов сгенерировать Python модули (pb2.py и pb2_grpc.py).

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

  4. А, да, при этом сгенерированные модули практически не читабельны и потребуют модификации (как минимум потому что там могут быть некорректно указаны импорты, как максимум - линтеры не пропустят).

Да, gRPC является более сложным протоколом по своей структуре (хотя бы потому что он не текстовый и передаваемые пакеты нельзя просто открыть и прочитать), но при этом сам он для понимания является более простым. Объясню, почему я так в этом уверен.

Для полноценного использования HTTP вам нужно обязательно знать:

  1. Какими методами можно выполнить запрос (GET, POST, PATCH, PUT, ...).

  2. С какими статус кодами может вернуться ответ и что они означают (200, 403, 502, ...).

  3. Как сформировать путь к странице.

  4. Какие бывают протоколы и в чём их отличие (http, https).

  5. Заголовки запроса и ответа (какие бывают, что означают).

  6. Зачем Query параметры и для чего их используют.

А ещё структура запроса отличается от структуры ответа. А если говорить ещё и о REST, то он тоже добавляет требования поверх всего этого списка. Таким образом, порог входа в работу с HTTP и REST оказывается довольно высоким, но при этом это стандартный протокол: фреймворков для построения веб-приложений - тьма, обучающих материалов - море, Quick Start-ов по созданию собственного веб-сервиса - выше крыши.

В gRPC же всё сведено к простому: методов как таковых нет (по факту используется только POST), путей тоже нет (вместо них используются определённые в .proto файле rpc), понятия протокола тоже нет (достаточно указать - secure или insecure), query параметры тоже отсутствуют. Но при этом написать сервер на gRPC на Python в разы сложнее, чем реализовать REST API интерфейс. Просто посмотрите на минимально рабочий пример реализации REST API на Python на фреймворке FastAPI:

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
async def root():
    return {"message": "Hello World"}

Очень просто, не правда ли? А вот для работы с gRPC на Python нужно выполнить все шаги, которые я описал выше. И кстати, при изменении интерфейса в .proto файле путь надо будет пройти снова. Такой процесс делает работу с gRPC нереально сложной для новичков.

В рамках Битвы пет-проектов мы в Sapphire тоже нашли потребность в общении сервисов между собой. Изначально для этого у нас была Kafka, но она точно не подходит для получения информации от другого сервиса. Мы могли бы использовать REST (вряд ли у нас будет высокая нагрузка на внутреннюю сеть), но так как у меня есть опыт работы с gRPC на работе, то реализовать gRPC интерфейс показалось хорошей идеей. Но когда дело дошло до реализации, оказалось что реализовать сервис с нуля сложно и он будет выглядеть крайне громоздко, требуя написания большего количества кода, чем весь реализованный REST API.

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

  1. Fast-GRPC - классный асинхронный фреймворк. Мне он понравился, но было несколько проблем: использования старого pydantic (что я изменил в форке) и баги (например, в метод, который обрабатывает запрос, в self передаётся значение None). В принципе именно эти баги и заставили от него отказаться, так как ковыряться в запутанной структуре оказалось сложно и требовало много времени.

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

  3. Остальное из awesome-grpc - я просмотрел всё и из подходящих оставалась только библиотека grpclib, которая хоть и не обладала проблемами вышеперечисленных, но и реализацию не делала простой.

После двухдневных поисков и проб я потерял веру в то, что найду подходящий инструмент и решил, что пришло время реализовать свою имплементацию. Встречайте: Fast-gRPC (да, я не придумал ничего лучше). Установить довольно просто:

pip install py-fast-grpc

А минимальная имплементация сервера выглядит так:

from fast_grpc import FastGRPC, FastGRPCService, grpc_method
from pydantic import BaseModel


class HelloRequest(BaseModel):
    name: str


class HelloResponse(BaseModel):
    text: str


class Greeter(FastGRPCService):
    @grpc_method(request_model=HelloRequest, response_model=HelloResponse)
    async def say_hello(self, request: HelloRequest) -> HelloResponse:
        return HelloResponse(text=f"Hello, {request.name}!")


app = FastGRPC(Greeter())
app.run()

Как видите, здесь пропущены шаги с составлением proto файла, с генерацией pb2 и pb2_grpc модулей, с их подключением к проекту, всё это работает под капотом сервиса, программисту нужно только описать pydantic модели для запроса и ответа. Дополнительно можно указать имя сервиса, название каждого метода, порт сервера и включить рефлексию, но по умолчанию о них можно не задумываться. Все proto, pb2 и pb2_grpc файлы, которые нужны для работы сервера, генерируются в корне проекта, но это также можно изменить, указав нужные директории в аргументах сервиса.

К сожалению, в моделях запросов и ответов сейчас поддерживаются только стандартные Python типы данных (не включая коллекции, такие как list и dict) и uuid (то что сейчас требовалось в рамках Битвы пет-проектов), но в дальнейшем я предполагаю расширение возможностей. Даёшь простой gRPC на Python всем!

P.S. Кстати, я также веду некоторые свои соцсети, в которых рассказываю о Python, об IT и вообще о жизни - VK и Telegram.

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


  1. serginfo2009
    19.11.2023 06:35

    Выглядит полезно, обязательно попробую при случае.


  1. JimmyHollyWood
    19.11.2023 06:35
    +2

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


    1. Ryder95 Автор
      19.11.2023 06:35

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


  1. BerkutEagle
    19.11.2023 06:35
    +4

    Получается, proto-файл генерируется новый при изменениях. А как быть клиентам вашего сервиса? py-клиенты не могут использовать вашу либу, т.к. чтобы им сгенерировать идентичный proto-файл, клиент сам должен быть сервисом. Таким образом, используя вашу библиотеку, могут общатся только одинаковые сервисы?

    Я думал, что protobuf предполагает другую схему работы - proto-файл единственный источник правды для всех сервисов и клиентов на любых языках. Вдумчиво меняем proto -> генерируем клиентов/серверы.

    У вас же, простое добавление поля в модель ведёт к критическим изменениям протокола.


    1. Ryder95 Автор
      19.11.2023 06:35

      Сейчас - никак, вы правы, но сама библиотека генерирует proto файл и на его основе делает gRPC сервер, совсем нет проблем так же в рантайме генерировать proto файл и получать его (или генерировать перед этим), а клиент уже генерировать на основе этого proto файла (или вообще чтобы сервер генерировал полноценный Python клиент).

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


      1. kozlyuk
        19.11.2023 06:35

        Еще, если бы сервер также автоматически выставлял сервис GRPC reflection, к нему можно было бы сразу делать тестовые запросы хотя бы grpcurl.


        1. Ryder95 Автор
          19.11.2023 06:35

          Уже сейчас есть возможность включить рефлексию, но возможно было бы лучше, если бы рефлексия была включена по умолчанию, возможно, вы правы


      1. BerkutEagle
        19.11.2023 06:35

        Можно попробовать развернуть схему работы с proto. Редактировать proto-файл руками, не генерировать. Через декоратор класса указывать путь к proto-файлу и имя реализуемого grpc-сервиса, через декоратор метода указывать реализуемый grpc-метод. В декораторе модели указывать путь к proto-файлу и имя message в нём.


  1. Sabirman
    19.11.2023 06:35
    +1

    Похоже вы используете бибоиотеку не по назначению. Если вам просто нужно уменьшить траффик, то вам нужно включить сжатие траффика, которое предусмотрено в протоколе http. Оно будет на порядок эффективнее и не сильно нагрузит сервер (по своему опыту, но на nodejs)


    1. Ryder95 Автор
      19.11.2023 06:35
      +2

      Да нет, тут скорее в рамках пробы, как я и описывал, не было потребности в Sapphire уменьшать размер трафика (его в принципе пока почти нет), просто хотелось попробовать


  1. PeterTK
    19.11.2023 06:35
    +1

    Выглядит крайне полезно, тут точно +rep

    Тоже при случае пощупаю


  1. hVostt
    19.11.2023 06:35

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


    1. gnomeby
      19.11.2023 06:35

      Например одностороннее переименование полей и добавление полей, без необходимости обновлять клиенты.


      1. hVostt
        19.11.2023 06:35

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

        А вот минусы:

        • в proto намеренно отключена поддержка null для передачи примитивных типов, и требуются дополнительные совершенно неудобные приседания, либо для обеспечения этой поддержки (через обёртки), либо через усложнённый маппинг, который будет пустые строки конвертить в null. А всегда ли null и пустая строка -- одно и то же? Нет. Или 0 и null, нет!

        • gRPC создан для работы через HTTP/2, а это HTTPS, не получится выполнять классическую детерминацию на прокси (Ingress), придётся возиться с сертификатами там, где с ними обычно никто не возится

        • особо доставляет, когда gRPC на костылях, нарушая требования, запускают на HTTP, потому что "нам так неудобно" и "мы так не привыкли", ну и в общем-то теряя в возможностях протокола, используя только унарные вызовы (ну т.е. тот же REST, только в с лишним обвесом и проблемами).

        Конечно, spec first выглядит действительно хорошо. Но это возня с protoc-ом, который просто отваливается, если сервер перешёл на новую версию, а клиентская поддержка ещё не подоспела.

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


        1. Ryder95 Автор
          19.11.2023 06:35

          Со всем согласен, с перечисленными проблемами в gRPC столкнулся ещё на работе, но хочу поспорить с тем, что если gRPC - это про экстремальные нагрузки, то и Python будет узким местом. Мне кажется, что это не всегда справедливо, ведь большая нагрузка на сеть (соответственно требуется экономия трафика и увеличение скорости передачи) не всегда связаны с невозможностью этот трафик обработать. Например, сеть у нас ограниченная, а вот вычислительных ресурсов достаточно.

          Очень хотел описать это и в статье, но не смог сформулировать. Если Python - интерпретируемый и относительно медленный, это не значит, что он не может решать задачи эффективно и работать с низкоуровневыми протоколами. Вопрос будет стоять скорее в том, насколько эффективно в определённой задаче Python сможет заниматься обработкой данных, полученных по этому самому протоколу

          И насколько уместен gRPC в той или иной задаче - решать каждому отдельному программисту, но мне кажется что связка Python + gRPC имеет право на существование


          1. hVostt
            19.11.2023 06:35
            +1

            Так я и не спорю :) Мой вопрос простой, как тапок. Вот, допустим, решали такую-то задачу... Столкнулись с проблемой, взяли gRPC -- получили решение проблемы, которое на классическом REST-е либо вообще не решалось, либо решение было сильно дороже, чем использование gRPC.

            Ну или хотя бы так. Провели нагрузочное тестирование. Потенциал не понравился, масштабирование дорого. А вот взяли gRPC, и по результатам НТ увидели вот такой-то профит.

            А просто так, брать и затаскивать gRPC, без целевой задачи, без НТ, без понимания профита, это уже выглядит как "потому что хочется". Ну или это эксперименты. Что вовсе даже и не плохо, только об этом не сказано :)

            Конечно могу сильно ошибаться, я со своей стороны вижу проблему применения gRPC в Python не в том, что он интерпретируемый. А в том, что gRPC прямо подразумевает многопоточный процесс с легковесными потоками, управлением памятью, ресурсами -- и это должна быть чистая и естественная среда обитания для платформы, а не приделанная на костылях и подпорках. Вот тогда можно выжимать из протокола максимум. А это постоянный двунаправленный канал, мультиплексирование, почти полное отсутствие сериализации (данные берутся AS IS, по максимуму memcopy). Экономия трафика тут не целевая задача, а скорее побочная.

            Если уж хочется просто экономить трафик, можно сжатие, или MessagePack вместо JSON, например :) Вообще позиционных бинарных форматов хватает.

            Это не камень в огород, просто хотелось понять, какие были предпосылки.


            1. gnomeby
              19.11.2023 06:35

              Конечно могу сильно ошибаться, я со своей стороны вижу проблему
              применения gRPC в Python не в том, что он интерпретируемый. А в том, что gRPC прямо подразумевает многопоточный процесс с легковесными потоками, управлением памятью, ресурсами -- и это должна быть чистая и
              естественная среда обитания для платформы, а не приделанная на костылях и подпорках. Вот тогда можно выжимать из протокола максимум. А это постоянный двунаправленный канал, мультиплексирование, почти полное отсутствие сериализации (данные берутся AS IS, по максимуму memcopy). Экономия трафика тут не целевая задача, а скорее побочная.

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


              1. hVostt
                19.11.2023 06:35

                Я ответ на свой вопрос не получил. Не то, чтобы мне кто-то его обязан давать. Но пока я вижу чистые домыслы, фантазии, и демагогию. Проводились ли какие-то сравнения? Получали какой-то видимый, ощутимый профит? Вообще, была ли обнаружена хоть крупица полезного при использовании gRPC? У инженеров принято как бы не просто занавесочки с розовыми цветочками вешать, потому что фиолетовые "устарели", а розовые они от goolge и вообще модные, но работать с результатом, с цифрами, бенчмарками.

                Со своей стороны могу сказать следующее, чистый опыт. Понятно, что это наш субъективный опыт, и вообще не показательно. Но факт, есть факт. Внедрение в наши проекты gRPC на PHP и Python, ничего не принесло, кроме боли, страданий, увеличения Т2М и постоянных проблем у девопсов. Вплоть до того, что проекты стали переносить обратно на REST.

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

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

                Опять же, я спрашиваю не потому что хочу кого-то захейтить, или задеть за живое. Действительно интересно. Ну правда. Не хочется выслушивать истории фейсбука, варгейминга и прочих волшебных каракатиц. А конкретный опыт. Взяли gRPC, был REST, стало действительно лучше вот настолько-то и настолько-то. Ну или хотябы бенчи какие-нибудь. Нельзя же просто так что-то внедрять, это же не мир модных красных трусов на подиуме. Мы же инженеры. Ну как так :)


                1. gnomeby
                  19.11.2023 06:35

                  Давайте я попробую.

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

                  2. gRPC сложнее, если вы в целом не готовы в нём глубоко разобраться, а может даже проверить производительность, то не стоит его брать.

                  3. Существуют ли преимущества при использовании gRPC? Конечно. Сжатие трафика, разные режимы работы, изменение АПИ без необходимости менять запущенных клиентов. Некоторая обязательная валидация сообщений. Это всё уже есть в коробке. Но при наличии пункта 1 выше, это всё можно написать ручками и поверх REST.


        1. micronull
          19.11.2023 06:35

          в proto намеренно отключена поддержка null

          https://protobuf.dev/programming-guides/proto3/#field-labels

          optional: An optional field is in one of two possible states:

          • the field is set, and contains a value that was explicitly set or parsed from the wire. It will be serialized to the wire.

          • the field is unset, and will return the default value. It will not be serialized to the wire.

          You can check to see if the value was explicitly set.

          С остальными проблемами не сталкивались. Работаем по HTTP. В спеки так же добавляем новые поля без необходимости обновления, главное соблюдать порядок.


        1. gnomeby
          19.11.2023 06:35

          gRPC создан для работы через HTTP/2, а это HTTPS, не получится выполнять
          классическую детерминацию на прокси (Ingress), придётся возиться с
          сертификатами там, где с ними обычно никто не возится

          Ложное утверждение, всё прекрасно терминируется и HTTP/2 допускает нешифрованный трафик.


        1. gnomeby
          19.11.2023 06:35
          +1

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

          Расскажите это компании Wargaming, у которой игровые сервера на python, а в каких-нибудь World of warplanes каждая пуля проходит через сервер.

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


  1. gnomeby
    19.11.2023 06:35
    +1

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

    Если нужно модифицировать эти файлы, то вы делаете что-то не так.


    1. Ryder95 Автор
      19.11.2023 06:35

      Всего оказалось две проблемы:

      1. protoc генерирует файлы с абсолютным импортом, то есть если он генерирует файл greeter_pb2.py, то в соседнем greeter_pb2_grpc.py будет импорт `import greeter_pb2`. Есть несколько решений, но все они требуют каких-то действий:

        1. Руками изменить импорт на относительный

        2. В модулях нашего приложения добавить путь к директории сgreeter_pb2.py в sys.path для простого импорта.

        3. Помещать сгенерированные файлы в корень проекта

      2. Изначально сгенерированный код не проходит линтеры, варианты:

        1. Править руками

        2. Править автоматически

        3. Не править и добавить в исключения

      Лично мне не нравится добавлять в игнорирование Python модуль, который явно добавляется в репозиторий, но это ИМХО.

      Если честно, у меня пока нет мыслей других опций, как можно эти две проблемы решать по другому


      1. gnomeby
        19.11.2023 06:35

        Всё верно.

        1. В sys.path надо дописать папку

        2. Не проверять файлы линтерами, это не ваши файлы и они не делаются вручную

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


  1. capissimo
    19.11.2023 06:35

    Питонистам? Это как раз тот случай когда прямая калька режет слух и опускает термин до унизительных шуточек. Автор поменяйте на питонщиков!


    1. fireSparrow
      19.11.2023 06:35

      Это какой-то странный юмор, или вы всерьёз?

      Вам действительно "питонист" режет ухо, а "питонщик" - не режет?


  1. paveltyurikov
    19.11.2023 06:35

    Странно сравнивать архитектуру REST и способ сериализации данных Protbuff внутри фреймворка для работы с этой сериализацией gRPC.Выбирая gRPC в самом начале проекта вы замедляете разработку. Сетевого трафика он потребляет меньше, но на этом надо ещё суметь что-то выйграть стоящего времени потраченного на обучение и возьню с прото-файлами. Самое главное, что даёт Protobuff это экономия процессорного времени при сериализации/десериализии данных, на которой тоже надо постараться сэкономить больше чем на времени разработчиков. Какая может быть эффективная экономия ресурсов при использовании Python (я люблю питон) не очень понятно.