gRPC и RPC: когда REST уже не хватает
REST хорошо подходит для публичных HTTP API: ресурсы, URL, методы, статус-коды, JSON, понятная отладка через браузер и curl. Но в микросервисах часто появляется другая боль: десятки внутренних сервисов должны быстро и строго общаться друг с другом, клиенты на разных языках должны получать одинаковый контракт, а схема запроса не должна жить только в README.
gRPC решает эту задачу через RPC и контракт-first подход. Вместо "ресурс + HTTP-метод" мы описываем сервис и его методы:
service CourseService {
rpc GetLesson(GetLessonRequest) returns (GetLessonResponse);
}
Для Go-разработчика это похоже на вызов обычного метода, только этот метод живёт на другом процессе или сервере:
lesson, err := client.GetLesson(ctx, &coursev1.GetLessonRequest{
Slug: "grpc-overview",
})
Под капотом всё равно сеть, таймауты, ошибки, сериализация и контракты. Просто gRPC прячет транспортную механику за сгенерированным клиентом и серверным интерфейсом.
REST и gRPC - разные модели
REST моделирует систему как набор ресурсов:
GET /lessons/grpc-overview
POST /submissions
gRPC моделирует систему как набор сервисов и методов:
service LessonService {
rpc GetLesson(GetLessonRequest) returns (GetLessonResponse);
rpc SubmitSolution(SubmitSolutionRequest) returns (SubmitSolutionResponse);
}
| Вопрос | REST | gRPC |
|---|---|---|
| Основная модель | Ресурсы | Сервисы и методы |
| Контракт | OpenAPI/документация, часто отдельно | .proto как источник истины |
| Данные | Обычно JSON | Обычно Protocol Buffers |
| Транспорт | HTTP/1.1 или HTTP/2 | HTTP/2 |
| Клиенты | Пишутся руками или генерируются | Обычно генерируются |
| Удобство для браузера | Высокое | Низкое без прокси/gRPC-Web |
| Service-to-service | Можно, но много ручной работы | Основной сценарий |
Важно: gRPC не "лучше REST" вообще. Это другой инструмент. REST проще для публичного API, интеграций и браузерного мира. gRPC сильнее там, где нужен строгий контракт, быстрый бинарный формат, много внутренних клиентов и streaming.
Что происходит под капотом
gRPC использует несколько слоёв:
Ваш Go-код
↓
Сгенерированный gRPC client/server
↓
Protocol Buffers messages
↓
gRPC status, metadata, deadlines
↓
HTTP/2 streams
↓
TCP/TLS
HTTP/2
gRPC работает поверх HTTP/2. Это даёт:
- multiplexing: несколько RPC могут идти по одному соединению;
- headers/trailers: metadata и финальный gRPC status передаются отдельно от body;
- flow control: транспорт умеет тормозить отправителя, если получатель не успевает читать;
- streaming: один RPC может передавать много сообщений в одну или обе стороны.
Обычно вы не работаете с HTTP/2 напрямую. Вы пишете .proto, генерируете код и реализуете Go-интерфейс.
Почему это не проверяют обычным curl
REST endpoint часто можно быстро потрогать так:
curl http://localhost:8080/lessons/grpc-overview
С gRPC так обычно не получится. У gRPC другой wire protocol: HTTP/2, protobuf body, специальные headers/trailers и gRPC status. Обычный HTTP-запрос не знает, какой protobuf message сериализовать и как прочитать gRPC trailers.
Для локальной проверки используют другие варианты:
- маленький Go-клиент на generated stub;
- интеграционный тест, который поднимает gRPC server на локальном listener;
grpcurlили GUI-клиент вроде Kreya/BloomRPC, если включена reflection или передан.proto;- server reflection в dev/staging, чтобы инструмент мог сам узнать список сервисов.
Главная мысль: gRPC debug-инструменты должны понимать контракт. Если хочется "просто открыть в браузере", значит для этого сценария лучше подходит REST, gateway или gRPC-Web, а не чистый service-to-service gRPC.
Protocol Buffers
Protocol Buffers, или protobuf, описывает структуру сообщений:
message GetLessonRequest {
string slug = 1;
}
message GetLessonResponse {
string title = 1;
string body_markdown = 2;
}
Числа 1, 2 - это не порядок для красоты, а часть wire-формата. Клиент и сервер используют номера полей, чтобы понимать бинарные данные. Поэтому номера существующих полей нельзя переиспользовать для другого смысла.
Четыре типа RPC
gRPC поддерживает не только один запрос - один ответ.
service LessonService {
// Unary: один request, один response.
rpc GetLesson(GetLessonRequest) returns (GetLessonResponse);
// Server-side streaming: один request, поток response.
rpc ListLessons(ListLessonsRequest) returns (stream Lesson);
// Client-side streaming: поток request, один response.
rpc UploadProgress(stream ProgressEvent) returns (ProgressSummary);
// Bidirectional streaming: поток request и поток response.
rpc MentorChat(stream ChatMessage) returns (stream ChatMessage);
}
В этом модуле мы пойдём от простого к сложному:
- Сначала разберём protobuf-схему.
- Потом сгенерируем Go-код.
- Потом напишем unary server/client.
- Потом разберём context, deadlines, metadata и ошибки.
- Потом встроим gRPC в Clean Architecture.
- В конце разберём streaming и production-практики.
Когда выбирать gRPC
Выбирайте gRPC, если:
- сервисы общаются внутри backend-системы;
- нужен строгий контракт и генерация клиентов;
- есть клиенты на нескольких языках;
- важны deadlines, cancellation и единая модель ошибок;
- нужны server/client/bidirectional streams;
- payload большой или запросов очень много.
REST обычно проще, если:
- API публичное и его будут вызывать внешние пользователи;
- нужен простой доступ из браузера;
- важна читаемость JSON руками;
- команда маленькая и контракт пока быстро меняется;
- нет выигрыша от codegen и HTTP/2 streaming.
Практический подход: публичный edge API часто оставляют REST/HTTP, а внутреннее service-to-service взаимодействие делают на gRPC.
Как выглядит маленький gRPC-проект
В учебном проекте удобно держать контракт отдельно от реализации:
grpc-demo/
proto/
calculator/v1/calculator.proto
gen/
calculator/v1/
calculator.pb.go
calculator_grpc.pb.go
cmd/
calculator-server/main.go
calculator-client/main.go
internal/
calculator/
server.go
service.go
mapper.go
proto/ - источник истины. gen/ - результат codegen, его не редактируют руками. cmd/calculator-server собирает приложение-сервер. cmd/calculator-client полезен как локальная проверка без браузера и curl. internal/calculator содержит вашу реализацию: бизнес-методы, маппинг и glue-код вокруг generated interface.
В реальных сервисах generated code часто лежит в отдельном module/repository с контрактами. Для обучения проще держать всё рядом, чтобы видеть весь путь от .proto до работающего вызова.
Мини-пример контракта
syntax = "proto3";
package course.v1;
option go_package = "example.com/mentor/gen/course/v1;coursev1";
service CourseService {
rpc GetLesson(GetLessonRequest) returns (GetLessonResponse);
}
message GetLessonRequest {
string slug = 1;
}
message GetLessonResponse {
string slug = 1;
string title = 2;
string body_markdown = 3;
}
Что здесь важно:
syntax = "proto3"выбирает современную версию языка protobuf;package course.v1задаёт пространство имён на уровне protobuf;option go_packageговорит генератору, какой Go package создать;serviceописывает API;messageописывает request/response DTO;- номера полей фиксируют wire-contract.
Вопросы на собеседовании
- Чем RPC отличается от REST?
- Почему gRPC обычно используют для service-to-service?
- Зачем gRPC нужен HTTP/2?
- Почему gRPC не всегда удобен как публичный браузерный API?
- Что такое unary RPC?
- Какие типы streaming есть в gRPC?
- Почему
.protoлучше держать как источник истины, а не как "ещё одну документацию"? - Почему чистый gRPC неудобно проверять обычным
curl? - Что должно лежать в
proto/,gen/,cmd/иinternal/?
Источники
Практика
Когда gRPC обычно выигрывает у REST внутри микросервисной системы?
Что выведет этот код?
package main
import "fmt"
func chooseAPI(internal bool, needsBrowser bool) string {
if internal && !needsBrowser {
return "rpc"
}
return "rest"
}
func main() {
fmt.Println(chooseAPI(true, false))
fmt.Println(chooseAPI(false, true))
}
Реализуй TransportChoice: если вызов внутренний и нужен строгий контракт, верни "grpc", иначе "rest".