gRPC Deadline Propagácia: Prevencia Kaskádových Zlyhaní
Deadline sme dali na edge, ale zabudli sme, ze musi cestovat s requestom. “Prečo je backend CPU na 100% keď frontend ukazuje ‘timeout’?” Pretože každý frontend request s timeoutom je stále spracovávaný backendom. Bez deadline propagácie mrháte resources na requesty na ktoré nikto nečaká.
Testované na: Go 1.21, gRPC 1.58, 3-vrstvová microservices architektúra
Problém
Bez Deadline Propagácie
Časová os requestu bez deadline propagácie:
Frontend (5s timeout) Backend A (30s timeout) Backend B (30s timeout)
│ │ │
0s │─── Request ───────────────▶ │
│ │─── Request ─────────────────▶
│ │ │
5s │ TIMEOUT! Odpoveď 504 │ │
│ (klient sa vzdal) │ │
│ │ │ Spracovanie...
15s │ │ │
│ │ │
25s │ │ │ Hotovo!
│ │◀── Response ────────────────│
│ │ │
30s │ │ Hotovo! │
│ │ (odpoveď zahodená) │
Výsledok: 25 sekúnd premárnenej práce na 2 backendoch
S Deadline Propagáciou
Časová os s deadline propagáciou:
Frontend (5s timeout) Backend A Backend B
│ │ │
0s │─── Request ───────────────▶ │
│ (deadline: 5s) │─── Request ─────────────────▶
│ │ (deadline: 4.9s) │
│ │ │
5s │ TIMEOUT! │ Context zrušený! │ Context zrušený!
│ Odpoveď 504 │ Zastavenie práce okamžite │ Zastavenie práce okamžite
│ │ │
Výsledok: Práca zastavená okamžite keď sa frontend vzdá
Implementácia
Server-Side: Rešpektovanie Contextu
// service.go
func (s *Server) ProcessOrder(ctx context.Context, req *pb.OrderRequest) (*pb.OrderResponse, error) {
// Skontroluj context pred drahými operáciami
if ctx.Err() != nil {
return nil, status.FromContextError(ctx.Err()).Err()
}
// Kontroluj periodicky počas dlhých operácií
for _, item := range req.Items {
select {
case <-ctx.Done():
// Klient sa vzdal, zastavíme spracovanie
log.Info("Context zrušený, prerušenie spracovania objednávky")
return nil, status.FromContextError(ctx.Err()).Err()
default:
}
if err := s.processItem(ctx, item); err != nil {
return nil, err
}
}
return &pb.OrderResponse{OrderId: "123"}, nil
}
// Predaj context downstream službám
func (s *Server) processItem(ctx context.Context, item *pb.Item) error {
// Context automaticky nesie deadline do downstream volania
resp, err := s.inventoryClient.CheckStock(ctx, &pb.StockRequest{
ItemId: item.Id,
})
if err != nil {
return err
}
// ...
}
Client-Side: Nastavenie Deadline
// client.go
func (c *OrderClient) CreateOrder(items []Item) (*Order, error) {
// Nastav deadline pre celú operáciu
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := c.grpcClient.ProcessOrder(ctx, &pb.OrderRequest{
Items: toPBItems(items),
})
if err != nil {
// Skontroluj či to bol timeout
if status.Code(err) == codes.DeadlineExceeded {
return nil, fmt.Errorf("spracovanie objednávky timeout-ovalo")
}
return nil, err
}
return fromPBOrder(resp), nil
}
Interceptor pre Automatickú Propagáciu
// interceptors.go
// UnaryClientInterceptor propaguje deadline cez metadáta
func DeadlinePropagationInterceptor() grpc.UnaryClientInterceptor {
return func(
ctx context.Context,
method string,
req, reply interface{},
cc *grpc.ClientConn,
invoker grpc.UnaryInvoker,
opts ...grpc.CallOption,
) error {
// gRPC automaticky propaguje deadline v contexte
// Tento interceptor pridáva logovanie/metriky
deadline, ok := ctx.Deadline()
if ok {
remaining := time.Until(deadline)
log.Debug("Volám %s s deadline za %v", method, remaining)
// Voliteľne pridaj buffer pre sieťovú latenciu
if remaining < 100*time.Millisecond {
return status.Error(codes.DeadlineExceeded,
"nedostatočný čas zostávajúci pre RPC")
}
}
return invoker(ctx, method, req, reply, cc, opts...)
}
}
// UnaryServerInterceptor loguje prichádzajúce deadline
func DeadlineLoggingInterceptor() grpc.UnaryServerInterceptor {
return func(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
deadline, ok := ctx.Deadline()
if ok {
remaining := time.Until(deadline)
log.Debug("Prijatý %s s deadline za %v", info.FullMethod, remaining)
// Pridaj do metrík
rpcDeadlineRemaining.WithLabelValues(info.FullMethod).Observe(remaining.Seconds())
} else {
log.Warn("Prijatý %s bez deadline", info.FullMethod)
}
return handler(ctx, req)
}
}
Spracovanie Streaming RPC
// Pre streaming, kontroluj context medzi správami
func (s *Server) StreamOrders(req *pb.StreamRequest, stream pb.OrderService_StreamOrdersServer) error {
ctx := stream.Context()
for {
select {
case <-ctx.Done():
return status.FromContextError(ctx.Err()).Err()
case order := <-s.orderChannel:
if err := stream.Send(order); err != nil {
return err
}
}
}
}
Databáza a Externé Volania
Propagácia do Databázy
// Predaj context databázovým queries
func (r *Repository) GetOrder(ctx context.Context, id string) (*Order, error) {
// Context sa propaguje do databázového drivera
row := r.db.QueryRowContext(ctx,
"SELECT id, status FROM orders WHERE id = $1", id)
var order Order
if err := row.Scan(&order.ID, &order.Status); err != nil {
// Vráti chybu ak context zrušený
return nil, err
}
return &order, nil
}
Propagácia do HTTP Volaní
// Urob HTTP request s context deadline
func (c *ExternalClient) CallAPI(ctx context.Context, data []byte) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, "POST", c.url, bytes.NewReader(data))
if err != nil {
return nil, err
}
resp, err := c.httpClient.Do(req)
if err != nil {
// Context zrušenie sa vracia tu
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
Propagácia do Redis
// go-redis rešpektuje context
func (c *Cache) Get(ctx context.Context, key string) (string, error) {
return c.rdb.Get(ctx, key).Result()
}
func (c *Cache) Set(ctx context.Context, key, value string, ttl time.Duration) error {
return c.rdb.Set(ctx, key, value, ttl).Err()
}
Deadline Budgeting
Rezervácia Času pre Odpoveď
// Rezervuj čas pre serializáciu a sieť
func WithResponseBudget(ctx context.Context, budget time.Duration) (context.Context, context.CancelFunc) {
deadline, ok := ctx.Deadline()
if !ok {
return ctx, func() {}
}
// Nový deadline = pôvodný - budget
newDeadline := deadline.Add(-budget)
if time.Now().After(newDeadline) {
// Už prekročený budget
ctx, cancel := context.WithCancel(ctx)
cancel() // Okamžite zrušený
return ctx, cancel
}
return context.WithDeadline(ctx, newDeadline)
}
// Použitie
func (s *Server) ProcessOrder(ctx context.Context, req *pb.OrderRequest) (*pb.OrderResponse, error) {
// Rezervuj 100ms pre odpoveď
ctx, cancel := WithResponseBudget(ctx, 100*time.Millisecond)
defer cancel()
// Teraz spracovanie má o 100ms menej času
result, err := s.doProcessing(ctx, req)
// ...
}
Per-Operácia Budgety
// Rozdeľ deadline medzi operácie
func (s *Server) ComplexOperation(ctx context.Context, req *Request) (*Response, error) {
deadline, ok := ctx.Deadline()
if !ok {
// Žiadny deadline, použi default
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, 30*time.Second)
defer cancel()
deadline = time.Now().Add(30 * time.Second)
}
total := time.Until(deadline)
// Fáza 1: Validácia (10% budgetu)
phase1Ctx, cancel1 := context.WithTimeout(ctx, total/10)
defer cancel1()
if err := s.validate(phase1Ctx, req); err != nil {
return nil, err
}
// Fáza 2: Spracovanie (70% budgetu)
phase2Ctx, cancel2 := context.WithTimeout(ctx, total*7/10)
defer cancel2()
result, err := s.process(phase2Ctx, req)
if err != nil {
return nil, err
}
// Fáza 3: Uloženie (zostávajúcich 20%)
if err := s.persist(ctx, result); err != nil {
return nil, err
}
return result, nil
}
Monitoring
Metriky
var (
rpcDeadlineRemaining = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "grpc_deadline_remaining_seconds",
Buckets: []float64{0.01, 0.1, 0.5, 1, 2, 5, 10, 30},
},
[]string{"method"},
)
rpcDeadlineExceeded = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "grpc_deadline_exceeded_total",
},
[]string{"method"},
)
rpcNoDeadline = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "grpc_no_deadline_total",
},
[]string{"method"},
)
)
Alerty
groups:
- name: grpc_deadlines
rules:
- alert: VysokaDeadlineExceededRate
expr: |
rate(grpc_deadline_exceeded_total[5m]) /
rate(grpc_server_handled_total[5m]) > 0.1
for: 5m
annotations:
summary: ">10% requestov prekračuje deadline"
- alert: TesneDeadlines
expr: |
histogram_quantile(0.5, rate(grpc_deadline_remaining_seconds_bucket[5m])) < 0.5
for: 10m
annotations:
summary: "Mediánový prichádzajúci deadline <500ms"
- alert: ChybajuceDeadlines
expr: |
rate(grpc_no_deadline_total[5m]) > 0
for: 5m
annotations:
summary: "Requesty prichádzajúce bez deadlines"
Checklist
## gRPC Deadline Propagácia
### Client-Side
- [ ] Vždy nastav context timeout/deadline
- [ ] Použi context.WithTimeout pre top-level volania
- [ ] Spracuj DeadlineExceeded chyby vhodne
### Server-Side
- [ ] Kontroluj ctx.Done() pred drahými operáciami
- [ ] Predaj context všetkým downstream volaniam
- [ ] Použi QueryContext/ExecContext pre databázu
- [ ] Použi NewRequestWithContext pre HTTP
### Interceptory
- [ ] Loguj prichádzajúce deadlines
- [ ] Metrika na zostávajúci deadline
- [ ] Odmietni volania s nedostatočným časom
### Monitoring
- [ ] Sleduj deadline exceeded rate
- [ ] Alert na tesné deadlines
- [ ] Dashboard ukazujúci distribúciu deadlines
Záver
Deadline propagácia zabraňuje premárnenej práci:
- Nastav deadlines na všetky RPC volania z klientov
- Predaj context všetkým downstream volaniam na serveroch
- Kontroluj ctx.Done() počas dlhých operácií
- Monitoruj deadline metriky pre zachytenie problémov
Prestaň spracovávať requesty na ktoré nikto nečaká.
Súvisiace články
- Circuit Breaker vs Rate Limiter vs Bulkhead - Resilience patterns
- gRPC Load Balancing v Kubernetes - gRPC v K8s
Súvisiace články
gRPC v Kubernetes: Prečo Service round-robin klame
Prečo má jeden pod 90% trafficu pri gRPC. Reprodukovateľný lab, riešenia od client-side LB po service mesh, a production checklist.
Circuit Breaker Anti-Patterns: Keď Ochrana Spôsobuje Výpadky
Circuit breakery bránia kaskádovým zlyhaniam ale zlá konfigurácia ich zhoršuje. Ukážem 5 anti-patternov: zdieľané breakery, zlé thresholdy, žiadny fallback.
Keď Prepared Statements Spravia PostgreSQL 10× Pomalším: Generic Plan Trap
Rovnaký query, rovnaké parametre, ale prod je pomalý a staging funguje. Ukážem ako reprodukovať generic plan problém s pgBouncer, Java/Go a ako ho fixnúť.
Structured Logging Performance: Keď Sa Logger Stane Bottleneckom
Pri 50k logov/sec JSON serializácia žerie 30% CPU. Štandardná knižnica encoding/json je pomalá. Benchmarkujem zap vs zerolog vs slog so skutočnými číslami.
Citujte tento článok
Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.