Как работает propagation дедлайнов и таймаутов в gRPC-Go с помощью context?
В gRPC-Go дедлайны передаются через context.WithDeadline/WithTimeout; фреймворк автоматически транслирует оставшееся время в HTTP/2-заголовок grpc-timeout, а все downstream-вызовы наследуют тот же ctx.
Как context переносит дедлайн в gRPC-Go
В gRPC-Go каждый RPC-вызов принимает context.Context в качестве первого аргумента. Если в контексте есть дедлайн (установленный через context.WithDeadline или context.WithTimeout), транспортный уровень gRPC-Go автоматически вычисляет оставшееся время и передаёт его серверу в HTTP/2-псевдозаголовке grpc-timeout. Сервер восстанавливает дедлайн в собственном контексте запроса, и к моменту, когда handler начинает работу, ctx.Deadline() уже содержит корректное значение.
Разница между Deadline и Timeout
context.WithDeadline задаёт абсолютный момент времени (time.Time), context.WithTimeout — относительную длительность, которая внутри тоже превращается в time.Time. gRPC-Go всегда работает с абсолютным дедлайном: при передаче вниз по цепочке вызовов он вычитает уже потраченное время и отправляет оставшееся. Это гарантирует сквозной end-to-end budget.
Propagation через цепочку сервисов
Ключевое правило: каждый промежуточный сервис обязан передавать полученный ctx в downstream-вызовы — и в другие RPC, и в запросы к базе данных. Если вместо этого создать новый независимый таймаут, сквозной бюджет теряется: downstream продолжает работу после того, как исходный клиент уже получил DEADLINE_EXCEEDED и ушёл.
// client.go
func callChain(addr string) error {
// Полный бюджет на всю цепочку — 500 мс
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
conn, _ := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
defer conn.Close()
client := pb.NewUserServiceClient(conn)
resp, err := client.GetUser(ctx, &pb.GetUserRequest{Id: "42"})
if err != nil {
// codes.DeadlineExceeded, если время вышло на любом hop
return fmt.Errorf("GetUser: %w", err)
}
fmt.Println(resp.Name)
return nil
}
// server.go — handler передаёт ctx дальше, не создаёт новый
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
// ctx уже содержит дедлайн, переданный клиентом
row, err := s.db.QueryContext(ctx, "SELECT name FROM users WHERE id = $1", req.Id)
if err != nil {
return nil, status.Errorf(codes.Internal, "db: %v", err)
}
defer row.Close()
// ...
return &pb.User{Name: name}, nil
}
Обработка ошибок дедлайна
Когда контекст истекает, gRPC-Go возвращает статус codes.DeadlineExceeded. Проверять его следует через пакет google.golang.org/grpc/status:
import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
if st, ok := status.FromError(err); ok {
if st.Code() == codes.DeadlineExceeded {
log.Println("deadline exceeded, fast-fail")
}
}
Отмена на стороне сервера
Если клиент закрывает соединение до завершения RPC, сервер получает сигнал через ctx.Done(). Handler должен периодически проверять канал и завершать работу, чтобы не тратить ресурсы на уже ненужный запрос:
select {
case <-ctx.Done():
return nil, status.FromContextError(ctx.Err()).Err()
default:
}
Подводные камни
- Создание нового
context.WithTimeoutна каждом hop вместо передачи исходного ctx — downstream может продолжать работу после того, как клиент уже получил ошибку. - Использование
context.Background()для запросов к БД внутри handler — SQL-запросы не отменяются при дедлайне RPC, соединения зависают. - Игнорирование
ctx.Done()в длинных циклах обработки — сервер продолжает работу после отмены клиентом, впустую тратя CPU и память. - Слишком короткий глобальный таймаут без учёта latency сети — частые
DeadlineExceededпри нормальной нагрузке. - Отсутствие метрик по
grpc_server_handling_seconds— невозможно понять, где именно бюджет тратится быстрее всего. - Путаница между
codes.DeadlineExceededиcodes.Canceled: первый означает истёкший таймаут, второй — явную отмену клиентом; retry-стратегии для них разные. - Не установленный
grpc.WithKeepaliveParamsна клиенте — при долгих idle-соединениях HTTP/2-поток молча закрывается, и дедлайн срабатывает неожиданно.
Common mistakes
- Давать ответ про deadlines и timeouts через context в gRPC-Go только на уровне определения, не показывая поведение в реальном приложении.
- Игнорировать границы ответственности вокруг темы «deadlines и timeouts через context в gRPC-Go»: кто отменяет работу, кто владеет ресурсом и где формируется ответ клиенту.
- Не связывать deadlines и timeouts через context в gRPC-Go с observability, тестированием или безопасностью, когда это влияет на продакшен-поведение.
What the interviewer is testing
- Точно объясняет, что именно делает deadlines и timeouts через context в gRPC-Go и где это используется в Go-коде.
- Связывает deadlines и timeouts через context в gRPC-Go с корректным lifecycle запроса, отменой, конкурентностью или конфигурацией сервера там, где это уместно.
- Не изобретает API и опирается на реальные контракты официальной документации.