gRPC Advanced
Зачем знать: gRPC — де-факто стандарт internal service-to-service коммуникации в Go-экосистеме. Middle 1 знает базу (proto, unary). Middle 2 обязан понимать streaming паттерны (включая flow control, half-close), interceptors chains, deadlines, retry/hedging, load balancing (pick-first, round-robin, xDS), health checks для k8s, gRPC-Gateway, и альтернативы вроде Connect-Go. Без этого нельзя правильно построить mesh, debug latency tail, и survive в продакшене.
Содержание
Заголовок раздела «Содержание»- Базовая концепция
- Глубокое погружение (под капотом)
- Gotchas (12+)
- Production-практики
- Вопросы (30)
- Practice
- Источники
1. Базовая концепция
Заголовок раздела «1. Базовая концепция»gRPC — что это
Заголовок раздела «gRPC — что это»gRPC = HTTP/2 + Protobuf + code generation + RPC семантика. Каждый RPC = один HTTP/2 stream. Stream идентифицируется по :path = /package.Service/Method. Сообщения сериализуются в Protobuf и оборачиваются в length-prefixed frames (5 байт header + payload).
Четыре типа RPC
Заголовок раздела «Четыре типа RPC»1. Unary Client: ----Req----> Server Client: <----Resp--- Server (один request, один response)
2. Server streaming Client: ----Req----> Server Client: <----Resp1-- Server Client: <----Resp2-- Server Client: <----RespN-- Server (один request, N responses)
3. Client streaming Client: ----Req1----> Server Client: ----Req2----> Server Client: ----ReqN----> Server Client: ----half-close (END_STREAM) Client: <----Resp--- Server (N requests, один response)
4. Bidirectional streaming Client: ----Req----> Server Client: <----Resp--- Server ... независимо в обе стороны ... обе стороны могут half-closeМинимальный proto-файл и сервер
Заголовок раздела «Минимальный proto-файл и сервер»syntax = "proto3";package chat.v1;option go_package = "example.com/chat/v1;chatv1";
service Chat { rpc SendMessage(SendMessageRequest) returns (SendMessageResponse); rpc Subscribe(SubscribeRequest) returns (stream Event); // server stream rpc Upload(stream Chunk) returns (UploadAck); // client stream rpc Channel(stream ClientMsg) returns (stream ServerMsg); // bidi}
message SendMessageRequest { string text = 1; }message SendMessageResponse { string id = 1; }
message SubscribeRequest { string channel = 1; }message Event { string body = 1; }
message Chunk { bytes data = 1; }message UploadAck { uint64 total = 1; }
message ClientMsg { string body = 1; }message ServerMsg { string body = 1; }Генерация:
# C использованием protoc:protoc --go_out=. --go-grpc_out=. chat.proto
# Или Buf (рекомендую):# buf.gen.yaml:# version: v1# plugins:# - plugin: buf.build/protocolbuffers/go# out: gen# opt: paths=source_relative# - plugin: buf.build/grpc/go# out: gen# opt: paths=source_relativebuf generateСервер: streaming impl
Заголовок раздела «Сервер: streaming impl»package main
import ( "io" "log" "net"
chatv1 "example.com/chat/v1" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status")
type chatServer struct { chatv1.UnimplementedChatServer}
// Server streaming: один Recv, N Sendfunc (s *chatServer) Subscribe(req *chatv1.SubscribeRequest, stream chatv1.Chat_SubscribeServer) error { for i := 0; i < 5; i++ { if err := stream.Send(&chatv1.Event{Body: "tick"}); err != nil { return err } } return nil}
// Client streaming: N Recv, один SendAndClosefunc (s *chatServer) Upload(stream chatv1.Chat_UploadServer) error { var total uint64 for { chunk, err := stream.Recv() if err == io.EOF { return stream.SendAndClose(&chatv1.UploadAck{Total: total}) } if err != nil { return err } total += uint64(len(chunk.Data)) }}
// Bidi streaming: read/write независимоfunc (s *chatServer) Channel(stream chatv1.Chat_ChannelServer) error { for { msg, err := stream.Recv() if err == io.EOF { return nil } if err != nil { return err } if err := stream.Send(&chatv1.ServerMsg{Body: "echo: " + msg.Body}); err != nil { return err } }}
func (s *chatServer) SendMessage(_ context.Context, req *chatv1.SendMessageRequest) (*chatv1.SendMessageResponse, error) { if req.Text == "" { return nil, status.Error(codes.InvalidArgument, "empty text") } return &chatv1.SendMessageResponse{Id: "msg-1"}, nil}
func main() { lis, err := net.Listen("tcp", ":9090") if err != nil { log.Fatal(err) } srv := grpc.NewServer() chatv1.RegisterChatServer(srv, &chatServer{}) log.Fatal(srv.Serve(lis))}Клиент: streaming
Заголовок раздела «Клиент: streaming»package main
import ( "context" "io" "log" "time"
chatv1 "example.com/chat/v1" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure")
func main() { conn, err := grpc.NewClient("localhost:9090", grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatal(err) } defer conn.Close()
cli := chatv1.NewChatClient(conn)
// Server streaming ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() stream, err := cli.Subscribe(ctx, &chatv1.SubscribeRequest{Channel: "x"}) if err != nil { log.Fatal(err) } for { ev, err := stream.Recv() if err == io.EOF { break } if err != nil { log.Fatal(err) } log.Println("got:", ev.Body) }
// Client streaming up, _ := cli.Upload(ctx) for i := 0; i < 3; i++ { up.Send(&chatv1.Chunk{Data: []byte("hello")}) } ack, _ := up.CloseAndRecv() log.Println("uploaded total:", ack.Total)
// Bidi ch, _ := cli.Channel(ctx) go func() { for { m, err := ch.Recv() if err != nil { return } log.Println("from server:", m.Body) } }() ch.Send(&chatv1.ClientMsg{Body: "ping"}) time.Sleep(time.Second) ch.CloseSend()}2. Глубокое погружение (под капотом)
Заголовок раздела «2. Глубокое погружение (под капотом)»Wire-формат gRPC поверх HTTP/2
Заголовок раздела «Wire-формат gRPC поверх HTTP/2»Каждое сообщение в gRPC обёрнуто в length-prefixed frame:
+------------+----------------------+----------------------------+| 1 byte | 4 bytes | N bytes || compressed | message length (BE) | serialized protobuf || flag | | |+------------+----------------------+----------------------------+- compressed flag: 0 = uncompressed, 1 = compressed (algorithm в header
grpc-encoding) - length: BE uint32
Эти 5-байтные frames кладутся в DATA frames HTTP/2.
Headers (gRPC + HTTP/2)
Заголовок раздела «Headers (gRPC + HTTP/2)»:method POST:scheme https:path /chat.v1.Chat/SendMessage:authority chat.example.comcontent-type application/grpc (или application/grpc+proto, application/grpc+json)grpc-encoding gzipgrpc-timeout 1S (1 секунда, propagated deadline)grpc-trace-bin <bin> (binary metadata)authorization Bearer ... (auth)user-agent grpc-go/1.60.0te trailers (обязательно!)⚠️ HTTP/2 требует te: trailers. Без него — protocol violation. gRPC сильно полагается на trailers (для status code в конце stream).
Trailers — где живёт grpc-status
Заголовок раздела «Trailers — где живёт grpc-status»В отличие от REST, в gRPC статус сообщается в HTTP/2 trailers (заголовки после payload):
grpc-status: 0 (codes.OK)grpc-message: ... (URL-encoded)grpc-status-details-bin (опционально, бинарные details)Это нужно, потому что для streaming не известен заранее final status — он определяется в конце stream.
⚠️ HTTP/1.1 proxy, не понимающий trailers, ломает gRPC. NGINX < 1.13.10 не передавал trailers.
Flow control (HTTP/2 underlying)
Заголовок раздела «Flow control (HTTP/2 underlying)»gRPC наследует flow control HTTP/2:
- per-stream window (default 64 КБ)
- per-connection window
При высокой пропускной способности увеличивайте:
srv := grpc.NewServer( grpc.InitialWindowSize(1<<20), // 1 MiB per stream grpc.InitialConnWindowSize(2<<20), // 2 MiB per connection)conn, _ := grpc.NewClient(addr, grpc.WithInitialWindowSize(1<<20), grpc.WithInitialConnWindowSize(2<<20), grpc.WithTransportCredentials(insecure.NewCredentials()),)Backpressure в streaming
Заголовок раздела «Backpressure в streaming»Server.Send(...) блокируется, если client не успел Recv → flow control window 0Client.Recv() ждёт пока server отправит data → блокируется
Если client медленный → server's Send блокирован → server's memory заполняется буферами⚠️ В client streaming с большими payload’ами это критично. Без deadlines клиент может «висеть» бесконечно.
Streaming half-close
Заголовок раздела «Streaming half-close»Bidi RPC:A → B: req1A → B: req2A → B: END_STREAM (CloseSend) (A больше не шлёт, но B может слать ещё)B → A: resp1B → A: resp2B → A: END_STREAM + trailersCloseSend в Go — это flush END_STREAM frame.
Interceptors
Заголовок раздела «Interceptors»Интерсепторы = middleware. Бывают:
- Unary Server / Unary Client
- Stream Server / Stream Client
// Unary Server interceptorfunc loggingUnary(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { start := time.Now() resp, err := handler(ctx, req) log.Printf("method=%s dur=%s err=%v", info.FullMethod, time.Since(start), err) return resp, err}
// Stream Server interceptor (оборачивает stream)func loggingStream(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { start := time.Now() err := handler(srv, ss) log.Printf("stream=%s dur=%s err=%v", info.FullMethod, time.Since(start), err) return err}
// Несколько interceptors в chain:srv := grpc.NewServer( grpc.ChainUnaryInterceptor( recoveryUnary, // panic recovery — должен быть первым loggingUnary, authUnary, ratelimitUnary, tracingUnary, ), grpc.ChainStreamInterceptor( recoveryStream, loggingStream, authStream, ),)⚠️ Порядок важен:
recovery— первый, чтобы поймать panic из любого нижестоящего.tracing/logging— раннее, чтобы видеть весь request lifecycle.auth— до бизнес-логики.rate limit— после auth (rate-limit per user).
Client-side interceptors
Заголовок раздела «Client-side interceptors»conn, _ := grpc.NewClient(addr, grpc.WithChainUnaryInterceptor( timeoutClientInterceptor, retryClientInterceptor, tracingClientInterceptor, ),)Deadlines & propagation
Заголовок раздела «Deadlines & propagation»// На клиентеctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)defer cancel()resp, err := cli.GetUser(ctx, req)gRPC передаёт оставшийся deadline в header grpc-timeout: 2S. На сервере:
func (s *server) GetUser(ctx context.Context, req *pb.Req) (*pb.Resp, error) { deadline, ok := ctx.Deadline() if ok { log.Printf("remaining=%s", time.Until(deadline)) } // ...}⚠️ Сервер ДОЛЖЕН пробрасывать ctx в downstream вызовы. Иначе deadline не propagated → orphaned работа.
gRPC status codes
Заголовок раздела «gRPC status codes»| Code | Name | HTTP Equivalent | Когда |
|---|---|---|---|
| 0 | OK | 200 | Успех |
| 1 | CANCELLED | 499 | Клиент отменил |
| 2 | UNKNOWN | 500 | Неизвестная ошибка |
| 3 | INVALID_ARGUMENT | 400 | Невалидный input |
| 4 | DEADLINE_EXCEEDED | 504 | Timeout |
| 5 | NOT_FOUND | 404 | Нет ресурса |
| 6 | ALREADY_EXISTS | 409 | Дубликат |
| 7 | PERMISSION_DENIED | 403 | Auth есть, прав нет |
| 8 | RESOURCE_EXHAUSTED | 429 | Rate limit / quota |
| 9 | FAILED_PRECONDITION | 400 | Состояние не подходит |
| 10 | ABORTED | 409 | Конфликт (optimistic) |
| 11 | OUT_OF_RANGE | 400 | Параметр вне диапазона |
| 12 | UNIMPLEMENTED | 501 | Метод не реализован |
| 13 | INTERNAL | 500 | Внутр. ошибка |
| 14 | UNAVAILABLE | 503 | Сервис недоступен |
| 15 | DATA_LOSS | 500 | Данные потеряны |
| 16 | UNAUTHENTICATED | 401 | Auth отсутствует/невалид |
Status + details
Заголовок раздела «Status + details»import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" epb "google.golang.org/genproto/googleapis/rpc/errdetails")
st := status.New(codes.InvalidArgument, "invalid email")st, _ = st.WithDetails(&epb.BadRequest_FieldViolation{ Field: "email", Description: "must be valid RFC 5322 address",})return nil, st.Err()
// На клиенте:if err != nil { st, ok := status.FromError(err) if ok { log.Printf("code=%s msg=%s", st.Code(), st.Message()) for _, d := range st.Details() { // type switch на *epb.BadRequest_FieldViolation и т.д. } }}Metadata
Заголовок раздела «Metadata»Headers/trailers через metadata.MD:
import "google.golang.org/grpc/metadata"
// Клиент → Сервер (outgoing headers)ctx := metadata.AppendToOutgoingContext(context.Background(), "authorization", "Bearer xxx", "x-request-id", "abc-123",)resp, err := cli.GetUser(ctx, req)
// Сервер читает incoming headersfunc (s *server) GetUser(ctx context.Context, req *pb.Req) (*pb.Resp, error) { md, ok := metadata.FromIncomingContext(ctx) if ok { if vals := md.Get("authorization"); len(vals) > 0 { token := strings.TrimPrefix(vals[0], "Bearer ") // ... } } // Сервер шлёт headers/trailers обратно header := metadata.Pairs("x-server-id", "srv-42") grpc.SendHeader(ctx, header) trailer := metadata.Pairs("x-response-id", "resp-42") grpc.SetTrailer(ctx, trailer) return resp, nil}⚠️ Headers vs trailers:
- Header — отправляется ПЕРЕД payload (start of stream).
- Trailer — отправляется ПОСЛЕ payload (end of stream). Здесь
grpc-status.
Retries (gRPC service config JSON)
Заголовок раздела «Retries (gRPC service config JSON)»Встроенный retry policy конфигурируется через JSON service config:
const policy = `{ "methodConfig": [{ "name": [{"service": "chat.v1.Chat", "method": "SendMessage"}], "retryPolicy": { "MaxAttempts": 4, "InitialBackoff": "0.1s", "MaxBackoff": "1s", "BackoffMultiplier": 2.0, "RetryableStatusCodes": ["UNAVAILABLE", "DEADLINE_EXCEEDED"] } }]}`
conn, _ := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithDefaultServiceConfig(policy),)⚠️ Retry только для idempotent RPCs. Если ваш SendMessage не идемпотентен — добавьте идемпотентный ключ (Idempotency-Key).
Hedging
Заголовок раздела «Hedging»Hedging — отправка дубликата запроса параллельно, для уменьшения tail latency:
{ "methodConfig": [{ "name": [{"service": "search.Search"}], "hedgingPolicy": { "MaxAttempts": 3, "HedgingDelay": "0.05s", "NonFatalStatusCodes": [] } }]}Через 50ms если ответа нет — отправляется второй запрос. Первый завершившийся побеждает. ⚠️ Только для idempotent RPCs и при наличии request idempotency на сервере.
Load balancing
Заголовок раздела «Load balancing»gRPC client держит одно соединение к одному адресу. Для нескольких backend’ов нужен балансировщик.
Варианты:
-
Pick-first (default) — один backend, один connection. Без LB.
-
Round-robin — несколько subconnections, по одному на backend. RR между ними:
conn, _ := grpc.NewClient(target,grpc.WithDefaultServiceConfig(`{"loadBalancingConfig":[{"round_robin":{}}]}`),) -
DNS resolver (default) — резолвит DNS, получает все A-records, открывает RR к ним. Refresh каждые 30s.
-
xDS (Envoy / Istio) — динамический discovery + LB через
xds://target.
import _ "google.golang.org/grpc/xds"conn, _ := grpc.NewClient("xds:///myservice", ...)- Custom resolver (etcd, Consul, k8s API) — реализация
resolver.Builder.
Name resolution
Заголовок раздела «Name resolution»target = scheme://[authority]/endpointdns:///example.com:9090(dns resolver, default)unix:///var/run/foo.sock(Unix socket)xds:///service-namepassthrough:///host:port(без resolution)
Compression
Заголовок раздела «Compression»import "google.golang.org/grpc/encoding/gzip"// автоматически регистрирует gzip кодек
// На клиенте включаем для конкретного RPC:resp, err := cli.GetUser(ctx, req, grpc.UseCompressor(gzip.Name))
// Или глобально:conn, _ := grpc.NewClient(addr, grpc.WithDefaultCallOptions(grpc.UseCompressor(gzip.Name)),)Trade-off: gzip CPU intensive на больших payload. Для small messages (<300 байт) compression может быть медленнее. Бенчмарк перед включением.
TLS / mTLS configs
Заголовок раздела «TLS / mTLS configs»// Server with TLScreds, _ := credentials.NewServerTLSFromFile("cert.pem", "key.pem")srv := grpc.NewServer(grpc.Creds(creds))
// Client with TLScreds := credentials.NewTLS(&tls.Config{ ServerName: "myservice.example.com",})conn, _ := grpc.NewClient("myservice.example.com:443", grpc.WithTransportCredentials(creds),)
// mTLS servercert, _ := tls.LoadX509KeyPair("server.crt", "server.key")caPool := x509.NewCertPool()caBytes, _ := os.ReadFile("ca.crt")caPool.AppendCertsFromPEM(caBytes)creds := credentials.NewTLS(&tls.Config{ Certificates: []tls.Certificate{cert}, ClientCAs: caPool, ClientAuth: tls.RequireAndVerifyClientCert,})srv := grpc.NewServer(grpc.Creds(creds))Health check protocol
Заголовок раздела «Health check protocol»Стандартизированный protocol grpc.health.v1:
service Health { rpc Check(HealthCheckRequest) returns (HealthCheckResponse); rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);}
message HealthCheckRequest { string service = 1; // empty = overall}message HealthCheckResponse { enum ServingStatus { UNKNOWN = 0; SERVING = 1; NOT_SERVING = 2; SERVICE_UNKNOWN = 3; } ServingStatus status = 1;}Реализация:
import ( "google.golang.org/grpc/health" healthpb "google.golang.org/grpc/health/grpc_health_v1")
srv := grpc.NewServer()hsrv := health.NewServer()healthpb.RegisterHealthServer(srv, hsrv)
hsrv.SetServingStatus("", healthpb.HealthCheckResponse_SERVING)hsrv.SetServingStatus("chat.v1.Chat", healthpb.HealthCheckResponse_SERVING)В Kubernetes:
livenessProbe: grpc: port: 9090 service: "" initialDelaySeconds: 5readinessProbe: grpc: port: 9090 service: "chat.v1.Chat"⚠️ Native gRPC probes в k8s доступны с 1.24 (GA в 1.27). Для старых версий используйте grpc_health_probe binary.
Reflection
Заголовок раздела «Reflection»import "google.golang.org/grpc/reflection"srv := grpc.NewServer()chatv1.RegisterChatServer(srv, &chatServer{})reflection.Register(srv) // включает grpc.reflection.v1Теперь можно использовать grpcurl без proto файлов:
grpcurl -plaintext localhost:9090 listgrpcurl -plaintext localhost:9090 describe chat.v1.Chatgrpcurl -plaintext -d '{"text":"hi"}' localhost:9090 chat.v1.Chat/SendMessage⚠️ В production reflection лучше отключать (security — экспонирует schema).
gRPC-Gateway
Заголовок раздела «gRPC-Gateway»HTTP/JSON ↔ gRPC прокси. Через protobuf annotations:
import "google/api/annotations.proto";
service Chat { rpc SendMessage(SendMessageRequest) returns (SendMessageResponse) { option (google.api.http) = { post: "/v1/messages" body: "*" }; } rpc GetMessage(GetMessageRequest) returns (Message) { option (google.api.http) = { get: "/v1/messages/{id}" }; }}Архитектура:
HTTP/JSON client → grpc-gateway (HTTP) → gRPC server (HTTP/2) ^^^^^^^^^^^^^^^^^^ обычно в том же процессе или sidecarConnect-Go (Buf)
Заголовок раздела «Connect-Go (Buf)»Connect — современный протокол от Buf, совместимый с gRPC, но также поддерживает HTTP/1.1 + JSON для browser/curl без gRPC-Web прокси:
// Сервис в том же proto:service Chat { rpc SendMessage(SendMessageRequest) returns (SendMessageResponse);}
// Генерация:// buf.gen.yaml// version: v2// plugins:// - remote: buf.build/connectrpc/go// out: genConnect-server:
import "connectrpc.com/connect"
mux := http.NewServeMux()mux.Handle(chatv1connect.NewChatServiceHandler(&chatServer{}))http.ListenAndServe(":8080", h2c.NewHandler(mux, &http2.Server{}))Connect-client может работать как gRPC или как HTTP/JSON:
client := chatv1connect.NewChatServiceClient( http.DefaultClient, "http://localhost:8080", connect.WithGRPC(), // или connect.WithGRPCWeb() или ничего (Connect protocol))Преимущества:
- HTTP/JSON без gateway, no codegen reflection.
- Совместим с gRPC.
- Pure Go без cgo.
- Из коробки браузер поддерживается.
gRPC-Web
Заголовок раздела «gRPC-Web»Для браузерных клиентов: gRPC-Web — это spec, требующий прокси (Envoy, grpcwebproxy), потому что браузер не имеет доступа к trailers и низкоуровневому HTTP/2.
Performance: gRPC vs REST
Заголовок раздела «Performance: gRPC vs REST»| Метрика | gRPC | REST/JSON |
|---|---|---|
| Encoding | Protobuf binary | JSON (text) |
| Transport | HTTP/2 | HTTP/1.1 (часто) |
| Throughput | ~2-5x быстрее | baseline |
| Latency (per req) | ~50% от REST | baseline |
| CPU encoding | Fast | Slow (JSON parse) |
| Network | -50..-80% | baseline |
| Browser support | Need gateway | Native |
3. Gotchas (⚠️)
Заголовок раздела «3. Gotchas (⚠️)»-
⚠️
grpc.Dialdeprecated в 2024, используйтеgrpc.NewClient. СтарыйDialблокировал до connection,NewClient— lazy. -
⚠️ Один client connection — один HTTP/2 connection. Все RPC мультиплексятся через streams. Для пика throughput иногда нужно несколько connections (по дефолту
MaxConcurrentStreams = 100, после — стримы queue’аtся). -
⚠️ Большие сообщения по умолчанию rejected. Default 4 МБ:
grpc.MaxRecvMsgSize(64 << 20) // 64 MiBgrpc.MaxSendMsgSize(64 << 20) -
⚠️ Streams не имеют per-message deadline. Deadline применяется ко всему streamу. Для долгих streams (часами) — без deadline или с very long.
-
⚠️ Stream send/recv не concurrent-safe. Одна goroutine отправляет, другая принимает — это OK (разные направления). Но
stream.Sendиз двух goroutines одновременно — race. -
⚠️ stream.Send блокируется при flow control. Если client медленный — server’s Send не вернётся, пока окно не освободится. Это backpressure, но может выглядеть как deadlock.
-
⚠️ После stream.CloseSend в client streaming — нельзя больше Send’ить. Иначе panic / error.
-
⚠️ Stream interceptor не имеет доступа к message data легко. Если нужно — оборачивайте
grpc.ServerStreamсвоим типом, реализующимRecvMsgиSendMsg. -
⚠️
UnimplementedFooServerобязателен (forward compat). Каждый сервер должен embed unimplemented type, иначе при добавлении методов в .proto старые серверы не компилируются. -
⚠️ context.Background() в interceptor — потеря deadline. Всегда пробрасывайте
ctx. -
⚠️ DNS resolver кэширует 30s. Если в k8s pod исчез, gRPC может слать в мертвый IP до next DNS refresh. Решение: explicit reconnect, headless service + RR, xDS.
-
⚠️ gRPC client держит idle connection бесконечно. Без keepalive — соединение может стать «зомби» (NAT timeout, proxy drop):
grpc.WithKeepaliveParams(keepalive.ClientParameters{Time: 10 * time.Second,Timeout: 3 * time.Second,PermitWithoutStream: true,}) -
⚠️ Server keepalive policy. Default Go-сервер банит клиентов с too-frequent pings (
MinTime: 5min). Должно быть согласовано:grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{MinTime: 5 * time.Second,PermitWithoutStream: true,}) -
⚠️ Bad load balancer + bidi streaming. Если LB на L4 (не gRPC-aware), он не балансирует streams — все streams идут через одну connection → backend uneven load. Решение: gRPC-aware proxy.
-
⚠️ Headers vs trailers недостаток. Headers — отправлены до payload, после нельзя изменить. Trailers — после, но не все proxy/middlebox их понимают (например, NGINX < 1.13).
-
⚠️ Status WithDetails требует protobuf-зарегистрированных типов. Custom
proto.Messageнужен в global registry. Иначе client приst.Details()получит unknown messages.
4. Production-практики
Заголовок раздела «4. Production-практики»Структура полного production-сервера
Заголовок раздела «Структура полного production-сервера»package main
import ( "context" "log" "net" "net/http" "os" "os/signal" "syscall" "time"
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/recovery" "google.golang.org/grpc" "google.golang.org/grpc/health" healthpb "google.golang.org/grpc/health/grpc_health_v1" "google.golang.org/grpc/keepalive" "google.golang.org/grpc/reflection"
chatv1 "example.com/chat/v1")
func main() { ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop()
lis, err := net.Listen("tcp", ":9090") if err != nil { log.Fatal(err) }
srv := grpc.NewServer( grpc.KeepaliveParams(keepalive.ServerParameters{ Time: 30 * time.Second, Timeout: 10 * time.Second, MaxConnectionIdle: 5 * time.Minute, MaxConnectionAge: 30 * time.Minute, MaxConnectionAgeGrace: 30 * time.Second, }), grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{ MinTime: 10 * time.Second, PermitWithoutStream: true, }), grpc.MaxConcurrentStreams(1000), grpc.MaxRecvMsgSize(16<<20), grpc.InitialWindowSize(1<<20), grpc.InitialConnWindowSize(2<<20), grpc.ChainUnaryInterceptor( recovery.UnaryServerInterceptor(), loggingUnary, authUnary, ratelimitUnary, ), grpc.ChainStreamInterceptor( recovery.StreamServerInterceptor(), loggingStream, authStream, ), )
chatv1.RegisterChatServer(srv, &chatServer{})
hsrv := health.NewServer() healthpb.RegisterHealthServer(srv, hsrv) hsrv.SetServingStatus("", healthpb.HealthCheckResponse_SERVING)
reflection.Register(srv) // снять в prod
// Параллельно HTTP метрик-сервер metrics := &http.Server{ Addr: ":9091", Handler: metricsRouter(), } go metrics.ListenAndServe()
go func() { if err := srv.Serve(lis); err != nil { log.Fatal(err) } }()
<-ctx.Done() log.Println("shutting down...") hsrv.Shutdown() // помечает все services как NOT_SERVING
done := make(chan struct{}) go func() { srv.GracefulStop() close(done) }() select { case <-done: case <-time.After(30 * time.Second): srv.Stop() } metrics.Shutdown(context.Background())}Production-клиент
Заголовок раздела «Production-клиент»func newClient(target string) (*grpc.ClientConn, error) { return grpc.NewClient(target, grpc.WithTransportCredentials(loadTLS()), grpc.WithDefaultServiceConfig(`{ "loadBalancingConfig": [{"round_robin": {}}], "methodConfig": [{ "name": [{"service": "chat.v1.Chat"}], "timeout": "5s", "retryPolicy": { "MaxAttempts": 3, "InitialBackoff": "0.1s", "MaxBackoff": "1s", "BackoffMultiplier": 2, "RetryableStatusCodes": ["UNAVAILABLE", "DEADLINE_EXCEEDED"] } }] }`), grpc.WithKeepaliveParams(keepalive.ClientParameters{ Time: 30 * time.Second, Timeout: 10 * time.Second, PermitWithoutStream: true, }), grpc.WithDefaultCallOptions( grpc.MaxCallRecvMsgSize(16<<20), grpc.WaitForReady(true), ), grpc.WithChainUnaryInterceptor( tracingUnaryClient, metricsUnaryClient, ), )}Auth interceptor (JWT)
Заголовок раздела «Auth interceptor (JWT)»func authUnary(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { if strings.HasPrefix(info.FullMethod, "/grpc.health.v1.") { return handler(ctx, req) } 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 auth") } tokenStr := strings.TrimPrefix(vals[0], "Bearer ") user, err := verifyJWT(tokenStr) if err != nil { return nil, status.Error(codes.Unauthenticated, err.Error()) } ctx = context.WithValue(ctx, "user", user) return handler(ctx, req)}Recovery interceptor (panic protection)
Заголовок раздела «Recovery interceptor (panic protection)»func recoveryUnary(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) { defer func() { if r := recover(); r != nil { stack := debug.Stack() log.Printf("PANIC method=%s err=%v\n%s", info.FullMethod, r, stack) err = status.Errorf(codes.Internal, "internal server error") } }() return handler(ctx, req)}Метрики (Prometheus)
Заголовок раздела «Метрики (Prometheus)»import "github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus"
srvMetrics := grpc_prom.NewServerMetrics( grpc_prom.WithServerHandlingTimeHistogram(),)prometheus.MustRegister(srvMetrics)
srv := grpc.NewServer( grpc.ChainUnaryInterceptor(srvMetrics.UnaryServerInterceptor()), grpc.ChainStreamInterceptor(srvMetrics.StreamServerInterceptor()),)Tracing (OpenTelemetry)
Заголовок раздела «Tracing (OpenTelemetry)»import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
srv := grpc.NewServer( grpc.StatsHandler(otelgrpc.NewServerHandler()),)conn, _ := grpc.NewClient(target, grpc.WithStatsHandler(otelgrpc.NewClientHandler()),)Rate limiting
Заголовок раздела «Rate limiting»import "golang.org/x/time/rate"
var lim = rate.NewLimiter(rate.Limit(1000), 2000) // 1K rps, burst 2K
func ratelimitUnary(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { if !lim.Allow() { return nil, status.Error(codes.ResourceExhausted, "rate limit") } return handler(ctx, req)}Schema management через Buf
Заголовок раздела «Schema management через Buf»version: v2modules: - path: protolint: use: - STANDARDbreaking: use: - FILE
# buf.gen.yamlversion: v2plugins: - remote: buf.build/protocolbuffers/go out: gen - remote: buf.build/grpc/go out: genbuf lintbuf breaking --against '.git#branch=main' # CI check на breaking changesbuf generatebuf push # push schema в Buf Schema RegistryMultiple connections trick для high-throughput
Заголовок раздела «Multiple connections trick для high-throughput»// Создаём pool из N connections к одному адресуtype connPool struct { conns []*grpc.ClientConn next uint64}
func (p *connPool) get() *grpc.ClientConn { i := atomic.AddUint64(&p.next, 1) % uint64(len(p.conns)) return p.conns[i]}Каждое соединение — свой HTTP/2 connection → больше параллельных streams. Полезно при >250 concurrent streams (default limit).
Real cases (2026)
Заголовок раздела «Real cases (2026)»- Internal microservices — gRPC mesh с Istio/Linkerd, mTLS встроен.
- Mobile API — gRPC (Android Java) или Connect-Go (iOS Swift), один stack для server + mobile.
- Browser apps — gRPC-Web через Envoy, или Connect-Go (предпочтительнее, без Envoy).
- Streaming — server streaming для feeds, bidi для chat/control planes.
- Public API — REST для legacy, gRPC + gRPC-Gateway (HTTP/JSON) или Connect-Go (single proto + два транспорта).
5. Вопросы (30)
Заголовок раздела «5. Вопросы (30)»-
Какие 4 типа RPC в gRPC? Unary, server streaming, client streaming, bidirectional streaming.
-
Что такое HTTP/2 stream и как mapping на gRPC? Каждый gRPC call = один HTTP/2 stream. Stream ID идентифицирует call. Multiplexing внутри одного TCP connection.
-
Что в HTTP/2 trailers для gRPC?
grpc-status,grpc-message, опциональноgrpc-status-details-bin. Trailers (HEADERS frame с END_STREAM) шлются в конце. -
Зачем
te: trailersheader? HTTP/2 spec требует. Без него — protocol violation. Сигнал, что клиент понимает trailers. -
Как deadline propagated через gRPC? Через header
grpc-timeout. Сервер видит deadline и должен прокинутьctxдальше. -
Что такое status.Status в gRPC? Struct {code, message, details}. Сериализуется в gRPC-status, gRPC-message, gRPC-status-details-bin.
status.Error(codes.NotFound, "msg")→ клиентstatus.FromError(err). -
Чем отличаются Unary и Stream interceptors? Unary получает req, возвращает resp. Stream получает ServerStream — оборачивает send/recv. Stream interceptor более сложен (часто wrapper вокруг ServerStream).
-
Как сделать chain interceptors?
grpc.ChainUnaryInterceptor(a, b, c). Порядок выполнения: a → b → c → handler → c → b → a (как middleware в HTTP). -
Где применять recovery interceptor? Первым в цепочке — чтобы поймать panic из любого нижестоящего (включая бизнес-логику).
-
Что такое half-close в bidi streaming? Одна сторона больше не отправляет (END_STREAM), другая ещё может. Client делает
CloseSend(). -
Что такое flow control в gRPC streaming? HTTP/2 flow control: per-stream window. Server.Send блокирует, если window 0 → backpressure. Тюнинг через
InitialWindowSize. -
Как реализовать retry в gRPC client? Через service config JSON:
retryPolicyс MaxAttempts, InitialBackoff, RetryableStatusCodes. Retry только для idempotent методов. -
Что такое hedging? Отправка дубликата запроса с небольшой задержкой → побеждает первый ответ. Уменьшает p99 latency. Только для idempotent.
-
Какие коды gRPC мапятся на 503? UNAVAILABLE — обычно временная недоступность, retry-friendly. RESOURCE_EXHAUSTED → 429.
-
Чем headers отличаются от trailers в gRPC? Headers — до payload, не могут меняться. Trailers — после, тут grpc-status. Использование: headers для request-id, trailers для final status.
-
Как пробросить authorization в gRPC? Через
metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer ..."). Сервер читает черезmetadata.FromIncomingContext. -
Какие методы name resolution в gRPC?
dns:///host:port(default),unix:///path,xds:///name, custom resolvers (etcd, Consul). -
Как работает round-robin LB в gRPC? Resolver возвращает список адресов → балансировщик открывает SubConn’ы → распределяет RPCs round-robin между ними. Конфиг:
loadBalancingConfig: [{"round_robin":{}}]. -
Что такое xDS? Envoy-style discovery API. gRPC может работать как xDS client → получает endpoints, LB policies, TLS config динамически (Istio управляет).
-
Зачем compression в gRPC? Уменьшение байт на провод. Trade-off CPU. Включается через
grpc.UseCompressor("gzip"). Не для маленьких сообщений. -
Health check protocol — зачем? Стандартизированный способ для k8s probes, LB health checks.
grpc.health.v1.Health.Check/Watch. -
Зачем gRPC reflection? Позволяет grpcurl, Postman, BloomRPC работать без .proto файла. В prod лучше отключать (security).
-
Что такое gRPC-Gateway? HTTP/JSON proxy → gRPC backend. Через
google.api.httpannotations в .proto. Один кодекс — два транспорта. -
Чем Connect-Go отличается от стандартного gRPC? Connect — protocol-совместимая альтернатива. Поддерживает gRPC, gRPC-Web, и собственный Connect protocol (HTTP/1.1 + JSON / Protobuf). Pure Go, нет cgo. Браузер из коробки.
-
Когда выбирать gRPC vs REST? gRPC: internal service-to-service, streaming, mobile native. REST: public API, browser (без proxy), low-frequency.
-
Какие настройки keepalive обязательны? Client:
Time: 30s, Timeout: 10s, PermitWithoutStream: true. Server: согласоватьMinTimeс client Time иначе server банит клиента. -
Что происходит, если client отменяет RPC? Context cancelled → клиент шлёт RST_STREAM (CANCELLED). Сервер видит
ctx.Done()→ должен прекратить работу. -
Можно ли передавать большие файлы через gRPC? Да, через client streaming (chunks). Default size limit 4 МБ — увеличьте
MaxRecvMsgSize. Лучше streaming chunks (1-4 МБ каждый). -
Что делать с long-running streams (часы)? Без deadline на ctx, периодические keepalive ping, обработка disconnect (network failures). Может потребоваться custom reconnect протокол на app-level.
-
Как сделать graceful shutdown gRPC сервера?
srv.GracefulStop()— ждёт окончания текущих RPC. Перед этимhealth.SetServingStatus(NOT_SERVING)→ LB убирает из rotation. Hard cap на timeout →srv.Stop().
6. Practice
Заголовок раздела «6. Practice»Задача 1 — Bidi chat
Заголовок раздела «Задача 1 — Bidi chat»Реализуйте bidi-stream Channel(stream ClientMsg) returns (stream ServerMsg). Server эхо-отражает сообщения с префиксом “echo:”.
Задача 2 — Server streaming с deadline
Заголовок раздела «Задача 2 — Server streaming с deadline»Реализуйте Subscribe(req) returns (stream Event) который шлёт по 1 event/sec. Клиент устанавливает 3-секундный deadline. Проверьте, что сервер видит ctx.Done() через 3 секунды.
Задача 3 — Interceptor chain
Заголовок раздела «Задача 3 — Interceptor chain»Соберите chain: recovery → logging → auth → ratelimit. Проверьте порядок логов при panic, при invalid token, при rate-limit.
Задача 4 — Retry policy
Заголовок раздела «Задача 4 — Retry policy»Сервер: возвращает UNAVAILABLE для 2 первых запросов, потом OK. Клиент: настроен MaxAttempts: 3. Проверьте через grpcurl или клиент, что RPC всё-таки succeeded.
Задача 5 — Hedging
Заголовок раздела «Задача 5 — Hedging»Сервер: с вероятностью 30% sleeps 1s, иначе сразу отвечает. Клиент с hedging policy MaxAttempts=3, delay=200ms. Замеряйте p50/p99 latency с и без hedging.
Задача 6 — Round-robin LB
Заголовок раздела «Задача 6 — Round-robin LB»Запустите 3 экземпляра сервера на портах 9090, 9091, 9092. Клиент: dns:///localhost:9090,localhost:9091,localhost:9092 + round_robin. Делайте 100 RPCs, проверьте равное распределение.
Задача 7 — Health check + k8s probe
Заголовок раздела «Задача 7 — Health check + k8s probe»Реализуйте health server. Сделайте Dockerfile, deployment в minikube с livenessProbe: grpc:. Через kubectl exec пометьте сервис как NOT_SERVING — k8s рестартит pod.
Задача 8 — gRPC-Gateway
Заголовок раздела «Задача 8 — gRPC-Gateway»Сделайте gRPC сервис + gateway. Endpoint POST /v1/messages → SendMessage. Откройте swagger UI (через protoc-gen-openapiv2).
Задача 9 — Custom error details
Заголовок раздела «Задача 9 — Custom error details»Возвращайте BadRequest_FieldViolation для invalid request. На клиенте распарсите detail через type switch и выведите field+description.
Задача 10 — Connect-Go
Заголовок раздела «Задача 10 — Connect-Go»Сконвертируйте свой gRPC сервис на Connect-Go. Один тот же proto — два варианта клиента: gRPC (connect.WithGRPC()) и Connect (HTTP/JSON). Сравните performance.
7. Источники
Заголовок раздела «7. Источники»- gRPC Documentation (Go): https://grpc.io/docs/languages/go/
- gRPC Spec: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md
- Buf docs: https://buf.build/docs/
- Connect-Go: https://connectrpc.com/docs/
- grpc-ecosystem/go-grpc-middleware: https://github.com/grpc-ecosystem/go-grpc-middleware
- gRPC Health Checking Protocol: https://github.com/grpc/grpc/blob/master/doc/health-checking.md
- gRPC Service Config: https://github.com/grpc/grpc/blob/master/doc/service_config.md
- OpenTelemetry gRPC: https://github.com/open-telemetry/opentelemetry-go-contrib/tree/main/instrumentation/google.golang.org/grpc/otelgrpc
- xDS (Envoy / gRPC): https://www.envoyproxy.io/docs/envoy/latest/api-docs/xds_protocol
- gRPC over HTTP/2 wire format: “gRPC under the hood” — https://www.cncf.io/blog/2018/08/31/grpc-on-http-2-engineering-a-robust-high-performance-protocol/