Модуль: 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.proto

2. Четыре типа RPC#

ТипЗапросОтветСигнатура (упрощённо)Применение
Unary11Get(ctx, req) (resp, err)обычный запрос-ответ
Server streaming1NList(ctx, req) (stream, err)выдача списка/фида, прогресс
Client streamingN1Upload(ctx) (stream, err)загрузка чанков, batch upsert
Bidi streamingNMChat(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.

HeadersTrailers
Когдадо тела ответапосле тела ответа
Когда отправитьSetHeader/SendHeader до первого SendSetTrailer, отправляются автоматически при завершении
Зачем в стримахмета до начала потокамета по итогам (метрики, финальный статус)
Сам 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 cancel499
Unknown (2)неизвестнаяpanic, не-status error500
InvalidArgument (3)плохой запросвалидация400
DeadlineExceeded (4)дедлайн истёктаймаут504
NotFound (5)нет ресурса404
AlreadyExists (6)конфликт созданиядубликат409
PermissionDenied (7)нет правRBAC403
ResourceExhausted (8)лимит/квотаrate limit429
FailedPrecondition (9)состояние не позволяет400
Aborted (10)конфликт конкурентностиoptimistic lock409
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: сравнение#

КритерийgRPCREST/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/reflectioncurl, браузер
Совместимость/эволюцияхорошая при дисциплине номеров полейгибкая, но без гарантий
Кэширование (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).