Späť na blog

TCP TIME_WAIT Vyčerpanie Portov: Keď Connection Pooling Nestačí

V den, ked nam dosli porty, som konecne zacal respektovat TIME_WAIT. “Náhle sa nedá pripojiť k databáze - address already in use.” Príčina: vaša vysokopriepustná služba vyčerpala ephemeral porty lebo TCP TIME_WAIT drží sockety otvorené 60 sekúnd po zatvorení.

Prostredie: Vysokopriepustné služby, microservices s mnohými odchádzajúcimi spojeniami, zlá konfigurácia connection pool, krátkodobé HTTP spojenia

Problém

Záhadné Zlyhania Spojení

Časová os vyčerpania portov:

T+0:00   Služba beží normálne
         Odchádzajúce spojenia na DB, cache, API
         Ephemeral port range: 32768-60999 (28,231 portov)

T+0:10   Traffic spike - 1000 req/sek
         Každý request robí 3 odchádzajúce volania
         3000 spojení/sek otvorených a zatvorených

T+0:30   TIME_WAIT sockety sa hromadia
         Každý zostáva 60 sekúnd
         3000 × 60 = 180,000 socketov potrebných!

T+0:35   Error: cannot assign requested address
         Všetky ephemeral porty k DB IP:port vyčerpané
         Nové spojenia nemožné

Čo Je Vlastne TIME_WAIT

Životný Cyklus TCP Spojenia:

Klient                              Server
   |                                   |
   |-------- SYN ------------------>  |
   |<------- SYN-ACK --------------- |
   |-------- ACK ------------------>  |
   |           ESTABLISHED             |
   |<======= DATA ==================>  |
   |                                   |
   |-------- FIN ------------------>  |  Klient iniciuje zatvorenie
   |<------- ACK ------------------- |
   |<------- FIN ------------------- |
   |-------- ACK ------------------>  |
   |                                   |
   |  TIME_WAIT (2 × MSL = 60s)       |  ← Socket nepoužiteľný!
   |                                   |
   ↓  Konečne zatvorený               |

Prečo TIME_WAIT existuje:
1. Zabezpečiť že finálne ACK dorazí na server
2. Nechať duplikátne packety expirovať
3. Zabrániť starým packetom kazeniu nového spojenia

Problém: Vysoká priepustnosť = veľa TIME_WAITov

Príčina

Problém 4-Tuple

TCP socket identifikovaný 4-tuple:
(source_ip, source_port, dest_ip, dest_port)

Vaša služba:     10.0.1.50
Databáza:        10.0.2.100:5432

Dostupné kombinácie:
10.0.1.50:32768 → 10.0.2.100:5432
10.0.1.50:32769 → 10.0.2.100:5432
...
10.0.1.50:60999 → 10.0.2.100:5432

Len 28,231 unikátnych 4-tuples možných!

Pri 500 spojeniach/sek s 60s TIME_WAIT:
500 × 60 = 30,000 socketov potrebných
30,000 > 28,231 dostupných → VYČERPANIE

Skontrolujte Stav Vašich Socketov

# Počet socketov podľa stavu
ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn

# Výstup:
# 24567 TIME-WAIT
#  2341 ESTABLISHED
#   234 LISTEN
#    45 FIN-WAIT-2

# TIME_WAIT na konkrétnu destináciu
ss -tan state time-wait | grep "10.0.2.100:5432" | wc -l
# 23456 ← Takmer všetky porty použité!

# Kontrola ephemeral port range
cat /proc/sys/net/ipv4/ip_local_port_range
# 32768   60999

Skutočná Matematika

# Výpočet maximálnej udržateľnej rýchlosti
# Ephemeral porty: 60999 - 32768 = 28,231
# TIME_WAIT trvanie: 60 sekúnd
# Max nových spojení/sek: 28,231 / 60 = 470/sek na destináciu

# Ak máte 10 unikátnych destinácií:
# Max celkom: 4,700 nových spojení/sek

# Ale ak všetky idú na JEDNU destináciu (vašu DB):
# Max: 470 spojení/sek udržateľne

# Reality check vášho workloadu:
ss -tan state time-wait dst 10.0.2.100:5432 | wc -l
# Ak blízko 28,231 → ste na limite

Diagnostika

Identifikujte Bottleneck

# Nájdite top TIME_WAIT destinácie
ss -tan state time-wait | awk '{print $4}' | sort | uniq -c | sort -rn | head
#  23456 10.0.2.100:5432    ← Databáza
#   3421 10.0.3.50:6379     ← Redis
#   1234 10.0.4.100:80      ← API služba

# Ak jedna destinácia dominuje → to je váš bottleneck

# Kontrola obchádzania connection pool
netstat -an | grep "10.0.2.100:5432" | grep -c ESTABLISHED
# Malo by byť stabilné (veľkosť poolu), nie kolísať

Diagnostika na Úrovni Aplikácie

# Trace vytvárania spojení (Java príklad)
jcmd <pid> VM.native_memory summary | grep -A5 "Internal"

# Kontrola HikariCP pool stats
curl localhost:8080/actuator/metrics/hikaricp.connections.active
curl localhost:8080/actuator/metrics/hikaricp.connections.idle

# Ak vytvorených spojení > veľkosť poolu
# → Pool je obchádzaný alebo zle nakonfigurovaný

Riešenie

Možnosť 1: Znovupoužitie Spojení (Najlepšie)

# HikariCP - správne dimenzovanie poolu
spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      minimum-idle: 10
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000
      # KĽÚČ: Nevytvárať nové spojenia pre každý query!
// Go - konfigurácia connection pool
db, _ := sql.Open("postgres", connStr)
db.SetMaxOpenConns(20)
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(30 * time.Minute)
db.SetConnMaxIdleTime(10 * time.Minute)
# Python - SQLAlchemy pool
engine = create_engine(
    "postgresql://...",
    pool_size=20,
    max_overflow=10,
    pool_pre_ping=True,
    pool_recycle=1800
)

Možnosť 2: HTTP Keep-Alive

// Go HTTP klient - znovupoužitie spojení
client := &http.Client{
    Transport: &http.Transport{
        MaxIdleConns:        100,
        MaxIdleConnsPerHost: 100,
        IdleConnTimeout:     90 * time.Second,
        // Znovupoužitie TCP spojení pre viaceré requesty
    },
}

// ZLE: Vytváranie nového klienta per request
func badHandler(w http.ResponseWriter, r *http.Request) {
    client := &http.Client{}  // Nový klient = nové spojenia!
    resp, _ := client.Get("http://api/endpoint")
}

// SPRÁVNE: Znovupoužitie klienta
var httpClient = &http.Client{...}  // Package-level

func goodHandler(w http.ResponseWriter, r *http.Request) {
    resp, _ := httpClient.Get("http://api/endpoint")
}

Možnosť 3: TCP Tuning (Opatrne!)

# Rozšírenie ephemeral port range
echo "1024 65535" > /proc/sys/net/ipv4/ip_local_port_range
# Teraz 64,511 portov namiesto 28,231

# Povolenie TIME_WAIT reuse (vyžaduje timestamps)
echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse
# Umožňuje znovupoužitie TIME_WAIT socketov pre nové odchádzajúce spojenia
# BEZPEČNÉ pre client-side spojenia

# VAROVANIE: tcp_tw_recycle je NEBEZPEČNÉ a odstránené v Linux 4.12
# Rozbíja spojenia cez NAT
# NIKDY nepoužívajte tcp_tw_recycle

# Zníženie TIME_WAIT trvania (neodporúča sa)
# Linux nepodporuje priamu zmenu
# Vyžadovalo by rekompláciu kernelu
# Kubernetes sysctl tuning
apiVersion: v1
kind: Pod
spec:
  securityContext:
    sysctls:
    - name: net.ipv4.ip_local_port_range
      value: "1024 65535"
    - name: net.ipv4.tcp_tw_reuse
      value: "1"

Možnosť 4: Viaceré Zdrojové IP

# Ak sa pripájate na jednu destináciu, pridajte zdrojové IP
# Každá zdrojová IP dostane vlastný port range

ip addr add 10.0.1.51/24 dev eth0
ip addr add 10.0.1.52/24 dev eth0

# Nakonfigurujte aplikáciu na rotáciu zdrojových IP
# Efektívny port range znásobený počtom IP
// Go - bind na konkrétnu zdrojovú IP
dialer := &net.Dialer{
    LocalAddr: &net.TCPAddr{
        IP: net.ParseIP("10.0.1.51"),
    },
}

transport := &http.Transport{
    DialContext: dialer.DialContext,
}

Možnosť 5: SO_LINGER (Posledná Možnosť)

// Vynútenie okamžitého zatvorenia socketu (nebezpečné!)
conn, _ := net.Dial("tcp", "10.0.2.100:5432")
tcpConn := conn.(*net.TCPConn)

// Nastavenie linger na 0 = poslať RST namiesto FIN
// Vyhne sa TIME_WAIT ale môže stratiť dáta!
tcpConn.SetLinger(0)
tcpConn.Close()

// VAROVANIE: Toto môže spôsobiť:
// - Stratu dát ak send buffer nie je prázdny
// - Server dostane RST (connection reset)
// - Použite len pre read-only spojenia alebo nekritické

Monitoring

groups:
  - name: tcp-exhaustion
    rules:
      - alert: TimeWaitSocketsHigh
        expr: |
          node_sockstat_TCP_tw > 20000
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "{{ $value }} socketov v TIME_WAIT"

      - alert: EphemeralPortsLow
        expr: |
          (node_sockstat_TCP_tw + node_sockstat_TCP_alloc) /
          (node_nf_conntrack_entries_limit) > 0.8
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "Vyčerpanie ephemeral portov blízko"

      - alert: ConnectionPoolBypass
        expr: |
          rate(hikaricp_connections_creation_seconds_count[5m]) > 10
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "Vysoká rýchlosť vytvárania spojení - pool môže byť obchádzaný"
# Rýchly monitoring script
watch -n 5 'echo "TIME_WAIT:"; ss -tan state time-wait | wc -l;
echo "ESTABLISHED:"; ss -tan state established | wc -l;
echo "Top destinácie:"; ss -tan state time-wait | awk "{print \$4}" | sort | uniq -c | sort -rn | head -5'

Checklist

## TCP TIME_WAIT Vyčerpanie Portov

### Diagnostika
- [ ] Spočítajte TIME_WAIT sockety: ss -tan state time-wait | wc -l
- [ ] Identifikujte top destinácie podľa počtu TIME_WAIT
- [ ] Skontrolujte ephemeral port range: cat /proc/sys/net/ipv4/ip_local_port_range
- [ ] Vypočítajte max udržateľnú rýchlosť na destináciu

### Opravy Aplikácie (Robte Prvé)
- [ ] Overte že connection pool je nakonfigurovaný
- [ ] Skontrolujte že pool sa skutočne používa (nie je obchádzaný)
- [ ] Povoľte HTTP keep-alive pre API volania
- [ ] Znovupoužívajte HTTP klientov naprieč requestami

### Systémový Tuning (Ak Potrebný)
- [ ] Rozšírte ephemeral port range: 1024-65535
- [ ] Povoľte tcp_tw_reuse (bezpečné pre klientov)
- [ ] Zvážte viaceré zdrojové IP pre jednu destináciu
- [ ] NIKDY nepoužívajte tcp_tw_recycle

### Monitoring
- [ ] Alertujte na počet TIME_WAIT socketov
- [ ] Alertujte na rýchlosť vytvárania spojení
- [ ] Monitorujte využitie port range

Záver

Poučenie: TCP TIME_WAIT je funkcia, nie bug - ale vysokopriepustné služby môžu vyčerpať ephemeral porty ak spojenia nie sú správne poolované a znovupoužívané.

Kľúčové princípy:

  1. Connection pooling je riešenie - znovupoužívajte spojenia, nevytvárajte nové
  2. TIME_WAIT existuje pre bezpečnosť - neeliminujte ho, obíďte ho
  3. tcp_tw_reuse je bezpečné - pre client-side spojenia s timestamps
  4. tcp_tw_recycle je nebezpečné - odstránené z moderných kernelov z dobrého dôvodu

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. "TCP TIME_WAIT Vyčerpanie Portov: Keď Connection Pooling Nestačí". https://www.michal-drozd.com/sk/blog/tcp-time-wait-port-exhaustion/ (Publikované 28. októbra 2024).