Всем привет, меня зовут Олег, я старший бэкенд разработчик, а также по совместительству ментор бэкенда в команде Sapphire в Битве пет-проектов. Вот уже на протяжении 10 лет Python является моим основным языком программирования.
В нашей компании, где я работаю над проектом для бессерверных вычислений, Python также - основной язык программирования (наряду с Go). Одним из корпоративных стандартов является внутренний обмен информацией по протоколу gRPC. Причины просты - данных огромное количество, нагрузка на сеть колоссальная, отсюда и потребность в экономии размера передаваемых данных.
Что такое gRPC (из вики)
gRPC (Remote Procedure Calls) — это система удалённого вызова процедур (RPC) с открытым исходным кодом, первоначально разработанная в Google в 2015 году. В качестве транспорта используется HTTP/2, в качестве языка описания интерфейса — Protocol Buffers. gRPC предоставляет такие функции как аутентификация, двунаправленная потоковая передача и управление потоком, блокирующие или неблокирующие привязки, а также отмена и тайм-ауты. Генерирует кроссплатформенные привязки клиента и сервера для многих языков. Чаще всего используется для подключения служб в микросервисном стиле архитектуры и подключения мобильных устройств и браузерных клиентов к серверным службам.
Протокол gRPC в данный момент является довольно распространённым решением (почему, очень хорошо описано в статье от Яндекса). На работе мы также используем его везде, где идёт речь об общении микросервисов друг с другом. Но, к сожалению, когда я начал вникать в устройство и применять его, то столкнулся с крайне сложным процессом имплементации gRPC сервиса на Python.
Для начала надо освоить protocol buffers и составить корректный .proto файл, чтобы описать интерфейс будущего gRPC сервиса.
Потом с помощью Python библиотеки Protobuf нужно на основе .proto файлов сгенерировать Python модули (pb2.py и pb2_grpc.py).
Позже надо подключить сгенерированные модули к вашему приложению (возможно, построить абстракции над ними или использовать напрямую).
А, да, при этом сгенерированные модули практически не читабельны и потребуют модификации (как минимум потому что там могут быть некорректно указаны импорты, как максимум - линтеры не пропустят).
Да, gRPC является более сложным протоколом по своей структуре (хотя бы потому что он не текстовый и передаваемые пакеты нельзя просто открыть и прочитать), но при этом сам он для понимания является более простым. Объясню, почему я так в этом уверен.
Для полноценного использования HTTP вам нужно обязательно знать:
Какими методами можно выполнить запрос (GET, POST, PATCH, PUT, ...).
С какими статус кодами может вернуться ответ и что они означают (200, 403, 502, ...).
Как сформировать путь к странице.
Какие бывают протоколы и в чём их отличие (http, https).
Заголовки запроса и ответа (какие бывают, что означают).
Зачем 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. Конечно же, я их нашёл:
Fast-GRPC - классный асинхронный фреймворк. Мне он понравился, но было несколько проблем: использования старого pydantic (что я изменил в форке) и баги (например, в метод, который обрабатывает запрос, в self передаётся значение None). В принципе именно эти баги и заставили от него отказаться, так как ковыряться в запутанной структуре оказалось сложно и требовало много времени.
grpcalchemy - классный синхронный фреймворк. Он тоже понравился, но его проблемой было то, что он только синхронный, а реализация асинхронного сервера уже давно висит в TODO. Я попытался его реализовать, но встроить его в существующую систему оказалось сложнее, чем я думал.
Остальное из 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.