gRPC Keepalive Nezhoda: Transport Closing Po Idle
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:
- Klient keepalive time < server MaxConnectionIdle
- Load balancery majú vlastné timeouty (často 60s default)
- PermitWithoutStream = true pre idle keepalives
- Retry Unavailable chyby - spojenie sa automaticky znova pripojí
Súvisiace články
- Go cgo DNS Thread Explosion - Go networking problémy
- Kubernetes Headless Service DNS - Service discovery problémy
Súvisiace články
Go p99 Latency Špičky: Vnorené context.WithTimeout Timer Búrky
Periodické latency špičky ktoré vyzerajú ako network jitter. Skutočná príčina: vnorené timeouty vytvárajú tisíce timerov ktoré zaťažujú Go runtime timer heap a spúšťajú GC scanning.
Ghost Pod: Prečo váš Service stále posiela traffic na mŕtve endpointy
Náhodné ECONNRESET na niektorých nodoch. Endpointy vyzerajú správne. Vinník: conntrack NAT záznamy držia dlhodobé spojenia pripnuté k podom, ktoré už neexistujú.
Kubernetes Ghost Connections: Zastarané Conntrack DNAT Záznamy
Service vracia zlé pod IP po škálovaní. Príčina: Linux conntrack drží DNAT záznamy dlhšie ako existujú pody, smeruje traffic na zmazané endpointy.
Gossip Protocol Ghost Nodes: IP Reuse Strašiaci Váš Cluster
Nový node sa pripája ku clusteru ale je odmietaný. IP starého nodu je stále v blackliste failure detection gossip protokolu. Zombie membership záznam žije ďalej.
Citujte tento článok
Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.