Späť na blog

gRPC Keepalive Nezhoda: Transport Closing Po Idle

|
| grpc, debugging, networking, golang, microservices

gRPC keepalive nas postipal hned po tom, co sme zvysili pocet spojeni. “Náhodné ‘transport is closing’ chyby po obdobiach nízkej prevádzky.” Príčina: gRPC klient keepalives sú menej časté než serverov MaxConnectionIdle, takže server zatvára spojenia ktoré klient považoval za zdravé.

Prostredie: gRPC 1.40+, Go/Java/Python klienti, dlhodobé spojenia, nepravidelné traffic vzory

Problém

Prerušované Úmrtia Spojení

Traffic vzor a zlyhania:

00:00 - 00:15  Vysoká prevádzka, veľa requestov, žiadne chyby
00:15 - 00:45  Nízka prevádzka, málo requestov
00:46         Burst requestov → "transport is closing" chyby
00:47         Nové spojenia vytvorené, requesty uspejú

Chyby sa objavujú:
- Po idle obdobiach (obed, noci, víkendy)
- Počas traffic burstov po tichých obdobiach
- Len na dlhodobých spojeniach
- Nie na čerstvých spojeniach

Chybové Hlášky

// Klient strana chyby
rpc error: code = Unavailable desc = transport is closing

// Server strana logy (ak verbose)
grpc: Server.Serve failed to complete security handshake
connection closed before server preface received

// Alebo len tiché ukončenie spojenia
// Žiadna chyba na serveri, klient dostane RST

Príčina

Nezhoda Časovania Keepalive

Server konfigurácia:
┌─────────────────────────────────────────────────────────────┐
│ MaxConnectionIdle: 5 minút                                  │
│ "Zatvor spojenia bez aktivity 5 minút"                     │
│                                                             │
│ MaxConnectionAge: 30 minút                                  │
│ "Zatvor spojenia staršie než 30 minút bez ohľadu na to"    │
│                                                             │
│ MaxConnectionAgeGrace: 10 sekúnd                            │
│ "Daj 10s pre in-flight RPC pred force close"               │
└─────────────────────────────────────────────────────────────┘

Klient konfigurácia:
┌─────────────────────────────────────────────────────────────┐
│ KeepAliveTime: 10 minút                                     │
│ "Pošli ping každých 10 minút ak žiadna aktivita"           │
│                                                             │
│ KeepAliveTimeout: 20 sekúnd                                 │
│ "Čakaj 20s na ping odpoveď pred označením za mŕtve"        │
└─────────────────────────────────────────────────────────────┘

Časová os:
T+0:00    Posledný RPC dokončený
T+5:00    Server: "Spojenie idle 5 min, zatváranie" → RST
T+5:01    Klient skúsi RPC → "transport is closing"
T+10:00   Klient by poslal keepalive ping (príliš neskoro!)

Prečo Sa To Deje

// Bežná chyba: Spoliehanie len na klient keepalives
conn, err := grpc.Dial(target,
    grpc.WithKeepaliveParams(keepalive.ClientParameters{
        Time:                10 * time.Minute,  // Príliš dlho!
        Timeout:             20 * time.Second,
        PermitWithoutStream: true,
    }),
)

// Server má striktnejšie nastavenia (často defaults alebo load balancer)
server := grpc.NewServer(
    grpc.KeepaliveParams(keepalive.ServerParameters{
        MaxConnectionIdle: 5 * time.Minute,  // Server zabíja prvý!
    }),
)

// Výsledok: Server zatvorí pred klient pingom

Diagnostika

Skontroluj Server Keepalive Nastavenia

// Go server - skontroluj čo je nakonfigurované
func printServerKeepalive(s *grpc.Server) {
    // Bohužiaľ žiadny priamy spôsob čítania nastavení
    // Skontroluj kód inicializácie servera
    // Bežné defaults:
    // - MaxConnectionIdle: infinity
    // - MaxConnectionAge: infinity
    // - Ale load balancery majú často vlastné!
}
# Skontroluj či load balancer terminuje spojenia
# Pozri sa na vek spojení keď nastanú chyby

# Na klientovi, sleduj životnosť spojení
# Pridaj logovanie pre zmeny stavu spojenia

Monitoruj Stav Spojenia

// Go klient - monitoruj zmeny stavu spojenia
import "google.golang.org/grpc/connectivity"

func monitorConnection(conn *grpc.ClientConn) {
    state := conn.GetState()
    for {
        changed := conn.WaitForStateChange(context.Background(), state)
        if !changed {
            return
        }
        newState := conn.GetState()
        log.Printf("gRPC stav spojenia: %s%s", state, newState)

        if newState == connectivity.TransientFailure {
            log.Printf("Spojenie vstúpilo do TransientFailure - znova sa pripojí")
        }
        state = newState
    }
}

Zachyť Metriky Spojení

// Zapni gRPC channelz pre debugging
import _ "google.golang.org/grpc/channelz/service"

// Spusti channelz service
grpc.EnableTracing = true

// Potom dotazuj cez grpc_cli alebo channelz web UI
// Ukazuje: vek spojení, stavy, časy poslednej aktivity

Riešenie

Možnosť 1: Zosúlaď Keepalive Časy

// Klient keepalive MUSÍ byť kratší než server MaxConnectionIdle

// Server konfigurácia
server := grpc.NewServer(
    grpc.KeepaliveParams(keepalive.ServerParameters{
        MaxConnectionIdle:     15 * time.Minute,
        MaxConnectionAge:      30 * time.Minute,
        MaxConnectionAgeGrace: 5 * time.Second,
        Time:                  5 * time.Minute,   // Server pingy
        Timeout:               1 * time.Second,
    }),
    grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
        MinTime:             1 * time.Minute,  // Povoľ klient pingy
        PermitWithoutStream: true,
    }),
)

// Klient konfigurácia - pinguj pred zatvorením serverom
conn, err := grpc.Dial(target,
    grpc.WithKeepaliveParams(keepalive.ClientParameters{
        Time:                5 * time.Minute,   // < MaxConnectionIdle
        Timeout:             10 * time.Second,
        PermitWithoutStream: true,              // Dôležité!
    }),
)

Možnosť 2: Ošetri Reconnection Elegantne

// Použi retry s backoff pre transientné zlyhania
import "google.golang.org/grpc/codes"
import "google.golang.org/grpc/status"

func callWithRetry(ctx context.Context, client pb.ServiceClient) error {
    var lastErr error
    for attempt := 0; attempt < 3; attempt++ {
        resp, err := client.SomeMethod(ctx, &pb.Request{})
        if err == nil {
            return nil
        }

        lastErr = err
        st, ok := status.FromError(err)
        if !ok {
            return err // Nie gRPC chyba
        }

        switch st.Code() {
        case codes.Unavailable:
            // Transport zatvára - retry okamžite, spojenie sa znova pripojí
            log.Printf("Spojenie nedostupné, opakujem (pokus %d)", attempt+1)
            time.Sleep(100 * time.Millisecond)
            continue
        case codes.DeadlineExceeded, codes.ResourceExhausted:
            // Backoff pre tieto
            time.Sleep(time.Duration(attempt+1) * time.Second)
            continue
        default:
            return err
        }
    }
    return lastErr
}

Možnosť 3: Nakonfiguruj Service Mesh Správne

# Istio DestinationRule - kontoluj connection pool
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: my-service
spec:
  host: my-service.default.svc.cluster.local
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 100
        connectTimeout: 5s
        tcpKeepalive:
          time: 300s      # 5 minút
          interval: 75s
      http:
        h2UpgradePolicy: UPGRADE
        idleTimeout: 900s  # 15 minút - dlhšie než klient keepalive

Možnosť 4: Load Balancer Konfigurácia

# AWS ALB - zvýš idle timeout
# Default je 60 sekúnd - často príliš krátke pre gRPC!

# Terraform
resource "aws_lb_target_group" "grpc" {
  protocol         = "HTTP"
  protocol_version = "GRPC"

  health_check {
    protocol = "HTTP"
    path     = "/grpc.health.v1.Health/Check"
  }
}

resource "aws_lb_listener" "grpc" {
  load_balancer_arn = aws_lb.main.arn

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.grpc.arn
  }
}

# Nastav idle timeout na ALB
resource "aws_lb" "main" {
  idle_timeout = 900  # 15 minút pre gRPC
}

Monitoring

groups:
  - name: grpc-connections
    rules:
      - alert: GRPCTransportClosing
        expr: |
          rate(grpc_client_handled_total{grpc_code="Unavailable"}[5m]) > 1
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Vysoká miera gRPC Unavailable chýb"

      - alert: GRPCConnectionChurn
        expr: |
          rate(grpc_client_connections_total[5m]) > 10
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "Vysoký gRPC connection churn - skontroluj keepalive nastavenia"

Checklist

## gRPC Keepalive Nezhoda

### Symptómy
- [ ] "transport is closing" chyby po idle obdobiach
- [ ] Chyby korelujú s obdobiami nízkej prevádzky
- [ ] Čerstvé spojenia fungujú dobre
- [ ] Problém horší cez víkendy/noci

### Diagnostika
- [ ] Skontroluj server MaxConnectionIdle nastavenie
- [ ] Skontroluj klient KeepAliveTime nastavenie
- [ ] Over load balancer idle timeout
- [ ] Skontroluj service mesh connection pool nastavenia
- [ ] Monitoruj prechody stavu spojenia

### Riešenia
- [ ] Klient keepalive < Server MaxConnectionIdle
- [ ] Zapni PermitWithoutStream na klientovi
- [ ] Nastav server EnforcementPolicy pre povolenie pingov
- [ ] Nakonfiguruj load balancer idle timeout > keepalive
- [ ] Pridaj retry logiku pre Unavailable chyby

Záver

Lekcia: gRPC keepalives fungujú len ak klient pinguje pred server timeoutom. Server, load balancer a service mesh majú každý vlastné idle timeouty - klient musí pingovať rýchlejšie než najkratší z nich.

Kľúčové princípy:

  1. Klient keepalive time < server MaxConnectionIdle
  2. Load balancery majú vlastné timeouty (často 60s default)
  3. PermitWithoutStream = true pre idle keepalives
  4. Retry Unavailable chyby - spojenie sa automaticky znova pripojí

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 Keepalive Nezhoda: Transport Closing Po Idle". https://www.michal-drozd.com/sk/blog/grpc-keepalive-transport-closing/ (Publikované 13. januára 2025).