Модуль: Backend · Уровень: Middle+/Senior
TL;DR#
- gRPC — RPC-фреймворк поверх HTTP/2, использующий Protocol Buffers как IDL и формат сериализации (бинарный, schema-first). Один TCP-коннект мультиплексирует множество параллельных стримов (нет head-of-line blocking на уровне приложения).
- 4 типа RPC: unary (1→1), server streaming (1→N), client streaming (N→1), bidirectional streaming (N→M, полнодуплексный).
- Интерсепторы = middleware. Отдельные типы для unary/stream и для client/server. Цепочки строятся через
grpc.ChainUnaryInterceptor/ChainStreamInterceptor. Типичные: logging, auth, recovery (panic→codes.Internal), metrics, tracing. - Контекст и дедлайны:
context.WithTimeoutна клиенте → дедлайн сериализуется в заголовокgrpc-timeout→ сервер получает его вctx→ дедлайн распространяется по цепочке при дальнейших вызовах. Отмена клиента доходит до сервера (ctx.Done()). - Метаданные (
metadata.MD) — пары ключ/значение (≈HTTP-заголовки).FromIncomingContext(чтение на сервере),NewOutgoingContext(отправка с клиента). Различают headers (до тела) и trailers (после тела, для стримов критично). - Error model: ошибки =
status.Statusсcodes.Code+ message + опциональныеdetails(google.rpc.*, напр.ErrorInfo,BadRequest).status.Errorf(codes.NotFound, ...). Есть стандартный маппинг codes ↔ HTTP. - gRPC vs REST: gRPC — для внутренних сервис-сервис вызовов (производительность, строгий контракт, streaming); REST/JSON — для публичных/браузерных API. Браузер не умеет gRPC напрямую → gRPC-Web / gRPC-Gateway.
Теория#
1. Что такое gRPC#
gRPC — это контрактно-ориентированный (schema-first) RPC-фреймворк. Ключевые составляющие:
- HTTP/2 как транспорт: бинарный фреймовый протокол с мультиплексированием стримов поверх одного TCP-соединения, сжатием заголовков (HPACK), flow control на уровне стрима и коннекта, server push (gRPC его не использует). Мультиплексирование убирает application-level head-of-line blocking (HTTP/1.1 требовал по соединению на запрос или pipelining). Но: TCP-level HOL blocking остаётся (это решает уже HTTP/3 / QUIC, который gRPC поддерживает экспериментально).
- Protocol Buffers как IDL и wire-формат: компактный бинарный формат с tag-number полями, обратно/прямо совместимый при правильной эволюции схемы. Кодогенерация (
protoc+protoc-gen-go+protoc-gen-go-grpc) даёт типобезопасные клиент и сервер. - Один RPC = один HTTP/2-стрим. Метод кодируется как путь
:path = /package.Service/Method. Тело —length-prefixed message: 1 байт compressed-flag + 4 байта big-endian длины + payload.
syntax = "proto3";
package user.v1;
option go_package = "example.com/gen/user/v1;userv1";
message GetUserRequest { string id = 1; }
message User { string id = 1; string name = 2; int32 age = 3; }
service UserService {
rpc GetUser(GetUserRequest) returns (User);
}protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
user/v1/user.proto2. Четыре типа RPC#
| Тип | Запрос | Ответ | Сигнатура (упрощённо) | Применение |
|---|---|---|---|---|
| Unary | 1 | 1 | Get(ctx, req) (resp, err) | обычный запрос-ответ |
| Server streaming | 1 | N | List(ctx, req) (stream, err) | выдача списка/фида, прогресс |
| Client streaming | N | 1 | Upload(ctx) (stream, err) | загрузка чанков, batch upsert |
| Bidi streaming | N | M | Chat(ctx) (stream, err) | чат, real-time, двусторонний обмен |
service Demo {
rpc Unary(Req) returns (Resp);
rpc ServerStream(Req) returns (stream Resp);
rpc ClientStream(stream Req) returns (Resp);
rpc BidiStream(stream Req) returns (stream Resp);
}Unary (сервер):
func (s *server) Unary(ctx context.Context, req *pb.Req) (*pb.Resp, error) {
return &pb.Resp{Value: req.GetValue()}, nil
}Server streaming (сервер): возвращаем ошибку из метода, чтобы завершить стрим; Send можно вызывать много раз.
func (s *server) ServerStream(req *pb.Req, stream pb.Demo_ServerStreamServer) error {
for i := 0; i < 10; i++ {
if err := stream.Context().Err(); err != nil { // клиент отменил/дедлайн
return err
}
if err := stream.Send(&pb.Resp{Value: int32(i)}); err != nil {
return err // транспорт сломался
}
}
return nil // завершение стрима = io.EOF на клиенте
}Client streaming (сервер): читаем до io.EOF, затем отправляем один ответ через SendAndClose.
func (s *server) ClientStream(stream pb.Demo_ClientStreamServer) error {
var sum int32
for {
req, err := stream.Recv()
if errors.Is(err, io.EOF) {
return stream.SendAndClose(&pb.Resp{Value: sum})
}
if err != nil {
return err
}
sum += req.GetValue()
}
}Bidirectional streaming (сервер): Recv и Send независимы (полный дуплекс). Часто Send делают из отдельной горутины.
func (s *server) BidiStream(stream pb.Demo_BidiStreamServer) error {
for {
req, err := stream.Recv()
if errors.Is(err, io.EOF) {
return nil // клиент закрыл свою сторону
}
if err != nil {
return err
}
if err := stream.Send(&pb.Resp{Value: req.GetValue() * 2}); err != nil {
return err
}
}
}Клиент (server streaming):
stream, err := client.ServerStream(ctx, &pb.Req{})
if err != nil { /* ошибка установки стрима */ }
for {
resp, err := stream.Recv()
if errors.Is(err, io.EOF) { break } // нормальное завершение
if err != nil {
st, _ := status.FromError(err) // здесь приходит статус-ошибка сервера
log.Printf("stream err: %v %v", st.Code(), st.Message())
break
}
use(resp)
}Важно: ошибка статуса в стримах приходит из Recv(), а не из вызова метода. Сам вызов client.ServerStream(...) обычно лишь открывает стрим.
3. Интерсепторы (middleware)#
Четыре базовых типа: server-unary, server-stream, client-unary, client-stream.
// Server unary
type UnaryServerInterceptor func(ctx context.Context, req any,
info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error)
// Server stream
type StreamServerInterceptor func(srv any, ss grpc.ServerStream,
info *grpc.StreamServerInfo, handler grpc.StreamHandler) errorЦепочки (порядок применения = порядок аргументов; первый — самый внешний):
srv := grpc.NewServer(
grpc.ChainUnaryInterceptor(recoveryUnary, loggingUnary, authUnary),
grpc.ChainStreamInterceptor(recoveryStream, loggingStream, authStream),
)Logging + metrics:
func loggingUnary(ctx context.Context, req any, info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler) (any, error) {
start := time.Now()
resp, err := handler(ctx, req)
code := status.Code(err)
log.Printf("method=%s code=%s dur=%s", info.FullMethod, code, time.Since(start))
metrics.ObserveRPC(info.FullMethod, code.String(), time.Since(start))
return resp, err
}Auth (читаем токен из метаданных, прокидываем claims в context):
func authUnary(ctx context.Context, req any, info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler) (any, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "no metadata")
}
vals := md.Get("authorization")
if len(vals) == 0 {
return nil, status.Error(codes.Unauthenticated, "missing token")
}
claims, err := verify(vals[0])
if err != nil {
return nil, status.Error(codes.Unauthenticated, "invalid token")
}
ctx = context.WithValue(ctx, claimsKey{}, claims)
return handler(ctx, req)
}Recovery (panic → codes.Internal, чтобы один паникнувший RPC не уронил процесс):
func recoveryUnary(ctx context.Context, req any, info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler) (resp any, err error) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in %s: %v\n%s", info.FullMethod, r, debug.Stack())
err = status.Errorf(codes.Internal, "internal error")
}
}()
return handler(ctx, req)
}Stream-интерсептор с подменой context. В стрим-интерсепторе нет прямого доступа к ctx хендлера — ServerStream имеет метод Context(). Чтобы прокинуть значения вниз, оборачивают ServerStream:
type wrappedStream struct {
grpc.ServerStream
ctx context.Context
}
func (w *wrappedStream) Context() context.Context { return w.ctx }
func authStream(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo,
handler grpc.StreamHandler) error {
ctx, err := authenticate(ss.Context())
if err != nil { return err }
return handler(srv, &wrappedStream{ServerStream: ss, ctx: ctx})
}Client-side интерсепторы (для исходящих вызовов: retry, добавление токена, трейсинг):
func authClientUnary(ctx context.Context, method string, req, reply any,
cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
ctx = metadata.AppendToOutgoingContext(ctx, "authorization", token())
return invoker(ctx, method, req, reply, cc, opts...)
}
conn, _ := grpc.NewClient(addr, grpc.WithChainUnaryInterceptor(authClientUnary))На практике почти всегда берут github.com/grpc-ecosystem/go-grpc-middleware (v2): готовые recovery/logging/auth/retry/ratelimit/validator.
4. Контекст и дедлайны#
Дедлайн задаётся на клиенте и передаётся серверу через wire (заголовок HTTP/2 grpc-timeout, напр. 100m). Сервер видит его как deadline в ctx.
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
defer cancel()
resp, err := client.GetUser(ctx, &pb.GetUserRequest{Id: "42"})
if status.Code(err) == codes.DeadlineExceeded {
// дедлайн истёк
}Propagation (распространение дедлайна по цепочке). Если сервис A с дедлайном 200мс вызывает сервис B, передавая полученный ctx, то оставшийся бюджет времени (минус прошедшее) уходит в B. Так дедлайн каскадируется по всей цепочке вызовов, и B не будет работать дольше, чем у A осталось времени.
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
// ctx уже содержит дедлайн клиента; пробрасываем его дальше
profile, err := s.profileClient.GetProfile(ctx, &pb.GetProfileRequest{Id: req.Id})
...
}Отмена. Если клиент отменяет вызов (cancel) или рвётся соединение, сервер получает ctx.Err() (context.Canceled) — нужно периодически проверять ctx.Err() / ctx.Done() в долгих операциях и в стримах (stream.Context()), чтобы не делать лишнюю работу.
Анти-паттерн: вызывать downstream-сервис с context.Background() вместо входящего ctx — теряется дедлайн и отмена; downstream продолжит работать, когда клиент уже ушёл.
5. Метаданные#
metadata.MD — это map[string][]string. Ключи приводятся к нижнему регистру. Бинарные значения — суффикс -bin (значение base64-кодируется на wire). Зарезервированы ключи grpc-*.
// КЛИЕНТ: отправка исходящих метаданных
md := metadata.Pairs("authorization", "Bearer xxx", "x-request-id", reqID)
ctx = metadata.NewOutgoingContext(ctx, md)
// или инкрементально:
ctx = metadata.AppendToOutgoingContext(ctx, "x-tenant", "acme")
// СЕРВЕР: чтение входящих метаданных
md, ok := metadata.FromIncomingContext(ctx)
reqID := first(md.Get("x-request-id"))Headers vs Trailers.
| Headers | Trailers | |
|---|---|---|
| Когда | до тела ответа | после тела ответа |
| Когда отправить | SetHeader/SendHeader до первого Send | SetTrailer, отправляются автоматически при завершении |
| Зачем в стримах | мета до начала потока | мета по итогам (метрики, финальный статус) |
| Сам gRPC | — | статус-код/сообщение передаются именно в trailers |
// Сервер
grpc.SetHeader(ctx, metadata.Pairs("x-served-by", host)) // header
grpc.SetTrailer(ctx, metadata.Pairs("x-rows-scanned", "100")) // trailer
// Клиент: получить header/trailer от unary-вызова
var hdr, trl metadata.MD
resp, err := client.GetUser(ctx, req, grpc.Header(&hdr), grpc.Trailer(&trl))Важная деталь: т.к. статус gRPC передаётся в trailers, при HTTP/1.1 он бы не работал — поэтому gRPC требует именно HTTP/2 (trailers). Для server streaming статус-код приходит после всех сообщений — отсюда модель «Recv() до io.EOF, ошибка в последнем Recv()».
6. Error model#
Ошибка gRPC = *status.Status: код (codes.Code), сообщение, и опциональные details (произвольные protobuf-сообщения, обычно из google.rpc).
// Создание
return nil, status.Errorf(codes.NotFound, "user %s not found", id)
return nil, status.Error(codes.InvalidArgument, "id is required")
// Разбор на клиенте
st, ok := status.FromError(err)
switch st.Code() {
case codes.NotFound: ...
case codes.DeadlineExceeded: ...
case codes.Unavailable: // транзиентно, можно ретраить
}Основные коды (google.golang.org/grpc/codes):
| Code | Семантика | Типичная причина | HTTP-маппинг |
|---|---|---|---|
OK (0) | успех | — | 200 |
Canceled (1) | отмена клиентом | client cancel | 499 |
Unknown (2) | неизвестная | panic, не-status error | 500 |
InvalidArgument (3) | плохой запрос | валидация | 400 |
DeadlineExceeded (4) | дедлайн истёк | таймаут | 504 |
NotFound (5) | нет ресурса | — | 404 |
AlreadyExists (6) | конфликт создания | дубликат | 409 |
PermissionDenied (7) | нет прав | RBAC | 403 |
ResourceExhausted (8) | лимит/квота | rate limit | 429 |
FailedPrecondition (9) | состояние не позволяет | — | 400 |
Aborted (10) | конфликт конкурентности | optimistic lock | 409 |
OutOfRange (11) | выход за границы | пагинация | 400 |
Unimplemented (12) | метод не реализован | старый сервер | 501 |
Internal (13) | внутренняя ошибка | баг | 500 |
Unavailable (14) | сервис недоступен | down/перегрузка | 503 |
DataLoss (15) | потеря данных | — | 500 |
Unauthenticated (16) | нет/невалидный auth | токен | 401 |
Различайте: InvalidArgument — запрос плох сам по себе (ретрай не поможет); FailedPrecondition — система не в том состоянии; Unavailable — транзиентно, ретрай уместен; Aborted — конфликт, ретрай на верхнем уровне.
Error details (богатые ошибки). Используются типы из google.golang.org/genproto/googleapis/rpc/errdetails:
import "google.golang.org/genproto/googleapis/rpc/errdetails"
st := status.New(codes.InvalidArgument, "validation failed")
st, _ = st.WithDetails(
&errdetails.BadRequest{FieldViolations: []*errdetails.BadRequest_FieldViolation{
{Field: "age", Description: "must be >= 0"},
}},
&errdetails.ErrorInfo{Reason: "BAD_AGE", Domain: "user.v1",
Metadata: map[string]string{"got": "-5"}},
)
return nil, st.Err()
// Клиент
st, _ := status.FromError(err)
for _, d := range st.Details() {
switch info := d.(type) {
case *errdetails.BadRequest:
for _, v := range info.GetFieldViolations() { ... }
case *errdetails.ErrorInfo:
log.Println(info.GetReason(), info.GetDomain())
}
}На wire это всё — google.rpc.Status (code, message, repeated google.protobuf.Any details), сериализованный в trailer grpc-status-details-bin.
Анти-паттерн: возвращать «голый» errors.New("...") из хендлера — клиент получит codes.Unknown. Всегда оборачивайте в status. Не утекайте внутренние детали (SQL, стектрейсы) в message публичного API.
7. Keepalive, connection management, load balancing#
Keepalive (HTTP/2 PING) детектит мёртвые соединения и держит коннект живым через NAT/LB:
// Клиент
grpc.WithKeepaliveParams(keepalive.ClientParameters{
Time: 30 * time.Second, // слать PING раз в 30с при простое
Timeout: 10 * time.Second, // ждать ответа на PING
PermitWithoutStream: true, // пинговать даже без активных RPC
})
// Сервер — политика и принудительный refresh коннектов
grpc.KeepaliveParams(keepalive.ServerParameters{
MaxConnectionAge: 30 * time.Minute, // принудительно закрывать старые коннекты
MaxConnectionAgeGrace: 5 * time.Second,
Time: 2 * time.Hour,
})
grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
MinTime: 10 * time.Second, // запрет слишком частых PING (иначе GOAWAY)
PermitWithoutStream: true,
})MaxConnectionAge важен с L4-балансировщиками: gRPC держит долгоживущий коннект, и без принудительного переподключения новые поды бэкенда не получат трафик.
Load balancing — client-side. gRPC не делает per-request балансировку через L4 LB (один коннект). Подходы:
- Client-side LB: resolver (DNS/xDS) отдаёт список адресов, балансировщик (
round_robin,pick_first) распределяет RPC по подключениям.
conn, _ := grpc.NewClient("dns:///user-service:50051",
grpc.WithDefaultServiceConfig(`{"loadBalancingConfig":[{"round_robin":{}}]}`),
)- Look-aside / proxy LB: L7-прокси с поддержкой HTTP/2 (Envoy, Linkerd) — балансирует по стримам.
- xDS (Envoy API) для продвинутого service discovery/LB/canary.
Retry настраивается через service config (MethodConfig.retryPolicy) — backoff, retryableStatusCodes (обычно UNAVAILABLE). Ретраить безопасно только идемпотентные методы.
Подводные камни / gotchas#
context.Background()вместо входящего ctx при downstream-вызовах — теряются дедлайн и отмена. Всегда прокидывайтеctx.- Голые ошибки →
codes.Unknown. Хендлер должен возвращатьstatus.Error(...), иначе клиент не отличит NotFound от Internal. - Утечка внутренних деталей в
message(SQL, пути, стектрейсы) — кладите их в логи/details, не в публичное сообщение. - Стрим-ошибка приходит из
Recv()/Send(), а не из вызова метода. Проверкаerrтолько на открытии стрима недостаточна. io.EOF≠ ошибка. Это нормальное завершение стрима; реальную ошибку смотрите черезstatus.FromError.- Trailers и статус. Если читаете заголовки на клиенте до завершения стрима, trailers ещё недоступны. Статус-код всегда в trailers → нужен HTTP/2.
- Большие сообщения. Дефолтный лимит приёма — 4 МБ (
MaxRecvMsgSize). Большие payload лучше стримить чанками, а не раздувать одно сообщение. - Долгоживущие коннекты + L4 LB = неравномерное распределение; нужен
MaxConnectionAgeили L7-прокси. - Слишком частый keepalive PING без согласования с
EnforcementPolicy.MinTime→ сервер шлётGOAWAYсENHANCE_YOUR_CALMи рвёт коннект. - proto3 и нулевые значения. Без
optionalнельзя отличить «не задано» от нулевого значения (0, “”, false). Для PATCH-семантики используйтеoptional(генерит pointer) илиFieldMask. - Изменение wire-контракта. Нельзя переиспользовать/менять номера полей; удалённые —
reserved. Иначе ломается совместимость. - panic в хендлере без recovery-интерсептора роняет обработку (и потенциально процесс). Recovery обязателен в проде.
- Метаданные регистронезависимы и могут дублироваться (
[]string).md.Get(key)возвращает срез — не берите слепо[0]. - Дедлайн без
defer cancel()— утечка таймера/горутины контекста.
Вопросы на собеседовании#
В: Почему gRPC требует именно HTTP/2, а не HTTP/1.1? О: gRPC опирается на мультиплексирование стримов (много параллельных RPC по одному коннекту без HOL-блокировки на уровне приложения), на потоковую передачу в обе стороны (streaming RPC) и, критически, на trailers — статус-код и сообщение gRPC передаются в trailing-заголовках после тела ответа. HTTP/1.1 не поддерживает trailers и мультиплексирование, поэтому полноценный gRPC поверх него невозможен (есть лишь компромисс gRPC-Web).
В: Чем отличаются 4 типа RPC и как на сервере понять, что стрим завершён?
О: unary (1→1), server streaming (1→N), client streaming (N→1), bidi (N→M, полный дуплекс). Завершение клиентской стороны стрима сервер видит как io.EOF из Recv(). Завершение серверного стрима = возврат из метода (на клиенте — io.EOF из Recv()). В bidi Recv и Send независимы.
В: Как дедлайн распространяется через цепочку gRPC-вызовов?
О: Клиент задаёт context.WithTimeout; gRPC сериализует оставшееся время в HTTP/2-заголовок grpc-timeout. Сервер получает его как deadline в ctx. Если сервер делает downstream-вызов, передавая тот же ctx, оставшийся бюджет (минус прошедшее время) уходит дальше — дедлайн каскадируется. Истечение даёт codes.DeadlineExceeded. Поэтому downstream нельзя вызывать с context.Background().
В: Чем интерсептор для unary отличается от интерсептора для stream и как пробросить значение в context внутри stream-интерсептора?
О: unary-интерсептор получает ctx и req напрямую и оборачивает вызов handler(ctx, req). stream-интерсептор получает grpc.ServerStream (без прямого ctx) и вызывает handler(srv, ss). Чтобы положить значение в контекст стрима, оборачивают ServerStream, переопределяя метод Context() (паттерн wrappedStream). Цепочки строятся через ChainUnaryInterceptor/ChainStreamInterceptor, первый аргумент — самый внешний.
В: Как устроена error model в gRPC? Что не так с возвратом errors.New?
О: Ошибка — это status.Status: codes.Code + сообщение + опциональные details (protobuf-сообщения, на wire — google.rpc.Status в trailer grpc-status-details-bin). Возврат «голой» ошибки маппится в codes.Unknown, теряя семантику. Правильно — status.Errorf(codes.NotFound, ...) и WithDetails(&errdetails.BadRequest{...}) для машиночитаемых деталей; на клиенте — status.FromError и обход st.Details().
В: В чём разница между InvalidArgument, FailedPrecondition, Unavailable и Aborted и какие из них имеет смысл ретраить?
О: InvalidArgument — запрос некорректен сам по себе, ретрай бесполезен. FailedPrecondition — система не в нужном состоянии (например, ресурс не пуст); ретрай без изменения состояния не поможет. Unavailable — транзиентная недоступность, ретрай с backoff уместен. Aborted — конфликт конкурентного доступа (optimistic lock); ретрай на уровне всей транзакции/бизнес-операции уместен. Идемпотентность — обязательное условие для безопасного ретрая.
В: Headers vs trailers в gRPC — зачем оба?
О: Headers отправляются до тела (метаданные начала ответа, можно до первого Send в стриме). Trailers — после тела; в них gRPC передаёт сам статус-код/сообщение и итоговые метаданные (метрики, число строк). В server streaming статус доступен только после всех сообщений — отсюда модель «читать до io.EOF, реальная ошибка в последнем Recv()». На клиенте header/trailer берут через grpc.Header(&md)/grpc.Trailer(&md).
В: Как gRPC балансирует нагрузку и почему обычный L4-балансировщик плохо подходит?
О: gRPC держит долгоживущий HTTP/2-коннект и мультиплексирует RPC. L4 LB балансирует на уровне TCP-коннектов, а не запросов — все RPC одного клиента уходят в один бэкенд, новые поды не получают трафик. Решения: client-side LB (resolver + round_robin/pick_first, опционально xDS), L7-прокси (Envoy/Linkerd) для per-stream балансировки, и MaxConnectionAge на сервере для принудительного периодического переподключения.
В: Когда выбрать gRPC, а когда REST? О: gRPC — внутренние сервис-сервис вызовы, где важны производительность (бинарный protobuf, HTTP/2), строгий версионируемый контракт, streaming, низкая латентность, polyglot-кодогенерация. REST/JSON — публичные API, браузерные клиенты, простота отладки (curl), широкая совместимость, кэшируемость по HTTP. Браузер не умеет gRPC напрямую → gRPC-Web (через прокси) или gRPC-Gateway (REST/JSON фасад над теми же proto).
gRPC vs REST: сравнение#
| Критерий | gRPC | REST/JSON |
|---|---|---|
| Транспорт | HTTP/2 (обязательно) | HTTP/1.1, HTTP/2 |
| Формат | protobuf (бинарный, компактный) | JSON (текст, человекочитаемый) |
| Контракт | строгий, schema-first (.proto), кодоген | OpenAPI/Swagger (опционально) |
| Производительность | выше (бинарь, мультиплекс, меньше overhead) | ниже (парсинг JSON, заголовки) |
| Streaming | нативно (4 типа) | ограниченно (SSE, chunked, websockets) |
| Браузер | напрямую нет → gRPC-Web | нативно |
| Отладка | нужны grpcurl/Postman/reflection | curl, браузер |
| Совместимость/эволюция | хорошая при дисциплине номеров полей | гибкая, но без гарантий |
| Кэширование (HTTP) | нет из коробки | да (GET, ETag, CDN) |
| Где применять | внутренние микросервисы, low-latency, polyglot | публичные/браузерные API, интеграции |
Мосты для браузера/REST:
- gRPC-Web: ограниченный gRPC из браузера, требует прокси (Envoy/grpcwebproxy), нет client-streaming/bidi в полном объёме.
- gRPC-Gateway: генерирует reverse-proxy REST/JSON ↔ gRPC из аннотаций в .proto (
google.api.http) — один контракт, два интерфейса.
На что копают на senior+#
- HTTP/2 механика: фреймы, стримы, flow control, HPACK, почему остаётся TCP-level HOL blocking и что меняет HTTP/3/QUIC; как
length-prefixed messageлежит в DATA-фреймах. - Deadline propagation в распределённой системе: бюджетирование времени по цепочке, защита от каскадных таймаутов, отличие
CanceledотDeadlineExceeded, что происходит с downstream при отмене. - Идемпотентность и retry-семантика: настройка
retryPolicyв service config,retryableStatusCodes, hedging, взаимодействие ретраев с дедлайнами (бюджет на все попытки). - Backpressure и flow control в стримах: что если клиент медленно читает; буферизация; почему bidi требует аккуратной горутинной модели (отдельные Recv/Send), дедлоки при синхронном Send в цикле Recv.
- Эволюция схемы: правила совместимости proto3,
reserved,optional,oneof,FieldMaskдля частичных апдейтов, отличие unknown fields поведения. - Безопасность: mTLS (
credentials.NewTLS), per-RPC credentials, ALTS, где валидировать auth (интерсептор vs хендлер), защита от утечки деталей в ошибках. - Observability: интеграция с OpenTelemetry (
stats.Handler), пропагация trace-контекста через метаданные, корреляция request-id, метрики поcodes. - Load balancing глубоко: pick_first vs round_robin, resolver/balancer API, xDS, subsetting, outlier detection, почему
MaxConnectionAgeнужен с L4 LB. - Производительность: сжатие (gzip), пулы коннектов vs мультиплексирование, лимиты сообщений, влияние размера сообщений и числа стримов, профилирование сериализации protobuf.
- Сравнение с альтернативами: gRPC vs GraphQL vs REST vs message queues; когда RPC-модель неуместна (event-driven, pub/sub).