Späť na blog

Vyčerpanie Ephemeral Portov: Node Ktorý 'Pokazí'

Ked som prvykrat videl “cannot assign requested address” v K8s, tipoval som DNS. “Jeden node náhodne ‘pokazí’—pripojenia k externým API zlyhávajú, ale len z toho nodu.” Pody vyzerali zdravé, CPU bolo v poriadku, pamäť tiež. Vinník: service mesh sidecary vytvárajúce tisíce krátkodobých spojení, vyčerpávajúce ephemeral port range.

Prostredie: Kubernetes 1.27, Istio service mesh, vysoko zaťažené API gateway pody

Problém

Symptómy

Čo sme pozorovali:

Node A (zdravý):
  Pod 1 → External API → 200 OK
  Pod 2 → External API → 200 OK

Node B (zlý):
  Pod 3 → External API → connection refused / timeout
  Pod 4 → External API → connection refused / timeout

Ale:
- Všetky pody ukazovali Ready
- Žiadny OOM, žiadne CPU throttling
- Rovnaký image, rovnaká konfigurácia
- Reštart podov dočasne pomohol
- Problém sa vrátil po ~30 minútach

Prečo Štandardný Debugging Toto Nezachytí

# Pod metriky vyzerajú dobre
kubectl top pod -n api-gateway
# Všetky pody: CPU 20%, Memory 40%

# Logy ukazujú connection failures ale nie root cause
kubectl logs api-gateway-xyz
# ERROR: connection refused to external-api.com:443

# Dokonca aj node metriky vyzerajú OK
kubectl top node problem-node
# CPU: 45%, Memory: 60%

# Problém je neviditeľný kým nekontrolujete ephemeral porty

Príčina

Problém Ephemeral Portov

Ako TCP spojenia fungujú:

┌─────────────┐                    ┌─────────────┐
│   Klient    │                    │   Server    │
│             │   Source Port      │             │
│             │   (ephemeral)      │             │
│   :34567 ───┼──────────────────▶ ├──── :443    │
│   :34568 ───┼──────────────────▶ ├──── :443    │
│   :34569 ───┼──────────────────▶ ├──── :443    │
└─────────────┘                    └─────────────┘

Default ephemeral port range: 32768-60999 = ~28,000 portov

Keď je zapojený SNAT (NodePort, externý traffic):
Všetky pody na node zdieľajú ephemeral porty nodu!

┌──────────────────────────────────────────────────┐
│                    Node                          │
│  ┌─────┐ ┌─────┐ ┌─────┐                        │
│  │Pod 1│ │Pod 2│ │Pod 3│  Všetky zdieľajú       │
│  └──┬──┘ └──┬──┘ └──┬──┘  rovnaké porty         │
│     │       │       │     pre SNAT              │
│     └───────┴───────┴────▶ iptables MASQUERADE  │
│                           32768-60999           │
└──────────────────────────────────────────────────┘

Service Mesh Amplifikácia

Bez meshu:
  Pod → External API
  = 1 spojenie per request

So sidecar proxy:
  Pod → Envoy sidecar → External API
  = 2 spojenia per request (interné + externé)

S agresívnymi retries:
  Pod → Envoy (retry 3x) → External API
  = Až 6 spojení per request

S vypnutým/pokazeným connection poolingom:
  Každý request = nové TCP spojenie
  Vysoké RPS = vyčerpanie portov za minúty

TIME_WAIT To Zhoršuje

# Skontroluj TIME_WAIT spojenia na node
ss -tan state time-wait | wc -l
# 25000  <-- Takmer na limite!

# TIME_WAIT trvá 60 sekúnd defaultne
# Ak vytváraš spojenia rýchlejšie než expirujú:

Rate: 500 nových spojení/sekundu
TIME_WAIT trvanie: 60 sekúnd
Steady state: 500 * 60 = 30,000 portov v TIME_WAIT

Dostupné porty: ~28,000
Výsledok: Vyčerpanie portov!

Diagnostika

Krok 1: Skontroluj Využitie Portov

# Na postihnutom node
# Spočítaj spojenia podľa stavu
ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn

# Vzorový výstup z vyčerpaného nodu:
# 25432 TIME-WAIT
#   523 ESTABLISHED
#   156 FIN-WAIT-2
#    42 SYN-SENT

# Skontroluj spojenia na špecifickú destináciu
ss -tan state time-wait dst :443 | wc -l

Krok 2: Identifikuj Vinný Pod

# Skontroluj ktorý proces vlastní najviac spojení
# Použi conntrack na trasovanie NAT spojení

conntrack -L -d <external-api-ip> 2>/dev/null | \
  awk '{print $5}' | sort | uniq -c | sort -rn | head -10

# Alebo skontroluj využitie source port range
ss -tan | awk -F: '/TIME-WAIT.*:443/ {print $2}' | \
  cut -d' ' -f1 | sort -n | uniq -c | sort -rn

Krok 3: Over Port Range

# Skontroluj aktuálny ephemeral port range
cat /proc/sys/net/ipv4/ip_local_port_range
# 32768	60999  (default, cca 28,000 portov)

# Skontroluj net.ipv4.tcp_tw_reuse nastavenie
cat /proc/sys/net/ipv4/tcp_tw_reuse
# 0 (defaultne vypnuté)

Riešenie

Možnosť 1: Rozšír Port Range

# Na všetkých nodoch (cez DaemonSet alebo node config)
sysctl -w net.ipv4.ip_local_port_range="1024 65535"

# Perzistentne cez /etc/sysctl.d/
echo "net.ipv4.ip_local_port_range = 1024 65535" > /etc/sysctl.d/99-ephemeral-ports.conf
# DaemonSet na aplikovanie sysctl
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: sysctl-tuning
  namespace: kube-system
spec:
  selector:
    matchLabels:
      app: sysctl-tuning
  template:
    metadata:
      labels:
        app: sysctl-tuning
    spec:
      hostNetwork: true
      hostPID: true
      initContainers:
        - name: sysctl
          image: busybox
          securityContext:
            privileged: true
          command:
            - /bin/sh
            - -c
            - |
              sysctl -w net.ipv4.ip_local_port_range="1024 65535"
              sysctl -w net.ipv4.tcp_tw_reuse=1
              sysctl -w net.ipv4.tcp_fin_timeout=30
      containers:
        - name: pause
          image: gcr.io/google_containers/pause:3.2

Možnosť 2: Zapni TCP Connection Reuse

# Povol znovupoužitie TIME_WAIT spojení pre nové odchádzajúce
sysctl -w net.ipv4.tcp_tw_reuse=1

# Zníž TIME_WAIT timeout (opatrne - môže rozbiť niektoré scenáre)
sysctl -w net.ipv4.tcp_fin_timeout=30

Možnosť 3: Oprav Aplikáciu

# Istio: Zapni connection pooling
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: external-api
spec:
  host: external-api.com
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 100
        connectTimeout: 10s
      http:
        h2UpgradePolicy: UPGRADE  # Použi HTTP/2 multiplexing
        maxRequestsPerConnection: 1000
// Go: Znovupoužívaj HTTP klienta s connection poolingom
var httpClient = &http.Client{
    Transport: &http.Transport{
        MaxIdleConns:        100,
        MaxIdleConnsPerHost: 100,
        IdleConnTimeout:     90 * time.Second,
        // Kritické: drž spojenia nažive
        DisableKeepAlives:   false,
    },
    Timeout: 30 * time.Second,
}

// NEROB toto:
// resp, err := http.Get(url)  // Vytvára nového klienta zakaždým!

Možnosť 4: Použi HTTP/2 alebo gRPC

HTTP/1.1: 1 request per connection
HTTP/2:   Viacero requestov per connection (multiplexing)

S HTTP/2:
  1000 RPS = môže použiť len 10-50 spojení
  vs HTTP/1.1 = 1000 spojení

Prepni na HTTP/2 pre externé API kde je to možné

Monitoring

Prometheus Metriky

groups:
  - name: ephemeral-ports
    rules:
      - alert: EphemeralPortExhaustion
        expr: |
          node_sockstat_TCP_tw > 20000
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Vysoké TIME_WAIT spojenia na {{ $labels.instance }}"
          description: "{{ $value }} spojení v TIME_WAIT, riziko vyčerpania portov"

      - alert: HighConnectionRate
        expr: |
          rate(node_netstat_Tcp_PassiveOpens[5m]) +
          rate(node_netstat_Tcp_ActiveOpens[5m]) > 1000
        for: 5m
        labels:
          severity: info
        annotations:
          summary: "Vysoká TCP connection rate na {{ $labels.instance }}"

Rýchly Health Check Skript

#!/bin/bash
# port-exhaustion-check.sh

echo "=== Ephemeral Port Status ==="

# Aktuálny port range
echo "Port range: $(cat /proc/sys/net/ipv4/ip_local_port_range)"
RANGE=$(cat /proc/sys/net/ipv4/ip_local_port_range)
LOW=$(echo $RANGE | awk '{print $1}')
HIGH=$(echo $RANGE | awk '{print $2}')
TOTAL=$((HIGH - LOW))
echo "Celkom dostupných: $TOTAL"

# Porty v použití
TIME_WAIT=$(ss -tan state time-wait | wc -l)
ESTABLISHED=$(ss -tan state established | wc -l)
echo "TIME_WAIT: $TIME_WAIT"
echo "ESTABLISHED: $ESTABLISHED"

# Využitie
USED=$((TIME_WAIT + ESTABLISHED))
PERCENT=$((USED * 100 / TOTAL))
echo "Využitie: $PERCENT%"

if [ $PERCENT -gt 80 ]; then
  echo "VAROVANIE: Riziko vyčerpania portov!"
fi

# Top destinácie v TIME_WAIT
echo -e "\n=== Top TIME_WAIT destinácie ==="
ss -tan state time-wait | awk '{print $4}' | sort | uniq -c | sort -rn | head -5

Checklist

## Vyčerpanie Ephemeral Portov

### Symptómy
- [ ] Jeden node zlyháva zatiaľ čo ostatné fungujú
- [ ] Connection refused na externé služby
- [ ] Pody vyzerajú zdravé (CPU/memory OK)
- [ ] Problém sa vracia po reštarte podov

### Diagnostika
- [ ] Skontroluj TIME_WAIT count: ss -tan state time-wait | wc -l
- [ ] Skontroluj port range: cat /proc/sys/net/ipv4/ip_local_port_range
- [ ] Identifikuj top connection destinations
- [ ] Hľadaj chýbajúci connection pooling

### Riešenia
- [ ] Rozšír port range (1024-65535)
- [ ] Zapni tcp_tw_reuse
- [ ] Zapni connection pooling v app/mesh
- [ ] Prepni na HTTP/2 kde je to možné
- [ ] Zníž connection-per-request vzory

Záver

Tento failure mode je obzvlášť záludný pretože:

  1. Pod metriky vyzerajú dobre - je to node-level zdroj
  2. Postihuje len SNAT traffic - interný traffic funguje
  3. Dočasné fixe (reštart) fungujú - skrývajú vzor
  4. Service mesh amplifikuje - viac spojení per request

Fix je zvyčajne kombinácia:

  • System tuning (port range, tcp_tw_reuse)
  • Aplikačné fixe (connection pooling, HTTP/2)
  • Architektonické zmeny (zníž krátkodobé spojenia)

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. "Vyčerpanie Ephemeral Portov: Node Ktorý 'Pokazí'". https://www.michal-drozd.com/sk/blog/ephemeral-port-vycerpanie-kubernetes/ (Publikované 11. novembra 2024).