Содержание урока · 16 разделов
- производительность grpc: сначала замер, потом ручки
- Карта ручек: где живут настройки транспорта
- keepalive: зачем пинговать соединение
- keepalive.ClientParameters: Time, Timeout, PermitWithoutStream
- keepalive.ServerParameters и EnforcementPolicy: серверная сторона
- Message size: какие значения ставить
- Окна HTTP/2 и BDP: динамика лучше ручек
- MaxConcurrentStreams: предохранитель мультиплексирования
- Idle-таймауты: закалка соединений
- Шпаргалка ручек тюнинга grpc-go
- Чек-лист тюнинга gRPC
- Практика: три эксперимента локально
- Что дальше: много сервисов, много настроек
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
производительность grpc: сначала замер, потом ручки#
У производительности gRPC три метрики, и тюнят их по-разному. Первая — latency: время одного вызова, причём важны не средние, а хвосты — p95 и p99, потому что именно хвосты формируют пользовательский опыт. Вторая — пропускная способность: сколько RPC в секунду выдерживает сервис при заданном latency. Третья — память: gRPC-транспорт буферизует сообщения и фреймы, и каждый поднятый лимит — это возможные мегабайты в куче под пиком нагрузки. Прежде чем крутить любую ручку из этого урока, зафиксируйте все три цифры: микробенчмарки testing.B и профили pprof вы строили в уроке про бенчмарки, нагрузку гоняют хотя бы grpcurl или скриптом из прошлого урока на bufconn — зелёный тест не отвечает на вопрос «быстро ли», но отвечает на вопрос «чёй-то сломалось после тюнинга». Правило одной ручки — то же, что в бенчмаркинге: за итерацию меняем ровно одну настройку и пересчитываем цифры, иначе не узнаете, что именно помогло. И запомните, где живут все ручки курса: DialOption у grpc.NewClient, ServerOption у grpc.NewServer и CallOption на отдельный вызов — ни одна из них не в proto-схеме и не в коде хендлеров.
Карта ручек: где живут настройки транспорта#
Все настройки транспорта grpc-go собираются в одном месте — в опциях конструктора. Сервер: grpc.NewServer(grpc.MaxRecvMsgSize(...), grpc.KeepaliveParams(...), ...) — каждая функция возвращает ServerOption. Клиент: grpc.NewClient(grpc.WithKeepaliveParams(...), grpc.WithDefaultCallOptions(...)) — DialOption. На один вызов: CallOption вроде grpc.MaxCallRecvMsgSize, который можно передать и точечно, и через WithDefaultCallOptions как дефолт канала. Сами параметры keepalive вынесены в отдельный пакет google.golang.org/grpc/keepalive — его документация лаконична: «Package keepalive defines configurable parameters for point-to-point healthcheck» — настраиваемые параметры точка-точка-проверки живости. Пять семейств ручек этого урока: размеры сообщений (MaxRecvMsgSize, MaxSendMsgSize и клиентские CallOption-близнецы), keepalive-пинги (ClientParameters, ServerParameters, EnforcementPolicy), окна HTTP/2 (InitialWindowSize и BDP-оценщик), MaxConcurrentStreams и idle-таймауты (MaxConnectionIdle, MaxConnectionAge, WithIdleTimeout). Важное следствие из прошлых уроков: все эти настройки — deploy-конфигурация, а не бизнес-логика, поэтому интеграционные тесты на bufconn из двадцать первого урока их не проверяют по умолчанию — тестовый сервер собирается с дефолтными опциями, и это честно: проверять надо контракт, а не конфиг. Конфиг проверяется экспериментом и замером.
Сервер: тюнинг-конфиг целиком
GO// cmd/orders-server/main.go — все транспортные ручки сервера в одном месте
package main
import (
"net"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/keepalive"
shopv1 "github.com/example/shop/gen/shopv1"
)
func main() {
lis, err := net.Listen("tcp", ":50051") // слушатель из четвёртого урока
if err != nil {
panic(err)
}
srv := grpc.NewServer(
// размеры сообщений: приём ограничиваем осознанно
grpc.MaxRecvMsgSize(8*1024*1024), // было 4 МБ — под любой легальный запрос
grpc.MaxSendMsgSize(16*1024*1024), // дефолт math.MaxInt32, ставим явно
// предохранитель: не больше 1000 одновременных RPC на соединение
grpc.MaxConcurrentStreams(1000),
// keepalive и max-age: как сервер держит соединения
grpc.KeepaliveParams(keepalive.ServerParameters{
MaxConnectionIdle: 5 * time.Minute, // простаивающее соединение — GoAway
MaxConnectionAge: 30 * time.Minute, // перекат соединений (джиттер ±10%)
Time: 2 * time.Minute, // пингуем клиента раз в 2 минуты
Timeout: 10 * time.Second, // нет ответа на пинг — разрыв
}),
// политика чужих пингов: что разрешено клиенту
grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
MinTime: 30 * time.Second, // чаще — страйк
PermitWithoutStream: true, // пинги без активных RPC разрешены
}),
)
shopv1.RegisterOrderServiceServer(srv, &v1Handler{orders: loadOrder})
if err := srv.Serve(lis); err != nil { // блокирующий Serve из четвёртого урока
panic(err)
}
}Разбор по строкам документации grpc-go. MaxRecvMsgSize: «MaxRecvMsgSize returns a ServerOption to set the max message size in bytes the server can receive. If this is not set, gRPC uses the default 4MB» — то есть 1024 1024 4 из server.go, четыре мебибайта. MaxSendMsgSize: «If this is not set, gRPC uses the default math.MaxInt32» — отправку gRPC по умолчанию не ограничивает вовсе. MaxConcurrentStreams: «MaxConcurrentStreams returns a ServerOption that will apply a limit on the number of concurrent streams to each ServerTransport» — лимит стримов на транспорт, а дефолт в defaultServerOptions — math.MaxUint32, то есть «без ограничения». KeepaliveParams: «KeepaliveParams returns a ServerOption that sets keepalive and max-age parameters for the server». KeepaliveEnforcementPolicy: «KeepaliveEnforcementPolicy returns a ServerOption that sets keepalive enforcement policy for the server». Обе функции принимают структуры из пакета keepalive — ServerParameters и EnforcementPolicy, — их поля и дефолты разбираем ниже по тексту. Заметьте: хендлер в этом сниппете тот же, что и в тестах прошлого урока, — тюнинг транспорта не трогает бизнес-код ни на строчку.
Клиент: зеркало настроек
GO// cmd/orders-client/main.go — ручки клиента: keepalive и лимиты вызовов
conn, err := grpc.NewClient(
"dns:///orders.example.com:443",
// keepalive: пинг каждые 30 секунд, даже когда RPC нет
grpc.WithKeepaliveParams(keepalive.ClientParameters{
Time: 30 * time.Second, // тишина 30s — отправляем PING
Timeout: 10 * time.Second, // нет ответа — рвём соединение
PermitWithoutStream: true, // пингуем и без активных RPC
}),
// лимиты размеров — дефолты всех вызовов канала
grpc.WithDefaultCallOptions(
grpc.MaxCallRecvMsgSize(16*1024*1024), // зеркалим MaxSendMsgSize сервера
grpc.MaxCallSendMsgSize(8*1024*1024), // зеркалим MaxRecvMsgSize сервера
),
grpc.WithTransportCredentials(creds), // TLS из двенадцатого урока
)
if err != nil {
panic(err)
}
client := shopv1.NewOrderServiceClient(conn)Документация WithKeepaliveParams: «WithKeepaliveParams returns a DialOption that specifies keepalive parameters for the client transport. Keepalive is disabled by default» — клиентский keepalive по умолчанию выключен: в internal/transport/defaults.go константа defaultClientKeepaliveTime = infinity. Первый же подводный камень спрятан в этой опции: если Time меньше KeepaliveMinPingTime (10 time.Second), grpc-go не ошибётся, а поправит вас предупреждением в логе — «Adjusting keepalive ping interval to minimum period of 10s». Документация поля Time из keepalive.go повторяет то же самое: «If set below 10s, a minimum value of 10s will be used instead». Timeout описан так: «After having pinged for keepalive check, the client waits for a duration of Timeout and if no activity is seen even after that the connection is closed» — дефолт 20 секунд (defaultClientKeepaliveTimeout = 20 time.Second). Лимиты вызовов: «MaxCallRecvMsgSize returns a CallOption which sets the maximum message size in bytes the client can receive. If this is not set, gRPC uses the default 4MB»; «MaxCallSendMsgSize ... If this is not set, gRPC uses the default math.MaxInt32». Through WithDefaultCallOptions эти CallOption становятся дефолтами всех вызовов канала — то, что в девятнадцатом уроке мы делали с grpc.UseCompressor.
Проверка: асимметрия лимитов send/recv
Сервер с полностью дефолтными настройками возвращает отчёт на 100 МБ одним сообщением. Где сломается вызов?
keepalive: зачем пинговать соединение#
TCP-соединение может тихо умереть: NAT-таблица маршрутизатора выкинула запись, балансировщик закрыл простой, пир упал вместе с сетевым кабелем — FIN не придёт, и соединение будет выглядеть живым, пока вы не попробуете в него написать. Дедлайны из четырнадцатого урока ловят это на уровне одного RPC, а keepalive — на уровне соединения: клиент сам, без вашего кода, отправляет PING-фрейм HTTP/2 — восьмибайтовый payload без данных, — и если ответа нет, закрывает соединение. Плюс пинги не дают инфраструктуре выкинуть «простаивающий» коннект: NAT и балансировщики смотрят на трафик, а пинг — это трафик. Всю механику задаёт пакет keepalive; документация ClientParameters предупреждает заранее: «Make sure these parameters are set in coordination with the keepalive policy on the server, as incompatible settings can result in closing of connection» — несовместимые настройки по обе стороны соединения приводят к разрывам. Дефолты асимметричны: сервер пингует сам (ServerParameters.Time по умолчанию 2 * time.Hour), а клиент выключен — включать его нужно осознанно, и делать это нужно парой с серверной политикой, иначе получите самую знаменитую ловушку курса — GOAWAY too_many_pings, до которой дойдём через два блока.
keepalive.ClientParameters: Time, Timeout, PermitWithoutStream#
Три поля, три дефолта, три решения. Time — документация: «After a duration of this time if the client doesn't see any activity it pings the server to see if the transport is still alive» — столько тишины, после чего клиент шлёт PING. Дефолт — infinity, то есть выключено. С практическим значением помогает простое правило: ставьте Time меньше, чем таймаут простоя у NAT и балансировщика на пути, — обычно это минуты, а не секунды. Timeout: «After having pinged for keepalive check, the client waits for a duration of Timeout and if no activity is seen even after that the connection is closed» — сколько ждать PING ACK; дефолт 20 секунд, разумно 5–10. PermitWithoutStream: «If true, client sends keepalive pings even with no active RPCs. If false, when there are no active RPCs, Time and Timeout will be ignored and no keepalive pings will be sent» — пинговать ли соединение, на котором сейчас нет ни одного RPC. Включайте, только если сервер разрешает (EnforcementPolicy.PermitWithoutStream), — иначе пинги без стримов превращаются в страйки. И ещё одна строка документации, которую стоит выучить наизусть: «Note that gRPC servers have a default EnforcementPolicy.MinTime of 5 minutes (which means the client shouldn't ping more frequently than every 5 minutes)». Хорошая новость сразу за ней: «Time will automatically double if the server disconnects due to its enforcement policy» — клиент grpc-go умеет сам отходить назад, удваивая интервал, когда сервер порвал соединение за нарушение политики; это поведение из пропозала A8 client-side keepalive.
Пинг без стримов против дефолтного сервера
Прочитайте код и выберите, что он выведет:
// клиент:
grpc.WithKeepaliveParams(keepalive.ClientParameters{
Time: 30 * time.Second,
Timeout: 10 * time.Second,
PermitWithoutStream: true,
}),
// сервер: keepalive-настройки не тронуты, то есть
// EnforcementPolicy.MinTime = 5 минут, PermitWithoutStream = false
// RPC на соединении сейчас нет.
// Клиент пингует раз в 30 секунд. Что произойдёт?keepalive.ServerParameters и EnforcementPolicy: серверная сторона#
ServerParameters — документация пакета: «ServerParameters is used to set keepalive and max-age parameters on the server-side». Пять полей. MaxConnectionIdle: «a duration for the amount of time after which an idle connection would be closed by sending a GoAway. Idleness duration is defined since the most recent time the number of outstanding RPCs became zero or the connection establishment» — сколько соединение простаивает без единого RPC, прежде чем сервер отправит GoAway. MaxConnectionAge: «a duration for the maximum amount of time a connection may exist before it will be closed by sending a GoAway. A random jitter of +/-10% will be added to MaxConnectionAge to spread out connection storms» — принудительный перекат соединений, причём джиттер ±10% добавлен специально, чтобы все коннекты, поднятые одновременно, не умирали одной волной. MaxConnectionAgeGrace: «an additive period after MaxConnectionAge after which the connection will be forcibly closed» — льгота на дообработку начатых RPC. Time: «After a duration of this time if the server doesn't see any activity it pings the client» — дефолт 2 часа, и минимум с подстраховкой: «If set below 1s, a minimum value of 1s will be used instead» (константа KeepaliveMinServerPingTime = time.Second; меньшее значение grpc-go поправит с предупреждением «Adjusting keepalive ping interval to minimum period of 1s»). Timeout — 20 секунд. EnforcementPolicy (опция grpc.KeepaliveEnforcementPolicy) — «used to set keepalive enforcement policy on the server-side. Server will close connection with a client that violates this policy»: MinTime по умолчанию 5 минут (defaultKeepalivePolicyMinTime = 5 * time.Minute), PermitWithoutStream — «If true, server allows keepalive pings even when there are no active streams(RPCs). If false, and client sends ping when there are no active streams, server will send GOAWAY and close the connection». Отдельная деталь механики: счётчик страйков обнуляется, когда сервер шлёт клиенту данные или заголовки — комментарий поля resetPingStrikes: «This is set whenever data or header frames are sent» — живой трафик реабилитирует даже нарушителя.
Проверка: чем лечится too_many_pings
Клиенты жалуются: соединения рвутся, в логах сервера мелькает GOAWAY с too_many_pings. Какое действие проблеме НЕ поможет?
Message size: какие значения ставить#
Дефолты и ошибку ResourceExhausted с текстом «grpc: received message larger than max (%d vs. %d)» вы уже видели в уроке про проводной формат — четыре мебибайта на приём, math.MaxInt32 на отправку, декомпрессия режется на границе лимита. Вопрос этого урока — какие значения ставить, когда легальный бизнес-объект не влезает. Первое: лимит приёма — защита памяти от чужих данных. Каждый принимаемый байт попадает в буферы до того, как вы разберёте поля, а буферы транспорта идут из пула (mem.BufferPool) и переиспользуются; потолок в гигабайт «на всякий случай» означает, что один недоброжелатель (или один баг клиента) в сто параллельных вызовах съест память сервера. Ставьте чуть выше самого большого легального сообщения — например, 8 МБ при реальном максимуме в 6. Второе: прежде чем поднимать потолок, спросите, не должен ли это быть стрим. Отчёт на двенадцать мегабайт — это кандидатов в server streaming по образцу WatchOrders из седьмого урока: каждый элемент стрима проходит лимит отдельно, память размазывается по времени, клиент начинает обрабатывать данные до конца передачи. Третье: лимиты ставятся парой на обоих концах — сервер отправил, клиент обязан принять. Четвёртое, одной фразой про компрессию: сжатие gzip из девятнадцатого урока экономит байты провода, но не спасает от лимита — gRPC применяет потолок и к результату декомпрессии. И потолок protobuf в два гибибайта из того же урока напоминает: физический предел вы всё равно не поднимете.
Большой отчёт: настраиваем оба конца
GO// Сервер: GetReport отдаёт сводку одним сообщением (~12 МБ)
func (h *v1Handler) GetReport(ctx context.Context,
req *shopv1.ReportRequest) (*shopv1.ReportResponse, error) {
o, err := h.orders(ctx, req.GetOrderId()) // канон: зависимость — поле-функция
if err != nil {
return nil, status.Errorf(codes.NotFound, "заказ %q: %v", req.GetOrderId(), err)
}
return buildReport(o), nil // сериализация уложится в 16 МБ с запасом
}
// main сервера — среди опций:
grpc.MaxSendMsgSize(16 * 1024 * 1024), // уходим выше дефолтных 4 МБ на приём у клиента
// main клиента — дефолт всех вызовов канала:
grpc.WithDefaultCallOptions(
grpc.MaxCallRecvMsgSize(16 * 1024 * 1024), // иначе Recv вернёт ResourceExhausted
),
client := shopv1.NewOrderServiceClient(conn)
rep, err := client.GetReport(ctx, &shopv1.ReportRequest{OrderId: "42"})Три числа в этом сниппете — 16 МБ у отправителя, 16 МБ у получателя и 12 МБ фактического сообщения: между потолком и реальностью оставлен запас на сериализационный оверхед и рост данных. Обратите внимание на симметрию ролей: MaxSendMsgSize у сервера — это CallOption-близнец MaxCallRecvMsgSize у клиента, и в конфиге они живут парами, зеркаля друг друга. Приёмная сторона сервера в этом сценарии не тронута: запрос крошечный, MaxRecvMsgSize 4 МБ хватает с запасом. И ещё раз про альтернативу: если такие отчёты летают регулярно, правильнее объявить server-streaming метод, который отдаёт сводку порциями, — лимиты тогда останутся дефолтными, а память сервиса не будет прыгать на десятки мегабайт под каждым вызовом.
Кто забыл зеркальную настройку
Прочитайте код и выберите, что он выведет:
// сервер: MaxSendMsgSize НЕ задан — по умолчанию math.MaxInt32
// клиент: MaxCallRecvMsgSize НЕ задан — по умолчанию 4 МБ (4194304)
rep, err := client.GetReport(ctx, &shopv1.ReportRequest{OrderId: "42"})
// rep сериализуется в ~10 МБ. Что вернёт вызов?Проверка: почему приём ограничен, а отправка нет
Почему дефолтный лимит приёма — 4 МБ, а отправки — math.MaxInt32?
Окна HTTP/2 и BDP: динамика лучше ручек#
Flow control HTTP/2 защищает от ситуации, когда быстрый отправитель топит медленного получателя: у каждого направления есть окна на двух уровнях — стрима и соединения; получатель продвигает их WINDOW_UPDATE-фреймами по мере чтения. Стартовое окно задаёт спецификация HTTP/2 — в defaults.go это defaultWindowSize = 65535 с комментарием «The default value of flow control window size in HTTP2 spec»: 64 КиБ. Но grpc-go не сидит на этом дефолте: транспорт носит BDP-оценщик — измеритель bandwidth-delay product, произведения пропускной способности канала на время его задержки. Как работает: транспорт отправляет служебный PING с восемью байтами данных {2, 4, 16, 16, 9, 14, 7, 7} — в исходниках это easter-egg, комментарий приглашает: «Easter-egg: what does the ping message say?» (байты читаются как буквы B-D-P-P-I-N-G-G), — и меряет, сколько данных успело приехать за один RTT. Оценка растёт множителем gamma = 2, сглаживается alpha = 0.9, включается при beta = 0.66, и потолок задан константой bdpLimit = (1 << 20) * 16 с комментарием: «bdpLimit is the maximum value the flow control windows will be increased to. TCP typically limits this to 4MB, but some systems go up to 16MB. Since this is only a limit, it is safe to make it optimistic» — то есть окна автоматически растут от 64 КиБ до 16 МиБ. Ручки под это есть: grpc.InitialWindowSize и grpc.InitialConnWindowSize у сервера, grpc.WithInitialWindowSize и grpc.WithInitialConnWindowSize у клиента — «The lower bound for window size is 64K and any value smaller than that will be ignored». Но документация StaticStreamWindowSize честно остужает энтузиазм: «Most users should not configure static flow control windows unless operating in a memory-constrained environment» — статические окна отключают BDP-динамику, и обычному приложению лезть туда не нужно. Правило курса: окна — последняя ручка, которую вы трогаете, и только под замером.
MaxConcurrentStreams: предохранитель мультиплексирования#
HTTP/2 мультиплексирует все RPC одного соединения в независимые стримы — десятки параллельных вызовов едут по одному TCP-сокету без головной блокировки на установке соединений. Оборотная сторона: без лимита один жадный клиент открывает столько стримов, сколько серверу придётся обслуживать, — соединение становится вектором перегрузки. Лимит и есть SETTINGS MAX_CONCURRENT_STREAMS; серверная ручка — grpc.MaxConcurrentStreams(n), документация: «MaxConcurrentStreams returns a ServerOption that will apply a limit on the number of concurrent streams to each ServerTransport» — на каждый транспорт, то есть на каждое соединение, а не на процесс в целом. Дефолт сервера — math.MaxUint32 из defaultServerOptions: не ограничено; ноль в вызове опции превращается в тот же MaxUint32. У клиента свой внутренний потолок — defaultMaxStreamsClient = 100 из internal/transport/defaults.go с механикой streamQuota: сверх квоты стримы ждут на соединении, а SETTINGS сервера обновляют клиентский лимит на лету (обработчик SETTINGS перечитывает MAX_CONCURRENT_STREAMS и двигает t.maxConcurrentStreams). Практика: ставьте явный MaxConcurrentStreams, когда за соединением стоит неуправляемый клиент или когда за сервером — медленный бэкенд (база, которую затопит тысяча параллельных GetOrder); лимит превращает лавину в аккуратную очередь и даёт клиенту сигнал уходить на другие соединения. Но помните связку с балансировщиком из шестнадцатого урока: лимит стримов — предохранитель, а не замена раскладке нагрузки.
Idle-таймауты: закалка соединений#
Продакшен любит соединения с истекающим сроком годности — они сами раскладывают нагрузку и вымывают утечки. Серверные ручки вы уже видели в ServerParameters. MaxConnectionIdle закрывает GoAway-ом простаивающее соединение — «idleness duration is defined since the most recent time the number of outstanding RPCs became zero»; MaxConnectionAge ограничивает максимальный возраст: «a duration for the maximum amount of time a connection may exist before it will be closed by sending a GoAway. A random jitter of +/-10% will be added to MaxConnectionAge to spread out connection storms»; MaxConnectionAgeGrace — «an additive period after MaxConnectionAge after which the connection will be forcibly closed». Все три дефолтно равны infinity. Клиентская сторона — DialOption grpc.WithIdleTimeout, и документация описывает два уровня: «If the channel is idle for the configured timeout, i.e there are no ongoing RPCs and no new RPCs are initiated, the channel will enter idle mode and as a result the name resolver and load balancer will be shut down. The channel will exit idle mode when the Connect() method is called or when an RPC is initiated» — канал уходит в idle и глушит резолвер с балансировщиком; «A default timeout of 30 minutes will be used if this dial option is not set at dial time and idleness can be disabled by passing a timeout of zero». Разница уровней важна: MaxConnectionAge — принудительный перекат транспорта (GoAway, клиент молча переоткроет соединение), idle-канал — экономия ресурсов клиента, когда RPC нет вовсе; дедлайны же из четырнадцатого урока — про тайм-аут отдельного вызова, это третья, независимая ось времени.
Соберите тюнинг-конфиг сервера
Вставьте пропущенные имена опций и структур: лимит приёма, серверные keepalive-параметры, политика пингов и её поле-минимум.
Порядок тюнинга: от замера до выката
Соберите дисциплину тюнинга из девяти шагов — ровно в том порядке, в котором её проходил бы инженер с бенчмарками из семнадцатого урока.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Шпаргалка ручек тюнинга grpc-go#
| Ручка | Сторона | Дефолт | Когда крутить |
|---|---|---|---|
| grpc.MaxRecvMsgSize | сервер | 4 MiB (1024 1024 4) | легальные большие запросы; помнить про память и поверхность атаки |
| grpc.MaxSendMsgSize | сервер | math.MaxInt32 | почти никогда — отправка и так не ограничена |
| grpc.MaxCallRecvMsgSize | клиент | 4 MiB | сервер отдаёт большие сообщения; зеркально серверному MaxSendMsgSize |
| grpc.MaxCallSendMsgSize | клиент | math.MaxInt32 | почти никогда |
| ClientParameters.Time | клиент | infinity — keepalive выключен | NAT/LB режут простаивающие соединения; не меньше 10s |
| ClientParameters.Timeout | клиент | 20 секунд | жёстче — быстрее обнаружение мёртвых коннектов |
| ServerParameters.Time / Timeout | сервер | 2 часа / 20 секунд | сервер сам проверяет живость клиентов |
| EnforcementPolicy.MinTime | сервер | 5 минут | разрешить клиентам пинговать чаще; иначе too_many_pings |
| MaxConnectionIdle / Age / Grace | сервер | infinity | перекат соединений, ре-балансировка за LB |
| grpc.MaxConcurrentStreams | сервер | math.MaxUint32 | предохранитель от лавины стримов на соединение |
| InitialWindowSize (окна) | обе | 65535, BDP поднимает до 16 MiB | почти никогда — BDP-оценщик справляется сам |
| grpc.WithIdleTimeout | клиент | 30 минут | глушить резолвер и LB при простое канала |
Легко перепутать два клампа keepalive-интервалов. Клиент: WithKeepaliveParams проверяет Time против KeepaliveMinPingTime = 10 * time.Second — меньше не выйдет, в лог уйдёт «Adjusting keepalive ping interval to minimum period of 10s» (и документация поля соглашается: «If set below 10s, a minimum value of 10s will be used instead»). Сервер: KeepaliveParams проверяет Time против KeepaliveMinServerPingTime = time.Second — минимум секунда, предупреждение «Adjusting keepalive ping interval to minimum period of 1s». Оба молча правят вашу конфигурацию: вы просили 3s — получили 10s, а тайминг рассинхронизировался с серверной политикой. Вывод практиков: не ставьте Time ниже разрешённого сервером MinTime и читайте предупреждения grpc-go при старте — они честно говорят, что вашу цифру поправили.
Чек-лист тюнинга gRPC#
- Сначала замер (baseline: latency, throughput, память), потом одна ручка за итерацию, потом повторный замер
- Лимиты сообщений: приём — чуть выше самого большого легального сообщения, отправка — зеркально на обоих концах
- Большой ответ чаще должен быть server streaming, чем поднятым MaxSendMsgSize
- keepalive включается парой: клиентский Time/PermitWithoutStream обязаны согласовываться с EnforcementPolicy сервера
- Клиентский Time не ниже 10s (кламп KeepaliveMinPingTime), серверный — не ниже 1s (KeepaliveMinServerPingTime)
- too_many_pings в логах — конфликт политик пингов: ослабить MinTime сервера или замедлить Time клиента
- Окна HTTP/2 не трогать без замера: BDP-оценщик сам растит окна от 64 КиБ до 16 МиБ
- MaxConcurrentStreams и MaxConnectionAge/Idle — предохранители продакшена; дефолты у них «бесконечность»
Практика: три эксперимента локально#
Честная оговорка про формат, одна на урок: в самоучителе есть песочница с интерпретатором Go, но сеть в браузере недоступна — gRPC-сервер и клиент в браузере не запустить, поэтому runner-ов в этом уроке, как и во всём gRPC-курсе, нет; каждое упражнение делается локально за несколько минут, а проверяют вас quiz, predict, fill, order и карточки. Эксперимент первый, самый зрелищный: поднимите дефолтный сервер из четвёртого урока, а клиент соберите с Time: 30 time.Second и PermitWithoutStream: true — и молчите; в логах клиента появится «Client received GoAway with error code ENHANCE_YOUR_CALM and debug data equal to ASCII "too_many_pings"», а сервер зафиксирует разрыв. Затем добавьте серверу EnforcementPolicy{MinTime: 30 time.Second, PermitWithoutStream: true} и убедитесь, что соединение стало вечным. Эксперимент второй: поднимите MaxRecvMsgSize на сервере до 8 МБ, но забудьте про клиентский MaxCallRecvMsgSize, отправьте 6-мегабайтный ответ и разберите ResourceExhausted по цифрам в тексте ошибки — какая сторона отстала видно мгновенно. Эксперимент третий: задайте MaxConnectionAge: 5 * time.Second на сервере и понаблюдайте в подробных логах, как соединения перекатываются GoAway-ами — и как джиттер разбрасывает моменты разрыва.
Что дальше: много сервисов, много настроек#
Курс подходит к развязке: у вас есть транспорт, безопасность, наблюдаемость, тесты и теперь — карта ручек производительности. Двадцать третий урок — gRPC в микросервисной архитектуре — поднимает масштаб: gateway-паттерны, облако и обзор service mesh, где половину ручек этого урока перекладывают на прокси вроде Envoy, а keepalive-политика из хитрого эксперимента превращается в контракт между командами, записанный в манифестах деплоя. Имеет смысл перечитать этот урок как справочник, а не как рассказ: таблица-шпаргалка выше — то, что вы будете открывать при каждом «прод тормозит». Напоследок три связки. Полный маршрут курса — в оглавлении gRPC-курса; форматы и gzip вы разбирали в уроке про проводной формат; и перед любым тюнингом прогоняйте bufconn-набор из урока про тестирование — зелёный тест после изменения настроек транспорта значит, что контракт цел, а выиграли вы или проиграли покажет только замер.
Карточки урока
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Производительность grpc начинается с замера: baseline по latency p95/p99, throughput и памяти, потом одна ручка за итерацию и повторный замер — дисциплина бенчмаркинга из go-advanced.
- Лимиты сообщений: приём по умолчанию 4 MiB (ResourceExhausted «grpc: received message larger than max»), отправка math.MaxInt32; значения ставить парой на обоих концах и чуть выше легального максимума.
- keepalive включается парой: клиентские Time/Timeout/PermitWithoutStream против серверных ServerParameters и EnforcementPolicy; несовместимость — GOAWAY too_many_pings.
- Два минимума интервалов: клиентский Time не ниже 10s (KeepaliveMinPingTime), серверный — не ниже 1s (KeepaliveMinServerPingTime); grpc-go молча поправит с предупреждением.
- Окна HTTP/2: дефолт 65535, BDP-оценщик сам растит их до 16 MiB — статические окна трогать только под замером и в памяти-констрейнт среде.
- MaxConcurrentStreams (сервер — math.MaxUint32, клиент — внутренние 100) и MaxConnectionIdle/Age с джиттером ±10% — предохранители продакшена от лавин и ржавых соединений.
Домашнее задание
Соберите тюнинг-конфиг для сервиса магазина: сервер с MaxRecvMsgSize 8 МБ, MaxConcurrentStreams 1000, ServerParameters{MaxConnectionIdle: 5 минут, MaxConnectionAge: 30 минут, Time: 2 минуты, Timeout: 10 секунд} и EnforcementPolicy{MinTime: 30 секунд, PermitWithoutStream: true}; клиент с зеркальными лимитами вызовов и ClientParameters{Time: 30 секунд, Timeout: 10 секунд, PermitWithoutStream: true}. Затем проведите цикл тюнинга по порядку из урока: baseline-замер latency на bufconn-тесте, одна ручка, повторный замер. Отдельно воспроизведите too_many_pings: дефолтный сервер против клиента с PermitWithoutStream и пингами раз в 30 секунд — и почините обеими сторонами по очереди, сравнив поведение в логах.
Личные заметкиПусто
Вопросы и ответы по уроку
Какой keepalive-интервал выставить клиенту?
Меньше, чем таймаут простоя у NAT и балансировщика на пути, но не меньше EnforcementPolicy.MinTime сервера. Дефолтный сервер разрешает пинг не чаще раза в 5 минут; если нужно чаще — ослабьте политику сервера (MinTime меньше, PermitWithoutStream: true) и только затем ускоряйте клиента. Ниже 10 секунд клиентский интервал всё равно не уедет — grpc-go поднимет его до минимума с предупреждением.
Почему соединения рвутся с too_many_pings и как это чинить?
Клиент пингует чаще, чем разрешает серверная EnforcementPolicy: без активных стримов и без PermitWithoutStream сервер засчитывает страйки за каждый пинг и после третьего шлёт GOAWAY с ErrCodeEnhanceYourCalm и too_many_pings. Лечится согласованием сторон: увеличьте клиентский Time, уменьшите серверный MinTime или включите PermitWithoutStream на сервере. Клиент grpc-go после такого разрыва сам удваивает Time для новых соединений.
Насколько поднимать MaxRecvMsgSize и MaxSendMsgSize?
Приём — чуть выше самого большого легального сообщения, с запасом на сериализационный оверхед: каждый поднятый мегабайт означает потенциальную память под буферы при пике. Отправка по умолчанию math.MaxInt32 и настраивается редко. Если большие ответы регулярны — переходите на server streaming: лимит остаётся на элемент потока, память размазывается по времени.
Стоит ли настраивать окна HTTP/2 вручную?
Обычно нет: grpc-go носит BDP-оценщик, который меряет пропускную способность и задержку канала и сам растит окна от 65535 до 16 MiB. Ручки InitialWindowSize и InitialConnWindowSize существуют, нижняя граница 64 КиБ (меньшее значение игнорируется), но документация прямо предупреждает: большинство пользователей не должны настраивать статические окна вне памяти-констрейнт окружений — динамика справляется лучше.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Connect: современный RPC от Buf
Курс Connect (Buf): протоколы, клиенты, ошибки, interceptors, браузерные клиенты, buf CLI и BSR. 25 уроков и итоговый проект API.
- Строить сервисы на connectrpc.com/connect
- Понимать три протокола и выбирать правильный
- Настраивать buf: lint, breaking changes, generate
- Писать interceptors и обрабатывать ошибки