gRPC и Protobuf: бинарные RPC поверх HTTP/2
REST мыслит ресурсами: «получи коллекцию заказов», «обнови клиента». gRPC мыслит вызовами функций: «вызови SearchProducts(query)», «выполни RecommendItems(userId)». Это переосмысление подхода —
gRPC родился в Google в 2015 году как open-source наследник внутреннего фреймворка Stubby. Под капотом — HTTP/2 как транспорт и Protocol Buffers (Protobuf) как формат сериализации и язык описания интерфейсов.
Почему HTTP/2 + бинарка
REST работает поверх HTTP/1.1, и это создаёт ограничения. gRPC использует HTTP/2 — и берёт у него три ключевые фичи:
- Мультиплексирование — несколько RPC-вызовов параллельно по одному TCP-соединению, без head-of-line blocking.
- Бинарные frame-ы — заголовки сжимаются HPACK-ом, payload в Protobuf-е.
- Двунаправленные потоки — server и client могут отправлять frame-ы в обе стороны независимо.
Бинарный Protobuf вместо JSON даёт компактность и скорость. Числа кодируются
REST + JSON
POST /api/v1/users { name: 'Alice', age: 30 }gRPC + Protobuf
UserService.Create(User { name = 'Alice', age = 30 })Schema-first: файл .proto
В REST контракт обычно живёт в OpenAPI спеке (которую часто пишут после кода), а в gRPC он первичен. Без .proto файла никакой gRPC-сервис не существует.
syntax = "proto3";
package marketplace.v1;
service ProductService {
rpc GetProduct(GetProductRequest) returns (Product);
rpc SearchProducts(SearchRequest) returns (stream Product);
rpc UploadCatalog(stream Product) returns (UploadStats);
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}
message GetProductRequest {
string product_id = 1;
}
message Product {
string id = 1;
string name = 2;
double price = 3;
Currency currency = 4;
repeated string tags = 5;
Inventory inventory = 6;
}
message Inventory {
int32 in_stock = 1;
int32 reserved = 2;
}
enum Currency {
CURRENCY_UNSPECIFIED = 0;
USD = 1;
EUR = 2;
RUB = 3;
}
message SearchRequest {
string query = 1;
int32 limit = 2;
Currency price_currency = 3;
}
message UploadStats {
int32 created = 1;
int32 updated = 2;
int32 errors = 3;
}
message ChatMessage {
string text = 1;
int64 ts_unix_ms = 2;
}
Несколько ключевых правил Protobuf 3:
- Numeric tag (
= 1,= 2) — идентификатор поля на проводе. После выпуска API менять нельзя — иначе старые клиенты сломаются. repeated— массив.map<string, int32>— словарь.- enum обязан иметь значение с тегом 0 (это default для не указанного поля).
- Reserved: чтобы не переиспользовать выпиленные поля, пишут
reserved 4, 5;илиreserved "old_field";. - Поля не обязательны по умолчанию — отсутствующее поле получает .zero value
Имена сервисов и методов — PascalCase, имена полей — snake_case. Это конвенция, на которой основан стандартный кодогенератор.
Кодогенерация
Из .proto файла генерируются типизированные клиентские и серверные stub-ы для нужного языка. Для Python это два пакета: protobuf (типы сообщений) и grpcio-tools (компилятор + gRPC-stubs).
pip install grpcio grpcio-tools
python -m grpc_tools.protoc \
--python_out=. \
--pyi_out=. \
--grpc_python_out=. \
--proto_path=./proto \
./proto/product_service.proto
На выходе появляются три файла:
product_service_pb2.py— классы сообщений (Product,GetProductRequest, …).product_service_pb2.pyi— type stubs для IDE.product_service_pb2_grpc.py—ProductServiceStub(клиент) иProductServiceServicer(база для сервера).
Сервер на Python — это унаследованный класс с реализованными методами:
import grpc
from concurrent import futures
import product_service_pb2 as pb
import product_service_pb2_grpc as pb_grpc
class ProductService(pb_grpc.ProductServiceServicer):
def GetProduct(self, request: pb.GetProductRequest, context) -> pb.Product:
product = db.find_product(request.product_id)
if not product:
context.abort(grpc.StatusCode.NOT_FOUND, "product not found")
return pb.Product(
id=product.id,
name=product.name,
price=product.price,
currency=pb.Currency.USD,
inventory=pb.Inventory(in_stock=product.in_stock),
)
server = grpc.server(futures.ThreadPoolExecutor(max_workers=16))
pb_grpc.add_ProductServiceServicer_to_server(ProductService(), server)
server.add_insecure_port("[::]:50051")
server.start()
server.wait_for_termination()
Клиент:
import grpc
import product_service_pb2 as pb
import product_service_pb2_grpc as pb_grpc
with grpc.insecure_channel("localhost:50051") as channel:
stub = pb_grpc.ProductServiceStub(channel)
response = stub.GetProduct(pb.GetProductRequest(product_id="P-100"))
print(response.name, response.price)
Никакой ручной сериализации, никакого парсинга URL — всё сводится к вызову метода.
Четыре типа RPC
gRPC поддерживает четыре сценария взаимодействия. Синтаксис задаётся словом stream слева или справа от типа:
rpc Get(Req) returns (Resp); // unary
rpc Search(Req) returns (stream Resp); // server streaming
rpc Upload(stream Req) returns (Resp); // client streaming
rpc Chat(stream Req) returns (stream Resp); // bidirectional streaming
Unary — классический request/response, как REST. 90% методов будут такими.
Server streaming — клиент шлёт один запрос, сервер отвечает потоком сообщений. Идеально для выгрузки больших каталогов, страничного чтения, push-уведомлений по подписке.
Client streaming — клиент шлёт поток, сервер отвечает одним сообщением. Используется для batch upload-а (телеметрия, события из IoT).
Bidirectional streaming — оба шлют поток одновременно. Ровно тот сценарий, для которого иначе пришлось бы изобретать WebSocket. Чат, real-time коллаборация, ML inference с протяжённым диалогом.
Преимущества gRPC
- Контракт-first. Изменение в
.protoломает кодогенерацию — нельзя случайно опубликовать несовместимое API. - Тип-безопасность. На обеих сторонах сильно типизированные классы; IDE подсказывает поля.
- Бинарный размер и скорость. В internal-сетях с большим QPS экономия 5-10x на сериализации = меньше CPU и сетевого трафика.
- Streaming из коробки. Не нужно изобретать SSE или WebSocket.
- Multi-language. Один
.proto-> клиенты на Python, Go, Java, C++, Node.js, Rust.
Недостатки и подводные камни
- Браузер не умеет HTTP/2 trailers так, как нужно gRPC. Поэтому из браузера ходят через или REST gateway.gRPC-Web
- Debugging сложнее. Нельзя открыть Postman и посмотреть JSON. Помогают
grpcurl, BloomRPC, расширения для Postman. - Schema evolution требует дисциплины. Добавлять поля можно (новые игнорируются старыми клиентами), удалять и менять tag — нет.
- Public API через интернет — gRPC не любит CDN, корпоративные прокси и L7-балансировщики, не понимающие HTTP/2 stream-ы.
- Нет встроенного API для browser caching — все запросы идут через POST, кэш заводится самостоятельно.
Где встретится у Data Engineer
gRPC — сетевой стандарт внутри компаний, особенно в инфраструктуре данных:
- ML serving. TensorFlow Serving, NVIDIA Triton, KServe, BentoML — все экспонируют модели через gRPC. Запрос
Predict(Tensor)-> ответTensor, до тысяч RPS. - Метаданные. Apache Atlas, DataHub, OpenMetadata — gRPC поверх Protobuf или Thrift для inter-service.
- Observability. OpenTelemetry Collector принимает spans/metrics/logs через gRPC (OTLP).
- Streaming движки. Apache Beam Flink runner общается с runner harness по gRPC. Spark Connect (3.4+) — gRPC API для работы со Spark из тонких клиентов.
- Внутренние API хранилищ. ClickHouse имеет gRPC interface; Etcd, Consul, Vitess — всё gRPC.
Junior DE регулярно сталкивается с gRPC при подключении к фичестору, ML-сервису или service mesh. Уметь прочитать .proto, сгенерировать stub и сделать вызов — необходимый навык на год-два карьеры.
Сравнение Protobuf и Avro: оба бинарные, оба schema-first. Protobuf лучше для межсервисных RPC (компактные сообщения, кодогенерация для языков). Avro лучше для долгосрочного storage и Kafka (schema внутри файла, embedded JSON-schema, легче эволюция). Это не конкуренты, а инструменты для разных задач.
Когда выбрать gRPC
Выбирайте gRPC, если:
- Internal микросервисы с высоким QPS и низкой latency.
- Команда комфортна со schema-first и code generation.
- Нужен streaming в обе стороны.
- Polyglot stack — клиенты на нескольких языках.
Не выбирайте, если:
- Public API через интернет с большим разнообразием клиентов.
- Браузер — основной потребитель и нет ресурсов на gRPC-Web.
- Команда из 3 человек на простую CRUD-задачу — overhead toolchain не окупится.