Späť na blog

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:

  1. Nastav deadlines na všetky RPC volania z klientov
  2. Predaj context všetkým downstream volaniam na serveroch
  3. Kontroluj ctx.Done() počas dlhých operácií
  4. Monitoruj deadline metriky pre zachytenie problémov

Prestaň spracovávať requesty na ktoré nikto nečaká.


Súvisiace články

Súvisiace články

Citujte tento článok

Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.

Michal Drozd. "gRPC Deadline Propagácia: Prevencia Kaskádových Zlyhaní". https://www.michal-drozd.com/sk/blog/grpc-deadline-propagacia/ (Publikované 23. augusta 2025).